fix(ui): Zentrierung fuer Anleitung & Einstellungen gefixt, zufaellige Mockup-Filme durch echte API-Daten ersetzt
Ampel / ampel (push) Successful in 28s

This commit is contained in:
Hitonabi
2026-07-24 16:31:45 +02:00
parent 0f20d9d8f4
commit 9bc8ad11b2
5 changed files with 262 additions and 160 deletions
+182 -59
View File
@@ -1,78 +1,201 @@
import { Disc } from 'lucide-react'
import { Disc, Film, Sparkles, CheckCircle, Shield, Zap } from 'lucide-react'
import { RadialProgress } from './ui/RadialProgress'
import { TypeBadge, StatusBadge } from './ui/Badge'
import { Button } from './ui/Button'
interface JobMeta {
year?: number
overview?: string
poster_path?: string
genres?: string[]
runtime?: number
type?: string
source?: string
}
interface Job {
id: string
type: 'cd' | 'dvd' | 'bluray' | 'uhd'
status: 'pending' | 'processing' | 'transcoding' | 'canceling' | 'completed' | 'failed'
device: string
startTime: string
endTime?: string
progress: number
title?: string
can_retry?: boolean
meta?: JobMeta | null
}
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/w500${p}`
}
interface HeroCinemaBannerProps {
aktiverJob?: Job | null
completedJobs?: Job[]
onDetailClick?: (jobId: string) => void
onCancelClick?: (jobId: string) => void
}
export function HeroCinemaBanner({
aktiverJob,
completedJobs = [],
onDetailClick,
onCancelClick,
}: HeroCinemaBannerProps) {
// Fall 1: Aktiver Job (Rippt oder Komprimiert)
if (aktiverJob) {
const isTranscoding = aktiverJob.status === 'transcoding'
const statusText = isTranscoding ? 'HandBrake Kompression' : 'MakeMKV Extraktion'
const poster = posterUrl(aktiverJob.meta)
export function HeroCinemaBanner() {
return (
<div className="relative rounded-3xl overflow-hidden border border-amber-500/30 bg-[#0e1322] shadow-[0_0_40px_-5px_rgba(245,158,11,0.18)] mb-8 p-3">
{/* Top Ambient Glow */}
<div className="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-amber-500/40 to-transparent pointer-events-none" />
{/* 5-Panel Movie Carousel Wall (Matching SC1 Mockup 1:1) */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 h-44 md:h-52">
{/* Panel 1 */}
<div className="relative rounded-2xl overflow-hidden group cursor-pointer border border-slate-800 hover:border-amber-500/50 transition-all">
<img
src="https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=800&auto=format&fit=crop"
alt="Sci-Fi"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 brightness-90"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent opacity-80" />
<div className="absolute bottom-3 left-3 right-3 text-xs font-bold text-slate-100 tracking-tight drop-shadow">
Sci-Fi Cinema
<div className="relative rounded-3xl overflow-hidden border-2 border-amber-500/50 bg-gradient-to-r from-slate-950 via-slate-900 to-indigo-950 p-6 md:p-8 shadow-[0_0_50px_-10px_rgba(245,158,11,0.3)] text-slate-100 mb-8">
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-8">
<div className="flex items-center gap-6">
<div className="relative flex-shrink-0">
{poster ? (
<div className="relative">
<img src={poster} alt="" className="w-28 h-40 rounded-2xl object-cover shadow-2xl border-2 border-amber-500/40" />
<div className="absolute -bottom-3 -right-3 w-14 h-14 rounded-full disc-texture shadow-xl flex items-center justify-center animate-disc-spin border border-amber-400">
<Disc size={20} className="text-amber-400" />
</div>
</div>
{/* Panel 2 */}
<div className="relative rounded-2xl overflow-hidden group cursor-pointer border border-slate-800 hover:border-amber-500/50 transition-all">
<img
src="https://images.unsplash.com/photo-1578632767115-351597cf2477?q=80&w=800&auto=format&fit=crop"
alt="Blade Runner 2049"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 brightness-90"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent opacity-80" />
<div className="absolute bottom-3 left-3 right-3 text-xs font-bold text-amber-300 tracking-tight drop-shadow">
Blade Runner 2049
</div>
</div>
{/* Center Panel 3: Spinning Optical Disc Showcase */}
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-slate-900 via-[#131929] to-slate-950 border-2 border-amber-500/40 flex items-center justify-center p-4 shadow-inner group">
<div className="w-32 h-32 md:w-36 md:h-36 rounded-full disc-texture shadow-2xl shadow-black flex items-center justify-center p-1.5 animate-disc-spin border-2 border-amber-400/60 group-hover:border-amber-300 transition-colors">
) : (
<div className="w-28 h-28 rounded-full disc-texture shadow-2xl flex items-center justify-center p-1.5 animate-disc-spin border-2 border-amber-500/60">
<div className="w-full h-full rounded-full border-2 border-dashed border-white/40 flex items-center justify-center bg-slate-950/70">
<Disc size={44} className="text-amber-400 drop-shadow-[0_0_16px_rgba(245,158,11,0.9)]" />
<Disc size={40} className="text-amber-400 drop-shadow-[0_0_15px_rgba(245,158,11,0.8)]" />
</div>
</div>
<div className="absolute bottom-2.5 px-3 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-widest bg-amber-500 text-slate-950 shadow-md">
DISC ENGINE
)}
</div>
<div className="space-y-2 text-center sm:text-left">
<div className="flex items-center justify-center sm:justify-start gap-2 flex-wrap">
<span className="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-amber-500 text-slate-950 shadow-md">
NOW RIPPING
</span>
<TypeBadge type={aktiverJob.type} />
<StatusBadge status={aktiverJob.status} />
</div>
<h2 className="text-2xl md:text-3xl font-extrabold tracking-tight bg-gradient-to-r from-white via-slate-100 to-amber-200 bg-clip-text text-transparent">
{aktiverJob.title || 'Aktiver Ripping-Prozess'}
</h2>
<p className="text-xs text-slate-400 font-mono">
Laufwerk: <strong className="text-amber-400">{aktiverJob.device}</strong> · {statusText}
</p>
</div>
</div>
{/* Panel 4 */}
<div className="relative rounded-2xl overflow-hidden group cursor-pointer border border-slate-800 hover:border-amber-500/50 transition-all hidden md:block">
<div className="flex flex-col sm:flex-row items-center gap-6 flex-shrink-0 w-full lg:w-auto justify-end">
<RadialProgress progress={aktiverJob.progress} size={96} strokeWidth={8} label="PROGRESS" />
<div className="flex flex-col gap-2 w-full sm:w-auto">
<Button variant="amber" size="md" onClick={() => onDetailClick && onDetailClick(aktiverJob.id)}>
<Sparkles size={16} /> Details
</Button>
<Button variant="danger" size="md" onClick={() => onCancelClick && onCancelClick(aktiverJob.id)}>
Abbrechen
</Button>
</div>
</div>
</div>
</div>
)
}
// Fall 2: Keine echten gerippten Medien vorhanden -> Standby Hero Banner (Keine Fake-Filme!)
if (completedJobs.length === 0) {
return (
<div className="relative rounded-3xl overflow-hidden border border-amber-500/30 bg-gradient-to-r from-slate-950 via-[#0d121f] to-slate-950 p-6 md:p-8 shadow-[0_0_35px_-5px_rgba(245,158,11,0.15)] text-slate-100 mb-8">
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-5">
<div className="w-24 h-24 rounded-full disc-texture shadow-2xl flex items-center justify-center p-1 animate-disc-spin border-2 border-amber-500/40 flex-shrink-0">
<div className="w-full h-full rounded-full border-2 border-dashed border-white/30 flex items-center justify-center bg-slate-950/70">
<Disc size={36} className="text-amber-400" />
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider bg-emerald-500/15 text-emerald-400 border border-emerald-500/30 flex items-center gap-1">
<CheckCircle size={11} /> READY FOR DISC
</span>
<span className="text-xs text-slate-400 font-mono">System Status: Bereit</span>
</div>
<h2 className="text-2xl md:text-3xl font-extrabold tracking-tight bg-gradient-to-r from-white via-slate-100 to-amber-200 bg-clip-text text-transparent">
Rippy Cinema Studio
</h2>
<p className="text-xs text-slate-400 mt-1">
Automatische Disc-Erkennung aktiv. Lege eine 4K UHD, Blu-ray, DVD oder CD in dein Laufwerk ein.
</p>
</div>
</div>
<div className="flex items-center gap-4 text-xs font-medium text-slate-300 bg-slate-900/90 px-4 py-3 rounded-2xl border border-slate-800 flex-shrink-0">
<div className="flex items-center gap-2">
<Zap size={16} className="text-amber-400" />
<span>Gerippte Discs: <strong className="text-amber-400 font-mono text-sm">0</strong></span>
</div>
<div className="h-4 w-px bg-slate-800" />
<div className="flex items-center gap-2">
<Shield size={16} className="text-indigo-400" />
<span>MakeMKV 1:1 Lossless</span>
</div>
</div>
</div>
</div>
)
}
// Fall 3: Echte gerippte Titel vorhanden -> Zeige ECHTE gerippte Discs in der Galerie (Maximal 5 echte Jobs)
const displayJobs = completedJobs.slice(0, 5)
return (
<div className="relative rounded-3xl overflow-hidden border border-amber-500/30 bg-[#0e1322] shadow-[0_0_40px_-5px_rgba(245,158,11,0.18)] mb-8 p-4">
<div className="flex items-center justify-between px-2 mb-3">
<h3 className="text-xs font-bold uppercase tracking-wider text-amber-400 flex items-center gap-2">
<Film size={14} /> ECHTE GERIPPTE MEDIEN ({completedJobs.length})
</h3>
<span className="text-[11px] text-slate-400 font-mono">Aus deiner Rippy-Datenbank</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
{displayJobs.map((job) => {
const poster = posterUrl(job.meta)
return (
<div
key={job.id}
onClick={() => onDetailClick && onDetailClick(job.id)}
className="relative rounded-2xl overflow-hidden group cursor-pointer border border-slate-800 hover:border-amber-500/50 transition-all bg-slate-900 aspect-[2/3] flex flex-col justify-end p-3"
>
{poster ? (
<img
src="https://images.unsplash.com/photo-1518709268805-4e9042af9f23?q=80&w=800&auto=format&fit=crop"
alt="Dune"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 brightness-90"
src={poster}
alt={job.title || ''}
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent opacity-80" />
<div className="absolute bottom-3 left-3 right-3 text-xs font-bold text-slate-100 tracking-tight drop-shadow">
Dune Part II
</div>
) : (
<div className="absolute inset-0 bg-gradient-to-br from-slate-800 via-slate-900 to-slate-950 flex flex-col items-center justify-center p-3 text-center">
<Disc size={32} className="text-amber-400 mb-2" />
<span className="text-[10px] font-bold text-slate-400 uppercase">{job.type}</span>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/40 to-transparent" />
{/* Panel 5 */}
<div className="relative rounded-2xl overflow-hidden group cursor-pointer border border-slate-800 hover:border-amber-500/50 transition-all hidden md:block">
<img
src="https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=800&auto=format&fit=crop"
alt="Interstellar"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 brightness-90"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent opacity-80" />
<div className="absolute bottom-3 left-3 right-3 text-xs font-bold text-slate-100 tracking-tight drop-shadow">
Interstellar
<div className="relative z-10 space-y-1">
<TypeBadge type={job.type} />
<h4 className="text-xs font-bold text-slate-100 truncate group-hover:text-amber-400 transition-colors">
{job.title || 'Unbekannt'}
</h4>
</div>
</div>
)
})}
</div>
</div>
)
+1 -1
View File
@@ -23,7 +23,7 @@ export default function AnleitungPage() {
const fett = "font-semibold text-slate-900 dark:text-slate-100"
return (
<div className="space-y-6 max-w-4xl">
<div className="space-y-6 max-w-4xl mx-auto">
<PageHeader
title="Anleitung & Hilfe"
subtitle="So funktioniert Rippy — vom Einlegen der Disc bis zum fertigen Film in deinem Media-Server"
+53 -74
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Cpu, HardDrive, Disc, Download, Trash2, RotateCcw, Activity } from 'lucide-react'
import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film } from 'lucide-react'
import { api } from '../lib/api'
import { useToast } from '../context/ToastContext'
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
@@ -93,17 +93,10 @@ export default function Dashboard() {
return () => clearInterval(interval)
}, [])
const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding') || jobs[0] || null
const queueJobs = jobs.filter(j => j.id !== aktiverJob?.id && j.status === 'pending')
const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding')
const queueJobs = jobs.filter(j => j.status === 'pending')
const completedJobs = jobs.filter(j => j.status === 'completed')
// Sample data fallback for Recently Ripped if list is short to match SC1 visual richness
const recentlyRippedList = completedJobs.length > 0 ? completedJobs.slice(0, 3) : [
{ id: 'sample-1', title: 'Everything Everywhere All At Once', year: 2021, poster: 'https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=400&auto=format&fit=crop' },
{ id: 'sample-2', title: 'Top Gun: Maverick', year: 2023, poster: 'https://images.unsplash.com/photo-1517604931442-7e0c8ed2963c?q=80&w=400&auto=format&fit=crop' },
{ id: 'sample-3', title: 'Black Panther: Wakanda Forever', year: 2022, poster: 'https://images.unsplash.com/photo-1626814026160-2237a95fc5a0?q=80&w=400&auto=format&fit=crop' },
]
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
@@ -115,10 +108,15 @@ export default function Dashboard() {
return (
<div className="space-y-6">
{/* 1. Top Hero Movie Wall Carousel (Identisch zu SC1 Top Banner) */}
<HeroCinemaBanner />
{/* 1. Top Hero Cinema Showcase (Nur echte API Daten!) */}
<HeroCinemaBanner
aktiverJob={aktiverJob}
completedJobs={completedJobs}
onDetailClick={id => setDetailJobId(id)}
onCancelClick={id => api.post(`/jobs/${id}/cancel`).catch(() => {})}
/>
{/* 2. Main 2-Column Grid Layout (Matching SC1 1:1) */}
{/* 2. Main 2-Column Grid Layout */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* LEFT COLUMN (lg:col-span-7) */}
@@ -128,14 +126,13 @@ export default function Dashboard() {
<Card className="border border-amber-500/30 bg-[#0f1422] shadow-xl">
<CardHeader className="border-b border-slate-800">
<CardTitle className="text-amber-400 flex items-center gap-2">
<Disc size={18} className="text-amber-400 animate-disc-spin" />
<Disc size={18} className={aktiverJob ? 'text-amber-400 animate-disc-spin' : 'text-slate-500'} />
Current Job
</CardTitle>
</CardHeader>
<CardContent className="p-5">
{aktiverJob ? (
<div className="flex items-start gap-4">
{/* Poster Thumbnail */}
{posterUrl(aktiverJob.meta) ? (
<img
src={posterUrl(aktiverJob.meta)!}
@@ -156,7 +153,6 @@ export default function Dashboard() {
<TypeBadge type={aktiverJob.type} />
</div>
{/* Amber Progress Bar */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs text-slate-400 font-mono">
<span>Status: {aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Extraktion'}</span>
@@ -171,8 +167,8 @@ export default function Dashboard() {
</div>
<div className="text-xs text-slate-400 space-y-0.5 font-mono pt-1">
<p>Ripping status: {aktiverJob.status === 'transcoding' ? 'HandBrake H.265' : 'MakeMKV Lossless Extraktion'}</p>
<p>Disc type: <strong className="text-amber-300">{aktiverJob.type.toUpperCase()} Blu-ray</strong></p>
<p>Status: {aktiverJob.status === 'transcoding' ? 'HandBrake H.265' : 'MakeMKV Lossless Extraktion'}</p>
<p>Disc typ: <strong className="text-amber-300">{aktiverJob.type.toUpperCase()}</strong></p>
<p>Gerät: {aktiverJob.device} · Gestartet: {new Date(aktiverJob.startTime).toLocaleTimeString('de-DE')}</p>
</div>
</div>
@@ -185,51 +181,25 @@ export default function Dashboard() {
</CardContent>
</Card>
{/* Ripping Queue Card */}
{/* Ripping Queue Card (NUR ECHTE QUEUED JOBS) */}
<Card className="border border-slate-800 bg-[#0f1422]">
<CardHeader className="border-b border-slate-800">
<CardTitle className="text-slate-200">Ripping Queue</CardTitle>
<CardTitle className="text-slate-200">Ripping Queue ({queueJobs.length})</CardTitle>
</CardHeader>
<CardContent className="p-4 space-y-3">
{queueJobs.length === 0 ? (
<div className="space-y-2">
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
<div className="flex items-center gap-3">
<span className="font-mono text-amber-400 font-bold">2.</span>
<span className="font-medium text-slate-300">The Menu</span>
</div>
<span className="text-xs text-slate-500 font-mono">Queue 1</span>
</div>
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
<div className="flex items-center gap-3">
<span className="font-mono text-amber-400 font-bold">3.</span>
<span className="font-medium text-slate-300">Glass Onion</span>
</div>
<span className="text-xs text-slate-500 font-mono">Queue 2</span>
</div>
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
<div className="flex items-center gap-3">
<span className="font-mono text-amber-400 font-bold">4.</span>
<span className="font-medium text-slate-300">Babylon</span>
</div>
<span className="text-xs text-slate-500 font-mono">Queue 3</span>
</div>
<div className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
<div className="flex items-center gap-3">
<span className="font-mono text-amber-400 font-bold">5.</span>
<span className="font-medium text-slate-300">Avatar: The Way of Water</span>
</div>
<span className="text-xs text-slate-500 font-mono">Queue 4</span>
</div>
<div className="text-center py-8 text-slate-500 text-xs font-mono">
Warteschlange ist leer (0 Jobs eingereiht)
</div>
) : (
queueJobs.map((j, idx) => (
<div key={j.id} className="flex items-center justify-between p-3 rounded-xl bg-slate-900/60 border border-slate-800 text-sm">
<div className="flex items-center gap-3">
<span className="font-mono text-amber-400 font-bold">{idx + 2}.</span>
<span className="font-mono text-amber-400 font-bold">{idx + 1}.</span>
<span className="font-medium text-slate-200">{j.title || 'Unbekannt'}</span>
<TypeBadge type={j.type} />
</div>
<span className="text-xs text-slate-400 font-mono">Queue {idx + 1}</span>
<span className="text-xs text-slate-400 font-mono">Wartend</span>
</div>
))
)}
@@ -241,48 +211,45 @@ export default function Dashboard() {
{/* RIGHT COLUMN (lg:col-span-5) */}
<div className="lg:col-span-5 space-y-6">
{/* Server Status Card with Live Sparkline Graph */}
{/* Server Status Card mit Live Sparkline Graph */}
<Card className="border border-slate-800 bg-[#0f1422]">
<CardHeader className="border-b border-slate-800">
<CardTitle className="text-slate-200">Server Status</CardTitle>
</CardHeader>
<CardContent className="p-5">
<div className="flex flex-col sm:flex-row items-center justify-between gap-6">
{/* Left: Usage Bars */}
<div className="space-y-4 flex-1 w-full">
<div>
<div className="flex items-center justify-between text-xs font-mono mb-1">
<span className="text-slate-400 flex items-center gap-1.5"><Cpu size={14} className="text-amber-400" /> CPU Usage:</span>
<span className="text-amber-400 font-bold">42%</span>
<span className="text-amber-400 font-bold">Aktiv</span>
</div>
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
<div className="h-full bg-amber-500 rounded-full w-[42%]" />
<div className="h-full bg-amber-500 rounded-full w-[35%]" />
</div>
</div>
<div>
<div className="flex items-center justify-between text-xs font-mono mb-1">
<span className="text-slate-400 flex items-center gap-1.5"><Activity size={14} className="text-amber-400" /> RAM Usage:</span>
<span className="text-slate-200">6.1GB / 18GB</span>
<span className="text-slate-400 flex items-center gap-1.5"><Activity size={14} className="text-amber-400" /> Status:</span>
<span className="text-slate-200">Bereit</span>
</div>
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
<div className="h-full bg-amber-500/80 rounded-full w-[34%]" />
<div className="h-full bg-amber-500/80 rounded-full w-[20%]" />
</div>
</div>
<div>
<div className="flex items-center justify-between text-xs font-mono mb-1">
<span className="text-slate-400 flex items-center gap-1.5"><HardDrive size={14} className="text-amber-400" /> Storage Used:</span>
<span className="text-slate-200">2.1TB / 8TB</span>
<span className="text-slate-400 flex items-center gap-1.5"><HardDrive size={14} className="text-amber-400" /> Speichermounts:</span>
<span className="text-slate-200">{plaetze.length ? `${plaetze[0].frei_gb} GB frei` : 'Aktiv'}</span>
</div>
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
<div className="h-full bg-amber-500/70 rounded-full w-[26%]" />
<div className="h-full bg-amber-500/70 rounded-full w-[50%]" />
</div>
</div>
</div>
{/* Right: Live Amber Sparkline Graph */}
<div className="w-full sm:w-36 h-24 flex-shrink-0 flex items-end justify-center pt-2">
<svg viewBox="0 0 100 50" className="w-full h-full stroke-amber-400 fill-amber-500/10">
<path
@@ -295,43 +262,55 @@ export default function Dashboard() {
</CardContent>
</Card>
{/* Recently Ripped Card (3 Poster Grid matching SC1) */}
{/* Recently Ripped Card (NUR ECHTE GERIPPTE JOBS) */}
<Card className="border border-slate-800 bg-[#0f1422]">
<CardHeader className="border-b border-slate-800">
<CardTitle className="text-slate-200">Recently Ripped</CardTitle>
<CardHeader className="border-b border-slate-800 flex items-center justify-between">
<CardTitle className="text-slate-200">Zuletzt gerippt ({completedJobs.length})</CardTitle>
{completedJobs.length > 0 && (
<Button variant="ghost" size="sm" onClick={() => setAufraeumenOffen(true)}>
<Trash2 size={13} /> Aufräumen
</Button>
)}
</CardHeader>
<CardContent className="p-4">
{completedJobs.length === 0 ? (
<div className="text-center py-8 text-slate-500 text-xs font-mono">
Noch keine gerippten Medien in der Datenbank
</div>
) : (
<div className="grid grid-cols-3 gap-3">
{recentlyRippedList.map((item: any) => {
const poster = item.poster || posterUrl(item.meta)
{completedJobs.slice(0, 3).map((item) => {
const poster = posterUrl(item.meta)
return (
<div key={item.id} className="group relative rounded-xl overflow-hidden bg-slate-900 border border-slate-800 flex flex-col justify-between p-2">
<div className="relative aspect-[2/3] rounded-lg overflow-hidden mb-2 bg-slate-800">
{poster ? (
<img src={poster} alt={item.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
<img src={poster} alt={item.title || ''} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
) : (
<div className="w-full h-full flex items-center justify-center text-slate-500">
<Disc size={24} />
<div className="w-full h-full flex flex-col items-center justify-center text-slate-500 p-2 text-center">
<Film size={24} className="text-amber-400 mb-1" />
<span className="text-[9px] uppercase font-bold text-slate-400">{item.type}</span>
</div>
)}
</div>
<div className="space-y-1 text-left">
<h4 className="text-xs font-bold text-slate-100 truncate" title={item.title}>{item.title}</h4>
<p className="text-[10px] text-slate-400 font-mono">{item.year || 2023}</p>
<h4 className="text-xs font-bold text-slate-100 truncate" title={item.title}>{item.title || 'Unbekannt'}</h4>
<p className="text-[10px] text-slate-400 font-mono">{new Date(item.startTime).toLocaleDateString('de-DE')}</p>
<Button
variant="amber"
size="sm"
className="w-full text-[10px] py-1 h-auto"
onClick={() => setDetailJobId(item.id)}
>
View
Details
</Button>
</div>
</div>
)
})}
</div>
)}
</CardContent>
</Card>
+1 -1
View File
@@ -82,7 +82,7 @@ export default function LogsPage() {
}
return (
<div className="space-y-6">
<div className="space-y-6 max-w-5xl mx-auto">
<PageHeader
title="System-Logs"
subtitle="Alle relevanten Ereignisse und Protokolle des Ripping-Systems auf einen Blick"
+1 -1
View File
@@ -198,7 +198,7 @@ export default function SettingsPage() {
]
return (
<div className="max-w-4xl space-y-6">
<div className="max-w-4xl mx-auto space-y-6">
<PageHeader
title="Einstellungen"
subtitle="Konfiguriere Ripping-Parameter, Speicherziele, APIs und Benachrichtigungen"