'use client' import { useEffect, useRef, useState, useCallback } from 'react' import { useUserSettings } from '@/hooks/useUserSettings' export interface DoomScrollItem { url: string name: string mediaType: 'video' | 'image' itemKey?: string } interface Props { items: DoomScrollItem[] videoContext?: 'mixed' | 'movies' | 'tv' onClose: () => void } function pickRandom(items: DoomScrollItem[], excludeRecent: DoomScrollItem[]): DoomScrollItem { const excludeCount = Math.min(excludeRecent.length, items.length - 1) const recentUrls = new Set(excludeRecent.slice(-excludeCount).map((i) => i.url)) const candidates = items.filter((i) => !recentUrls.has(i.url)) const pool = candidates.length > 0 ? candidates : items return pool[Math.floor(Math.random() * pool.length)] } export default function DoomScrollView({ items, videoContext = 'mixed', onClose }: Props) { const settings = useUserSettings() const settingsMuted = videoContext === 'mixed' ? settings.mixedMuted : videoContext === 'movies' ? settings.moviesMuted : settings.tvMuted const [history, setHistory] = useState(() => { if (items.length === 0) return [] return [pickRandom(items, [])] }) const [historyIndex, setHistoryIndex] = useState(0) const [localMuted, setLocalMuted] = useState(settingsMuted) const [isPaused, setIsPaused] = useState(false) const [autoPlayEnabled, setAutoPlayEnabled] = useState(false) const [autoPlaySeconds, setAutoPlaySeconds] = useState(5) const videoRef = useRef(null) const cooldownRef = useRef(false) const touchStartY = useRef(null) const current = history[historyIndex] ?? null const isVideo = current?.mediaType === 'video' const backCount = history.length - 1 - historyIndex const goNext = useCallback(() => { if (items.length === 0) return setHistoryIndex((idx) => { if (idx < history.length - 1) { return idx + 1 } const next = pickRandom(items, history) setHistory((h) => { const updated = [...h, next] return updated.length > 100 ? updated.slice(-100) : updated }) return idx + 1 }) }, [items, history]) const goPrev = useCallback(() => { setHistoryIndex((idx) => Math.max(0, idx - 1)) }, []) const navigate = useCallback((dir: 'next' | 'prev') => { if (cooldownRef.current) return cooldownRef.current = true if (dir === 'next') goNext() else goPrev() setTimeout(() => { cooldownRef.current = false }, 300) }, [goNext, goPrev]) // On navigation to a new item: reset pause state and start playing. // Merging the reset + play() into one effect prevents the old isPaused=true // value from calling pause() on the freshly-mounted video element before the // reset fires. If autoplay is blocked by browser policy (common when unmuted), // fall back to muted and retry — the user can unmute manually afterward. useEffect(() => { setIsPaused(false) if (!videoRef.current) return videoRef.current.play().catch(() => { if (!videoRef.current) return videoRef.current.muted = true setLocalMuted(true) videoRef.current.play().catch(() => {}) }) }, [current?.url]) // Sync muted imperatively — React's muted prop is not reliable useEffect(() => { if (videoRef.current) videoRef.current.muted = localMuted }, [localMuted, current?.url]) // Sync play/pause imperatively for user-initiated pause/unpause only. // current?.url is intentionally excluded: navigation is handled above. useEffect(() => { if (!videoRef.current) return if (isPaused) { videoRef.current.pause() } else { videoRef.current.play().catch(() => {}) } }, [isPaused]) // Auto-play timer — resets on each new item, pause, enable/disable, or interval change useEffect(() => { if (!autoPlayEnabled || isPaused) return const id = setTimeout(() => goNext(), autoPlaySeconds * 1000) return () => clearTimeout(id) }, [autoPlayEnabled, isPaused, autoPlaySeconds, current?.url, goNext]) useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); return } if (e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown') { e.preventDefault(); navigate('next') } if (e.key === 'ArrowUp' || e.key === 'PageUp') { e.preventDefault(); navigate('prev') } } const handleWheel = (e: WheelEvent) => { e.preventDefault() navigate(e.deltaY > 0 ? 'next' : 'prev') } const handleTouchStart = (e: TouchEvent) => { touchStartY.current = e.touches[0].clientY } const handleTouchEnd = (e: TouchEvent) => { if (touchStartY.current === null) return const delta = touchStartY.current - e.changedTouches[0].clientY if (Math.abs(delta) > 50) navigate(delta > 0 ? 'next' : 'prev') touchStartY.current = null } document.addEventListener('keydown', handleKey) document.addEventListener('wheel', handleWheel, { passive: false }) document.addEventListener('touchstart', handleTouchStart, { passive: true }) document.addEventListener('touchend', handleTouchEnd, { passive: true }) document.body.style.overflow = 'hidden' return () => { document.removeEventListener('keydown', handleKey) document.removeEventListener('wheel', handleWheel) document.removeEventListener('touchstart', handleTouchStart) document.removeEventListener('touchend', handleTouchEnd) document.body.style.overflow = '' } }, [navigate, onClose]) return (
{/* Keyframe for auto-play progress bar */} {/* Top bar */}
{backCount > 0 ? `← ${backCount}` : 'Doom Scroll'} {/* Auto-play controls */}
{autoPlayEnabled && (
{autoPlaySeconds}s
)}
{/* Media */}
{isVideo && current ? (
{/* Bottom bar: mute | filename | play-pause */}
{isVideo && ( )}
{current?.name}
{isVideo && ( )}
{/* Auto-play progress bar — key on current URL restarts animation on each new item */} {autoPlayEnabled && !isPaused && (
)} {/* Prev / Next hint arrows */} {historyIndex > 0 && ( )}
) }