Files
R3ap3r 3178b8f4f7 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
2026-09-25 23:20:57 -05:00

110 lines
3.5 KiB
TypeScript

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;
}