13c1ae6064
- ToastContext: jede Aktion quittiert sichtbar ('Erfolgreich aktualisiert'
ploppt oben rechts rein) — Refresh-Knoepfe drehen jetzt wirklich
(Laufwerke, Logs, Worker, Speicherziele).
- JobDetailModal: Klick auf Titel in 'Neueste Jobs' -> Poster, Jahr,
Beschreibung, Genres, Ablagepfad, Fehler.
- Logs: Filter-Pills um Warnung + Fehler ergaenzt (vorher nur
Alle/Info/Erfolg).
- Speicherziele: Benutzer/Passwort VOR dem 'Freigaben auflisten'-Knopf
(Windows/NAS lehnen Gast-Abfragen ab — genau daher kam das
NT_STATUS_ACCESS_DENIED), Hinweistext, Toasts beim Einhaengen/Entfernen.
- Datei-Browser (Speicherziele + Rip-Ziel-Dialog) zeigt Dateien grau mit
Groesse — Ordner wirkten faelschlich leer.
- Einstellungen: neuer System-Tab (Werkzeug-Versionen je Worker, Key-Status,
Plattenplatz, MakeMKV-Beta-Key-Feld), Media-Server-Wahl im Ripping-Tab,
Arbeitsverzeichnis im Verarbeitung-Tab, Benachrichtigungs-Anleitung
(Discord/ntfy/Slack) + 'Test senden'.
- First-Run-Wizard: Media-Server-Schritt (Jellyfin/Emby/Kodi/Plex/Keins).
- Favicon (Disc im Logo-Verlauf, public/favicon.svg — /vite.svg war 404)
+ Footer 'Created with (Herz) by LucyAI, Claude and KrBrZ'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
174 lines
7.6 KiB
TypeScript
174 lines
7.6 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { X, Film, Loader2, FolderOpen, AlertCircle } from 'lucide-react'
|
|
import { api } from '../lib/api'
|
|
import { useDarkMode } from '../context/ThemeContext'
|
|
|
|
// Klick auf den Titel in „Neueste Jobs" (Commander-Wunsch 24.07.): sofort
|
|
// sehen, WAS da gerippt wurde — Poster, Jahr, Beschreibung — plus die
|
|
// nüchternen Job-Fakten (Ziel, Ausgabepfad, Fehler).
|
|
|
|
interface JobMeta {
|
|
year?: number
|
|
overview?: string
|
|
poster_path?: string
|
|
genres?: string[]
|
|
runtime?: number
|
|
type?: string
|
|
source?: string
|
|
}
|
|
|
|
interface JobDetail {
|
|
id: string
|
|
type: string
|
|
status: string
|
|
device: string
|
|
startTime: string
|
|
endTime?: string
|
|
progress: number
|
|
title?: string
|
|
error?: string
|
|
target_dir?: string
|
|
output_path?: string
|
|
meta?: JobMeta | null
|
|
}
|
|
|
|
// TMDB liefert relative Poster-Pfade, OMDb/Jikan absolute URLs
|
|
function posterUrl(meta?: JobMeta | null): string | null {
|
|
const p = meta?.poster_path
|
|
if (!p) return null
|
|
return p.startsWith('http') ? p : `https://image.tmdb.org/t/p/w342${p}`
|
|
}
|
|
|
|
const STATUS_LABEL: Record<string, string> = {
|
|
pending: 'Wartend',
|
|
processing: 'Rippen',
|
|
transcoding: 'Komprimieren',
|
|
canceling: 'Wird abgebrochen…',
|
|
completed: 'Fertig',
|
|
failed: 'Fehler',
|
|
}
|
|
|
|
export default function JobDetailModal({ jobId, onClose }: { jobId: string | null, onClose: () => void }) {
|
|
const [detail, setDetail] = useState<JobDetail | null>(null)
|
|
const [laedt, setLaedt] = useState(false)
|
|
const { theme } = useDarkMode()
|
|
|
|
useEffect(() => {
|
|
if (!jobId) {
|
|
setDetail(null)
|
|
return
|
|
}
|
|
setLaedt(true)
|
|
api.get(`/jobs/${jobId}/detail`)
|
|
.then(r => setDetail(r.data))
|
|
.catch(() => setDetail(null))
|
|
.finally(() => setLaedt(false))
|
|
}, [jobId])
|
|
|
|
if (!jobId) return null
|
|
|
|
const meta = detail?.meta
|
|
const poster = posterUrl(meta)
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" onClick={onClose}>
|
|
<div
|
|
className={`w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-xl shadow-2xl ${theme === 'dark' ? 'bg-slate-800' : 'bg-white'}`}
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<div className={`px-6 py-4 border-b flex items-center justify-between ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'}`}>
|
|
<h2 className={`text-lg font-bold truncate ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
|
|
{detail?.title || 'Job-Details'}
|
|
</h2>
|
|
<button onClick={onClose} className={`p-1.5 rounded flex-shrink-0 ${theme === 'dark' ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-slate-100 text-slate-500'}`}>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
{laedt ? (
|
|
<div className="flex items-center justify-center py-16">
|
|
<Loader2 className="animate-spin text-indigo-500" size={28} />
|
|
</div>
|
|
) : !detail ? (
|
|
<p className={`px-6 py-10 text-sm text-center ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
|
|
Job nicht gefunden.
|
|
</p>
|
|
) : (
|
|
<div className="p-6 space-y-5">
|
|
{/* Film/Serien-Info aus der Disc-Erkennung */}
|
|
<div className="flex gap-5">
|
|
{poster ? (
|
|
<img src={poster} alt={detail.title || ''} className="w-32 rounded-lg object-cover flex-shrink-0 shadow-lg" />
|
|
) : (
|
|
<div className={`w-32 h-48 rounded-lg flex items-center justify-center flex-shrink-0 ${theme === 'dark' ? 'bg-slate-900 text-slate-600' : 'bg-slate-100 text-slate-400'}`}>
|
|
<Film size={32} />
|
|
</div>
|
|
)}
|
|
<div className="min-w-0">
|
|
<p className={`text-xl font-bold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
|
|
{detail.title || 'Unbekannt'}
|
|
{meta?.year ? <span className={`font-normal ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}> ({meta.year})</span> : null}
|
|
</p>
|
|
<p className={`text-xs mt-1 ${theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}`}>
|
|
{meta?.type === 'tv' ? 'Serie' : 'Film'}
|
|
{meta?.source ? ` · Quelle: ${meta.source}` : ''}
|
|
{meta?.runtime ? ` · ${meta.runtime} min` : ''}
|
|
</p>
|
|
{meta?.genres && meta.genres.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
{meta.genres.map(g => (
|
|
<span key={g} className={`text-xs px-2 py-0.5 rounded-full ${theme === 'dark' ? 'bg-slate-700 text-slate-300' : 'bg-slate-100 text-slate-600'}`}>{g}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
{meta?.overview ? (
|
|
<p className={`text-sm mt-3 leading-relaxed ${theme === 'dark' ? 'text-slate-300' : 'text-slate-600'}`}>
|
|
{meta.overview}
|
|
</p>
|
|
) : (
|
|
<p className={`text-sm mt-3 italic ${theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}`}>
|
|
Keine Metadaten zu diesem Job gespeichert — die Disc wurde ohne Erkennung gerippt.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Nüchterne Job-Fakten */}
|
|
<div className={`rounded-lg p-4 grid grid-cols-[auto_1fr] gap-x-6 gap-y-1.5 text-sm ${theme === 'dark' ? 'bg-slate-900' : 'bg-slate-50'}`}>
|
|
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Status</span>
|
|
<span className={theme === 'dark' ? 'text-slate-200' : 'text-slate-700'}>
|
|
{STATUS_LABEL[detail.status] || detail.status}{detail.progress > 0 && detail.progress < 100 ? ` · ${detail.progress} %` : ''}
|
|
</span>
|
|
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Laufwerk</span>
|
|
<span className={`font-mono text-xs self-center ${theme === 'dark' ? 'text-slate-200' : 'text-slate-700'}`}>{detail.device || '—'}</span>
|
|
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Gestartet</span>
|
|
<span className={theme === 'dark' ? 'text-slate-200' : 'text-slate-700'}>
|
|
{detail.startTime ? new Date(detail.startTime).toLocaleString('de-DE') : '—'}
|
|
</span>
|
|
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Beendet</span>
|
|
<span className={theme === 'dark' ? 'text-slate-200' : 'text-slate-700'}>
|
|
{detail.endTime ? new Date(detail.endTime).toLocaleString('de-DE') : '—'}
|
|
</span>
|
|
{(detail.output_path || detail.target_dir) && (
|
|
<>
|
|
<span className={`flex items-center gap-1 ${theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}`}><FolderOpen size={13} /> Ablage</span>
|
|
<span className={`font-mono text-xs self-center break-all ${theme === 'dark' ? 'text-slate-200' : 'text-slate-700'}`}>
|
|
{detail.output_path || detail.target_dir}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{detail.error && (
|
|
<div className={`rounded-lg p-4 flex items-start gap-2.5 text-sm ${theme === 'dark' ? 'bg-rose-900/30 text-rose-300' : 'bg-rose-50 text-rose-700'}`}>
|
|
<AlertCircle size={16} className="flex-shrink-0 mt-0.5" />
|
|
<span className="break-words min-w-0">{detail.error}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|