This repository has been archived on 2026-06-15. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MediaLore/src/components/mixed/VideoPlayerModal.tsx
Garret Patti 5ba73b2e56 doom scroll and viewer improvements
- move play/pause to clicking the video directly; remove dedicated button
- replace emoji mute icons with flat minimal SVGs
- add view-in-library button in doom scroll that navigates to the file's
  directory and opens it in the regular viewer
- add display text overlay button in doom scroll and image lightbox;
  shows extracted text (translated by default when available) in a
  semi-transparent box at the bottom; toggle between translated/original
- hide tag panel by default in image lightbox and video player modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 08:16:34 -04:00

217 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useRef, useState } from 'react'
import TagSelector from '@/components/tags/TagSelector'
import { useUserSettings } from '@/hooks/useUserSettings'
interface Props {
url: string
name: string
onClose: () => void
onPrev?: () => void
onNext?: () => void
itemKey?: string
onTagsChanged?: () => void
onAiTag?: () => Promise<void>
context?: 'mixed' | 'movies' | 'tv'
}
export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, itemKey, onTagsChanged, onAiTag, 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
const muted = context === 'mixed' ? settings.mixedMuted : context === 'movies' ? settings.moviesMuted : settings.tvMuted
const overlayRef = useRef<HTMLDivElement>(null)
const [showTags, setShowTags] = useState(false)
const [aiTagging, setAiTagging] = useState(false)
const [aiTagError, setAiTagError] = useState<string | null>(null)
const [tagRefreshKey, setTagRefreshKey] = useState(0)
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'
return () => {
document.removeEventListener('keydown', handleKey)
document.body.style.overflow = ''
}
}, [onClose, onPrev, onNext])
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === overlayRef.current) onClose()
}
return (
<div
ref={overlayRef}
className="fixed inset-0 z-50 flex flex-col items-center p-4 gap-3 overflow-hidden max-h-screen"
style={{ backgroundColor: 'rgba(0,0,0,0.9)', height: '100vh', maxHeight: '100vh' }}
onClick={handleOverlayClick}
>
{/* Toolbar */}
<div className={`flex items-center justify-between w-full flex-shrink-0 ${showTags ? '' : 'max-w-4xl'}`}>
<span className="text-sm truncate max-w-[80%]" style={{ color: 'var(--text-secondary)' }}>
{name}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
{itemKey && (
<button
onClick={(e) => { e.stopPropagation(); setShowTags((v) => !v) }}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm transition-colors"
style={{
backgroundColor: showTags ? 'var(--accent)' : 'var(--surface)',
color: showTags ? '#fff' : 'var(--text-primary)',
}}
onMouseEnter={(e) => {
if (!showTags) (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)'
}}
onMouseLeave={(e) => {
if (!showTags) (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface)'
}}
aria-label={showTags ? 'Hide tags' : 'Show tags'}
title="Tags"
>
🏷
</button>
)}
{onAiTag && (
<button
onClick={async (e) => {
e.stopPropagation()
setAiTagging(true)
setAiTagError(null)
try {
await onAiTag()
setTagRefreshKey((k) => k + 1)
onTagsChanged?.()
} catch (err) {
setAiTagError(err instanceof Error ? err.message : 'AI tagging failed')
setTimeout(() => setAiTagError(null), 4000)
} finally {
setAiTagging(false)
}
}}
disabled={aiTagging}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm transition-colors disabled:opacity-50"
style={{
backgroundColor: aiTagError ? '#7f1d1d' : 'var(--surface)',
color: aiTagError ? '#fca5a5' : 'var(--text-primary)',
}}
onMouseEnter={(e) => {
if (!aiTagging && !aiTagError) (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)'
}}
onMouseLeave={(e) => {
if (!aiTagError) (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface)'
}}
aria-label="AI Tag this video"
title={aiTagError ?? (aiTagging ? 'Tagging…' : 'AI Tag')}
>
{aiTagging ? (
<span className="animate-spin" style={{ display: 'inline-block' }}></span>
) : '✨'}
</button>
)}
<button
onClick={onClose}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm flex-shrink-0 transition-colors"
style={{ backgroundColor: 'var(--surface)', color: 'var(--text-primary)' }}
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)')}
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface)')}
aria-label="Close"
>
</button>
</div>
</div>
{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 relative">
<video
key={url}
src={url}
controls
autoPlay={autoPlay}
muted={muted}
loop={loop}
className="w-full h-full object-contain rounded-lg"
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
className="w-80 h-full max-h-full flex-shrink-0 rounded-xl overflow-y-auto p-4"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
onClick={(e) => e.stopPropagation()}
>
<p className="text-xs font-semibold uppercase tracking-wider mb-3" style={{ color: 'var(--text-secondary)' }}>
Tags
</p>
<TagSelector itemKey={itemKey!} onTagsChanged={onTagsChanged} refreshKey={tagRefreshKey} />
</div>
</div>
) : (
<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}
muted={muted}
loop={loop}
className="w-full h-full max-w-4xl object-contain rounded-lg"
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>
)
}