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)} />
+
+
+
+
+
+
+
+
+ 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 (