From d84600bce808ae0dfc6de199ddd7b42000f23dcc Mon Sep 17 00:00:00 2001 From: Garret Patti Date: Sun, 17 May 2026 17:02:53 -0400 Subject: [PATCH] touch media viewer fixes --- .../components/MediaViewer/MediaViewer.tsx | 91 ++++++++++++++++++- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/MediaViewer/MediaViewer.tsx b/frontend/src/components/MediaViewer/MediaViewer.tsx index bffb5bd..fa4fe74 100644 --- a/frontend/src/components/MediaViewer/MediaViewer.tsx +++ b/frontend/src/components/MediaViewer/MediaViewer.tsx @@ -13,6 +13,9 @@ interface Props { export default function MediaViewer({ mediaId, siblings, onClose, onNavigate }: Props) { const [showTags, setShowTags] = useState(() => window.innerWidth >= 768); const touchStartX = useRef(null); + const touchStartY = useRef(null); + const swipeAxis = useRef<"horizontal" | "vertical" | null>(null); + const contentRef = useRef(null); const { data: item } = useQuery({ queryKey: ["media", mediaId], @@ -24,28 +27,105 @@ export default function MediaViewer({ mediaId, siblings, onClose, onNavigate }: const prevId = currentIndex > 0 ? mediaSiblings[currentIndex - 1].media_item_id : null; const nextId = currentIndex < mediaSiblings.length - 1 ? mediaSiblings[currentIndex + 1].media_item_id : null; + // Clear inline styles when a new item loads so the card appears cleanly + useEffect(() => { + if (contentRef.current) { + contentRef.current.style.transition = ""; + contentRef.current.style.transform = ""; + contentRef.current.style.opacity = ""; + } + }, [mediaId]); + useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); if (e.key === "ArrowLeft" && prevId) onNavigate(prevId); if (e.key === "ArrowRight" && nextId) onNavigate(nextId); } - const onTouchStart = (e: TouchEvent) => { touchStartX.current = e.touches[0].clientX; }; + + const onTouchStart = (e: TouchEvent) => { + touchStartX.current = e.touches[0].clientX; + touchStartY.current = e.touches[0].clientY; + swipeAxis.current = null; + if (contentRef.current) contentRef.current.style.transition = "none"; + }; + + const onTouchMove = (e: TouchEvent) => { + if (touchStartX.current === null || touchStartY.current === null) return; + const dx = e.touches[0].clientX - touchStartX.current; + const dy = e.touches[0].clientY - touchStartY.current; + + // Commit to an axis on the first significant movement + if (swipeAxis.current === null && (Math.abs(dx) > 8 || Math.abs(dy) > 8)) { + swipeAxis.current = Math.abs(dx) >= Math.abs(dy) ? "horizontal" : "vertical"; + } + + // Vertical gestures (tag panel scroll, etc.) pass through untouched + if (swipeAxis.current !== "horizontal") return; + + e.preventDefault(); + if (!contentRef.current) return; + contentRef.current.style.transform = `translate(calc(-50% + ${dx}px), -50%)`; + contentRef.current.style.opacity = String(Math.max(0.4, 1 - Math.abs(dx) / 400)); + }; + const onTouchEnd = (e: TouchEvent) => { if (touchStartX.current === null) return; const delta = touchStartX.current - e.changedTouches[0].clientX; - if (Math.abs(delta) > 50) { - if (delta > 0 && nextId) onNavigate(nextId); - if (delta < 0 && prevId) onNavigate(prevId); - } touchStartX.current = null; + touchStartY.current = null; + + // Non-horizontal gesture: just reset the transition we disabled on touchstart + if (swipeAxis.current !== "horizontal") { + swipeAxis.current = null; + if (contentRef.current) { + contentRef.current.style.transition = ""; + contentRef.current.style.transform = ""; + contentRef.current.style.opacity = ""; + } + return; + } + swipeAxis.current = null; + + const targetId = delta > 0 ? nextId : prevId; + + if (Math.abs(delta) > 80 && targetId) { + const el = contentRef.current; + if (el) { + const slideX = delta > 0 ? -120 : 120; + el.style.transition = "opacity 0.2s ease, transform 0.2s ease"; + el.style.transform = `translate(calc(-50% + ${slideX}px), -50%)`; + el.style.opacity = "0"; + setTimeout(() => onNavigate(targetId), 200); + } else { + onNavigate(targetId); + } + } else { + // Snap back to center + const el = contentRef.current; + if (el) { + el.style.transition = "opacity 0.25s ease, transform 0.25s ease"; + el.style.transform = "translate(-50%, -50%)"; + el.style.opacity = "1"; + setTimeout(() => { + if (contentRef.current) { + contentRef.current.style.transition = ""; + contentRef.current.style.transform = ""; + contentRef.current.style.opacity = ""; + } + }, 260); + } + } }; + window.addEventListener("keydown", onKey); window.addEventListener("touchstart", onTouchStart); + window.addEventListener("touchmove", onTouchMove, { passive: false }); window.addEventListener("touchend", onTouchEnd); return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("touchstart", onTouchStart); + window.removeEventListener("touchmove", onTouchMove); window.removeEventListener("touchend", onTouchEnd); }; }, [prevId, nextId, onClose, onNavigate]); @@ -78,6 +158,7 @@ export default function MediaViewer({ mediaId, siblings, onClose, onNavigate }: {/* Media card */}
e.stopPropagation()} style={{ position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: 101, background: "#1a1a1a", borderRadius: 8, padding: 16, display: "flex", flexDirection: "column", alignItems: "center", gap: 12, maxWidth: "80vw", maxHeight: "90vh", overflow: "auto" }} >