Files
rippy/docker/ui/src/components/MetadataKorrektur.tsx
T

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>
)
}