feat(v5): 5.6.0 — Vorgangs-Leiste, mehrere Filme je Disc, Filmnamen für Ordner und Dateien, Untertitel-Vorwahl je Extra, Restzeit, Hell-Modus

Die sechs Punkte des Commanders vom 02.09.2026 abends und seine Entscheide:

* Restzeit der Kompression aus HandBrakes eigener ETA-Klammer (Formatstring
  aus dem Binary 1.11.2 gelesen), KompressionStand.restS.
* Kachel zeigt nach dem Rip nur noch eine Zeile — Balken, Prozent, Ziel in
  der Kompressions-Karte und im Verlauf ("nur in der Kompressions-Karte").
* Kopfzeile bleibt beim Scrollen stehen (sticky), Sektionswahl darunter.
* Untertitel-Vorwahl je Extra ("NUR für Extras, IMMER OPTIONAL"): welche
  Untertitel-Spur beim Abspielen an ist — weiche Spur, nie eingebrannt;
  RipAuftragTitel/RohDatei.untertitelVorwahl, sprachwahl.
* Mehrere Filme je Disc: "anderer Film …" je Titel mit TMDb-Suche;
  filme.ts filmGruppen trennt am Rip-Ende in eigene Roh-Ordner, Vorgänge
  und Kompressions-Aufträge mit voller Zuordnung (erkennung.zuordnungAus).
* Roh-Ordner und Dateien tragen den Filmnamen (filme.ts rohOrdnerName,
  ablage/dateinamen.ts: Akira (1988).mkv, - Fassung n, - Extra nn).
* Hell-Modus: Einstellung design (dunkel/hell/system), data-design auf
  <html>, Token --color-tinte statt white/-Alpha-Klassen.
* Vorgangs-Leiste (Commander: "Phase 1–4 plus Komprimierung — zu einem
  machen"): gemeinsam/vorgangsleiste.ts + uebersicht/VorgangsLeiste.tsx,
  vier Stationen (Rettung als Zwischenstation), Segment-Balken in der
  aktiven Station; Kachel, Kompressions-Karte, Verlauf. PHASE-x/4-Texte weg.
* Automatik-Kasten leert sich, sobald ein Rip startet.

Beweise: 446 Tests (neu: filme, dateinamen, vorgangsleiste; erweitert:
auftrag, handbrake, automatik, ablegen), drei Typprüfungen, Klick-Beweis
33 Schritte inkl. Vorwahl, anderer Film, Vorgangs-Leiste, Restzeit,
Hell-Modus (Bilder beweise/klick/01…11).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-03 19:17:37 +02:00
co-authored by Claude Fable 5.1
parent 76ed3b337a
commit f5dab5d1c1
59 changed files with 1284 additions and 182 deletions
@@ -24,7 +24,6 @@ import {
InfoKachel,
LAUFENDE_PHASEN,
PHASEN_MONO,
SegmentBalken,
TYP_NAMEN,
backdropUrl,
discHerkunft,
@@ -46,6 +45,8 @@ import {
ripUeberspringen,
type Verbindung,
} from '../kernverbindung'
import { leisteAusRip } from '../../gemeinsam/vorgangsleiste'
import { VorgangsLeiste } from './VorgangsLeiste'
/** Welches Preset diese Disc bekäme — dieselbe Kette wie im Kern
* (eigene Wahl → Empfehlung → Default). */
@@ -113,10 +114,10 @@ export function LaufwerkKachel({
? 'text-warn border-warn/45 bg-warn/10'
: bericht.urteil === 'hardware'
? 'text-rot border-rot/45 bg-rot/10'
: 'text-dunst border-white/12 bg-samt/60'
: 'text-dunst border-tinte/12 bg-samt/60'
return (
<div className="relative overflow-hidden rounded-2xl border border-white/10 bg-karte/50">
<div className="relative overflow-hidden rounded-2xl border border-tinte/10 bg-karte/50">
{backdrop.length > 0 && (
<>
<img src={backdrop} alt="" className="absolute inset-0 h-full w-full object-cover" />
@@ -135,7 +136,7 @@ export function LaufwerkKachel({
: discDa
? 'bg-gruen'
: geraet.status === 'empty'
? 'bg-white/25'
? 'bg-tinte/25'
: 'bg-rot'
}`}
/>
@@ -166,7 +167,7 @@ export function LaufwerkKachel({
<button
onClick={() => auswerfen(geraet.id)}
disabled={!discDa || ripLaeuft}
className="inline-flex items-center gap-2 rounded-xl border border-white/15 bg-gradient-to-b from-karte/80 to-samt/80 px-4 py-1.5 text-xs text-dunst shadow-[inset_0_1px_0_oklch(1_0_0/0.07)] hover:text-schnee disabled:cursor-not-allowed disabled:opacity-35"
className="inline-flex items-center gap-2 rounded-xl border border-tinte/15 bg-gradient-to-b from-karte/80 to-samt/80 px-4 py-1.5 text-xs text-dunst shadow-[inset_0_1px_0_oklch(1_0_0/0.07)] hover:text-schnee disabled:cursor-not-allowed disabled:opacity-35"
title="Entriegeln, auswerfen, nachsehen — in dieser Reihenfolge"
>
<IconEject />
@@ -189,7 +190,7 @@ export function LaufwerkKachel({
{poster.length > 0 ? (
<img src={poster} alt="" className="h-44 w-[118px] flex-none rounded-xl object-cover shadow-2xl shadow-black/60 ring-1 ring-gold/35" />
) : (
<div className="flex h-44 w-[118px] flex-none items-center justify-center rounded-xl bg-gradient-to-br from-karte to-samt ring-1 ring-white/10">
<div className="flex h-44 w-[118px] flex-none items-center justify-center rounded-xl bg-gradient-to-br from-karte to-samt ring-1 ring-tinte/10">
<IconDisc groesse={40} farbe="oklch(0.5 0.05 75)" />
</div>
)}
@@ -244,7 +245,7 @@ export function LaufwerkKachel({
{d.bildPfad.length > 0 ? (
<img src={profilUrl(d.bildPfad)} alt="" className="h-9 w-9 rounded-full object-cover ring-1 ring-gold/30" />
) : (
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-samt font-mono text-[11px] text-nebel ring-1 ring-white/10">
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-samt font-mono text-[11px] text-nebel ring-1 ring-tinte/10">
{d.name
.split(' ')
.map((w) => w[0] ?? '')
@@ -266,7 +267,7 @@ export function LaufwerkKachel({
<button
onClick={aufAendern}
disabled={ripLaeuft}
className="inline-flex items-center gap-2 rounded-lg border border-white/15 bg-samt/60 px-3 py-1 text-xs text-dunst hover:text-schnee disabled:opacity-35"
className="inline-flex items-center gap-2 rounded-lg border border-tinte/15 bg-samt/60 px-3 py-1 text-xs text-dunst hover:text-schnee disabled:opacity-35"
title="Falsch erkannt? Selbst suchen und wählen — die Ablage folgt deiner Wahl."
>
<IconStift />
@@ -277,7 +278,7 @@ export function LaufwerkKachel({
setBerichtOffen(true)
laufwerkPruefen(geraet.id)
}}
className="inline-flex items-center gap-2 rounded-lg border border-white/15 bg-samt/60 px-3 py-1 text-xs text-dunst hover:text-schnee"
className="inline-flex items-center gap-2 rounded-lg border border-tinte/15 bg-samt/60 px-3 py-1 text-xs text-dunst hover:text-schnee"
title="Liest die cdrom-Ereignisse aus dem Windows-Protokoll: Disc oder Hardware?"
>
Laufwerk prüfen
@@ -317,7 +318,7 @@ export function LaufwerkKachel({
{/* Automatik (5.5.0): Countdown mit Stopp und Jetzt — oder der
Grund, warum Rippy wartet. */}
{discDa && !ripLaeuft && automatik !== undefined && automatik.text.length > 0 && (
<div className={`mt-4 flex flex-wrap items-center gap-3 rounded-xl border p-3 ${automatik.aktiv ? 'border-gold/45 bg-gold/10' : 'border-white/10 bg-samt/60'}`}>
<div className={`mt-4 flex flex-wrap items-center gap-3 rounded-xl border p-3 ${automatik.aktiv ? 'border-gold/45 bg-gold/10' : 'border-tinte/10 bg-samt/60'}`}>
{automatik.aktiv && (
<span className="font-mono text-[28px] font-bold leading-none text-gold-hell">
{automatik.sekunden}
@@ -330,7 +331,7 @@ export function LaufwerkKachel({
<button onClick={() => automatikJetzt(geraet.id)} className="rounded-lg border border-gold/40 bg-gold/15 px-3 py-1 text-[11px] font-semibold text-gold-hell hover:bg-gold/25">
Jetzt
</button>
<button onClick={() => automatikStopp(geraet.id)} className="rounded-lg border border-white/15 px-3 py-1 text-[11px] text-dunst hover:text-schnee">
<button onClick={() => automatikStopp(geraet.id)} className="rounded-lg border border-tinte/15 px-3 py-1 text-[11px] text-dunst hover:text-schnee">
Stopp
</button>
</div>
@@ -366,7 +367,7 @@ export function LaufwerkKachel({
)}
{rip !== undefined && (
<div className="mt-4 rounded-xl border border-white/10 bg-samt/80 p-4 backdrop-blur">
<div className="mt-4 rounded-xl border border-tinte/10 bg-samt/80 p-4 backdrop-blur">
<div className="flex items-end justify-between gap-5">
<div className="min-w-0 flex-1">
<div className="mb-2 flex items-baseline justify-between gap-3">
@@ -380,7 +381,9 @@ export function LaufwerkKachel({
</span>
{ripLaeuft && <span className="font-mono text-[10.5px] text-nebel">{restzeitText(rip.prozent, ripStartMs)}</span>}
</div>
{ripLaeuft && <SegmentBalken prozent={rip.prozent} />}
{/* 5.6.0: die Vorgangs-Leiste — der ganze Weg, hier mit den
Details der Laufwerks-Stationen; nach dem Rip nur der Stand. */}
<VorgangsLeiste stand={leisteAusRip(rip, vorgang, wartePlatz)} />
{ripLaeuft && (rip.titelGesamt ?? 0) > 1 && (
<p className="mt-1.5 font-mono text-[10.5px] text-nebel">
Titel {rip.titelNr} von {rip.titelGesamt}
@@ -425,35 +428,22 @@ export function LaufwerkKachel({
</p>
))}
{/* Nach dem Rip (5.4.0): Wo steht die Kompression? Das
Laufwerk ist schon frei — die Arbeit läuft im Hintergrund. */}
{/* Nach dem Rip (5.6.0, Commander: „Nur in der Kompressions-Karte"):
hier nur noch EINE Zeile, wo es weitergeht — Balken, Prozent,
Restzeit und Ziel stehen in der Kompressions-Karte, danach im
Verlauf. Fehler bleiben sichtbar (R4). */}
{rip.phase === 'fertig' && vorgang !== undefined && (
<div className="mt-3 rounded-lg border border-white/8 bg-buehne/60 p-2.5">
{kompression !== null ? (
<>
<p className="font-mono text-[10.5px] tracking-[0.12em] text-gold-hell">
KOMPRESSION LÄUFT{kompression.prozent >= 0 ? ` ${kompression.prozent} %` : ''}
</p>
<div className="mt-1.5">
<SegmentBalken prozent={kompression.prozent} />
</div>
<p className="mt-1.5 truncate font-mono text-[11px] text-nebel" title={kompression.text}>
&gt; {kompression.text}
</p>
</>
) : wartePlatz > 0 ? (
<p className="font-mono text-[11px] text-dunst">Kompression wartet Platz {wartePlatz} in der Warteschlange.</p>
) : vorgang.phase === 'fertig' ? (
<p className="break-all font-mono text-[11px] text-gruen">
{vorgang.komprimiert ? 'Komprimiert und abgelegt' : 'Verlustfrei abgelegt'}: {vorgang.zielOrdner}
{vorgang.fertigBytes > 0 && ` (${gb(vorgang.fertigBytes)})`}
</p>
) : vorgang.phase === 'fehler' || vorgang.phase === 'abgebrochen' ? (
<p className="break-all font-mono text-[11px] text-warn"> {vorgang.fehler.length > 0 ? vorgang.fehler : `Kompression ${vorgang.phase}`}</p>
) : (
<p className="font-mono text-[11px] text-dunst">Kompression: {vorgang.phase}</p>
)}
</div>
<p className={`mt-2 font-mono text-[11px] ${vorgang.phase === 'fehler' || vorgang.phase === 'abgebrochen' ? 'text-warn' : 'text-nebel'}`}>
{kompression !== null
? 'Kompression läuft — Stand in der Kompressions-Karte unten.'
: wartePlatz > 0
? `Kompression wartet — Platz ${wartePlatz} in der Warteschlange (Karte unten).`
: vorgang.phase === 'fertig'
? 'Kompression fertig — das Ergebnis steht im Verlauf.'
: vorgang.phase === 'fehler' || vorgang.phase === 'abgebrochen'
? `${vorgang.fehler.length > 0 ? vorgang.fehler : `Kompression ${vorgang.phase}`}`
: `Kompression: ${vorgang.phase}`}
</p>
)}
</div>
{ripLaeuft && rip.prozent >= 0 && (