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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user