navigation #9

Merged
gpatti merged 2 commits from navigation into main 2026-04-06 01:48:01 +00:00
Showing only changes of commit 0c234b691e - Show all commits

View File

@@ -26,17 +26,25 @@ function pickRandom(items: DoomScrollItem[], excludeRecent: DoomScrollItem[]): D
export default function DoomScrollView({ items, videoContext = 'mixed', onClose }: Props) { export default function DoomScrollView({ items, videoContext = 'mixed', onClose }: Props) {
const settings = useUserSettings() const settings = useUserSettings()
const muted = videoContext === 'mixed' ? settings.mixedMuted : videoContext === 'movies' ? settings.moviesMuted : settings.tvMuted const settingsMuted = videoContext === 'mixed' ? settings.mixedMuted : videoContext === 'movies' ? settings.moviesMuted : settings.tvMuted
const [history, setHistory] = useState<DoomScrollItem[]>(() => { const [history, setHistory] = useState<DoomScrollItem[]>(() => {
if (items.length === 0) return [] if (items.length === 0) return []
return [pickRandom(items, [])] return [pickRandom(items, [])]
}) })
const [historyIndex, setHistoryIndex] = useState(0) 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<HTMLVideoElement>(null)
const cooldownRef = useRef(false) const cooldownRef = useRef(false)
const touchStartY = useRef<number | null>(null) const touchStartY = useRef<number | null>(null)
const current = history[historyIndex] ?? null const current = history[historyIndex] ?? null
const isVideo = current?.mediaType === 'video'
const backCount = history.length - 1 - historyIndex
const goNext = useCallback(() => { const goNext = useCallback(() => {
if (items.length === 0) return if (items.length === 0) return
@@ -65,6 +73,33 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
setTimeout(() => { cooldownRef.current = false }, 300) setTimeout(() => { cooldownRef.current = false }, 300)
}, [goNext, goPrev]) }, [goNext, goPrev])
// Reset pause when switching items
useEffect(() => {
setIsPaused(false)
}, [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
useEffect(() => {
if (!videoRef.current) return
if (isPaused) {
videoRef.current.pause()
} else {
videoRef.current.play().catch(() => {})
}
}, [isPaused, current?.url])
// 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(() => { useEffect(() => {
const handleKey = (e: KeyboardEvent) => { const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') { onClose(); return } if (e.key === 'Escape') { onClose(); return }
@@ -100,18 +135,58 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
} }
}, [navigate, onClose]) }, [navigate, onClose])
const backCount = history.length - 1 - historyIndex
return ( return (
<div className="fixed inset-0 z-50 flex flex-col" style={{ backgroundColor: '#000' }}> <div className="fixed inset-0 z-50 flex flex-col" style={{ backgroundColor: '#000' }}>
{/* Keyframe for auto-play progress bar */}
<style>{`@keyframes doom-progress { from { width: 0% } to { width: 100% } }`}</style>
{/* Top bar */} {/* Top bar */}
<div className="absolute top-0 left-0 right-0 flex items-center justify-between p-3 z-10"> <div className="absolute top-0 left-0 right-0 flex items-center gap-2 p-3 z-10">
<span className="text-xs px-2 py-1 rounded" style={{ color: 'rgba(255,255,255,0.5)', backgroundColor: 'rgba(0,0,0,0.4)' }}> <span className="text-xs px-2 py-1 rounded flex-shrink-0" style={{ color: 'rgba(255,255,255,0.5)', backgroundColor: 'rgba(0,0,0,0.4)' }}>
{backCount > 0 ? `${backCount} back` : 'Doom Scroll'} {backCount > 0 ? `${backCount}` : 'Doom Scroll'}
</span>
{/* Auto-play controls */}
<div className="flex-1 flex items-center justify-center gap-2">
<button
onClick={() => setAutoPlayEnabled((v) => !v)}
className="px-3 py-1 rounded-full text-xs font-medium transition-colors flex-shrink-0"
style={{
backgroundColor: autoPlayEnabled ? 'var(--accent)' : 'rgba(0,0,0,0.5)',
color: '#fff',
}}
aria-label={autoPlayEnabled ? 'Disable auto-play' : 'Enable auto-play'}
>
Auto
</button>
{autoPlayEnabled && (
<div className="flex items-center gap-1">
<button
onClick={() => setAutoPlaySeconds((s) => Math.max(1, s - 1))}
className="w-6 h-6 rounded-full flex items-center justify-center text-sm flex-shrink-0"
style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: '#fff' }}
aria-label="Decrease interval"
>
</button>
<span className="text-xs text-center flex-shrink-0" style={{ color: 'rgba(255,255,255,0.8)', minWidth: '2.25rem' }}>
{autoPlaySeconds}s
</span> </span>
<button
onClick={() => setAutoPlaySeconds((s) => Math.min(60, s + 1))}
className="w-6 h-6 rounded-full flex items-center justify-center text-sm flex-shrink-0"
style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: '#fff' }}
aria-label="Increase interval"
>
+
</button>
</div>
)}
</div>
<button <button
onClick={onClose} onClick={onClose}
className="w-9 h-9 rounded-full flex items-center justify-center text-sm transition-opacity hover:opacity-100 opacity-80" className="w-9 h-9 rounded-full flex items-center justify-center text-sm flex-shrink-0 transition-opacity hover:opacity-100 opacity-80"
style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: '#fff' }} style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: '#fff' }}
aria-label="Close doom scroll" aria-label="Close doom scroll"
> >
@@ -121,13 +196,14 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
{/* Media */} {/* Media */}
<div className="flex-1 flex items-center justify-center overflow-hidden"> <div className="flex-1 flex items-center justify-center overflow-hidden">
{current?.mediaType === 'video' ? ( {isVideo && current ? (
<video <video
ref={videoRef}
key={current.url} key={current.url}
src={current.url} src={current.url}
autoPlay autoPlay
loop loop={!autoPlayEnabled}
muted={muted} muted={localMuted}
playsInline playsInline
className="max-w-full max-h-full object-contain" className="max-w-full max-h-full object-contain"
style={{ backgroundColor: '#000' }} style={{ backgroundColor: '#000' }}
@@ -143,18 +219,57 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
) : null} ) : null}
</div> </div>
{/* Bottom bar */} {/* Bottom bar: mute | filename | play-pause */}
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-center p-4 z-10"> <div className="absolute bottom-0 left-0 right-0 flex items-center gap-3 px-4 pb-3 pt-2 z-10">
<span className="text-xs truncate max-w-sm text-center" style={{ color: 'rgba(255,255,255,0.4)' }}> <div className="w-9 flex-shrink-0">
{isVideo && (
<button
onClick={() => setLocalMuted((v) => !v)}
className="w-9 h-9 rounded-full flex items-center justify-center text-base transition-opacity hover:opacity-100 opacity-70"
style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: '#fff' }}
aria-label={localMuted ? 'Unmute' : 'Mute'}
>
{localMuted ? '🔇' : '🔊'}
</button>
)}
</div>
<span className="flex-1 text-xs truncate text-center" style={{ color: 'rgba(255,255,255,0.4)' }}>
{current?.name} {current?.name}
</span> </span>
<div className="w-9 flex-shrink-0 flex justify-end">
{isVideo && (
<button
onClick={() => setIsPaused((v) => !v)}
className="w-9 h-9 rounded-full flex items-center justify-center text-sm transition-opacity hover:opacity-100 opacity-70"
style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: '#fff' }}
aria-label={isPaused ? 'Play' : 'Pause'}
>
{isPaused ? '▶' : '⏸'}
</button>
)}
</div> </div>
</div>
{/* Auto-play progress bar — key on current URL restarts animation on each new item */}
{autoPlayEnabled && !isPaused && (
<div
key={current?.url}
className="absolute bottom-0 left-0 h-0.5 z-20"
style={{
backgroundColor: 'var(--accent)',
animationName: 'doom-progress',
animationDuration: `${autoPlaySeconds}s`,
animationTimingFunction: 'linear',
animationFillMode: 'forwards',
}}
/>
)}
{/* Prev / Next hint arrows */} {/* Prev / Next hint arrows */}
{historyIndex > 0 && ( {historyIndex > 0 && (
<button <button
onClick={() => navigate('prev')} onClick={() => navigate('prev')}
className="absolute left-1/2 top-14 -translate-x-1/2 w-10 h-10 rounded-full flex items-center justify-center text-xl transition-opacity hover:opacity-100 opacity-50" className="absolute left-1/2 top-16 -translate-x-1/2 w-10 h-10 rounded-full flex items-center justify-center text-xl transition-opacity hover:opacity-100 opacity-50 z-10"
style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }} style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
aria-label="Previous" aria-label="Previous"
> >
@@ -163,7 +278,7 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
)} )}
<button <button
onClick={() => navigate('next')} onClick={() => navigate('next')}
className="absolute left-1/2 bottom-12 -translate-x-1/2 w-10 h-10 rounded-full flex items-center justify-center text-xl transition-opacity hover:opacity-100 opacity-50" className="absolute left-1/2 bottom-14 -translate-x-1/2 w-10 h-10 rounded-full flex items-center justify-center text-xl transition-opacity hover:opacity-100 opacity-50 z-10"
style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }} style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
aria-label="Next" aria-label="Next"
> >