add viewer navigation and doom scroll mode

- Add prev/next arrow buttons and ArrowLeft/ArrowRight keyboard shortcuts to ImageLightbox and VideoPlayerModal
- Wire prev/next navigation in MixedView (through filtered media entries), TvView (through season episodes), and MoviesView/MovieDetailModal (through filtered movie list)
- Add new DoomScrollView component: fullscreen random-media mode with scroll/swipe/keyboard navigation, 100-item back-history, and per-library mute settings
- Add Doom Scroll button to mixed, movies, and TV library views
- Doom scroll respects active filters: mixed uses filtered entries, movies uses filtered movie list, TV fetches episodes from matching series only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garret Patti
2026-04-05 21:34:32 -04:00
parent 334d62e3b3
commit 4f54a7c888
7 changed files with 535 additions and 39 deletions

View File

@@ -8,12 +8,14 @@ interface Props {
url: string
name: string
onClose: () => void
onPrev?: () => void
onNext?: () => void
mediaKey?: string
onTagsChanged?: () => void
context?: 'mixed' | 'movies' | 'tv'
}
export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsChanged, context = 'mixed' }: Props) {
export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, mediaKey, onTagsChanged, context = 'mixed' }: Props) {
const settings = useUserSettings()
const autoPlay = context === 'mixed' ? settings.mixedAutoplay : context === 'movies' ? settings.moviesAutoplay : settings.tvAutoplay
const loop = context === 'mixed' ? settings.mixedLoop : context === 'movies' ? settings.moviesLoop : settings.tvLoop
@@ -26,6 +28,8 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'ArrowLeft') onPrev?.()
if (e.key === 'ArrowRight') onNext?.()
}
document.addEventListener('keydown', handleKey)
document.body.style.overflow = 'hidden'
@@ -33,7 +37,7 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
document.removeEventListener('keydown', handleKey)
document.body.style.overflow = ''
}
}, [onClose])
}, [onClose, onPrev, onNext])
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === overlayRef.current) onClose()
@@ -88,8 +92,9 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
{showTags ? (
<div className="flex gap-4 w-full flex-1 min-h-0 items-start overflow-hidden">
{/* Video */}
<div className="flex-1 min-w-0 min-h-0 flex items-center justify-center max-h-full">
<div className="flex-1 min-w-0 min-h-0 flex items-center justify-center max-h-full relative">
<video
key={url}
src={url}
controls
autoPlay={autoPlay}
@@ -99,6 +104,26 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
style={{ backgroundColor: '#000' }}
onClick={(e) => e.stopPropagation()}
/>
{onPrev && (
<button
onClick={(e) => { e.stopPropagation(); onPrev() }}
className="absolute left-2 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full flex items-center justify-center text-lg transition-opacity hover:opacity-100 opacity-70"
style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
aria-label="Previous"
>
</button>
)}
{onNext && (
<button
onClick={(e) => { e.stopPropagation(); onNext() }}
className="absolute right-2 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full flex items-center justify-center text-lg transition-opacity hover:opacity-100 opacity-70"
style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
aria-label="Next"
>
</button>
)}
</div>
{/* Tag panel */}
<div
@@ -113,8 +138,9 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
</div>
</div>
) : (
<div className="w-full flex-1 min-h-0 flex items-center justify-center overflow-hidden max-h-full">
<div className="w-full flex-1 min-h-0 flex items-center justify-center overflow-hidden max-h-full relative">
<video
key={url}
src={url}
controls
autoPlay={autoPlay}
@@ -124,6 +150,26 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
style={{ backgroundColor: '#000' }}
onClick={(e) => e.stopPropagation()}
/>
{onPrev && (
<button
onClick={(e) => { e.stopPropagation(); onPrev() }}
className="absolute left-2 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full flex items-center justify-center text-lg transition-opacity hover:opacity-100 opacity-70"
style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
aria-label="Previous"
>
</button>
)}
{onNext && (
<button
onClick={(e) => { e.stopPropagation(); onNext() }}
className="absolute right-2 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full flex items-center justify-center text-lg transition-opacity hover:opacity-100 opacity-70"
style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
aria-label="Next"
>
</button>
)}
</div>
)}
</div>