139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Search, Loader2, Film } from 'lucide-react'
|
|
import { api } from '../lib/api'
|
|
import { Modal } from './ui/Modal'
|
|
import { Button } from './ui/Button'
|
|
import { Input } from './ui/Input'
|
|
|
|
interface SuchKandidat {
|
|
title: string
|
|
year?: number
|
|
poster?: string
|
|
overview?: string
|
|
type: string
|
|
source: string
|
|
id?: string
|
|
}
|
|
|
|
interface Props {
|
|
devicePath: string
|
|
vorschlag?: string
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
onUebernommen: () => void
|
|
}
|
|
|
|
export default function MetadataKorrektur({ devicePath, vorschlag, isOpen, onClose, onUebernommen }: Props) {
|
|
const [suchbegriff, setSuchbegriff] = useState('')
|
|
const [kandidaten, setKandidaten] = useState<SuchKandidat[]>([])
|
|
const [laeuft, setLaeuft] = useState(false)
|
|
const [fehler, setFehler] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setSuchbegriff(vorschlag || '')
|
|
setKandidaten([])
|
|
setFehler(null)
|
|
}
|
|
}, [isOpen, vorschlag])
|
|
|
|
const suchen = async () => {
|
|
if (!suchbegriff) return
|
|
setLaeuft(true)
|
|
setFehler(null)
|
|
try {
|
|
const r = await api.get('/metadata/search', { params: { q: suchbegriff } })
|
|
setKandidaten(r.data)
|
|
if (r.data.length === 0) setFehler('Nichts gefunden — anderen Suchbegriff probieren.')
|
|
} catch {
|
|
setFehler('Suche fehlgeschlagen.')
|
|
} finally {
|
|
setLaeuft(false)
|
|
}
|
|
}
|
|
|
|
const waehlen = async (k: SuchKandidat) => {
|
|
try {
|
|
await api.post('/metadata/override', {
|
|
device_path: devicePath,
|
|
title: k.title,
|
|
year: k.year,
|
|
poster: k.poster,
|
|
overview: k.overview,
|
|
type: k.type,
|
|
source: k.source,
|
|
id: k.id,
|
|
})
|
|
onUebernommen()
|
|
onClose()
|
|
} catch {
|
|
setFehler('Übernehmen fehlgeschlagen.')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title="Richtigen Titel wählen"
|
|
maxWidth="2xl"
|
|
>
|
|
<div className="space-y-4">
|
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
Deine Wahl gilt für diese Disc — auch beim nächsten Einlegen.
|
|
</p>
|
|
|
|
<div className="flex gap-3 items-end">
|
|
<div className="flex-1">
|
|
<Input
|
|
value={suchbegriff}
|
|
onChange={e => setSuchbegriff(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && suchen()}
|
|
autoFocus
|
|
placeholder="Titel eingeben…"
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="amber"
|
|
onClick={suchen}
|
|
disabled={!suchbegriff || laeuft}
|
|
>
|
|
{laeuft ? <Loader2 className="animate-spin" size={18} /> : <Search size={18} />}
|
|
Suchen
|
|
</Button>
|
|
</div>
|
|
|
|
{fehler && <p className="text-sm text-rose-500 font-medium">{fehler}</p>}
|
|
|
|
{kandidaten.length > 0 && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3.5 pt-2 max-h-[60vh] overflow-y-auto">
|
|
{kandidaten.map((k, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => waehlen(k)}
|
|
className="text-left rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/80 overflow-hidden transition-all hover:border-amber-500/50 hover:shadow-lg hover:shadow-amber-500/10 group"
|
|
>
|
|
{k.poster ? (
|
|
<img src={k.poster} alt={k.title} className="w-full h-40 object-cover group-hover:scale-105 transition-transform duration-300" />
|
|
) : (
|
|
<div className="w-full h-40 flex items-center justify-center bg-slate-200 dark:bg-slate-900 text-slate-400 dark:text-slate-600">
|
|
<Film size={32} />
|
|
</div>
|
|
)}
|
|
<div className="p-2.5">
|
|
<p className="text-xs font-semibold leading-tight line-clamp-2 text-slate-900 dark:text-slate-200 group-hover:text-amber-500 dark:group-hover:text-amber-400 transition-colors">
|
|
{k.title}
|
|
</p>
|
|
<p className="text-[11px] mt-1 text-slate-500 dark:text-slate-400">
|
|
{k.year || '—'} · {k.type === 'tv' ? 'Serie' : 'Film'} · {k.source}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|