feat(v5): Etappe W-5 — Oberflaeche: Tray, Toasts, Taskleiste, Einstellungen, Bibliothek
Ampel / ampel (push) Successful in 1m23s

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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-30 18:47:39 +02:00
co-authored by Claude Fable 5
parent a9fa8fa2d8
commit c78ff22ab7
13 changed files with 782 additions and 128 deletions
+363 -100
View File
@@ -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<GeraeteInfo['typ'], string> = {
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
</button>
@@ -135,6 +136,11 @@ function LaufwerkKachel({
{info.quelle.length > 0 ? ` · ${info.quelle}` : ''}
</p>
)}
{geraet.status === 'ready' && info !== undefined && info.schonGerippt !== null && (
<p className="text-amber-300">
Diese Disc wurde schon einmal gerippt ({info.schonGerippt.slice(0, 10)}) siehe Bibliothek.
</p>
)}
{geraet.status === 'empty' && <p>keine Disc eingelegt</p>}
{geraet.status === 'unknown' && <p className="text-red-300">{geraet.grund}</p>}
{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 (
<label className="block">
<span className="text-xs font-semibold text-slate-300">{name}</span>
{hinweis !== undefined && <span className="ml-2 text-xs text-slate-500">{hinweis}</span>}
<input
type={geheim === true ? 'password' : 'text'}
value={entwurf}
onChange={(e) => setEntwurf(e.target.value)}
onBlur={() => {
if (entwurf !== wert) aufAendern(entwurf)
}}
className="mt-1 w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-1.5 font-mono text-xs text-slate-200 focus:border-sky-400 focus:outline-none"
/>
</label>
)
}
function PresetWahl({
name,
schluessel,
wert,
presets,
aufAendern,
}: {
name: string
schluessel: string
wert: string
presets: string[]
aufAendern: (schluessel: string, wert: string) => void
}) {
return (
<label className="block">
<span className="text-xs font-semibold text-slate-300">{name}</span>
<select
value={wert}
onChange={(e) => aufAendern(schluessel, e.target.value)}
className="mt-1 w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-1.5 font-mono text-xs text-slate-200 focus:border-sky-400 focus:outline-none"
>
<option value="">(allgemeines Preset)</option>
<option value="keine">NICHT komprimieren (verlustfrei lassen)</option>
{presets.map((preset) => (
<option key={preset} value={preset}>
{preset}
</option>
))}
</select>
</label>
)
}
function Einstellungen({ verbindung }: { verbindung: Verbindung }) {
const werte = verbindung.einstellungen
const presets = verbindung.werkzeuge?.presets ?? []
const setzen = einstellungSetzen
const ordnerWaehlen = async (): Promise<void> => {
const ordner = await window.rippy.ordnerWaehlen('Ablage-Ordner wählen')
if (ordner !== null) setzen('ablage', ordner)
}
return (
<div className="max-w-2xl space-y-6">
<section className="space-y-3">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Ablage</h2>
<div className="flex items-end gap-2">
<div className="flex-1">
<Feld name="Ablage-Ordner" wert={werte['ablage'] ?? ''} aufAendern={(w) => setzen('ablage', w)} />
</div>
<button
onClick={() => void ordnerWaehlen()}
className="rounded-lg border border-slate-600 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700"
>
Durchsuchen
</button>
</div>
</section>
<section className="space-y-3">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">
Kompression {verbindung.werkzeuge !== null && verbindung.werkzeuge.handbrake.length === 0 && (
<span className="text-red-400"> HandBrakeCLI nicht gefunden</span>
)}
</h2>
<label className="flex items-center gap-2 text-xs text-slate-300">
<input
type="checkbox"
checked={werte['transcodeEnabled'] !== 'false'}
onChange={(e) => setzen('transcodeEnabled', e.target.checked ? 'true' : 'false')}
/>
Nach dem Rip komprimieren (der verlustfreie Rip bleibt bis zum Erfolg erhalten)
</label>
<div className="grid gap-3 sm:grid-cols-3">
<PresetWahl name="DVD" schluessel="transcodePresetDvd" wert={werte['transcodePresetDvd'] ?? ''} presets={presets} aufAendern={setzen} />
<PresetWahl name="Blu-ray" schluessel="transcodePresetBluray" wert={werte['transcodePresetBluray'] ?? ''} presets={presets} aufAendern={setzen} />
<PresetWahl name="4K-UHD" schluessel="transcodePresetUhd" wert={werte['transcodePresetUhd'] ?? ''} presets={presets} aufAendern={setzen} />
</div>
<Feld
name="Allgemeines Preset"
hinweis="gilt, wo oben nichts gewählt ist"
wert={werte['transcodePreset'] ?? ''}
aufAendern={(w) => setzen('transcodePreset', w)}
/>
<div className="grid gap-3 sm:grid-cols-2">
<Feld
name="Tonspuren (Sprachen)"
hinweis="ISO-639-2, z. B. deu,eng — leer = alle"
wert={werte['audioSprachen'] ?? ''}
aufAendern={(w) => setzen('audioSprachen', w)}
/>
<Feld
name="Untertitel (Sprachen)"
hinweis="leer = alle"
wert={werte['untertitelSprachen'] ?? ''}
aufAendern={(w) => setzen('untertitelSprachen', w)}
/>
</div>
</section>
<section className="space-y-3">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Metadaten</h2>
<div className="grid gap-3 sm:grid-cols-2">
<Feld name="TMDb-API-Key" geheim wert={werte['tmdbApiKey'] ?? ''} aufAendern={(w) => setzen('tmdbApiKey', w)} />
<Feld name="OMDb-API-Key" geheim wert={werte['omdbApiKey'] ?? ''} aufAendern={(w) => setzen('omdbApiKey', w)} />
</div>
</section>
<section className="space-y-3">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Medienserver</h2>
<div className="grid gap-3 sm:grid-cols-3">
<label className="block">
<span className="text-xs font-semibold text-slate-300">System</span>
<select
value={werte['mediaServer'] ?? 'none'}
onChange={(e) => setzen('mediaServer', e.target.value)}
className="mt-1 w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-1.5 font-mono text-xs text-slate-200 focus:border-sky-400 focus:outline-none"
>
<option value="none">keiner</option>
<option value="jellyfin">Jellyfin</option>
<option value="emby">Emby</option>
<option value="kodi">Kodi</option>
<option value="plex">Plex (nur Benennung)</option>
</select>
</label>
<Feld name="Adresse" hinweis="http(s)://…" wert={werte['mediaServerUrl'] ?? ''} aufAendern={(w) => setzen('mediaServerUrl', w)} />
<Feld name="API-Key" geheim hinweis="Kodi: benutzer:passwort" wert={werte['mediaServerApiKey'] ?? ''} aufAendern={(w) => setzen('mediaServerApiKey', w)} />
</div>
</section>
<section className="space-y-3">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Werkzeuge</h2>
<div className="space-y-1 rounded-xl border border-slate-700/60 bg-slate-800/40 p-4 font-mono text-xs">
<p className={verbindung.werkzeuge === null ? 'text-slate-400' : verbindung.werkzeuge.makemkvcon.length > 0 ? 'text-emerald-300' : 'text-red-300'}>
makemkvcon: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.makemkvcon || 'NICHT GEFUNDEN — MakeMKV installieren'}
</p>
<p className={verbindung.werkzeuge === null ? 'text-slate-400' : verbindung.werkzeuge.handbrake.length > 0 ? 'text-emerald-300' : 'text-amber-300'}>
HandBrakeCLI: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.handbrake || 'nicht gefunden — Rippen geht, Kompression nicht'}
</p>
<p className={verbindung.werkzeuge === null ? 'text-slate-400' : verbindung.werkzeuge.flac.length > 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'}
</p>
{verbindung.werkzeuge !== null && verbindung.werkzeuge.backends.length > 0 && (
<p className="text-slate-400">Encoder: {verbindung.werkzeuge.backends.join(', ')}</p>
)}
</div>
</section>
</div>
)
}
// ── Bibliothek ──────────────────────────────────────────────────────────
function Bibliothek({ verbindung }: { verbindung: Verbindung }) {
if (verbindung.bibliothek.length === 0) {
return (
<p className="font-mono text-xs text-slate-500">
Noch nichts gerippt die Bibliothek füllt sich mit jedem fertigen Vorgang.
</p>
)
}
return (
<div className="overflow-x-auto rounded-xl border border-slate-700/60">
<table className="w-full text-left font-mono text-xs">
<thead className="bg-slate-800/60 text-slate-400">
<tr>
<th className="px-3 py-2">Titel</th>
<th className="px-3 py-2">Typ</th>
<th className="px-3 py-2">Größe</th>
<th className="px-3 py-2">Datum</th>
<th className="px-3 py-2">Dauer</th>
<th className="px-3 py-2">Ablageort</th>
</tr>
</thead>
<tbody className="text-slate-300">
{verbindung.bibliothek.map((eintrag) => (
<tr key={eintrag.fingerprint} className="border-t border-slate-700/40">
<td className="px-3 py-2">
{eintrag.titel}
{eintrag.jahr !== null ? ` (${eintrag.jahr})` : ''}
</td>
<td className="px-3 py-2">{eintrag.typ}</td>
<td className="px-3 py-2">{gb(eintrag.groesseBytes)}</td>
<td className="px-3 py-2">{eintrag.datum.slice(0, 16).replace('T', ' ')}</td>
<td className="px-3 py-2">{Math.round(eintrag.dauerS / 60)} min</td>
<td className="max-w-64 truncate px-3 py-2 text-slate-500" title={eintrag.ablageort}>
{eintrag.ablageort}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek'
export default function App() {
const [haupt, setHaupt] = useState<HauptStatus | null>(null)
const [bereich, setBereich] = useState<Bereich>('uebersicht')
const [verbindung, setVerbindung] = useState<Verbindung>({
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 => (
<button
onClick={() => setBereich(ziel)}
className={`rounded-lg px-3 py-1 text-xs ${
bereich === ziel
? 'bg-sky-500/20 text-sky-300'
: 'text-slate-400 hover:bg-slate-800 hover:text-slate-200'
}`}
>
{text}
</button>
)
return (
<div className="flex min-h-screen flex-col bg-slate-900 text-slate-100">
<header className="border-b border-slate-700/60 px-6 py-4">
<div className="flex items-baseline gap-3">
<h1 className="text-2xl font-bold tracking-tight">Rippy</h1>
<span className="font-mono text-xs text-slate-400">
v{haupt?.version ?? '…'} · Etappe W-1 Laufwerk
</span>
<div className="flex items-baseline justify-between gap-3">
<div className="flex items-baseline gap-3">
<h1 className="text-2xl font-bold tracking-tight">Rippy</h1>
<span className="font-mono text-xs text-slate-400">v{haupt?.version ?? '…'}</span>
</div>
<nav className="flex gap-1">
{knopf('uebersicht', 'Übersicht')}
{knopf('einstellungen', 'Einstellungen')}
{knopf('bibliothek', `Bibliothek${verbindung.bibliothek.length > 0 ? ` (${verbindung.bibliothek.length})` : ''}`)}
</nav>
</div>
<p className="mt-1 text-sm text-slate-400">
Drei Prozesse, eine Datenbank, eine Leine und die Disc-Wache läuft.
</p>
</header>
<main className="flex-1 space-y-6 p-6">
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
Laufwerke
</h2>
{verbindung.laufwerke.length === 0 ? (
<p className="font-mono text-xs text-slate-500">
{verbindung.verbunden ? 'Kein optisches Laufwerk gefunden.' : 'Warte auf den Kern …'}
</p>
) : (
<div className="grid gap-4 sm:grid-cols-2">
{verbindung.laufwerke.map((geraet) => (
<LaufwerkKachel
key={geraet.id}
geraet={geraet}
rip={verbindung.rips[geraet.id]}
info={verbindung.discInfos[geraet.id]}
/>
))}
</div>
)}
</section>
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
Gerüst
</h2>
<div className="grid content-start gap-4 sm:grid-cols-2">
<Kachel
titel="Kern (utilityProcess)"
zustand={verbindung.kern !== null ? 'ok' : haupt?.kern.laeuft ? 'wartet' : 'fehler'}
zeilen={
verbindung.kern !== null
? [
`PID ${verbindung.kern.pid} · Node ${verbindung.kern.nodeVersion}`,
`Neustarts: ${haupt?.kern.neustarts ?? 0}`,
]
: ['wartet auf Meldung …']
}
/>
<Kachel
titel="Datenbank (node:sqlite)"
zustand={db === null ? 'wartet' : db.ok ? 'ok' : 'fehler'}
zeilen={db === null ? ['wartet auf Kern …'] : db.ok ? [db.pfad] : [db.fehler]}
/>
<Kachel
titel="Prozess-Leine (Job Object)"
zustand={haupt === null ? 'wartet' : haupt.leine.ok ? 'ok' : 'fehler'}
zeilen={
haupt === null
? ['wartet auf Haupt …']
: haupt.leine.ok
? ['gesetzt — Kern und Werkzeuge sterben mit Rippy']
: [haupt.leine.fehler ?? 'nicht gesetzt']
}
/>
<Kachel
titel="Fenster ↔ Kern (MessagePort)"
zustand={verbindung.pongLatenzMs !== null ? 'ok' : 'wartet'}
zeilen={
verbindung.pongLatenzMs !== null
? [`Ping → Pong in ${verbindung.pongLatenzMs} ms`]
: ['Ping unterwegs …']
}
/>
</div>
</section>
{verbindung.meldungen.length > 0 && (
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
Ereignisse
</h2>
<div className="space-y-1 rounded-xl border border-slate-700/60 bg-slate-800/40 p-4">
{verbindung.meldungen.map((zeile, i) => (
<p key={i} className="font-mono text-xs text-slate-400">
{zeile}
{bereich === 'uebersicht' && (
<>
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
Laufwerke
</h2>
{verbindung.laufwerke.length === 0 ? (
<p className="font-mono text-xs text-slate-500">
{verbindung.verbunden ? 'Kein optisches Laufwerk gefunden.' : 'Warte auf den Kern …'}
</p>
))}
</div>
</section>
) : (
<div className="grid gap-4 sm:grid-cols-2">
{verbindung.laufwerke.map((geraet) => (
<LaufwerkKachel
key={geraet.id}
geraet={geraet}
rip={verbindung.rips[geraet.id]}
info={verbindung.discInfos[geraet.id]}
/>
))}
</div>
)}
</section>
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
Status
</h2>
<div className="grid content-start gap-4 sm:grid-cols-2">
<Kachel
titel="Kern (utilityProcess)"
zustand={verbindung.kern !== null ? 'ok' : haupt?.kern.laeuft ? 'wartet' : 'fehler'}
zeilen={
verbindung.kern !== null
? [
`PID ${verbindung.kern.pid} · Node ${verbindung.kern.nodeVersion}`,
`Neustarts: ${haupt?.kern.neustarts ?? 0}`,
]
: ['wartet auf Meldung …']
}
/>
<Kachel
titel="Datenbank (node:sqlite)"
zustand={db === null ? 'wartet' : db.ok ? 'ok' : 'fehler'}
zeilen={db === null ? ['wartet auf Kern …'] : db.ok ? [db.pfad] : [db.fehler]}
/>
<Kachel
titel="Prozess-Leine (Job Object)"
zustand={haupt === null ? 'wartet' : haupt.leine.ok ? 'ok' : 'fehler'}
zeilen={
haupt === null
? ['wartet auf Haupt …']
: haupt.leine.ok
? ['gesetzt — Kern und Werkzeuge sterben mit Rippy']
: [haupt.leine.fehler ?? 'nicht gesetzt']
}
/>
<Kachel
titel="Fenster ↔ Kern (MessagePort)"
zustand={verbindung.pongLatenzMs !== null ? 'ok' : 'wartet'}
zeilen={
verbindung.pongLatenzMs !== null
? [`Ping → Pong in ${verbindung.pongLatenzMs} ms`]
: ['Ping unterwegs …']
}
/>
</div>
</section>
{verbindung.meldungen.length > 0 && (
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
Ereignisse
</h2>
<div className="space-y-1 rounded-xl border border-slate-700/60 bg-slate-800/40 p-4">
{verbindung.meldungen.map((zeile, i) => (
<p key={i} className="break-all font-mono text-xs text-slate-400">
{zeile}
</p>
))}
</div>
</section>
)}
</>
)}
{bereich === 'einstellungen' && <Einstellungen verbindung={verbindung} />}
{bereich === 'bibliothek' && <Bibliothek verbindung={verbindung} />}
</main>
<footer className="border-t border-slate-700/60 px-6 py-3 text-center text-xs text-slate-500">
+1
View File
@@ -7,6 +7,7 @@ declare global {
bereit(): void
aufStatus(horcher: (status: unknown) => void): void
melden(art: string): void
ordnerWaehlen(titel: string): Promise<string | null>
}
}
}
@@ -8,10 +8,12 @@
// Laufwerksliste: Sie ändert sich NUR durch eine neue Liste vom Kern.
import {
istKernNachricht,
type BibliothekEintrag,
type DiscInfoStand,
type GeraeteInfo,
type KernNachricht,
type RipStatus,
type WerkzeugAuskunft,
} from '../gemeinsam/nachrichten'
export interface Verbindung {
@@ -24,6 +26,9 @@ export interface Verbindung {
rips: Record<string, RipStatus>
/** Was Rippy über die eingelegte Disc weiß (§ 5 Schritt 3). */
discInfos: Record<string, DiscInfoStand>
einstellungen: Record<string, string>
werkzeuge: WerkzeugAuskunft | null
bibliothek: BibliothekEintrag[]
/** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */
meldungen: string[]
}
@@ -39,6 +44,9 @@ const stand: Verbindung = {
laufwerke: [],
rips: {},
discInfos: {},
einstellungen: {},
werkzeuge: null,
bibliothek: [],
meldungen: [],
}
const horcher = new Set<Horcher>()
@@ -52,6 +60,8 @@ function melden(): void {
laufwerke: [...stand.laufwerke],
rips: { ...stand.rips },
discInfos: { ...stand.discInfos },
einstellungen: { ...stand.einstellungen },
bibliothek: [...stand.bibliothek],
meldungen: [...stand.meldungen],
})
}
@@ -103,6 +113,15 @@ function kernNachricht(nachricht: KernNachricht): void {
}
break
}
case 'einstellungen':
stand.einstellungen = nachricht.werte
break
case 'werkzeuge':
stand.werkzeuge = nachricht.auskunft
break
case 'bibliothek':
stand.bibliothek = nachricht.eintraege
break
case 'rip-status': {
stand.rips = { ...stand.rips, [nachricht.status.id]: nachricht.status }
if (nachricht.status.phase === 'fertig') {
@@ -130,6 +149,9 @@ function portAnnehmen(neuerPort: MessagePort): void {
}
neuerPort.start()
neuerPort.postMessage({ art: 'ping', zeit: Date.now() })
neuerPort.postMessage({ art: 'einstellungen-laden' })
neuerPort.postMessage({ art: 'werkzeuge-laden' })
neuerPort.postMessage({ art: 'bibliothek-laden' })
melden()
}
@@ -159,6 +181,8 @@ export function aufVerbindung(h: Horcher): () => void {
laufwerke: [...stand.laufwerke],
rips: { ...stand.rips },
discInfos: { ...stand.discInfos },
einstellungen: { ...stand.einstellungen },
bibliothek: [...stand.bibliothek],
meldungen: [...stand.meldungen],
})
return () => horcher.delete(h)
@@ -184,3 +208,12 @@ export function ripStarten(id: string): void {
export function ripAbbrechen(id: string): void {
anKern({ art: 'rip-abbruch', id }, `Rip-Abbruch ${id}`)
}
export function einstellungSetzen(schluessel: string, wert: string): void {
if (port === null) {
meldung(`⚠ Einstellung ${schluessel}: keine Verbindung zum Kern`)
melden()
return
}
port.postMessage({ art: 'einstellung-setzen', schluessel, wert })
}