From 0f30400c7d35b5bd4415210af1d94fc302c379d4 Mon Sep 17 00:00:00 2001 From: Garret Patti Date: Sun, 17 May 2026 10:34:01 -0400 Subject: [PATCH] doom scroll touch edit --- .../DoomScrollViewer/DoomScrollViewer.tsx | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/DoomScrollViewer/DoomScrollViewer.tsx b/frontend/src/components/DoomScrollViewer/DoomScrollViewer.tsx index b8180fc..ebdcb3a 100644 --- a/frontend/src/components/DoomScrollViewer/DoomScrollViewer.tsx +++ b/frontend/src/components/DoomScrollViewer/DoomScrollViewer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { api, type MediaItem } from "../../api/client"; interface Props { @@ -12,10 +12,11 @@ export default function DoomScrollViewer({ items, onClose, onViewInLibrary }: Pr const [fading, setFading] = useState(false); const wheelLock = useRef(false); const touchStartY = useRef(null); + const contentRef = useRef(null); const item = items[index]; - function go(delta: 1 | -1) { + const go = useCallback((delta: 1 | -1) => { if (wheelLock.current) return; const next = index + delta; if (next < 0 || next >= items.length) return; @@ -26,7 +27,7 @@ export default function DoomScrollViewer({ items, onClose, onViewInLibrary }: Pr setFading(false); wheelLock.current = false; }, 200); - } + }, [index, items.length]); useEffect(() => { const onWheel = (e: WheelEvent) => { e.deltaY > 0 ? go(1) : go(-1); }; @@ -35,24 +36,68 @@ export default function DoomScrollViewer({ items, onClose, onViewInLibrary }: Pr if (e.key === "ArrowUp") { e.preventDefault(); go(-1); } if (e.key === "Escape") onClose(); }; - const onTouchStart = (e: TouchEvent) => { touchStartY.current = e.touches[0].clientY; }; - const onTouchEnd = (e: TouchEvent) => { - if (touchStartY.current === null) return; - const delta = touchStartY.current - e.changedTouches[0].clientY; - if (Math.abs(delta) > 50) delta > 0 ? go(1) : go(-1); - touchStartY.current = null; - }; window.addEventListener("wheel", onWheel, { passive: true }); window.addEventListener("keydown", onKey); - window.addEventListener("touchstart", onTouchStart); - window.addEventListener("touchend", onTouchEnd); return () => { window.removeEventListener("wheel", onWheel); window.removeEventListener("keydown", onKey); + }; + }, [go, onClose]); + + useEffect(() => { + const onTouchStart = (e: TouchEvent) => { + touchStartY.current = e.touches[0].clientY; + if (contentRef.current) contentRef.current.style.transition = "none"; + }; + + const onTouchMove = (e: TouchEvent) => { + e.preventDefault(); + if (touchStartY.current === null || !contentRef.current) return; + const offset = e.touches[0].clientY - touchStartY.current; + contentRef.current.style.transform = `translateY(${offset}px)`; + contentRef.current.style.opacity = String(Math.max(0.3, 1 - Math.abs(offset) / 300)); + }; + + const onTouchEnd = (e: TouchEvent) => { + if (touchStartY.current === null) return; + const delta = touchStartY.current - e.changedTouches[0].clientY; + touchStartY.current = null; + + if (Math.abs(delta) > 80) { + // Hand off to the fading animation + if (contentRef.current) { + contentRef.current.style.transition = ""; + contentRef.current.style.transform = ""; + contentRef.current.style.opacity = ""; + } + go(delta > 0 ? 1 : -1); + } else { + // Snap back to center + if (contentRef.current) { + const el = contentRef.current; + el.style.transition = "opacity 0.25s ease, transform 0.25s ease"; + el.style.transform = "translateY(0)"; + el.style.opacity = "1"; + setTimeout(() => { + if (contentRef.current) { + contentRef.current.style.transition = ""; + contentRef.current.style.transform = ""; + contentRef.current.style.opacity = ""; + } + }, 260); + } + } + }; + + window.addEventListener("touchstart", onTouchStart); + window.addEventListener("touchmove", onTouchMove, { passive: false }); + window.addEventListener("touchend", onTouchEnd); + return () => { window.removeEventListener("touchstart", onTouchStart); + window.removeEventListener("touchmove", onTouchMove); window.removeEventListener("touchend", onTouchEnd); }; - }, [index, fading]); + }, [go]); return ( <> @@ -61,6 +106,7 @@ export default function DoomScrollViewer({ items, onClose, onViewInLibrary }: Pr {/* Media area */}