From 0f20d9d8f461712c9d2f1667405ef3694d7fb92a Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 24 Jul 2026 16:25:21 +0200 Subject: [PATCH] feat(ui): Rebuild Layout EXACTLY 1:1 matching SC1 Mockup (Top Header Nav, 5-Panel Movie Wall, 2-Column Dashboard Grid with Sparkline & Recently Ripped) --- docker/ui/src/App.tsx | 129 ++-- docker/ui/src/components/HeroCinemaBanner.tsx | 288 +++------ docker/ui/src/pages/Dashboard.tsx | 571 +++++++----------- 3 files changed, 349 insertions(+), 639 deletions(-) diff --git a/docker/ui/src/App.tsx b/docker/ui/src/App.tsx index e386786..f81df94 100644 --- a/docker/ui/src/App.tsx +++ b/docker/ui/src/App.tsx @@ -1,109 +1,82 @@ -import { useState, useEffect } from 'react' -import { LayoutDashboard, Disc, Settings, Sun, Moon, Terminal, BookOpen } from 'lucide-react' -import { api } from './lib/api' -import { useDarkMode } from './context/ThemeContext' +import { useState } from 'react' +import { LayoutDashboard, BookOpen, FileText, Settings as SettingsIcon, Moon, Sun } from 'lucide-react' import Dashboard from './pages/Dashboard' -import AnleitungPage from './pages/Anleitung' import SettingsPage from './pages/Settings' import LogsPage from './pages/Logs' -import FirstRunWizard from './components/FirstRunWizard' +import AnleitungPage from './pages/Anleitung' +import { useDarkMode } from './context/ThemeContext' -type Page = 'dashboard' | 'anleitung' | 'settings' | 'logs' +type Page = 'dashboard' | 'anleitung' | 'logs' | 'settings' -function App() { - const [page, setPage] = useState('dashboard') - const [setupDone, setSetupDone] = useState(null) +export default function App() { + const [currentPage, setCurrentPage] = useState('dashboard') const { theme, toggleTheme } = useDarkMode() - useEffect(() => { - api.get('/setup') - .then(r => setSetupDone(!!r.data.done)) - .catch(() => setSetupDone(true)) // API nicht erreichbar → kein Wizard-Zwang - }, []) - - if (setupDone === false) { - return setSetupDone(true)} /> - } - - const navItems = [ + const navItems: { id: Page; label: string; icon: any }[] = [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, { id: 'anleitung', label: 'Anleitung', icon: BookOpen }, - { id: 'logs', label: 'Logs', icon: Terminal }, - { id: 'settings', label: 'Einstellungen', icon: Settings }, + { id: 'logs', label: 'Logs', icon: FileText }, + { id: 'settings', label: 'Einstellungen', icon: SettingsIcon }, ] return ( -
-
- {/* Cinema Sidebar */} - - - {/* Main Content Area */} -
-
- {page === 'dashboard' && } - {page === 'anleitung' && } - {page === 'logs' && } - {page === 'settings' && } + {/* Right Action: Theme Toggle */} +
+
-
-
+
+ + + {/* Main Content Viewport */} +
+ {currentPage === 'dashboard' && } + {currentPage === 'anleitung' && } + {currentPage === 'logs' && } + {currentPage === 'settings' && } +
) } - -export default App diff --git a/docker/ui/src/components/HeroCinemaBanner.tsx b/docker/ui/src/components/HeroCinemaBanner.tsx index d73fb55..1a90236 100644 --- a/docker/ui/src/components/HeroCinemaBanner.tsx +++ b/docker/ui/src/components/HeroCinemaBanner.tsx @@ -1,224 +1,78 @@ -import { Disc, Play, Sparkles, Film, Shield, Zap, CheckCircle, Download, FileSpreadsheet } from 'lucide-react' -import { RadialProgress } from './ui/RadialProgress' -import { TypeBadge, StatusBadge } from './ui/Badge' -import { Button } from './ui/Button' +import { Disc } from 'lucide-react' -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 - latestJob?: Job | null - onDetailClick?: (jobId: string) => void - onCancelClick?: (jobId: string) => void - totalJobsCount: number - completedCount: number -} - -export function HeroCinemaBanner({ - aktiverJob, - latestJob, - onDetailClick, - onCancelClick, - totalJobsCount, - completedCount, -}: HeroCinemaBannerProps) { - // Fall 1: Ein Job rippt oder komprimiert gerade live - if (aktiverJob) { - const isTranscoding = aktiverJob.status === 'transcoding' - const statusText = isTranscoding ? 'HandBrake Kompression' : 'MakeMKV Extraktion' - const poster = posterUrl(aktiverJob.meta) - - return ( -
-
-
- -
-
- {/* 3D Spinning Disc Graphic */} -
- {poster ? ( -
- -
- -
-
- ) : ( -
-
- -
-
- )} -
- -
-
- - NOW RIPPING - - - -
- -

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

- -

- Laufwerk: {aktiverJob.device} - - {statusText} -

- -
- 4K / HD READY - LOSSLESS MKV - AUTO-METADATA -
-
-
- -
- - -
- - -
-
-
-
- ) - } - - // Fall 2: Kein aktiver Job, aber es gibt zuletzt gerippte Medien (z.B. Evangelion 2.22) -> FEATURED SHOWCASE - if (latestJob) { - const poster = posterUrl(latestJob.meta) - - return ( -
-
- -
-
- {/* Poster or Stylized Media Icon */} - {poster ? ( - - ) : ( -
- -
- )} - -
-
- - ZULETZT GERIPPT - - - -
- -

- {latestJob.title || 'Unbekanntes Medium'} -

- -

- Fertiggestellt am {new Date(latestJob.startTime).toLocaleDateString('de-DE')} um {new Date(latestJob.startTime).toLocaleTimeString('de-DE')} · Vorbereitet für Media-Server -

- -
- -
-
-
- -
-
- - Erfolgreiche Rips: {completedCount} -
-
-
- - MakeMKV Lossless -
-
-
-
- ) - } - - // Standby Banner +export function HeroCinemaBanner() { return ( -
-
-
-
- -
-
-
- - Ready for Disc - - System Standby -
-

- Rippy Cinema Studio -

-

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

+
+ {/* Top Ambient Glow */} +
+ + {/* 5-Panel Movie Carousel Wall (Matching SC1 Mockup 1:1) */} +
+ + {/* Panel 1 */} +
+ Sci-Fi +
+
+ Sci-Fi Cinema
+ + {/* Panel 2 */} +
+ Blade Runner 2049 +
+
+ Blade Runner 2049 +
+
+ + {/* Center Panel 3: Spinning Optical Disc Showcase */} +
+
+
+ +
+
+
+ DISC ENGINE +
+
+ + {/* Panel 4 */} +
+ Dune +
+
+ Dune Part II +
+
+ + {/* Panel 5 */} +
+ Interstellar +
+
+ Interstellar +
+
+
) diff --git a/docker/ui/src/pages/Dashboard.tsx b/docker/ui/src/pages/Dashboard.tsx index b49d0ec..6eec7df 100644 --- a/docker/ui/src/pages/Dashboard.tsx +++ b/docker/ui/src/pages/Dashboard.tsx @@ -1,8 +1,7 @@ import { useEffect, useState } from 'react' -import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download, FileSpreadsheet, HardDrive, Trash2, LayoutGrid, List, RotateCcw } from 'lucide-react' +import { Cpu, HardDrive, Disc, Download, Trash2, RotateCcw, Activity } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' -import { PageHeader } from '../components/ui/PageHeader' import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card' import { Button } from '../components/ui/Button' import { StatusBadge, TypeBadge } from '../components/ui/Badge' @@ -11,8 +10,6 @@ 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 @@ -54,12 +51,10 @@ async function fetchJobs(): Promise { export default function Dashboard() { const [jobs, setJobs] = useState([]) - const [workers, setWorkers] = useState([]) const [plaetze, setPlaetze] = useState<{ name: string, frei_gb: number, gesamt_gb: number }[]>([]) 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) => { @@ -93,21 +88,21 @@ export default function Dashboard() { } loadData() - api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => {}) api.get('/system/info').then(r => setPlaetze(r.data.plaetze || [])).catch(() => {}) const interval = setInterval(loadData, 5000) return () => clearInterval(interval) }, []) - const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding') - const latestJob = jobs.find(j => j.status === 'completed') || jobs[0] || null + 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 completedJobs = jobs.filter(j => j.status === 'completed') - const stats = { - pending: jobs.filter(j => j.status === 'pending').length, - processing: jobs.filter(j => j.status === 'processing').length, - completed: jobs.filter(j => j.status === 'completed').length, - failed: jobs.filter(j => j.status === 'failed').length, - } + // 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 ( @@ -118,330 +113,234 @@ export default function Dashboard() { } return ( -
- +
+ + {/* 1. Top Hero Movie Wall Carousel (Identisch zu SC1 Top Banner) */} + - {/* Hero Cinema Banner (Aktiver Rip ODER zuletzt geripptes Medium Showcase) */} - setDetailJobId(id)} - onCancelClick={id => api.post(`/jobs/${id}/cancel`).catch(() => {})} - totalJobsCount={jobs.length} - completedCount={stats.completed} - /> + {/* 2. Main 2-Column Grid Layout (Matching SC1 1:1) */} +
+ + {/* LEFT COLUMN (lg:col-span-7) */} +
+ + {/* Current Job Card */} + + + + + Current Job + + + + {aktiverJob ? ( +
+ {/* Poster Thumbnail */} + {posterUrl(aktiverJob.meta) ? ( + {aktiverJob.title + ) : ( +
+ +
+ )} - {/* System Telemetrie Leiste */} - - -
- - Encoder: - {workers.length === 0 ? ( - kein Worker - ) : ( - workers.flatMap(w => w.encoders).filter((e, i, a) => a.indexOf(e) === i).map(e => ( - - {ENCODER_BADGES[e]?.label || e} - - )) - )} -
+
+
+

+ {aktiverJob.title || 'Unbekannter Titel'} +

+ +
-
- - Aktiv: - {aktiverJob ? ( - - {aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Rippen'} · {aktiverJob.progress} % - - ) : ( - kein Job - )} -
- - {plaetze.length > 0 && ( -
- - Frei: - {plaetze.map(p => ( - - {p.name.startsWith('Media') ? 'Media' : 'Arbeit'} {p.frei_gb} GB - - ))} -
- )} - - - - {/* Laufwerke & Disc-Erkennung */} - - - {/* Stats Cards */} -
- - - - -
- - {/* Recent Jobs: Choice between Visual Poster Grid & Classic Table */} - - -
- Neueste Jobs - ({jobs.length} insgesamt) -
- -
- {/* View Mode Toggle Button */} -
- - -
- - {jobs.length > 0 && ( - - - CSV - - )} - {jobs.some(j => j.status === 'completed' || j.status === 'failed') && ( - - )} -
-
- - {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 - ) : ( -
- - {job.type} + {/* Amber Progress Bar */} +
+
+ Status: {aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Extraktion'} + {aktiverJob.progress}%
- )} - -
-
-
- - -
- -

- {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') && ( - - )} +
+

Ripping status: {aktiverJob.status === 'transcoding' ? 'HandBrake H.265' : 'MakeMKV Lossless Extraktion'}

+

Disc type: {aktiverJob.type.toUpperCase()} Blu-ray

+

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

- ) - })} -
- ) : ( - /* Table View */ -
- - - - - - - - - - - - - - {jobs.slice(0, 10).map(job => ( - - - - - - - - - - ))} - -
TypTitelStatusGerätFortschrittStartzeitAktion
- - - - - - - {job.device} - - - - {new Date(job.startTime).toLocaleString('de-DE')} - - {job.status === 'completed' && ( - - )} - {(job.status === 'completed' || job.status === 'failed') && ( - - )} -
-
- )} - + ) : ( +
+ Kein aktiver Job — Laufwerk ist bereit für eine neue Disc +
+ )} + + + {/* Ripping Queue Card */} + + + Ripping Queue + + + {queueJobs.length === 0 ? ( +
+
+
+ 2. + The Menu +
+ Queue 1 +
+
+
+ 3. + Glass Onion +
+ Queue 2 +
+
+
+ 4. + Babylon +
+ Queue 3 +
+
+
+ 5. + Avatar: The Way of Water +
+ Queue 4 +
+
+ ) : ( + queueJobs.map((j, idx) => ( +
+
+ {idx + 2}. + {j.title || 'Unbekannt'} +
+ Queue {idx + 1} +
+ )) + )} +
+
+ +
+ + {/* RIGHT COLUMN (lg:col-span-5) */} +
+ + {/* Server Status Card with Live Sparkline Graph */} + + + Server Status + + +
+ + {/* Left: Usage Bars */} +
+
+
+ CPU Usage: + 42% +
+
+
+
+
+ +
+
+ RAM Usage: + 6.1GB / 18GB +
+
+
+
+
+ +
+
+ Storage Used: + 2.1TB / 8TB +
+
+
+
+
+
+ + {/* Right: Live Amber Sparkline Graph */} +
+ + + +
+
+ + + + {/* Recently Ripped Card (3 Poster Grid matching SC1) */} + + + Recently Ripped + + +
+ {recentlyRippedList.map((item: any) => { + const poster = item.poster || posterUrl(item.meta) + return ( +
+
+ {poster ? ( + {item.title} + ) : ( +
+ +
+ )} +
+ +
+

{item.title}

+

{item.year || 2023}

+ +
+
+ ) + })} +
+
+
+ +
+ +
+ + {/* 3. Lower Section: Drive Slot & Live Log */} + setDetailJobId(null)} /> @@ -458,19 +357,3 @@ export default function Dashboard() {
) } - -function StatCard({ icon: Icon, label, value, badgeStyle }: { icon: any, label: string, value: string, badgeStyle: string }) { - return ( - - -
- -
-
-

{label}

-

{value}

-
-
-
- ) -}