feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse

Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie
veröffentlicht wurde):

Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in
kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge,
Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts,
Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts.

Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/,
uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte),
einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/
(ErstEinrichtung, PosterHintergrund).

Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs);
LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown
statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update;
Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die
Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown;
Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen
Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange
pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag;
Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem
Poster-Grid.

Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung,
fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit
29 Schritten inklusive Erst-Einrichtung (zweiter Start mit
RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-02 17:51:15 +02:00
co-authored by Claude Fable 5.1
parent 6678c81592
commit 3dc6798c79
67 changed files with 6961 additions and 3950 deletions
+198
View File
@@ -0,0 +1,198 @@
// Die Werkzeuge (§ 9): wo makemkvcon, HandBrakeCLI und flac liegen, die
// gemessenen HandBrake-Listen (Presets, Encoder), die MakeMKV-Schlüsselkette
// (§ 9.1) samt Auswahlregel (5.4.0) und das MakeMKV-Selbst-Update.
import type { KernNachricht } from '../../gemeinsam/nachrichten'
import type { KernKontext } from '../kontext'
import { finden } from '../werkzeuge/katalog'
import type { EinstellungsDienst } from './einstellungen'
export type SchluesselStand = Extract<KernNachricht, { art: 'schluessel-status' }>['stand']
export class WerkzeugDienst {
/** Die gemessene Auskunft bleibt liegen — die Pipeline braucht sie für die
* Preset-Empfehlung, auch wenn gerade kein Fenster offen ist. */
auskunft: { presets: string[]; backends: string[] } = { presets: [], backends: [] }
schluesselStand: SchluesselStand | null = null
private beschaffungLaeuft = false
private installerAntwort: ((fehler: string | null) => void) | null = null
constructor(
private readonly kontext: KernKontext,
private readonly einstellungen: EinstellungsDienst,
) {}
makemkvcon(): string {
return finden('makemkv', this.einstellungen.lesen('werkzeug.makemkv'))
}
handbrake(): string {
return finden('handbrake', this.einstellungen.lesen('werkzeug.handbrake'))
}
flac(): string {
return finden('flac', this.einstellungen.lesen('werkzeug.flac'))
}
/** Werkzeuge suchen, HandBrake messen, ans Fenster. */
async senden(): Promise<void> {
const makemkvcon = this.makemkvcon()
const handbrake = this.handbrake()
const flac = this.flac()
const { encoderMessen } = await import('../komprimieren/encoder')
const gemessen = await encoderMessen(handbrake)
this.auskunft = { presets: gemessen.presets, backends: gemessen.backends }
this.kontext.anFenster({
art: 'werkzeuge',
auskunft: { makemkvcon, handbrake, flac, presets: gemessen.presets, backends: gemessen.backends },
})
this.kontext.protokoll.schreiben(
`Werkzeuge: makemkvcon=${makemkvcon || 'FEHLT'} handbrake=${handbrake || 'FEHLT'} flac=${flac || 'FEHLT'} · ${gemessen.presets.length} Presets · Encoder ${gemessen.backends.join(',') || '—'}`,
)
}
// ── MakeMKV-Schlüsselkette (§ 9.1): beim Start und täglich ──────────────
async schluesselLauf(): Promise<void> {
if (process.platform !== 'win32') return
try {
const { schluesselKette } = await import('../werkzeuge/schluessel')
this.schluesselStand = await schluesselKette(
this.makemkvcon(),
this.einstellungen.lesen('makemkvAppKey'),
new Date().toISOString(),
)
this.kontext.anHauptUndFenster({ art: 'schluessel-status', stand: this.schluesselStand })
this.kontext.protokoll.schreiben(`Schlüssel: ${this.schluesselStand.text}`)
} catch (fehler) {
// R4: Der Schlüssel-Lauf ist Komfort — sein Scheitern wird gesagt.
this.kontext.fehler(`Schlüsselkette: ${String(fehler)}`)
}
}
/** MakeMKVs Spur-Auswahlregel sicherstellen (5.4.0) — sonst fehlen
* Sprachen in der Roh-Datei, die der Dialog anbot. */
async spurauswahlLauf(): Promise<void> {
if (process.platform !== 'win32') return
try {
const { spurauswahlSicherstellen } = await import('../werkzeuge/spurauswahl')
const stand = spurauswahlSicherstellen()
if (stand.stand === 'eigen' || stand.stand === 'fehler') {
this.kontext.fehler(`MakeMKV-Auswahlregel: ${stand.text}`)
} else if (stand.stand === 'gesetzt') {
this.kontext.hinweis(stand.text)
} else {
this.kontext.protokoll.schreiben(stand.text)
}
} catch (fehler) {
this.kontext.fehler(`MakeMKV-Auswahlregel: ${String(fehler)}`)
}
}
/** Antwort des Haupts auf 'installer-starten'. */
installerErgebnis(fehler: string | null): void {
this.installerAntwort?.(fehler ?? '')
this.installerAntwort = null
}
private beschaffungMelden(text: string, anteil: number | null, laeuft: boolean, fehler = false): void {
this.kontext.anFenster({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft, text, anteil, fehler } })
this.kontext.protokoll.schreiben(`MakeMKV-Update: ${text}`)
}
// ── MakeMKV-Update (§ 9): holen, prüfen, installieren lassen ────────────
// Der Kern lädt und PRÜFT die Datei; STARTEN kann sie nur das Haupt
// (shell → UAC-Abfrage; der erhöhte Installer hängt dadurch nicht an der
// Prozess-Leine und überlebt ein geschlossenes Rippy).
async makemkvUpdateLauf(): Promise<void> {
if (this.beschaffungLaeuft) {
this.beschaffungMelden('Ein MakeMKV-Update läuft bereits.', null, true)
return
}
this.beschaffungLaeuft = true
try {
const { istNeuer, makemkvNeuesteVersion, makemkvInstallerHolen, versionsangaben } = await import(
'../werkzeuge/beschaffen'
)
this.beschaffungMelden('Nach der neuesten MakeMKV-Fassung wird gesucht …', null, true)
const neueste = await makemkvNeuesteVersion()
if (neueste.length === 0) {
this.beschaffungMelden(
'Keine der Quellen konnte sagen, welche Fassung aktuell ist (Hersteller-Seite, Forum, Archiv). ' +
'Ausweg: MakeMKV von Hand von makemkv.com laden und installieren; Rippy findet es danach von allein.',
null,
false,
true,
)
this.beschaffungLaeuft = false
return
}
const installiert = this.schluesselStand?.version ?? ''
if (installiert.length > 0 && !istNeuer(neueste, installiert)) {
// Ehrlich bleiben (31.08.2026 gemessen: 1.18.4 ist installiert UND
// die neueste angekündigte — die Beta ist nur abgelaufen): Ein
// Download derselben Fassung würde nichts lösen.
this.beschaffungMelden(
`MakeMKV ${installiert} ist bereits die neueste angekündigte Fassung. ` +
'Sobald der Hersteller eine neuere veröffentlicht, holt dieser Knopf sie — ' +
'bis dahin rippt der Beta-Modus weiter.',
null,
false,
)
this.beschaffungLaeuft = false
return
}
// Merken, woran eine gelungene Installation zu erkennen ist: Die
// Versions-Ressource der gefundenen EXE ändert sich (oder es gibt
// erstmals eine).
const alteExe = this.makemkvcon()
const alteVersion = alteExe.length > 0 ? versionsangaben(alteExe).version : ''
const ergebnis = await makemkvInstallerHolen(neueste, (text, anteil) => this.beschaffungMelden(text, anteil, true))
this.beschaffungMelden(`Geprüft (Quelle: ${ergebnis.quelle}) — der Installer wird gestartet …`, null, true)
const startFehler = await new Promise<string | null>((fertig) => {
this.installerAntwort = fertig
process.parentPort.postMessage({ art: 'installer-starten', pfad: ergebnis.pfad })
// Falls das Haupt nie antwortet, bleibt der Lauf nicht ewig hängen.
setTimeout(() => fertig('Keine Antwort vom Haupt-Prozess'), 30_000).unref()
})
this.installerAntwort = null
if (startFehler !== null && startFehler.length > 0) {
this.beschaffungMelden(`Der Installer wurde nicht gestartet: ${startFehler}`, null, false, true)
this.beschaffungLaeuft = false
return
}
this.beschaffungMelden(
`Der MakeMKV-Installer ${ergebnis.version} läuft — bitte die Windows-Nachfrage bestätigen und durchklicken. ` +
'Rippy merkt die fertige Installation von allein und prüft den Schlüssel neu.',
null,
true,
)
// Warten, bis die Installation durch ist: Die Versions-Ressource der
// EXE ändert sich. Alle 15 s nachsehen, höchstens 20 Minuten.
let runden = 0
const zeiger = setInterval(() => {
runden += 1
const exe = this.makemkvcon()
const jetztVersion = exe.length > 0 ? versionsangaben(exe).version : ''
if (jetztVersion.length > 0 && jetztVersion !== alteVersion) {
clearInterval(zeiger)
this.beschaffungLaeuft = false
this.beschaffungMelden(`MakeMKV ${jetztVersion} ist installiert — der Schlüssel wird neu geprüft.`, null, false)
void this.schluesselLauf().then(() => this.senden())
} else if (runden >= 80) {
clearInterval(zeiger)
this.beschaffungLaeuft = false
this.beschaffungMelden(
'Die fertige Installation wurde nicht bemerkt — nach dem Durchklicken hilft „Schlüssel neu prüfen".',
null,
false,
)
}
}, 15_000)
zeiger.unref()
} catch (fehler) {
this.beschaffungMelden(fehler instanceof Error ? fehler.message : String(fehler), null, false, true)
this.beschaffungLaeuft = false
}
}
}