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:
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
|
||||
import type { Movie } from '@/types'
|
||||
import MovieDetailModal from './MovieDetailModal'
|
||||
import FilterPanel from '@/components/FilterPanel'
|
||||
import DoomScrollView, { type DoomScrollItem } from '@/components/DoomScrollView'
|
||||
|
||||
interface Props {
|
||||
libraryId: string
|
||||
@@ -13,12 +14,14 @@ export default function MoviesView({ libraryId }: Props) {
|
||||
const [movies, setMovies] = useState<Movie[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selected, setSelected] = useState<Movie | null>(null)
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
||||
const [search, setSearch] = useState('')
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
||||
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
||||
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
||||
const [showFilters, setShowFilters] = useState(true)
|
||||
const [doomScrollActive, setDoomScrollActive] = useState(false)
|
||||
const [doomScrollItems, setDoomScrollItems] = useState<DoomScrollItem[]>([])
|
||||
|
||||
const toggleTag = (tagId: string) =>
|
||||
setSelectedTagIds((prev) => {
|
||||
@@ -60,15 +63,36 @@ export default function MoviesView({ libraryId }: Props) {
|
||||
return true
|
||||
})
|
||||
|
||||
const selected = selectedIndex !== null ? filtered[selectedIndex] ?? null : null
|
||||
|
||||
const handleDeleted = (movieId: string) => {
|
||||
setSelected(null)
|
||||
setSelectedIndex(null)
|
||||
setMovies((prev) => prev.filter((m) => m.id !== movieId))
|
||||
}
|
||||
|
||||
const filtersActive = search !== '' || selectedTagIds.size > 0
|
||||
|
||||
const handleDoomScroll = () => {
|
||||
// Use filtered movies — respects any active search/tag filters automatically
|
||||
const items: DoomScrollItem[] = filtered.map((m) => ({
|
||||
url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(m.videoPath)}`,
|
||||
name: m.title,
|
||||
mediaType: 'video' as const,
|
||||
}))
|
||||
setDoomScrollItems(items)
|
||||
setDoomScrollActive(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{doomScrollActive && doomScrollItems.length > 0 && (
|
||||
<DoomScrollView
|
||||
items={doomScrollItems}
|
||||
videoContext="movies"
|
||||
onClose={() => setDoomScrollActive(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setShowFilters((v) => !v)}
|
||||
@@ -82,6 +106,19 @@ export default function MoviesView({ libraryId }: Props) {
|
||||
>
|
||||
Filters{filtersActive ? ' ●' : ''}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDoomScroll}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface)',
|
||||
color: 'var(--text-secondary)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)' }}
|
||||
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface)' }}
|
||||
>
|
||||
Doom Scroll
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-6 md:items-start">
|
||||
{showFilters && (
|
||||
@@ -111,10 +148,10 @@ export default function MoviesView({ libraryId }: Props) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{filtered.map((movie) => (
|
||||
{filtered.map((movie, idx) => (
|
||||
<button
|
||||
key={movie.id}
|
||||
onClick={() => setSelected(movie)}
|
||||
onClick={() => setSelectedIndex(idx)}
|
||||
className="group text-left rounded-xl overflow-hidden border transition-all focus:outline-none focus-visible:ring-2"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)' }}
|
||||
onMouseEnter={(e) => {
|
||||
@@ -159,11 +196,13 @@ export default function MoviesView({ libraryId }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected && (
|
||||
{selected && selectedIndex !== null && (
|
||||
<MovieDetailModal
|
||||
movie={selected}
|
||||
libraryId={libraryId}
|
||||
onClose={() => setSelected(null)}
|
||||
onClose={() => setSelectedIndex(null)}
|
||||
onPrev={selectedIndex > 0 ? () => setSelectedIndex((i) => (i !== null ? i - 1 : null)) : undefined}
|
||||
onNext={selectedIndex < filtered.length - 1 ? () => setSelectedIndex((i) => (i !== null ? i + 1 : null)) : undefined}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments() }}
|
||||
onDeleted={handleDeleted}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user