diff --git a/docker/ui/src/components/HeroCinemaBanner.tsx b/docker/ui/src/components/HeroCinemaBanner.tsx new file mode 100644 index 0000000..86d8b1b --- /dev/null +++ b/docker/ui/src/components/HeroCinemaBanner.tsx @@ -0,0 +1,151 @@ +import { Disc, Play, Sparkles, Film, Shield, Zap, CheckCircle } from 'lucide-react' +import { RadialProgress } from './ui/RadialProgress' +import { TypeBadge, StatusBadge } from './ui/Badge' +import { Button } from './ui/Button' + +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 +} + +interface HeroCinemaBannerProps { + aktiverJob?: Job | null + onDetailClick?: (jobId: string) => void + onCancelClick?: (jobId: string) => void + totalJobsCount: number + completedCount: number +} + +export function HeroCinemaBanner({ + aktiverJob, + onDetailClick, + onCancelClick, + totalJobsCount, + completedCount, +}: HeroCinemaBannerProps) { + if (aktiverJob) { + const isTranscoding = aktiverJob.status === 'transcoding' + const statusText = isTranscoding ? 'HandBrake Kompression' : 'MakeMKV Extraktion' + + return ( +
+ {/* Background Ambient Glow Effects */} +
+
+ +
+ {/* Left: 3D Metallic Disc Graphic */} +
+
+
+
+ +
+
+
+ NOW RIPPING +
+
+ +
+
+ + + + {statusText} + +
+ +

+ {aktiverJob.title || 'Aktiver Ripping-Prozess'} +

+ +

+ Laufwerk: {aktiverJob.device} + + Gestartet: {new Date(aktiverJob.startTime).toLocaleTimeString('de-DE')} +

+ + {/* Quality & Audio Badges Mockup */} +
+ HDR10+ + DOLBY ATMOS + LOSSLESS MKV +
+
+
+ + {/* Right: Radial Progress Ring & Controls */} +
+ + +
+ + +
+
+
+
+ ) + } + + // Idle Banner (Kein aktiver Job) + return ( +
+
+ +
+
+
+ +
+
+
+ + Ready for Disc + + System Status: Standby +
+

+ Rippy Cinema Studio +

+

+ Automatische Disc-Erkennung aktiv. Lege eine 4K UHD, Blu-ray, DVD oder CD ein. +

+
+
+ +
+
+ + Erledigte Jobs: {completedCount} +
+
+
+ + MakeMKV Lossless 1:1 +
+
+
+
+ ) +} diff --git a/docker/ui/src/components/ui/Card.tsx b/docker/ui/src/components/ui/Card.tsx index 6a2b277..d808cbc 100644 --- a/docker/ui/src/components/ui/Card.tsx +++ b/docker/ui/src/components/ui/Card.tsx @@ -11,12 +11,16 @@ export function Card({ children, className = '', glow = false, onClick }: CardPr return (
+ {/* Top subtle highlight line for 3D glass depth */} +
{children}
) @@ -32,7 +36,7 @@ export function CardHeader({ children, className = '' }: { children: ReactNode; export function CardTitle({ children, className = '' }: { children: ReactNode; className?: string }) { return ( -

+

{children}

) diff --git a/docker/ui/src/components/ui/RadialProgress.tsx b/docker/ui/src/components/ui/RadialProgress.tsx new file mode 100644 index 0000000..0b91c44 --- /dev/null +++ b/docker/ui/src/components/ui/RadialProgress.tsx @@ -0,0 +1,56 @@ +interface RadialProgressProps { + progress: number + size?: number + strokeWidth?: number + label?: string + status?: string +} + +export function RadialProgress({ progress, size = 80, strokeWidth = 7, label, status }: RadialProgressProps) { + const radius = (size - strokeWidth) / 2 + const circumference = radius * 2 * Math.PI + const strokeDashoffset = circumference - (progress / 100) * circumference + + const isComplete = progress === 100 + + return ( +
+ + {/* Background track */} + + {/* Progress track */} + + + + + + + + + +
+ + {progress}% + + {label && {label}} +
+
+ ) +} diff --git a/docker/ui/src/context/ThemeContext.tsx b/docker/ui/src/context/ThemeContext.tsx index 63c466c..9967f58 100644 --- a/docker/ui/src/context/ThemeContext.tsx +++ b/docker/ui/src/context/ThemeContext.tsx @@ -10,18 +10,18 @@ interface ThemeContextType { const ThemeContext = createContext(undefined) export function ThemeProvider({ children }: { children: ReactNode }) { - const [theme, setTheme] = useState('light') + const [theme, setTheme] = useState('dark') useEffect(() => { - const savedTheme = localStorage.getItem('theme') - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const savedTheme = localStorage.getItem('theme') as Theme | null + const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches - if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { - setTheme('dark') - document.documentElement.classList.add('dark') - } else { + if (savedTheme === 'light' || (!savedTheme && prefersLight)) { setTheme('light') document.documentElement.classList.remove('dark') + } else { + setTheme('dark') + document.documentElement.classList.add('dark') } }, []) diff --git a/docker/ui/src/index.css b/docker/ui/src/index.css index 284ec59..5af6763 100644 --- a/docker/ui/src/index.css +++ b/docker/ui/src/index.css @@ -4,7 +4,11 @@ @layer base { body { - @apply m-0 bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 font-sans antialiased selection:bg-amber-500/30 selection:text-amber-400; + @apply m-0 bg-slate-950 text-slate-100 font-sans antialiased selection:bg-amber-500/30 selection:text-amber-300; + background-image: + radial-gradient(ellipse at 10% 0%, rgba(99, 102, 241, 0.08) 0%, transparent 50%), + radial-gradient(ellipse at 90% 100%, rgba(245, 158, 11, 0.06) 0%, transparent 50%); + background-attachment: fixed; } } @@ -35,5 +39,45 @@ } .animate-disc-spin { - animation: disc-spin 14s linear infinite; + animation: disc-spin 12s linear infinite; +} + +@keyframes pulse-slow { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 0.9; } +} + +.animate-pulse-slow { + animation: pulse-slow 3s ease-in-out infinite; +} + +/* Custom Glass & Shimmer Effects */ +.glass-panel { + background: rgba(15, 23, 42, 0.75); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.glass-panel-amber { + background: linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(15, 23, 42, 0.85) 100%); + backdrop-filter: blur(16px); + border: 1px solid rgba(245, 158, 11, 0.25); + box-shadow: 0 0 35px -5px rgba(245, 158, 11, 0.15); +} + +/* Metallic Disc Texture Gradient */ +.disc-texture { + background: conic-gradient( + from 0deg, + #1e293b 0deg, + #475569 45deg, + #1e293b 90deg, + #64748b 135deg, + #1e293b 180deg, + #475569 225deg, + #1e293b 270deg, + #64748b 315deg, + #1e293b 360deg + ); } diff --git a/docker/ui/src/pages/Dashboard.tsx b/docker/ui/src/pages/Dashboard.tsx index 685b42a..587418d 100644 --- a/docker/ui/src/pages/Dashboard.tsx +++ b/docker/ui/src/pages/Dashboard.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download, FileSpreadsheet, HardDrive, Trash2 } from 'lucide-react' +import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download, FileSpreadsheet, HardDrive, Trash2, LayoutGrid, List, RotateCcw } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' import { PageHeader } from '../components/ui/PageHeader' @@ -10,8 +10,20 @@ import ConfirmDialog from '../components/ConfirmDialog' import DeviceDiscovery from '../components/DeviceDiscovery' import JobDetailModal from '../components/JobDetailModal' import LiveLogSection from '../components/LiveLogSection' +import { HeroCinemaBanner } from '../components/HeroCinemaBanner' +import { RadialProgress } from '../components/ui/RadialProgress' import { ENCODER_BADGES } from '../lib/design' +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' @@ -22,6 +34,13 @@ interface Job { 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/w342${p}` } async function fetchJobs(): Promise { @@ -33,32 +52,6 @@ async function fetchJobs(): Promise { } } -function ProgressBar({ progress }: { progress: number }) { - return ( -
-
- Fortschritt - {progress}% -
-
-
-
-
- ) -} - -interface WorkerInfo { - name: string - encoders: string[] -} - export default function Dashboard() { const [jobs, setJobs] = useState([]) const [workers, setWorkers] = useState([]) @@ -66,6 +59,7 @@ export default function Dashboard() { const [loading, setLoading] = useState(true) const [detailJobId, setDetailJobId] = useState(null) const [aufraeumenOffen, setAufraeumenOffen] = useState(false) + const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid') const { toast } = useToast() const jobLoeschen = async (job: Job) => { @@ -126,70 +120,19 @@ export default function Dashboard() {
- {/* Cinematic Spotlight Card für aktiven Rip/Transcode */} - {aktiverJob && ( - -
-
-
-
- -
-
- - - - -
+ {/* Hero Cinema Banner (3D Spinning Disc, Live Progress & Showcase) */} + setDetailJobId(id)} + onCancelClick={id => api.post(`/jobs/${id}/cancel`).catch(() => {})} + totalJobsCount={jobs.length} + completedCount={stats.completed} + /> -
-
- - {aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Rippen'} aktiv - - -
-

- {aktiverJob.title || 'Aktiver Job'} -

-

- Gerät: {aktiverJob.device} · Gestartet: {new Date(aktiverJob.startTime).toLocaleTimeString('de-DE')} -

- -
-
- {aktiverJob.status === 'transcoding' ? 'HandBrake Kompression' : 'MakeMKV Extraktion'} - {aktiverJob.progress}% -
-
-
-
-
-
- -
- - -
-
- - )} - - {/* System-Telemetrie Leiste */} + {/* System Telemetrie Leiste */}
@@ -244,7 +187,7 @@ export default function Dashboard() { - {/* Laufwerke & Erkennung */} + {/* Laufwerke & Disc-Erkennung */} {/* Stats Cards */} @@ -275,17 +218,46 @@ export default function Dashboard() { />
- {/* Recent Jobs Table */} + {/* Recent Jobs: Choice between Visual Poster Grid & Classic Table */} - Neueste Jobs
- {jobs.length} Jobs insgesamt + Neueste Jobs + ({jobs.length} insgesamt) +
+ +
+ {/* View Mode Toggle Button */} +
+ + +
+ {jobs.length > 0 && ( @@ -297,38 +269,129 @@ export default function Dashboard() { variant="secondary" size="sm" onClick={() => setAufraeumenOffen(true)} - title="Alle fertigen und fehlgeschlagenen Jobs aus der Liste entfernen" + title="Erledigte Jobs aufräumen" > - Erledigte aufräumen + Aufräumen )}
-
- - - - - - - - - - - - - - {jobs.length === 0 ? ( + {jobs.length === 0 ? ( +
+
+ +
+
Noch keine Jobs
+

Lege eine CD, DVD oder Blu-ray ein, um zu starten

+
+ ) : viewMode === 'grid' ? ( + /* Visual Poster Cards Grid View */ +
+ {jobs.slice(0, 9).map(job => { + const poster = posterUrl(job.meta) + return ( +
+
+ {poster ? ( + {job.title + ) : ( +
+ +
+ )} + +
+
+
+ + +
+ +

+ Laufwerk: {job.device} +

+
+ +
+ + + {new Date(job.startTime).toLocaleTimeString('de-DE')} + +
+
+
+ +
+ + +
+ {job.status === 'completed' && ( + + )} + {job.status === 'failed' && job.can_retry && ( + + )} + {(job.status === 'completed' || job.status === 'failed') && ( + + )} + {(job.status === 'pending' || job.status === 'processing' || job.status === 'transcoding') && ( + + )} +
+
+
+ ) + })} +
+ ) : ( + /* Table View */ +
+
TypTitelStatusGerätFortschrittStartzeitAktion
+ - + + + + + + + - ) : ( - jobs.slice(0, 10).map(job => ( + + + {jobs.slice(0, 10).map(job => ( - )) - )} - -
-
Noch keine Jobs
-

Füge eine CD, DVD oder Blu-ray hinzu, um zu starten

-
TypTitelStatusGerätFortschrittStartzeitAktion
@@ -336,7 +399,6 @@ export default function Dashboard() { - + {new Date(job.startTime).toLocaleString('de-DE')} {job.status === 'completed' && ( - - )} - {job.status === 'failed' && job.can_retry && ( - )} {(job.status === 'completed' || job.status === 'failed') && ( )} - {(job.status === 'pending' || job.status === 'processing' || job.status === 'transcoding') && ( - - )}
-
+ ))} + + +
+ )}
@@ -434,7 +465,7 @@ function StatCard({ icon: Icon, label, value, badgeStyle }: { icon: any, label:

{label}

-

{value}

+

{value}