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

@@ -9,11 +9,13 @@ interface Props {
movie: Movie
libraryId: string
onClose: () => void
onPrev?: () => void
onNext?: () => void
onTagsChanged?: () => void
onDeleted: (movieId: string) => void
}
export default function MovieDetailModal({ movie, libraryId, onClose, onTagsChanged, onDeleted }: Props) {
export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, onNext, onTagsChanged, onDeleted }: Props) {
const overlayRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const [playing, setPlaying] = useState(false)
@@ -72,6 +74,8 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onTagsChan
mediaKey={`${libraryId}:${movie.id}`}
onTagsChanged={onTagsChanged}
onClose={() => setPlaying(false)}
onPrev={onPrev}
onNext={onNext}
context="movies"
/>
)
@@ -102,6 +106,32 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onTagsChan
</button>
{/* Prev / Next buttons on the detail card */}
{onPrev && (
<button
onClick={onPrev}
className="absolute top-3 left-3 z-10 w-8 h-8 rounded-full flex items-center justify-center text-sm transition-colors"
style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: 'var(--text-primary)' }}
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(0,0,0,0.8)')}
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(0,0,0,0.5)')}
aria-label="Previous movie"
>
</button>
)}
{onNext && (
<button
onClick={onNext}
className="absolute top-3 z-10 w-8 h-8 rounded-full flex items-center justify-center text-sm transition-colors"
style={{ backgroundColor: 'rgba(0,0,0,0.5)', color: 'var(--text-primary)', right: onPrev ? '3rem' : undefined }}
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(0,0,0,0.8)')}
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(0,0,0,0.5)')}
aria-label="Next movie"
>
</button>
)}
{/* Hero image */}
<div className="w-full" style={{ backgroundColor: 'var(--border)' }}>
{heroUrl ? (