'use client' import { useEffect, useRef, useState } from 'react' import type { Movie } from '@/types' import MediaTagPanel from '@/components/tags/MediaTagPanel' import AssignedTagBadges from '@/components/tags/AssignedTagBadges' import VideoPlayerModal from '@/components/mixed/VideoPlayerModal' interface Props { movie: Movie libraryId: string onClose: () => void onPrev?: () => void onNext?: () => void onTagsChanged?: () => void onDeleted: (movieId: string) => void onMetadataRefreshed?: () => void } export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, onNext, onTagsChanged, onDeleted, onMetadataRefreshed }: Props) { const overlayRef = useRef(null) const menuRef = useRef(null) const [playing, setPlaying] = useState(false) const [menuOpen, setMenuOpen] = useState(false) const [confirming, setConfirming] = useState(false) const [deleting, setDeleting] = useState(false) const [refreshing, setRefreshing] = useState(false) const [editing, setEditing] = useState(false) const [saving, setSaving] = useState(false) const [editForm, setEditForm] = useState({ title: '', year: '', plot: '', genres: '' }) const [warnRefresh, setWarnRefresh] = useState(false) const [renaming, setRenaming] = useState(false) const [renameName, setRenameName] = useState('') const [renameError, setRenameError] = useState(null) const [renameSaving, setRenameSaving] = useState(false) const [showTagPanel, setShowTagPanel] = useState(false) const [tagRefreshKey, setTagRefreshKey] = useState(0) const smallBtn = 'w-7 h-7 rounded-full flex items-center justify-center transition-colors flex-shrink-0' useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (menuOpen) { setMenuOpen(false); return } if (confirming) { setConfirming(false); return } if (warnRefresh) { setWarnRefresh(false); return } if (editing) { setEditing(false); return } if (renaming) { setRenaming(false); return } if (showTagPanel) { setShowTagPanel(false); return } onClose() } } document.addEventListener('keydown', handleKey) document.body.style.overflow = 'hidden' return () => { document.removeEventListener('keydown', handleKey) document.body.style.overflow = '' } }, [onClose, menuOpen, confirming, editing, warnRefresh, renaming, showTagPanel]) // Close menu on outside click useEffect(() => { if (!menuOpen) return const handler = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setMenuOpen(false) } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [menuOpen]) const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === overlayRef.current) onClose() } const videoUrl = `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(movie.videoPath)}` const handleConfirmDelete = () => { setDeleting(true) fetch(`/api/movies?libraryId=${encodeURIComponent(libraryId)}&movieId=${encodeURIComponent(movie.id)}`, { method: 'DELETE', }) .then(() => onDeleted(movie.id)) .catch(() => setDeleting(false)) } const doRefreshMetadata = () => { setRefreshing(true) setWarnRefresh(false) const itemKey = `${libraryId}:movie:${movie.id}` fetch( `/api/nfo-refresh?libraryId=${encodeURIComponent(libraryId)}&itemType=movie&itemKey=${encodeURIComponent(itemKey)}`, { method: 'POST' } ) .then(() => onMetadataRefreshed?.()) .finally(() => setRefreshing(false)) } const handleRefreshMetadata = () => { setMenuOpen(false) if (movie.manuallyEdited) { setWarnRefresh(true) } else { doRefreshMetadata() } } const handleStartEditing = () => { setMenuOpen(false) setEditForm({ title: movie.title, year: movie.year?.toString() ?? '', plot: movie.plot ?? '', genres: movie.genres.join(', '), }) setEditing(true) } const handleSaveMetadata = () => { setSaving(true) const genres = editForm.genres.split(',').map((g) => g.trim()).filter(Boolean) const yearNum = editForm.year ? parseInt(editForm.year, 10) : null fetch('/api/metadata', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ itemKey: movie.item_key, title: editForm.title, year: isNaN(yearNum as number) ? null : yearNum, plot: editForm.plot || null, genres, }), }) .then(() => { setEditing(false); onMetadataRefreshed?.() }) .finally(() => setSaving(false)) } const handleStartRename = () => { setMenuOpen(false) setRenameName(decodeURIComponent(movie.id)) setRenameError(null) setRenaming(true) } const handleRename = () => { const trimmed = renameName.trim() if (!trimmed) return setRenameSaving(true) setRenameError(null) fetch('/api/rename', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ libraryId, oldPath: decodeURIComponent(movie.id), newName: trimmed, itemType: 'movie', }), }) .then(async (res) => { if (res.status === 409) { const data = await res.json() setRenameError(data.error) return } if (!res.ok) throw new Error() setRenaming(false) onMetadataRefreshed?.() }) .catch(() => setRenameError('Rename failed')) .finally(() => setRenameSaving(false)) } if (playing) { return ( setPlaying(false)} onPrev={onPrev} onNext={onNext} context="movies" /> ) } const heroUrl = movie.backdropUrl ?? movie.posterUrl return (
{/* Outer flex — row on md+, col on mobile when panel open */}
{/* ── Left pane — relative container for floating controls ── */}
onClose()}> {/* Scrollable card area */}
e.stopPropagation()} > {/* Hero image */}
{heroUrl ? ( // eslint-disable-next-line @next/next/no-img-element {movie.title} ) : (
🎬
)}
{/* Info */}
{/* Title row with kebab menu */}

{movie.title}

{movie.year && ( {movie.year} )} {/* Kebab menu */}
{menuOpen && (
)}
{/* Rename inline input */} {renaming && (
setRenameName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setRenaming(false) }} className="flex-1 px-3 py-1.5 rounded-lg text-sm min-w-0" style={{ backgroundColor: 'var(--border)', color: 'var(--text-primary)', border: '1px solid var(--border)' }} autoFocus />
{renameError &&

{renameError}

}
)} {editing ? (
setEditForm((f) => ({ ...f, title: e.target.value }))} className="w-full px-3 py-1.5 rounded-lg text-sm" style={{ backgroundColor: 'var(--border)', color: 'var(--text-primary)', border: '1px solid var(--border)' }} autoFocus />
setEditForm((f) => ({ ...f, year: e.target.value }))} className="w-full px-3 py-1.5 rounded-lg text-sm" style={{ backgroundColor: 'var(--border)', color: 'var(--text-primary)', border: '1px solid var(--border)' }} />