Aurora SpotiRip desktop and web app
- Spotify playlist, library, album and artist browsing with in-app playback - Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks - Electron desktop app (castLabs Widevine) with a permanent sidebar layout - Linux and Windows packaging scripts
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallbackRef } from "./useCallbackRef";
|
||||
|
||||
export interface ScrollRevealOptions {
|
||||
threshold?: number | number[];
|
||||
rootMargin?: string;
|
||||
}
|
||||
export function useScrollReveal(options: ScrollRevealOptions = {}) {
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const observedElementsRef = useRef<Set<HTMLElement>>(new Set());
|
||||
const mutationObserversRef = useRef<Map<Element, MutationObserver>>(
|
||||
new Map(),
|
||||
);
|
||||
const optionsRef = useRef(options);
|
||||
|
||||
useEffect(() => {
|
||||
optionsRef.current = options;
|
||||
}, [options]);
|
||||
|
||||
const getObserver = () => {
|
||||
if (!observerRef.current) {
|
||||
observerRef.current = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.setAttribute("data-revealed", "true");
|
||||
|
||||
const revealChildren = (el: Element) => {
|
||||
Array.from(el.children).forEach((child) => {
|
||||
if (child.getAttribute("data-revealed") !== "true") {
|
||||
child.setAttribute("data-revealed", "true");
|
||||
}
|
||||
revealChildren(child);
|
||||
});
|
||||
};
|
||||
|
||||
revealChildren(entry.target);
|
||||
|
||||
if (!mutationObserversRef.current.has(entry.target)) {
|
||||
const mo = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "childList") {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node instanceof Element) {
|
||||
if (node.getAttribute("data-revealed") !== "true") {
|
||||
node.setAttribute("data-revealed", "true");
|
||||
}
|
||||
revealChildren(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
mo.observe(entry.target, { childList: true, subtree: true });
|
||||
mutationObserversRef.current.set(entry.target, mo);
|
||||
}
|
||||
|
||||
observerRef.current?.unobserve(entry.target);
|
||||
if (entry.target instanceof HTMLElement) {
|
||||
observedElementsRef.current.delete(entry.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: optionsRef.current.threshold ?? 0.15,
|
||||
rootMargin: optionsRef.current.rootMargin ?? "0px 0px -40px 0px",
|
||||
},
|
||||
);
|
||||
}
|
||||
return observerRef.current;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
observerRef.current?.disconnect();
|
||||
observedElementsRef.current.clear();
|
||||
mutationObserversRef.current.forEach((mo) => mo.disconnect());
|
||||
mutationObserversRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const observe = useCallbackRef((element: HTMLElement | null) => {
|
||||
if (!element) return;
|
||||
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
element.setAttribute("data-revealed", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && window.matchMedia) {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
if (mediaQuery.matches) {
|
||||
element.setAttribute("data-revealed", "true");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!observedElementsRef.current.has(element) &&
|
||||
element.getAttribute("data-revealed") !== "true"
|
||||
) {
|
||||
getObserver().observe(element);
|
||||
observedElementsRef.current.add(element);
|
||||
}
|
||||
});
|
||||
|
||||
return observe;
|
||||
}
|
||||
Reference in New Issue
Block a user