// Die Bühne: Backdrop als Panorama, Poster mit Goldkante, Plakat-Titel — // und beim Arbeiten die Aussteuerungsanzeige samt Mono-Protokoll. // 5.4.0: Kino-Banner v2 (Logo, Tagline, Meta-Zeile, Darsteller), Laufwerks- // Gesundheit, Stand der Kompression nach dem Rip. 5.5.0: Automatik-Countdown // und das Rettungs-Abbild — nur, wenn Rippy Lesefehler gesehen hat. import { useState } from 'react' import type { AutomatikStand, DiscInfoStand, GeraeteInfo, KompressionStand, LaufwerkBericht, RipStatus, VorgangEintrag, } from '../../gemeinsam/nachrichten' import { DEFAULT_HB_PRESET, PRESET_KEINE, empfehlungText, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung' import { Badge, IconDisc, IconEject, IconPlay, IconStift, IconStop, InfoKachel, LAUFENDE_PHASEN, PHASEN_MONO, TYP_NAMEN, backdropUrl, datumKurz, discHerkunft, gb, logoUrl, metaZeile, posterUrlGross, profilUrl, restzeitText, sicherheitsWort, } from '../bausteine' import { automatikJetzt, automatikStopp, auswerfen, laufwerkPruefen, rettungStarten, ripAbbrechen, 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). */ export function presetAnzeige(typ: GeraeteInfo['typ'], verbindung: Verbindung): string { if (typ === 'cd') return 'FLAC (verlustfrei)' if (typ === 'unknown') return 'ISO-Abbild' const werte = verbindung.einstellungen if (werte['transcodeEnabled'] === 'false') return 'ohne Kompression' const schluessel = typ === 'dvd' ? 'transcodePresetDvd' : typ === 'uhd' ? 'transcodePresetUhd' : 'transcodePresetBluray' const eigene = werte[schluessel] ?? '' if (eigene === PRESET_KEINE) return 'ohne Kompression' if (eigene.length > 0) return eigene const empfehlung = presetEmpfehlung(typ, verbindung.werkzeuge?.backends ?? [], verbindung.werkzeuge?.presets ?? []) if (empfehlung === PRESET_KEINE) return typ === 'uhd' ? 'verlustfrei (nur CPU-Encoder)' : 'ohne Kompression' return empfehlungText(typ, empfehlung.length > 0 ? empfehlung : DEFAULT_HB_PRESET) } export function LaufwerkKachel({ geraet, rip, info, ripStartMs, protokoll, verbindung, bericht, vorgang, kompression, wartePlatz, automatik, aufRippen, aufAendern, }: { geraet: GeraeteInfo rip: RipStatus | undefined info: DiscInfoStand | undefined ripStartMs: number | undefined protokoll: string[] verbindung: Verbindung /** Was das Windows-Protokoll zu diesem Laufwerk sagt (5.4.0). */ bericht: LaufwerkBericht | undefined /** Der Vorgang des letzten Rips — für den Stand der Kompression (5.4.0). */ vorgang: VorgangEintrag | undefined kompression: KompressionStand | null wartePlatz: number /** Der Automatik-Countdown (5.5.0). */ automatik: AutomatikStand | undefined /** Die Dialoge leben im App-Zustand (5.4.0): Ein leerer Takt der Wache * räumte sonst Kachel UND Dialog ab. */ aufRippen: () => void aufAendern: () => void }) { const [protokollOffen, setProtokollOffen] = useState(false) const [berichtOffen, setBerichtOffen] = useState(false) const ripLaeuft = rip !== undefined && LAUFENDE_PHASEN.includes(rip.phase) const discDa = geraet.status === 'ready' const backdrop = discDa && info !== undefined ? backdropUrl(info.backdropPfad) : '' const poster = discDa && info !== undefined ? posterUrlGross(info.posterPfad) : '' const erkannt = discDa && info !== undefined && info.titel.length > 0 const logo = erkannt ? logoUrl(info.logoPfad) : '' const rettungMoeglich = discDa && !ripLaeuft && rip !== undefined && rip.rettungMoeglich === true const berichtTon = bericht === undefined ? '' : bericht.urteil === 'ruhig' ? 'text-gruen border-gruen/40 bg-gruen/10' : bericht.urteil === 'disc' ? 'text-warn border-warn/45 bg-warn/10' : bericht.urteil === 'hardware' ? 'text-rot border-rot/45 bg-rot/10' : 'text-dunst border-tinte/12 bg-samt/60' return (
{backdrop.length > 0 && ( <>
)}
LAUFWERK {geraet.id}: · {geraet.modell.length > 0 ? geraet.modell : 'MODELL UNBEKANNT'}
{ripLaeuft ? ( ) : ( )}
{!discDa && (

{geraet.status === 'empty' ? 'Keine Disc eingelegt — Disc rein genügt, Rippy meldet sich.' : geraet.grund}

)} {discDa && (
{poster.length > 0 ? ( ) : (
)}
{/* Kino-Banner v2 (5.4.0): Logo statt Text, wo TMDb eines hat — der größte optische Sprung. Titel und Jahr bleiben lesbar. */} {erkannt ? ( logo.length > 0 ? (
{info.titel} {info.titel} {info.jahr !== null && ` (${info.jahr})`}
) : (

{info.titel} {info.jahr !== null && ({info.jahr})}

) ) : (

{TYP_NAMEN[geraet.typ]}

)} {erkannt && info.tagline.length > 0 &&

{info.tagline}

} {erkannt && metaZeile(info).length > 0 &&

{metaZeile(info)}

} {erkannt && info.beschreibung.length > 0 &&

{info.beschreibung}

} {!erkannt && (

{geraet.typ === 'cd' ? 'Titel kommen beim Rippen von MusicBrainz.' : 'Noch keine Zuordnung — „Ändern …" hilft nach.'}

)} {/* Zwei Kacheln nebeneinander im schmalen Fenster, vier im breiten — vorher immer vier, also im schmalen gequetscht. */}
0 ? gb(geraet.groesseBytes) : '—'} /> 0 ? ` · ${info.quelle}` : ''}` : '—'} farbe={erkannt ? (info.confidence >= 0.8 ? 'text-gruen' : 'text-warn') : undefined} />
{/* Darsteller-Streifen (5.4.0) */} {erkannt && info.besetzung.length > 0 && (
{info.besetzung.map((d) => (
{d.bildPfad.length > 0 ? ( ) : ( {d.name .split(' ') .map((w) => w[0] ?? '') .join('') .slice(0, 2) .toUpperCase()} )} {d.name} {d.rolle.length > 0 && {d.rolle}}
))}
)}
{rettungMoeglich && ( )} {erkannt && = 0.8 ? 'gruen' : 'gelb'} />} {info !== undefined && discHerkunft(info).length > 0 && } {info !== undefined && info.schonGerippt !== null && }
{/* Was Rippy NACH dem Info-Lauf dazugelernt hat (Erkennung v2). */} {erkannt && info.hinweise.length > 0 && (
{info.hinweise.map((h) => (

{h.includes('NICHT') ? '⚠ ' : h.includes('bestätigt') ? '✓ ' : '· '} {h}

))}
)} {geraet.grund.length > 0 &&

{geraet.grund}

}
)} {/* Automatik (5.5.0): Countdown mit Stopp und Jetzt — oder der Grund, warum Rippy wartet. */} {discDa && !ripLaeuft && automatik !== undefined && automatik.text.length > 0 && (
{automatik.aktiv && ( {automatik.sekunden} s )}

{automatik.text}

{automatik.aktiv && (
)}
)} {/* Laufwerks-Gesundheit (5.4.0, Punkt 8): Disc oder Hardware? Kommt von selbst nach einem gescheiterten Rip — oder auf Knopfdruck. */} {bericht !== undefined && (berichtOffen || bericht.urteil !== 'ruhig') && (

{bericht.urteil === 'ruhig' ? 'LAUFWERK RUHIG' : bericht.urteil === 'disc' ? 'VERDACHT: DIE DISC' : bericht.urteil === 'hardware' ? 'VERDACHT: DIE HARDWARE' : 'PROTOKOLL'} {' '} — {bericht.text}

{bericht.letzte.length > 0 && (
{bericht.letzte.map((z, i) => (

{z}

))}
)}
)} {rip !== undefined && (
{PHASEN_MONO[rip.phase]} {rip.lesefehler && rip.phase === 'fertig' ? ' — MIT LESEFEHLERN' : ''} {ripLaeuft && {restzeitText(rip.prozent, ripStartMs)}}
{/* 5.6.0: die Vorgangs-Leiste — der ganze Weg, hier mit den Details der Laufwerks-Stationen; nach dem Rip nur der Stand. */} {ripLaeuft && (rip.titelGesamt ?? 0) > 1 && (

Titel {rip.titelNr} von {rip.titelGesamt}

)} {rip.rettung !== undefined && (

Rettungs-Abbild: {rip.rettung.unlesbareSektoren} unlesbare Sektoren an {rip.rettung.stellen} Stellen mit Nullen gefüllt

)} {/* Der Defekt-Waechter (§ 6.2): Dieselbe Stelle wieder und wieder ist kein Fortschritt, sondern ein physischer Defekt. Vorher lief MakeMKV dort stundenlang weiter, waehrend identische Zeilen durchs Protokoll rauschten. */} {rip.haengt != null && (

Die Disc hat an einer Stelle einen Defekt.

MakeMKV versucht es seit {rip.haengt.wiederholungen} Meldungen immer wieder an derselben Stelle (Byte {rip.haengt.offset}). Das wird nicht besser — der Rest der Disc ist davon nicht betroffen. Nach dem Abbruch bietet Rippy das Rettungs-Abbild an.

oder weiter versuchen — abbrechen geht oben.
)}

> {rip.text}

{rip.dateien.map((datei) => (

{datei}

))} {/* 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 && (

{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}`}

)}
{ripLaeuft && rip.prozent >= 0 && (
{rip.prozent} %
)}
{protokoll.length > 1 && ( )} {protokollOffen && (
{protokoll.map((zeile, i) => (

> {zeile}

))}
)}
)}
) }