diff --git a/rippy-windows/bau/icon.png b/rippy-windows/bau/icon.png new file mode 100644 index 0000000..cb5d7f4 Binary files /dev/null and b/rippy-windows/bau/icon.png differ diff --git a/rippy-windows/bau/tray.png b/rippy-windows/bau/tray.png new file mode 100644 index 0000000..12e8754 Binary files /dev/null and b/rippy-windows/bau/tray.png differ diff --git a/rippy-windows/src/bruecke/index.ts b/rippy-windows/src/bruecke/index.ts index 75f2818..ad963b0 100644 --- a/rippy-windows/src/bruecke/index.ts +++ b/rippy-windows/src/bruecke/index.ts @@ -48,4 +48,8 @@ contextBridge.exposeInMainWorld('rippy', { melden(art: string): void { ipcRenderer.send('fenster-meldung', String(art)) }, + /** Echter Windows-Ordnerdialog (§ 7). null = abgebrochen. */ + ordnerWaehlen(titel: string): Promise { + 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)} /> +
+ +
+
+ +
+

+ Kompression {verbindung.werkzeuge !== null && verbindung.werkzeuge.handbrake.length === 0 && ( + — HandBrakeCLI nicht gefunden + )} +

+ +
+ + + +
+ setzen('transcodePreset', w)} + /> +
+ setzen('audioSprachen', w)} + /> + setzen('untertitelSprachen', 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 ( +
+ + + + + + + + + + + + + {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} +
+
+ ) +} + +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 (
-
-

Rippy

- - v{haupt?.version ?? '…'} · Etappe W-1 — Laufwerk - +
+
+

Rippy

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

- Drei Prozesse, eine Datenbank, eine Leine — und die Disc-Wache läuft. -

-
-

- Laufwerke -

- {verbindung.laufwerke.length === 0 ? ( -

- {verbindung.verbunden ? 'Kein optisches Laufwerk gefunden.' : 'Warte auf den Kern …'} -

- ) : ( -
- {verbindung.laufwerke.map((geraet) => ( - - ))} -
- )} -
- -
-

- Gerüst -

-
- - - - -
-
- - {verbindung.meldungen.length > 0 && ( -
-

- Ereignisse -

-
- {verbindung.meldungen.map((zeile, i) => ( -

- {zeile} + {bereich === 'uebersicht' && ( + <> +

+

+ Laufwerke +

+ {verbindung.laufwerke.length === 0 ? ( +

+ {verbindung.verbunden ? 'Kein optisches Laufwerk gefunden.' : 'Warte auf den Kern …'}

- ))} -
-
+ ) : ( +
+ {verbindung.laufwerke.map((geraet) => ( + + ))} +
+ )} + + +
+

+ Status +

+
+ + + + +
+
+ + {verbindung.meldungen.length > 0 && ( +
+

+ Ereignisse +

+
+ {verbindung.meldungen.map((zeile, i) => ( +

+ {zeile} +

+ ))} +
+
+ )} + )} + + {bereich === 'einstellungen' && } + {bereich === 'bibliothek' && }