From c78ff22ab76f657be1ebd58007a9c8ed05187bed Mon Sep 17 00:00:00 2001
From: Hitonabi
Date: Sun, 30 Aug 2026 18:47:39 +0200
Subject: [PATCH] =?UTF-8?q?feat(v5):=20Etappe=20W-5=20=E2=80=94=20Oberflae?=
=?UTF-8?q?che:=20Tray,=20Toasts,=20Taskleiste,=20Einstellungen,=20Bibliot?=
=?UTF-8?q?hek?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
haupt/: tray.ts (Symbol mit Zustand, Kontextmenue, Fenster zu = weiter-
laufen, Beenden nur noch gewollt), Windows-Toasts bei Disc erkannt /
fertig / Fehler (Klick holt das Fenster), Taskleisten-Fortschritt
(setProgressBar, Fehlermodus), echter Windows-Ordnerdialog per IPC.
Eigenes Icon (bau/icon.png + tray.png, Disc-Optik).
Kern: Einstellungen lesen/schreiben NUR ueber die eine Quelle (§ 6.7,
Whitelist der Schluessel), Werkzeug-Auskunft (Katalog + gemessene
HandBrake-Presets/Backends), Bibliothek in db.ts (Schema v2 —
fingerprint, Titel, Groesse, Ort, Dauer), Duplikat-Erkennung beim
Einlegen (§ 6.6: 'schon einmal gerippt am …' als disc-info + Toast),
Pipeline traegt fertige Vorgaenge ein.
Fenster: drei Bereiche (Uebersicht / Einstellungen / Bibliothek).
Einstellungen: Ablage mit Durchsuchen-Dialog, Kompression je Disc-Typ
(Preset-Liste VOM eigenen HandBrake gemessen, 'NICHT komprimieren'
waehlbar), Sprachlisten, TMDb/OMDb-Keys, Medienserver, Werkzeug-Stand
mit Klartext (gefunden/fehlt + Folgen). Bibliothek als Tabelle.
Der Smoke-Screenshot zeigt nebenbei den Ernstfall in schoen: Das
Laufwerk ist nach dem rc11-Vorfall inzwischen auch am Storage-Stack in
'unknown' gekippt — und die Kachel zeigt den ZUGRIFFS_GRUENDE-Klartext
samt Abhilfe statt eines stummen Zustands.
165 Tests gruen, Smoke gruen.
Co-Authored-By: Claude Fable 5
---
rippy-windows/bau/icon.png | Bin 0 -> 7358 bytes
rippy-windows/bau/tray.png | Bin 0 -> 486 bytes
rippy-windows/src/bruecke/index.ts | 4 +
rippy-windows/src/fenster/App.tsx | 463 +++++++++++++++-----
rippy-windows/src/fenster/global.d.ts | 1 +
rippy-windows/src/fenster/kernverbindung.ts | 33 ++
rippy-windows/src/gemeinsam/nachrichten.ts | 42 +-
rippy-windows/src/haupt/index.ts | 126 +++++-
rippy-windows/src/haupt/tray.ts | 52 +++
rippy-windows/src/kern/ablauf/pipeline.ts | 35 +-
rippy-windows/src/kern/index.ts | 92 +++-
rippy-windows/src/kern/speicher/db.ts | 58 ++-
rippy-windows/test/db.test.ts | 4 +-
13 files changed, 782 insertions(+), 128 deletions(-)
create mode 100644 rippy-windows/bau/icon.png
create mode 100644 rippy-windows/bau/tray.png
create mode 100644 rippy-windows/src/haupt/tray.ts
diff --git a/rippy-windows/bau/icon.png b/rippy-windows/bau/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb5d7f4dd20e8a3a991dd026b977ba297d712a4b
GIT binary patch
literal 7358
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
Giu@mdQAyqa
literal 0
HcmV?d00001
diff --git a/rippy-windows/bau/tray.png b/rippy-windows/bau/tray.png
new file mode 100644
index 0000000000000000000000000000000000000000..12e8754a62a59e0941a165b6366a6c19ebaf6de3
GIT binary patch
literal 486
zcmV@P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0eeYAK~z{r)t5a^
z!$1&)bAXs2p*RH%6&o6&NGM9l5jX-R$Drc^G&B_SG*mR{($Udy0=hKm=!iDTSd-7Y
z8}F_y!IM5IKQr$=+mY<_djDO6k-6q)o$06?|2BoYS)zFi^ommd7dtYyNu3o?6O6b#`5=`jPFE4JGxhEN%zIky_Fue*N`ST*x!OK!R$_tUZAhD0Dm-XLT_&YF1AUl1>}fgcjR?e0UaDu
zLeC9G=4=;k?py;QILG%s^jtAC_kZIIq!f@@;rMh?``(52ufkIcq|m59S0(7}EFgni
zZUg%670@N@An)V3OrAdcM=#O~Y?}-Hm|aN* {
+ return ipcRenderer.invoke('ordner-waehlen', String(titel)) as Promise
+ },
})
diff --git a/rippy-windows/src/fenster/App.tsx b/rippy-windows/src/fenster/App.tsx
index 6bfce0c..14cee29 100644
--- a/rippy-windows/src/fenster/App.tsx
+++ b/rippy-windows/src/fenster/App.tsx
@@ -1,11 +1,12 @@
-// Statusbild W-0/W-1: die drei Prozesse plus die echten Laufwerke aus der
-// Disc-Wache. Die große Oberfläche (Dashboard, Bibliothek, Einstellungen)
-// ist Etappe W-5 — hier wird das Gerüst SICHTBAR bewiesen.
+// 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.
import { useEffect, useState } from 'react'
import type { DiscInfoStand, GeraeteInfo, HauptStatus, RipStatus } from '../gemeinsam/nachrichten'
import {
aufVerbindung,
auswerfen,
+ einstellungSetzen,
ripAbbrechen,
ripStarten,
verbindungStarten,
@@ -32,6 +33,15 @@ const TYP_NAMEN: Record = {
unknown: 'unbekannt',
}
+const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab']
+
+function sicherheitsWort(confidence: number): string {
+ if (confidence >= 0.9) return 'sehr wahrscheinlich'
+ if (confidence >= 0.8) return 'wahrscheinlich'
+ if (confidence >= 0.6) return 'Vorschlag'
+ return 'unsicher'
+}
+
interface KachelProps {
titel: string
zustand: 'ok' | 'fehler' | 'wartet'
@@ -58,15 +68,6 @@ function Kachel({ titel, zustand, zeilen }: KachelProps) {
)
}
-const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab']
-
-function sicherheitsWort(confidence: number): string {
- if (confidence >= 0.9) return 'sehr wahrscheinlich'
- if (confidence >= 0.8) return 'wahrscheinlich'
- if (confidence >= 0.6) return 'Vorschlag'
- return 'unsicher'
-}
-
function LaufwerkKachel({
geraet,
rip,
@@ -105,7 +106,7 @@ function LaufwerkKachel({
onClick={() => ripStarten(geraet.id)}
disabled={geraet.status !== 'ready'}
className="rounded-lg border border-emerald-500/60 px-3 py-1 text-xs text-emerald-300 hover:bg-emerald-500/10 disabled:cursor-not-allowed disabled:opacity-40"
- title="Alle Titel, verlustfrei (MakeMKV)"
+ title="Alle Titel, verlustfrei (MakeMKV) — danach Kompression laut Einstellungen"
>
Rippen
@@ -135,6 +136,11 @@ function LaufwerkKachel({
{info.quelle.length > 0 ? ` · ${info.quelle}` : ''}
)}
+ {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 && (
@@ -176,8 +182,240 @@ function LaufwerkKachel({
)
}
+// ── Einstellungen ───────────────────────────────────────────────────────
+function Feld({
+ name,
+ wert,
+ hinweis,
+ geheim,
+ aufAendern,
+}: {
+ name: string
+ wert: string
+ hinweis?: string
+ geheim?: boolean
+ aufAendern: (wert: string) => void
+}) {
+ const [entwurf, setEntwurf] = useState(wert)
+ useEffect(() => setEntwurf(wert), [wert])
+ return (
+
+ )
+}
+
+function PresetWahl({
+ name,
+ schluessel,
+ wert,
+ presets,
+ aufAendern,
+}: {
+ name: string
+ schluessel: string
+ wert: string
+ presets: string[]
+ aufAendern: (schluessel: string, wert: string) => void
+}) {
+ return (
+
+ )
+}
+
+function Einstellungen({ verbindung }: { verbindung: Verbindung }) {
+ const werte = verbindung.einstellungen
+ const presets = verbindung.werkzeuge?.presets ?? []
+ const setzen = einstellungSetzen
+
+ const ordnerWaehlen = async (): Promise => {
+ const ordner = await window.rippy.ordnerWaehlen('Ablage-Ordner wählen')
+ if (ordner !== null) setzen('ablage', ordner)
+ }
+
+ return (
+
+
+ Ablage
+
+
+ setzen('ablage', w)} />
+
+
+
+
+
+
+
+
+ Metadaten
+
+ setzen('tmdbApiKey', w)} />
+ setzen('omdbApiKey', w)} />
+
+
+
+
+ Medienserver
+
+
+ setzen('mediaServerUrl', w)} />
+ setzen('mediaServerApiKey', w)} />
+
+
+
+
+ Werkzeuge
+
+
0 ? 'text-emerald-300' : 'text-red-300'}>
+ makemkvcon: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.makemkvcon || 'NICHT GEFUNDEN — MakeMKV installieren'}
+
+
0 ? 'text-emerald-300' : 'text-amber-300'}>
+ HandBrakeCLI: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.handbrake || 'nicht gefunden — Rippen geht, Kompression nicht'}
+
+
0 ? 'text-emerald-300' : 'text-amber-300'}>
+ flac: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.flac || 'nicht gefunden — nur für Audio-CDs nötig'}
+
+ {verbindung.werkzeuge !== null && verbindung.werkzeuge.backends.length > 0 && (
+
Encoder: {verbindung.werkzeuge.backends.join(', ')}
+ )}
+
+
+
+ )
+}
+
+// ── Bibliothek ──────────────────────────────────────────────────────────
+function Bibliothek({ verbindung }: { verbindung: Verbindung }) {
+ if (verbindung.bibliothek.length === 0) {
+ return (
+
+ Noch nichts gerippt — die Bibliothek füllt sich mit jedem fertigen Vorgang.
+
+ )
+ }
+ return (
+
+
+
+
+ | Titel |
+ Typ |
+ Größe |
+ Datum |
+ Dauer |
+ Ablageort |
+
+
+
+ {verbindung.bibliothek.map((eintrag) => (
+
+ |
+ {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}
+ |
+
+ ))}
+
+
+
+ )
+}
+
+type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek'
+
export default function App() {
const [haupt, setHaupt] = useState(null)
+ const [bereich, setBereich] = useState('uebersicht')
const [verbindung, setVerbindung] = useState({
verbunden: false,
kern: null,
@@ -185,6 +423,9 @@ export default function App() {
laufwerke: [],
rips: {},
discInfos: {},
+ einstellungen: {},
+ werkzeuge: null,
+ bibliothek: [],
meldungen: [],
})
@@ -198,102 +439,124 @@ export default function App() {
const db = verbindung.kern?.datenbank ?? null
+ const knopf = (ziel: Bereich, text: string): React.JSX.Element => (
+
+ )
+
return (