diff --git a/beweise/ui-kino-look.png b/beweise/ui-kino-look.png new file mode 100644 index 0000000..2941114 Binary files /dev/null and b/beweise/ui-kino-look.png differ diff --git a/rippy-windows/.gitignore b/rippy-windows/.gitignore index ef99b70..a541bc9 100644 --- a/rippy-windows/.gitignore +++ b/rippy-windows/.gitignore @@ -5,6 +5,7 @@ out/ # Setup-Bau (electron-builder) und die MITGELIEFERTEN Binaerdateien — # vendor/ wird beim Paketieren aus LOCALAPPDATA\Rippy\tools befuellt # (BAUEN.md), 74 MB gehoeren nicht ins Git. -dist-setup/ +# Auch die Ausweich-Ordner der win-unpacked-Sperre (BAUEN.md) +dist-setup*/ vendor/ .electron-cache/ diff --git a/rippy-windows/BAUEN.md b/rippy-windows/BAUEN.md index 500209a..a5fca40 100644 --- a/rippy-windows/BAUEN.md +++ b/rippy-windows/BAUEN.md @@ -59,6 +59,18 @@ Adminrechte) plus `latest.yml` und `.blockmap` — **alle drei Dateien gehören zum Release** (§ 6.8: `latest.yml` ist die Update-Auskunft, die `.blockmap` macht aus dem Voll-Download einen Differenz-Download). +⚠ **Die `win-unpacked`-Sperre (30./31.08.2026 gemessen):** Nachdem der +Paket-Smoke (`win-unpacked\Rippy.exe --smoke`) gelaufen ist, hält ein +Filter-Treiber (Virenscanner/Indexer — KEIN Prozess mit sichtbarem Pfad) +`resources\app.asar` mitunter STUNDENLANG fest. Der nächste Bau in +denselben Ordner scheitert dann mit `EBUSY … unlink app.asar`, und zwar +mit **Exit-Code 0** (electron-builder!) — nicht vom Log-Ende täuschen +lassen, die ⨯-Zeile zählt. Ausweg: in ein frisches Verzeichnis bauen +(`npx electron-builder --win --config electron-builder.yml +-c.directories.output=dist-setup-neu`), die fertige Setup-EXE nach +`dist-setup/` KOPIEREN (eine Kopie ist eine neue Datei ohne Sperre) und +den gesperrten Ordner löschen, sobald er wieder frei ist. + Vorher einmalig `vendor/` befüllen (die MITGELIEFERTEN Werkzeuge, § 9 — liegen nicht im Git, 74 MB): diff --git a/rippy-windows/src/fenster/App.tsx b/rippy-windows/src/fenster/App.tsx index d5cd2b1..da879b0 100644 --- a/rippy-windows/src/fenster/App.tsx +++ b/rippy-windows/src/fenster/App.tsx @@ -1,6 +1,8 @@ -// Die Oberfläche (W-5): Übersicht (Laufwerke + Gerüst-Status), -// Einstellungen (EINE Quelle — alles läuft über den Kern, § 6.7) und die -// Bibliothek (§ 6.6). Design-Sprache wie das heutige Rippy: dunkel, Kacheln. +// Die Oberfläche — seit dieser Runde im Kino-Look: Die Laufwerks-Kachel +// wird zur Bühne (TMDb-Backdrop als Panorama, Poster, Beschreibung), die +// Bibliothek zum Poster-Regal, der MakeMKV-Schlüssel bekommt seinen +// Update-Knopf (§ 9). Die Regeln bleiben: Übersicht/Einstellungen/ +// Bibliothek, EINE Einstellungs-Quelle über den Kern (§ 6.7), R2/R4. import { useEffect, useState } from 'react' import type { DiscInfoStand, GeraeteInfo, HauptStatus, RipStatus } from '../gemeinsam/nachrichten' import { presetEmpfehlung } from '../gemeinsam/preset-empfehlung' @@ -8,9 +10,11 @@ import { aufVerbindung, auswerfen, einstellungSetzen, + makemkvUpdate, metadatenSuchen, ripAbbrechen, ripStarten, + schluesselPruefen, verbindungStarten, zuordnungSetzen, type Verbindung, @@ -23,12 +27,24 @@ function posterUrlKlein(pfad: string): string { return `https://image.tmdb.org/t/p/w154${pfad}` } +function posterUrlGross(pfad: string): string { + if (pfad.length === 0) return '' + if (pfad.startsWith('http')) return pfad + return `https://image.tmdb.org/t/p/w342${pfad}` +} + +function backdropUrl(pfad: string): string { + if (pfad.length === 0) return '' + if (pfad.startsWith('http')) return pfad + return `https://image.tmdb.org/t/p/w780${pfad}` +} + function restzeitText(prozent: number, startMs: number | undefined): string { if (startMs === undefined || prozent <= 2 || prozent >= 100) return '' const verstrichen = (Date.now() - startMs) / 1000 const restS = Math.round((verstrichen * (100 - prozent)) / prozent) - if (restS < 90) return `· ~${restS} s` - return `· ~${Math.round(restS / 60)} min` + if (restS < 90) return `noch ~${restS} s` + return `noch ~${Math.round(restS / 60)} min` } function istHauptStatus(wert: unknown): wert is HauptStatus { @@ -51,6 +67,24 @@ const TYP_NAMEN: Record = { unknown: 'unbekannt', } +const TYP_SYMBOL: Record = { + cd: '💿', + dvd: '📀', + bluray: '📀', + uhd: '📀', + iso: '💽', +} + +const PHASEN_NAMEN: Record = { + 'liest-info': 'liest Titel-Info', + rippt: 'rippt (verlustfrei)', + komprimiert: 'komprimiert', + 'legt-ab': 'legt ab', + fertig: 'fertig', + fehler: 'Fehler', + abgebrochen: 'abgebrochen', +} + const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab'] function sicherheitsWort(confidence: number): string { @@ -60,6 +94,20 @@ function sicherheitsWort(confidence: number): string { return 'unsicher' } +function Badge({ text, ton }: { text: string; ton: 'gruen' | 'gelb' | 'blau' | 'grau' }) { + const farben = { + gruen: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-300', + gelb: 'border-amber-500/40 bg-amber-500/10 text-amber-300', + blau: 'border-sky-500/40 bg-sky-500/10 text-sky-300', + grau: 'border-slate-600/60 bg-slate-800/60 text-slate-300', + } + return ( + + {text} + + ) +} + interface KachelProps { titel: string zustand: 'ok' | 'fehler' | 'wartet' @@ -70,7 +118,7 @@ function Kachel({ titel, zustand, zeilen }: KachelProps) { const punktFarbe = zustand === 'ok' ? 'bg-emerald-400' : zustand === 'fehler' ? 'bg-red-400' : 'bg-amber-400' return ( -
+

{titel}

@@ -105,9 +153,12 @@ function AendernDialog({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( -
+
e.stopPropagation()} >

Was ist das wirklich?

@@ -167,109 +218,224 @@ function AendernDialog({ ) } +// Die Bühne: Backdrop als Panorama, Poster, Titel — und darunter der +// laufende Vorgang mit Phase, Restzeit und aufklappbarem Protokoll. function LaufwerkKachel({ geraet, rip, info, vorschlaege, ripStartMs, + protokoll, }: { geraet: GeraeteInfo rip: RipStatus | undefined info: DiscInfoStand | undefined vorschlaege: MetaVorschlag[] ripStartMs: number | undefined + protokoll: string[] }) { const [aendernOffen, setAendernOffen] = useState(false) + const [protokollOffen, setProtokollOffen] = useState(false) const ripLaeuft = rip !== undefined && LAUFENDE_PHASEN.includes(rip.phase) - const zustand = geraet.status === 'ready' ? 'ok' : geraet.status === 'empty' ? 'wartet' : 'fehler' - const punktFarbe = ripLaeuft - ? 'animate-pulse bg-sky-400' - : zustand === 'ok' - ? 'bg-emerald-400' - : zustand === 'fehler' - ? 'bg-red-400' - : 'bg-slate-500' + const discDa = geraet.status === 'ready' + const backdrop = discDa && info !== undefined ? backdropUrl(info.backdropPfad) : '' + const poster = discDa && info !== undefined ? posterUrlGross(info.posterPfad) : '' + const erkannt = discDa && info !== undefined && info.titel.length > 0 + return ( -
-
-
- -

{geraet.name}

-
-
- {ripLaeuft ? ( +
+ {backdrop.length > 0 && ( + <> + +
+
+ + )} + +
+
+
+ +

+ Laufwerk {geraet.id}: + {geraet.modell} +

+
+
+ {ripLaeuft ? ( + + ) : ( + + )} - ) : ( - - )} - +
-
-
- {geraet.status === 'ready' && info !== undefined && info.posterPfad.length > 0 && ( - + + {!discDa && ( +
+ 💿 +

+ {geraet.status === 'empty' ? 'Keine Disc eingelegt — Disc rein genügt.' : geraet.grund} +

+
+ )} + + {discDa && ( +
+ {poster.length > 0 ? ( + + ) : ( +
+ {TYP_SYMBOL[geraet.typ] ?? '💿'} +
+ )} +
+ {erkannt ? ( + <> +

+ {info.titel} + {info.jahr !== null && ({info.jahr})} +

+
+ + {geraet.groesseBytes > 0 && } + 0 ? ` · ${info.quelle}` : ''}`} + ton={info.confidence >= 0.8 ? 'gruen' : 'gelb'} + /> +
+ {info.beschreibung.length > 0 && ( +

+ {info.beschreibung} +

+ )} + + ) : ( + <> +

+ {TYP_NAMEN[geraet.typ]} + {geraet.groesseBytes > 0 && ( + {gb(geraet.groesseBytes)} + )} +

+

+ {geraet.typ === 'cd' + ? 'Titel kommen beim Rippen von MusicBrainz.' + : 'Noch keine Zuordnung — „Ändern …" hilft nach.'} +

+ + )} +
+ + {info !== undefined && info.schonGerippt !== null && ( + + )} +
+ {geraet.grund.length > 0 &&

{geraet.grund}

} +
+
+ )} + + {rip !== undefined && ( +
+
+

+ {PHASEN_NAMEN[rip.phase]} + {rip.lesefehler && rip.phase === 'fertig' ? ' — mit Lesefehlern' : ''} +

+ {ripLaeuft && rip.prozent >= 0 && ( +

+ {rip.prozent} % + {restzeitText(rip.prozent, ripStartMs)} +

+ )} +
+ {ripLaeuft && ( +
+
+
+ )} +

{rip.text}

+ {rip.dateien.map((datei) => ( +

+ {datei} +

+ ))} + {protokoll.length > 1 && ( + + )} + {protokollOffen && ( +
+ {protokoll.map((zeile, i) => ( +

+ {zeile} +

+ ))} +
+ )} +
)} -
-

{geraet.modell.length > 0 ? geraet.modell : 'Modell unbekannt'}

- {geraet.status === 'ready' && ( -

- {TYP_NAMEN[geraet.typ]} - {geraet.groesseBytes > 0 ? ` · ${gb(geraet.groesseBytes)}` : ''} -

- )} - {geraet.status === 'ready' && info !== undefined && info.titel.length > 0 && ( -

= 0.8 ? 'text-emerald-300' : 'text-amber-300'}> - {sicherheitsWort(info.confidence)}: {info.titel} - {info.jahr !== null ? ` (${info.jahr})` : ''} · {Math.round(info.confidence * 100)} % - {info.quelle.length > 0 ? ` · ${info.quelle}` : ''} -

- )} - {geraet.status === 'ready' && ( - - )} - {geraet.status === 'ready' && info !== undefined && info.schonGerippt !== null && ( -

- ⚠ Diese Disc wurde schon einmal gerippt ({info.schonGerippt.slice(0, 10)}) — siehe Bibliothek. -

- )} - {geraet.status === 'empty' &&

keine Disc eingelegt

} - {geraet.status === 'unknown' &&

{geraet.grund}

} - {geraet.status !== 'unknown' && geraet.grund.length > 0 && ( -

{geraet.grund}

- )} -
+ {aendernOffen && ( setAendernOffen(false)} /> )} - {rip !== undefined && ( -
- {ripLaeuft && ( -
-
-
- )} -

- {ripLaeuft && rip.prozent >= 0 - ? `${rip.prozent} % ${restzeitText(rip.prozent, ripStartMs)} — ` - : ''} - {rip.text} -

- {rip.dateien.map((datei) => ( -

- {datei} -

- ))} +
+ ) +} + +// Die MakeMKV-Schlüssel-Karte (§ 9.1) — mit dem Update-Weg aus § 9: +// „MakeMKV aktualisieren" holt den Installer über die Quellenkette, +// prüft ihn und lässt Windows fragen (UAC). Danach prüft Rippy neu. +function SchluesselKarte({ verbindung }: { verbindung: Verbindung }) { + const s = verbindung.schluessel + const b = verbindung.beschaffung + const zustand: 'ok' | 'fehler' | 'wartet' = + s === null ? 'wartet' : s.angenommen === true ? 'ok' : s.weiterBetrieb ? 'wartet' : 'fehler' + const punktFarbe = + zustand === 'ok' ? 'bg-emerald-400' : zustand === 'fehler' ? 'bg-red-400' : 'bg-amber-400' + return ( +
+
+
+ +

MakeMKV-Schlüssel

+ {s !== null && s.version.length > 0 && }
- )} +
+ {s !== null && s.angenommen === false && ( + + )} + +
+
+
+ {s === null ? ( +

wird geprüft …

+ ) : ( + <> +

{s.text}

+

+ zuletzt geprüft: {s.geholtAm.slice(0, 16).replace('T', ' ')} + {s.gueltigBis.length > 0 && ` · Key gültig bis ${s.gueltigBis}`} +

+ + )} + {b !== null && ( +
+

+ {b.text} +

+ {b.laeuft && b.anteil !== null && ( +
+
+
+ )} +
+ )} +
) } @@ -517,7 +721,7 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H

Werkzeuge

-
+

0 ? 'text-emerald-300' : 'text-red-300'}> makemkvcon: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.makemkvcon || 'NICHT GEFUNDEN — MakeMKV installieren'}

@@ -536,7 +740,7 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H ) } -// ── Bibliothek ────────────────────────────────────────────────────────── +// ── Bibliothek: das Poster-Regal (§ 6.6) ──────────────────────────────── function Bibliothek({ verbindung }: { verbindung: Verbindung }) { if (verbindung.bibliothek.length === 0) { return ( @@ -545,37 +749,41 @@ function Bibliothek({ verbindung }: { verbindung: Verbindung }) {

) } + const gesamt = verbindung.bibliothek.reduce((summe, e) => summe + e.groesseBytes, 0) return ( -
- - - - - - - - - - - - - {verbindung.bibliothek.map((eintrag) => ( - - - - - - - - - ))} - -
TitelTypGrößeDatumDauerAblageort
- {eintrag.titel} - {eintrag.jahr !== null ? ` (${eintrag.jahr})` : ''} - {eintrag.typ}{gb(eintrag.groesseBytes)}{eintrag.datum.slice(0, 16).replace('T', ' ')}{Math.round(eintrag.dauerS / 60)} min - {eintrag.ablageort} -
+
+

+ {verbindung.bibliothek.length} {verbindung.bibliothek.length === 1 ? 'Eintrag' : 'Einträge'} ·{' '} + {gb(gesamt)} gesamt +

+
+ {verbindung.bibliothek.map((eintrag) => ( +
+ {eintrag.posterPfad.length > 0 ? ( + + ) : ( +
+ {TYP_SYMBOL[eintrag.typ] ?? '🎬'} +
+ )} +

+ {eintrag.titel} + {eintrag.jahr !== null ? ` (${eintrag.jahr})` : ''} +

+

+ {TYP_NAMEN[eintrag.typ as GeraeteInfo['typ']] ?? eintrag.typ} · {gb(eintrag.groesseBytes)} +

+
+ ))} +
) } @@ -757,10 +965,12 @@ export default function App() { discInfos: {}, vorschlaege: {}, ripStart: {}, + ripProtokoll: {}, einstellungen: {}, werkzeuge: null, bibliothek: [], schluessel: null, + beschaffung: null, meldungen: [], }) @@ -773,6 +983,7 @@ export default function App() { }, []) const db = verbindung.kern?.datenbank ?? null + const einRipLaeuft = Object.values(verbindung.rips).some((r) => LAUFENDE_PHASEN.includes(r.phase)) // § 6.9: Die Erst-Einrichtung läuft NACH der Installation, im Programm — // solange sie nicht abgeschlossen ist, ist sie die ganze Ansicht. @@ -793,12 +1004,23 @@ export default function App() { ) return ( -
-
-
-
-

Rippy

- v{haupt?.version ?? '…'} +
+
+
+
+ + 💿 + +
+

Rippy

+ v{haupt?.version ?? '…'} + {einRipLaeuft && ( + + + arbeitet + + )} +