diff --git a/beweise/ui-einstellungen-neu.png b/beweise/ui-einstellungen-neu.png new file mode 100644 index 0000000..a8e55ec Binary files /dev/null and b/beweise/ui-einstellungen-neu.png differ diff --git a/rippy-windows/package.json b/rippy-windows/package.json index d82f3b8..fd92739 100644 --- a/rippy-windows/package.json +++ b/rippy-windows/package.json @@ -1,7 +1,7 @@ { "name": "rippy", "productName": "Rippy", - "version": "5.0.0-w0", + "version": "5.1.0", "description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)", "main": "out/haupt/index.js", "author": "KrBrZ", diff --git a/rippy-windows/src/fenster/App.tsx b/rippy-windows/src/fenster/App.tsx index b27bafa..7fcea57 100644 --- a/rippy-windows/src/fenster/App.tsx +++ b/rippy-windows/src/fenster/App.tsx @@ -609,6 +609,59 @@ function Kicker({ text }: { text: string }) { return

{text}

} +/** Ein richtiger Schalter statt einer nackten Checkbox. */ +function Schalter({ an, aufWechsel, label }: { an: boolean; aufWechsel: (an: boolean) => void; label: string }) { + return ( + + ) +} + +/** Eine Einstellungs-Karte: Kicker, ein Satz Einordnung, dann die Felder. */ +function SektionsKarte({ + id, + titel, + intro, + children, +}: { + id: string + titel: string + intro: string + children: React.ReactNode +}) { + return ( +
+ +

{intro}

+
{children}
+
+ ) +} + +/** Kleiner Status-Punkt für Schlüssel-Felder: hinterlegt oder leer. */ +function HinterlegtPunkt({ wert }: { wert: string }) { + return ( + 0 ? 'text-gruen' : 'text-nebel'}`}> + {wert.length > 0 ? '● hinterlegt' : '○ fehlt'} + + ) +} + function Feld({ name, wert, @@ -672,12 +725,64 @@ function PresetWahl({ ) } +/** Eine Werkzeug-Zeile: Fundort in Klartext plus der eigene Pfad, der + * die automatische Suche übersteuert (Katalog-Schritt 1). */ +function WerkzeugZeile({ + name, + pfad, + fehlt, + pflicht, + schluessel, + wert, + aufAendern, +}: { + name: string + pfad: string | null + fehlt: string + pflicht: boolean + schluessel: string + wert: string + aufAendern: (schluessel: string, wert: string) => void +}) { + return ( +
+

0 ? 'text-gruen' : pflicht ? 'text-rot' : 'text-warn'}`}> + {name}: {pfad === null ? 'wird gesucht …' : pfad.length > 0 ? pfad : fehlt} +

+ aufAendern(schluessel, w)} + /> +
+ ) +} + function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: HauptStatus | null }) { const werte = verbindung.einstellungen const presets = verbindung.werkzeuge?.presets ?? [] + const backends = verbindung.werkzeuge?.backends ?? [] const setzen = einstellungSetzen - const empfehlungFuer = (typ: string): string => - presetEmpfehlung(typ, verbindung.werkzeuge?.backends ?? [], presets) + const s = verbindung.schluessel + const b = verbindung.beschaffung + const empfehlungFuer = (typ: string): string => presetEmpfehlung(typ, backends, presets) + const [aktiv, setAktiv] = useState('ablage') + + const SEKTIONEN: Array<[string, string]> = [ + ['ablage', 'Ablage'], + ['kompression', 'Kompression'], + ['metadaten', 'Metadaten'], + ['makemkv', 'MakeMKV'], + ['medienserver', 'Medienserver'], + ['programm', 'Programm'], + ['werkzeuge', 'Werkzeuge'], + ] + + const springe = (id: string): void => { + setAktiv(id) + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } const ordnerWaehlen = async (): Promise => { const ordner = await window.rippy.ordnerWaehlen('Ablage-Ordner wählen') @@ -685,138 +790,277 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H } return ( -
-
- -
-
- setzen('ablage', w)} /> -
+
+
-
+ ))} + -
- - -
- - - -
- setzen('transcodePreset', w)} - /> -
- setzen('audioSprachen', w)} - /> - setzen('untertitelSprachen', w)} - /> -
-
+
+ +
+
+ setzen('ablage', w)} /> +
+ +
+
+

So legt Rippy ab:

+

├─ Filme\Akira (1988)\ — Film, Poster und Infodatei beieinander

+

├─ Musik\ — Audio-CDs als FLAC, benannt nach MusicBrainz

+

├─ ISO\ — unerkannte Discs als 1:1-Abbild

+

└─ roh\ — Arbeitsstand während des Rips, räumt sich selbst auf

+
+
-
- -
- setzen('tmdbApiKey', w)} /> - setzen('omdbApiKey', w)} /> -
- setzen('makemkvAppKey', w)} - /> -
- -
- -
- - setzen('mediaServerUrl', w)} /> - setzen('mediaServerApiKey', w)} /> -
-
- -
- - - setzen('updateUrl', w)} - /> - {haupt !== null && ( -

- Selbst-Update: {haupt.update.text} - {haupt.update.letzteErfolgreichePruefung.length > 0 && - ` · letzte erfolgreiche Prüfung: ${haupt.update.letzteErfolgreichePruefung.slice(0, 16).replace('T', ' ')}`} -

- )} -
- -
- -
-

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

-

0 ? 'text-gruen' : 'text-warn'}> - HandBrakeCLI: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.handbrake || 'nicht gefunden — Rippen geht, Kompression nicht'} -

-

0 ? 'text-gruen' : 'text-warn'}> - 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(', ')}

+ + {verbindung.werkzeuge !== null && verbindung.werkzeuge.handbrake.length === 0 && ( +

⚠ HandBrakeCLI fehlt — Rippen geht, Komprimieren nicht (siehe Werkzeuge).

)} -
-
+ setzen('transcodeEnabled', an ? 'true' : 'false')} + label="Nach dem Rip automatisch komprimieren" + /> + {backends.length > 0 && ( +
+ GEMESSENE ENCODER: + {backends.map((backend) => ( + + ))} +
+ )} +
+ + + +
+

+ „automatisch" nimmt die Empfehlung aus der gemessenen Liste deines HandBrake — auf dieser Maschine + die Grafikkarte, wo es sie gibt. Eine eigene Wahl schlägt die Empfehlung immer. +

+ setzen('transcodePreset', w)} + /> +
+ setzen('audioSprachen', w)} + /> + setzen('untertitelSprachen', w)} + /> +
+ + + +
+
+ setzen('tmdbApiKey', w)} /> +

+ www.themoviedb.org → Einstellungen → API + +

+
+
+ setzen('omdbApiKey', w)} /> +

+ www.omdbapi.com/apikey.aspx + +

+
+
+

+ Ohne Schlüssel rippt Rippy trotzdem — die Filme heißen dann wie das Disc-Label, und „Ändern …" + an der Laufwerks-Kachel bleibt ohne Suche. +

+
+ + + {s !== null && ( +
+

+ + + {s.angenommen === true ? 'SCHLÜSSEL ANGENOMMEN' : s.weiterBetrieb ? 'BETA-MODUS — RIPS LAUFEN' : 'AUSFALL'} + + {s.version.length > 0 && MakeMKV {s.version}} +

+

{s.text}

+

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

+
+ )} +
+ {s !== null && s.angenommen === false && ( + + )} + +
+ {b !== null && ( +

+ {b.text} +

+ )} + setzen('makemkvAppKey', w)} + /> +
+ + +
+ + setzen('mediaServerUrl', w)} /> + setzen('mediaServerApiKey', w)} /> +
+

+ Bei Plex benennt Rippy nur nach dessen Regeln — angestoßen wird dort nichts. Die Ordnerstruktur + aus „Ablage" passt für alle vier. +

+
+ + + void window.rippy.autostartSetzen(an)} + label="Rippy mit Windows starten (Autostart im Benutzerkonto — kein Dienst, keine Adminrechte)" + /> + setzen('updateUrl', w)} + /> + {haupt !== null && ( +
+

Selbst-Update: {haupt.update.text}

+ {haupt.update.letzteErfolgreichePruefung.length > 0 && ( +

letzte erfolgreiche Prüfung: {haupt.update.letzteErfolgreichePruefung.slice(0, 16).replace('T', ' ')}

+ )} +

+ Rippy v{haupt.version} + {verbindung.kern !== null && ` · Kern: Node ${verbindung.kern.nodeVersion} (PID ${verbindung.kern.pid})`} +

+

Updates warten, solange ein Rip läuft — nie mitten im Vorgang.

+
+ )} +
+ + + + + + {verbindung.werkzeuge !== null && ( +

+ HandBrake gemessen: {verbindung.werkzeuge.presets.length} Presets · Encoder:{' '} + {backends.length > 0 ? backends.join(', ') : '—'} +

+ )} +
+
) } @@ -1037,7 +1281,12 @@ type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek' export default function App() { const [haupt, setHaupt] = useState(null) - const [bereich, setBereich] = useState('uebersicht') + // #einstellungen / #bibliothek als Startanker — der Smoke-Beweis + // fotografiert damit auch die anderen Bereiche. + const [bereich, setBereich] = useState(() => { + const anker = window.location.hash.replace('#', '') + return anker === 'einstellungen' || anker === 'bibliothek' ? anker : 'uebersicht' + }) const [verbindung, setVerbindung] = useState({ verbunden: false, kern: null, diff --git a/rippy-windows/src/haupt/fenster.ts b/rippy-windows/src/haupt/fenster.ts index 8f874d1..3d0f007 100644 --- a/rippy-windows/src/haupt/fenster.ts +++ b/rippy-windows/src/haupt/fenster.ts @@ -28,11 +28,17 @@ export function fensterErstellen(): BrowserWindow { // electron-vite: Im dev-Modus bedient ein Vite-Server das Fenster // (ELECTRON_RENDERER_URL), gebaut wird aus out/fenster geladen. + // RIPPY_START_BEREICH öffnet einen bestimmten Bereich (#einstellungen / + // #bibliothek) — der Smoke-Beweis fotografiert damit auch diese Seiten. const devUrl = process.env['ELECTRON_RENDERER_URL'] + const startBereich = process.env['RIPPY_START_BEREICH'] ?? '' if (devUrl !== undefined && devUrl.length > 0) { - void fenster.loadURL(devUrl) + void fenster.loadURL(startBereich.length > 0 ? `${devUrl}#${startBereich}` : devUrl) } else { - void fenster.loadFile(join(__dirname, '../fenster/index.html')) + void fenster.loadFile( + join(__dirname, '../fenster/index.html'), + startBereich.length > 0 ? { hash: startBereich } : undefined, + ) } return fenster } diff --git a/rippy-windows/src/haupt/index.ts b/rippy-windows/src/haupt/index.ts index f5cb9b8..e63ce91 100644 --- a/rippy-windows/src/haupt/index.ts +++ b/rippy-windows/src/haupt/index.ts @@ -279,8 +279,11 @@ function start(): void { try { // Der Pong kommt schneller, als der Compositor das erste Bild malt — // ein sofortiges capturePage lieferte hier »UnknownVizError« - // (gemessen 30.08.2026). Kurz warten, bis wirklich gemalt ist. - await new Promise((fertig) => setTimeout(fertig, 750)) + // (gemessen 30.08.2026). Kurz warten, bis wirklich gemalt ist; + // RIPPY_SMOKE_WARTE_MS verlängert das für Bild-Beweise, die auf + // späte Kern-Antworten warten. + const warteMs = Number(process.env['RIPPY_SMOKE_WARTE_MS'] ?? '750') + await new Promise((fertig) => setTimeout(fertig, Number.isFinite(warteMs) ? warteMs : 750)) const bild = await fenster.webContents.capturePage() await writeFile(bildPfad, bild.toPNG()) console.log(`SMOKE: Bild abgelegt unter ${bildPfad}`)