fix tv view
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { Movie } from '@/types'
|
||||
import TagSelector from '@/components/tags/TagSelector'
|
||||
import AssignedTagBadges from '@/components/tags/AssignedTagBadges'
|
||||
import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
|
||||
|
||||
interface Props {
|
||||
@@ -32,6 +33,10 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
|
||||
const [renameName, setRenameName] = useState('')
|
||||
const [renameError, setRenameError] = useState<string | null>(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) => {
|
||||
@@ -41,6 +46,7 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
|
||||
if (warnRefresh) { setWarnRefresh(false); return }
|
||||
if (editing) { setEditing(false); return }
|
||||
if (renaming) { setRenaming(false); return }
|
||||
if (showTagPanel) { setShowTagPanel(false); return }
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
@@ -50,7 +56,7 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
|
||||
document.removeEventListener('keydown', handleKey)
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [onClose, menuOpen, confirming, editing, warnRefresh, renaming])
|
||||
}, [onClose, menuOpen, confirming, editing, warnRefresh, renaming, showTagPanel])
|
||||
|
||||
// Close menu on outside click
|
||||
useEffect(() => {
|
||||
@@ -132,7 +138,6 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
|
||||
|
||||
const handleStartRename = () => {
|
||||
setMenuOpen(false)
|
||||
// movie.id is the encoded folder name
|
||||
setRenameName(decodeURIComponent(movie.id))
|
||||
setRenameError(null)
|
||||
setRenaming(true)
|
||||
@@ -187,339 +192,423 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
|
||||
return (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.75)' }}
|
||||
className="fixed inset-0 z-50 overflow-hidden"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.75)', height: '100vh' }}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div
|
||||
className="relative w-full max-w-lg rounded-2xl overflow-hidden shadow-2xl"
|
||||
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-3 right-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="Close"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
{/* Outer flex — row on md+, col on mobile when panel open */}
|
||||
<div className={`flex h-full w-full ${showTagPanel ? 'flex-col md:flex-row' : ''}`}>
|
||||
|
||||
{/* 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"
|
||||
{/* ── Left pane — relative container for floating controls ── */}
|
||||
<div className="flex-1 min-h-0 min-w-0 relative" onClick={() => onClose()}>
|
||||
{/* Scrollable card area */}
|
||||
<div className="h-full overflow-y-auto flex items-start justify-center p-4">
|
||||
<div
|
||||
className="w-full max-w-lg rounded-2xl overflow-hidden shadow-2xl"
|
||||
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
‹
|
||||
</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 ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={heroUrl}
|
||||
alt={movie.title}
|
||||
className="w-full object-cover max-h-64"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-40 flex items-center justify-center text-5xl">🎬</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Hero image */}
|
||||
<div className="w-full" style={{ backgroundColor: 'var(--border)' }}>
|
||||
{heroUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={heroUrl}
|
||||
alt={movie.title}
|
||||
className="w-full object-cover max-h-64"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-40 flex items-center justify-center text-5xl">🎬</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="p-5">
|
||||
{/* Title row with kebab menu */}
|
||||
<div className="flex items-start gap-2 mb-1">
|
||||
<h2 className="text-lg font-semibold flex-1 min-w-0" style={{ color: 'var(--text-primary)' }}>
|
||||
{movie.title}
|
||||
</h2>
|
||||
{movie.year && (
|
||||
<span className="text-sm flex-shrink-0 mt-0.5" style={{ color: 'var(--text-secondary)' }}>
|
||||
{movie.year}
|
||||
</span>
|
||||
)}
|
||||
{/* Kebab menu */}
|
||||
<div className="relative flex-shrink-0" ref={menuRef}>
|
||||
<button
|
||||
onClick={() => { setMenuOpen((o) => !o); setConfirming(false) }}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'transparent' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
aria-label="More options"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
{menuOpen && (
|
||||
{/* Info */}
|
||||
<div className="p-5">
|
||||
{/* Title row with kebab menu */}
|
||||
<div className="flex items-start gap-2 mb-1">
|
||||
<h2 className="text-lg font-semibold flex-1 min-w-0" style={{ color: 'var(--text-primary)' }}>
|
||||
{movie.title}
|
||||
</h2>
|
||||
{movie.year && (
|
||||
<span className="text-sm flex-shrink-0 mt-0.5" style={{ color: 'var(--text-secondary)' }}>
|
||||
{movie.year}
|
||||
</span>
|
||||
)}
|
||||
{/* Kebab menu */}
|
||||
<div className="relative flex-shrink-0" ref={menuRef}>
|
||||
<button
|
||||
onClick={() => { setMenuOpen((o) => !o); setConfirming(false) }}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'transparent' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
aria-label="More options"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
{menuOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-1 rounded-lg shadow-lg overflow-hidden z-20 min-w-max"
|
||||
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
|
||||
>
|
||||
<button
|
||||
onClick={handleRefreshMetadata}
|
||||
disabled={refreshing}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors disabled:opacity-50"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
{refreshing ? 'Refreshing…' : 'Refresh metadata'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStartEditing}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
Edit metadata
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStartRename}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
Rename folder
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setMenuOpen(false); setConfirming(true) }}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: '#fca5a5' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
Delete movie
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rename inline input */}
|
||||
{renaming && (
|
||||
<div className="flex flex-col gap-2 mb-3">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={renameName}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<button
|
||||
onClick={() => setRenaming(false)}
|
||||
className="px-2 py-1.5 rounded-lg text-sm transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRename}
|
||||
disabled={renameSaving}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: 'var(--accent)', color: '#fff' }}
|
||||
>
|
||||
{renameSaving ? '…' : 'Rename'}
|
||||
</button>
|
||||
</div>
|
||||
{renameError && <p className="text-xs" style={{ color: '#fca5a5' }}>{renameError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editing ? (
|
||||
<div className="flex flex-col gap-3 mb-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.title}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Year</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.year}
|
||||
onChange={(e) => 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)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Plot</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={editForm.plot}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, plot: e.target.value }))}
|
||||
className="w-full px-3 py-1.5 rounded-lg text-sm resize-none"
|
||||
style={{ backgroundColor: 'var(--border)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Genres (comma-separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.genres}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, genres: 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)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
onClick={() => setEditing(false)}
|
||||
className="px-3 py-1.5 rounded-lg text-sm transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveMetadata}
|
||||
disabled={saving}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: 'var(--accent)', color: '#fff' }}
|
||||
>
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Meta row */}
|
||||
{(movie.rating !== null || movie.runtime !== null || movie.genres.length > 0) && (
|
||||
<div className="flex flex-wrap items-center gap-2 mb-3">
|
||||
{movie.rating !== null && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded" style={{ backgroundColor: 'var(--border)', color: 'var(--text-secondary)' }}>
|
||||
★ {movie.rating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
{movie.runtime !== null && (
|
||||
<span className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{movie.runtime} min
|
||||
</span>
|
||||
)}
|
||||
{movie.genres.map((g) => (
|
||||
<span key={g} className="text-xs px-1.5 py-0.5 rounded" style={{ backgroundColor: 'var(--border)', color: 'var(--text-secondary)' }}>
|
||||
{g}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{movie.plot && (
|
||||
<p className="text-sm mb-4 line-clamp-4" style={{ color: 'var(--text-secondary)' }}>
|
||||
{movie.plot}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* NFO refresh warning */}
|
||||
{warnRefresh && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-1 rounded-lg shadow-lg overflow-hidden z-20 min-w-max"
|
||||
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
|
||||
className="flex items-center gap-3 mb-4 px-3 py-2.5 rounded-lg text-sm"
|
||||
style={{ backgroundColor: '#78350f33', border: '1px solid #78350f' }}
|
||||
>
|
||||
<p className="flex-1 text-xs" style={{ color: '#fbbf24' }}>
|
||||
Refreshing from NFO will overwrite your manual edits.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleRefreshMetadata}
|
||||
disabled={refreshing}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors disabled:opacity-50"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
onClick={() => setWarnRefresh(false)}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
>
|
||||
{refreshing ? 'Refreshing…' : 'Refresh metadata'}
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStartEditing}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
onClick={doRefreshMetadata}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors"
|
||||
style={{ backgroundColor: '#78350f', color: '#fbbf24' }}
|
||||
>
|
||||
Edit metadata
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStartRename}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
Rename folder
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setMenuOpen(false); setConfirming(true) }}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: '#fca5a5' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
Delete movie
|
||||
Overwrite
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rename inline input */}
|
||||
{renaming && (
|
||||
<div className="flex flex-col gap-2 mb-3">
|
||||
{/* Confirmation banner */}
|
||||
{confirming && (
|
||||
<div
|
||||
className="flex items-center gap-3 mb-4 px-3 py-2.5 rounded-lg text-sm"
|
||||
style={{ backgroundColor: '#7f1d1d33', border: '1px solid #7f1d1d' }}
|
||||
>
|
||||
<p className="flex-1 text-xs" style={{ color: '#fca5a5' }}>
|
||||
Permanently delete this movie and all its files?
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setConfirming(false)}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-primary)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-secondary)')}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={deleting}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: '#7f1d1d', color: '#fca5a5' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = '#991b1b')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = '#7f1d1d')}
|
||||
>
|
||||
{deleting ? 'Deleting…' : 'Yes, delete'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assigned tags (read-only) above action buttons */}
|
||||
{movie.item_key && (
|
||||
<div className="mb-3">
|
||||
<AssignedTagBadges itemKey={movie.item_key} refreshKey={tagRefreshKey} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons row: Play + Download */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={renameName}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<button
|
||||
onClick={() => setRenaming(false)}
|
||||
className="px-2 py-1.5 rounded-lg text-sm transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRename}
|
||||
disabled={renameSaving}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
||||
onClick={() => setPlaying(true)}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-colors"
|
||||
style={{ backgroundColor: 'var(--accent)', color: '#fff' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--accent-hover)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--accent)')}
|
||||
>
|
||||
{renameSaving ? '…' : 'Rename'}
|
||||
<span>▶</span>
|
||||
Play
|
||||
</button>
|
||||
</div>
|
||||
{renameError && <p className="text-xs" style={{ color: '#fca5a5' }}>{renameError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editing ? (
|
||||
<div className="flex flex-col gap-3 mb-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.title}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Year</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.year}
|
||||
onChange={(e) => 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)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Plot</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={editForm.plot}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, plot: e.target.value }))}
|
||||
className="w-full px-3 py-1.5 rounded-lg text-sm resize-none"
|
||||
style={{ backgroundColor: 'var(--border)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium mb-1 block" style={{ color: 'var(--text-secondary)' }}>Genres (comma-separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.genres}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, genres: 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)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
onClick={() => setEditing(false)}
|
||||
className="px-3 py-1.5 rounded-lg text-sm transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
<a
|
||||
href={videoUrl}
|
||||
download
|
||||
className="flex items-center justify-center px-3 py-2.5 rounded-lg text-sm font-medium transition-colors"
|
||||
style={{ backgroundColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Download"
|
||||
aria-label="Download"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveMetadata}
|
||||
disabled={saving}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: 'var(--accent)', color: '#fff' }}
|
||||
>
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
↓
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Meta row */}
|
||||
{(movie.rating !== null || movie.runtime !== null || movie.genres.length > 0) && (
|
||||
<div className="flex flex-wrap items-center gap-2 mb-3">
|
||||
{movie.rating !== null && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded" style={{ backgroundColor: 'var(--border)', color: 'var(--text-secondary)' }}>
|
||||
★ {movie.rating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
{movie.runtime !== null && (
|
||||
<span className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{movie.runtime} min
|
||||
</span>
|
||||
)}
|
||||
{movie.genres.map((g) => (
|
||||
<span key={g} className="text-xs px-1.5 py-0.5 rounded" style={{ backgroundColor: 'var(--border)', color: 'var(--text-secondary)' }}>
|
||||
{g}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{movie.plot && (
|
||||
<p className="text-sm mb-4 line-clamp-4" style={{ color: 'var(--text-secondary)' }}>
|
||||
{movie.plot}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* NFO refresh warning */}
|
||||
{warnRefresh && (
|
||||
<div
|
||||
className="flex items-center gap-3 mb-4 px-3 py-2.5 rounded-lg text-sm"
|
||||
style={{ backgroundColor: '#78350f33', border: '1px solid #78350f' }}
|
||||
>
|
||||
<p className="flex-1 text-xs" style={{ color: '#fbbf24' }}>
|
||||
Refreshing from NFO will overwrite your manual edits.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setWarnRefresh(false)}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={doRefreshMetadata}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors"
|
||||
style={{ backgroundColor: '#78350f', color: '#fbbf24' }}
|
||||
>
|
||||
Overwrite
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation banner */}
|
||||
{confirming && (
|
||||
<div
|
||||
className="flex items-center gap-3 mb-4 px-3 py-2.5 rounded-lg text-sm"
|
||||
style={{ backgroundColor: '#7f1d1d33', border: '1px solid #7f1d1d' }}
|
||||
>
|
||||
<p className="flex-1 text-xs" style={{ color: '#fca5a5' }}>
|
||||
Permanently delete this movie and all its files?
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setConfirming(false)}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-primary)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-secondary)')}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={deleting}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: '#7f1d1d', color: '#fca5a5' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = '#991b1b')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = '#7f1d1d')}
|
||||
>
|
||||
{deleting ? 'Deleting…' : 'Yes, delete'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Play button */}
|
||||
<button
|
||||
onClick={() => setPlaying(true)}
|
||||
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-lg font-medium text-sm transition-colors"
|
||||
style={{ backgroundColor: 'var(--accent)', color: '#fff' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--accent-hover)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--accent)')}
|
||||
>
|
||||
<span>▶</span>
|
||||
Play
|
||||
</button>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="mt-4 pt-4" style={{ borderTop: '1px solid var(--border)' }}>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--text-secondary)' }}>
|
||||
Tags
|
||||
</p>
|
||||
<TagSelector itemKey={movie.item_key!} onTagsChanged={onTagsChanged} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating controls — tag + close */}
|
||||
<div className="absolute top-4 right-4 z-10 flex items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
{movie.item_key && !showTagPanel && (
|
||||
<button
|
||||
onClick={() => setShowTagPanel(true)}
|
||||
className={smallBtn}
|
||||
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="Show tags"
|
||||
title="Tags"
|
||||
>
|
||||
🏷
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={smallBtn}
|
||||
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>
|
||||
|
||||
{/* Prev / Next */}
|
||||
{onPrev && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onPrev() }}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 z-10 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 z-10 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 — bottom half on mobile, right sidebar on desktop ── */}
|
||||
{showTagPanel && (
|
||||
<div
|
||||
className="flex-shrink-0 flex flex-col overflow-hidden w-full max-h-[50vh] md:w-80 md:max-h-none md:h-full"
|
||||
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Panel header — ‹ hide | ✕ close */}
|
||||
<div className="flex items-center justify-between p-4 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setShowTagPanel(false)}
|
||||
className={smallBtn}
|
||||
style={{ backgroundColor: 'var(--border)', color: 'var(--text-secondary)', fontSize: '0.85rem' }}
|
||||
onMouseEnter={(e) => (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--text-secondary)'}
|
||||
onMouseLeave={(e) => (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)'}
|
||||
aria-label="Hide panel"
|
||||
title="Hide panel"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={smallBtn}
|
||||
style={{ backgroundColor: 'var(--border)', color: 'var(--text-secondary)', fontSize: '0.85rem' }}
|
||||
onMouseEnter={(e) => (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--text-secondary)'}
|
||||
onMouseLeave={(e) => (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)'}
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="overflow-y-auto flex-1 min-h-0 px-4 pb-4" style={{ borderTop: '1px solid var(--border)' }}>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider mt-4 mb-3" style={{ color: 'var(--text-secondary)' }}>
|
||||
Tags
|
||||
</p>
|
||||
<TagSelector
|
||||
itemKey={movie.item_key!}
|
||||
onTagsChanged={() => { setTagRefreshKey((k) => k + 1); onTagsChanged?.() }}
|
||||
refreshKey={tagRefreshKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user