WAS: presetEmpfehlung liefert für UHD das 4K-Preset der gemessenen Grafikkarten-Familie (VCN/NVENC/QSV) — sonst PRESET_KEINE (verlustfrei); komprimierenFuer folgt der Empfehlung, wenn der Typ auf „automatisch" steht. Eigene Wahl schlägt beides. Fenster: Kachel, Kompressions-Karte und Erst-Einrichtung sagen „(8 Bit)" bzw. „verlustfrei — nur CPU-Encoder". Gemessen am 12.09.2026 (HandBrake 1.11.2, RX 9070 XT, Akira-UHD-Abbild): die 4K-Hardware-Presets sind 8-Bit (vce_h265, Profil main); der 10-Bit- HEVC-Encoder vce_h265_10bit stürzt beim Start ab (Segfault, mit Profil auto und main10); AV1 10-Bit („AV1 VCN 2160p 4K", vce_av1_10bit) läuft — steht als eigene Wahl in der Liste, mit dem Hinweis auf AV1-fähige Abspielgeräte. WARUM: Durchsicht 12.09.2026, A4 — Commander-Entscheid: „Rippy analysiert doch die Performance des PCs, könnten wir das nicht davon abhängig machen?" Vorher rechnete der CPU-Default 4K auf 1080p klein, tagelang. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
479 lines
24 KiB
TypeScript
479 lines
24 KiB
TypeScript
// 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 (
|
|
<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" />
|
|
<div className="absolute inset-0 bg-gradient-to-r from-buehne via-buehne/85 to-buehne/45" />
|
|
<div className="absolute inset-0 bg-gradient-to-t from-buehne via-transparent to-transparent" />
|
|
</>
|
|
)}
|
|
|
|
<div className="relative p-5">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-center gap-2.5 font-mono text-[10.5px] tracking-[0.12em] text-nebel">
|
|
<span
|
|
className={`inline-block h-2 w-2 rounded-full ${
|
|
ripLaeuft
|
|
? 'animate-pulse bg-rec shadow-[0_0_9px_oklch(0.72_0.17_45/0.9)]'
|
|
: discDa
|
|
? 'bg-gruen'
|
|
: geraet.status === 'empty'
|
|
? 'bg-tinte/25'
|
|
: 'bg-rot'
|
|
}`}
|
|
/>
|
|
<span className="text-dunst">LAUFWERK {geraet.id}:</span>
|
|
<span>·</span>
|
|
<span className="truncate">{geraet.modell.length > 0 ? geraet.modell : 'MODELL UNBEKANNT'}</span>
|
|
</div>
|
|
<div className="flex flex-none gap-2">
|
|
{ripLaeuft ? (
|
|
<button
|
|
onClick={() => ripAbbrechen(geraet.id)}
|
|
className="inline-flex items-center gap-2 rounded-xl border border-rot/60 bg-rot/10 px-4 py-1.5 text-xs font-semibold text-rot hover:bg-rot/20"
|
|
>
|
|
<IconStop />
|
|
Abbrechen
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={aufRippen}
|
|
disabled={!discDa}
|
|
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-b from-gold-hell to-gold-tief px-5 py-1.5 text-xs font-bold text-buehne shadow-[inset_0_1px_0_oklch(1_0_0/0.4),0_8px_24px_oklch(0.75_0.13_60/0.3)] hover:from-gold-hell hover:to-gold disabled:cursor-not-allowed disabled:opacity-35 disabled:shadow-none"
|
|
title="Titel der Disc lesen und auswählen — danach verlustfrei rippen"
|
|
>
|
|
<IconPlay />
|
|
Rippen
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => auswerfen(geraet.id)}
|
|
disabled={!discDa || ripLaeuft}
|
|
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 />
|
|
Auswerfen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{!discDa && (
|
|
<div className="mt-5 flex items-center gap-5 py-5">
|
|
<IconDisc groesse={52} farbe={geraet.status === 'empty' ? 'oklch(0.4 0.02 75)' : 'oklch(0.68 0.19 25)'} />
|
|
<p className={`max-w-2xl font-mono text-xs leading-5 ${geraet.status === 'empty' ? 'text-nebel' : 'text-rot'}`}>
|
|
{geraet.status === 'empty' ? 'Keine Disc eingelegt — Disc rein genügt, Rippy meldet sich.' : geraet.grund}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{discDa && (
|
|
<div className="mt-4 flex gap-5">
|
|
{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-tinte/10">
|
|
<IconDisc groesse={40} farbe="oklch(0.5 0.05 75)" />
|
|
</div>
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
{/* 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 ? (
|
|
<div className="flex flex-wrap items-end gap-x-3 gap-y-1">
|
|
<img src={logo} alt={info.titel} className="max-h-16 max-w-[380px] object-contain object-left drop-shadow-[0_2px_12px_rgba(0,0,0,0.75)]" />
|
|
<span className="pb-0.5 text-sm text-dunst">
|
|
{info.titel}
|
|
{info.jahr !== null && ` (${info.jahr})`}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<h3 className="truncate font-display text-[26px] font-bold leading-tight text-schnee drop-shadow">
|
|
{info.titel}
|
|
{info.jahr !== null && <span className="ml-2 font-medium text-dunst">({info.jahr})</span>}
|
|
</h3>
|
|
)
|
|
) : (
|
|
<h3 className="font-display text-[26px] font-bold leading-tight text-schnee">{TYP_NAMEN[geraet.typ]}</h3>
|
|
)}
|
|
{erkannt && info.tagline.length > 0 && <p className="mt-1 font-display text-[15px] italic text-gold-hell/90">{info.tagline}</p>}
|
|
{erkannt && metaZeile(info).length > 0 && <p className="mt-1 font-mono text-[11px] tracking-wide text-dunst">{metaZeile(info)}</p>}
|
|
{erkannt && info.beschreibung.length > 0 && <p className="mt-1.5 line-clamp-2 max-w-2xl text-xs leading-5 text-dunst">{info.beschreibung}</p>}
|
|
{!erkannt && (
|
|
<p className="mt-1.5 font-mono text-xs text-nebel">
|
|
{geraet.typ === 'cd' ? 'Titel kommen beim Rippen von MusicBrainz.' : 'Noch keine Zuordnung — „Ändern …" hilft nach.'}
|
|
</p>
|
|
)}
|
|
|
|
{/* Zwei Kacheln nebeneinander im schmalen Fenster, vier im
|
|
breiten — vorher immer vier, also im schmalen gequetscht. */}
|
|
<div className="mt-3 grid max-w-3xl grid-cols-2 gap-2.5 sm:grid-cols-4">
|
|
<InfoKachel label="MEDIUM" wert={TYP_NAMEN[geraet.typ]} />
|
|
<InfoKachel label="GRÖSSE" wert={geraet.groesseBytes > 0 ? gb(geraet.groesseBytes) : '—'} />
|
|
<InfoKachel
|
|
label="ERKENNUNG"
|
|
wert={erkannt ? `${Math.round(info.confidence * 100)} %${info.quelle.length > 0 ? ` · ${info.quelle}` : ''}` : '—'}
|
|
farbe={erkannt ? (info.confidence >= 0.8 ? 'text-gruen' : 'text-warn') : undefined}
|
|
/>
|
|
<InfoKachel label="PRESET" wert={presetAnzeige(geraet.typ, verbindung)} />
|
|
</div>
|
|
|
|
{/* Darsteller-Streifen (5.4.0) */}
|
|
{erkannt && info.besetzung.length > 0 && (
|
|
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-2">
|
|
{info.besetzung.map((d) => (
|
|
<div key={`${d.name}-${d.rolle}`} className="flex items-center gap-2" title={d.rolle}>
|
|
{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-tinte/10">
|
|
{d.name
|
|
.split(' ')
|
|
.map((w) => w[0] ?? '')
|
|
.join('')
|
|
.slice(0, 2)
|
|
.toUpperCase()}
|
|
</span>
|
|
)}
|
|
<span className="leading-tight">
|
|
<span className="block text-[11px] text-schnee">{d.name}</span>
|
|
{d.rolle.length > 0 && <span className="block text-[10px] text-nebel">{d.rolle}</span>}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
<button
|
|
onClick={aufAendern}
|
|
disabled={ripLaeuft}
|
|
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 />
|
|
Ändern …
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setBerichtOffen(true)
|
|
laufwerkPruefen(geraet.id)
|
|
}}
|
|
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
|
|
</button>
|
|
{rettungMoeglich && (
|
|
<button
|
|
onClick={() => rettungStarten(geraet.id)}
|
|
className="inline-flex items-center gap-2 rounded-lg border border-warn/50 bg-warn/12 px-3 py-1 text-xs font-semibold text-warn hover:bg-warn/20"
|
|
title="Rippy hat an dieser Disc Lesefehler gesehen: erst ein Abbild Sektor für Sektor (Unlesbares mit Nullen), dann derselbe Rip aus dem Abbild — es kann nicht mehr hängen. Braucht doppelten Platz."
|
|
>
|
|
Rettungs-Abbild …
|
|
</button>
|
|
)}
|
|
{erkannt && <Badge text={`${sicherheitsWort(info.confidence)}`} ton={info.confidence >= 0.8 ? 'gruen' : 'gelb'} />}
|
|
{info !== undefined && discHerkunft(info).length > 0 && <Badge text={discHerkunft(info)} ton="blau" />}
|
|
{info !== undefined && info.schonGerippt !== null && <Badge text={`schon gerippt am ${datumKurz(info.schonGerippt).slice(0, 10)}`} ton="gelb" />}
|
|
</div>
|
|
{/* Was Rippy NACH dem Info-Lauf dazugelernt hat (Erkennung v2). */}
|
|
{erkannt && info.hinweise.length > 0 && (
|
|
<div className="mt-2 space-y-0.5">
|
|
{info.hinweise.map((h) => (
|
|
<p
|
|
key={h}
|
|
className={`font-mono text-[11px] ${h.includes('NICHT') ? 'text-warn' : h.includes('bestätigt') || h.includes('passt') ? 'text-gruen' : 'text-nebel'}`}
|
|
>
|
|
{h.includes('NICHT') ? '⚠ ' : h.includes('bestätigt') ? '✓ ' : '· '}
|
|
{h}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
{geraet.grund.length > 0 && <p className="mt-2 font-mono text-xs text-warn">{geraet.grund}</p>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 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-tinte/10 bg-samt/60'}`}>
|
|
{automatik.aktiv && (
|
|
<span className="font-mono text-[28px] font-bold leading-none text-gold-hell">
|
|
{automatik.sekunden}
|
|
<span className="text-sm text-gold/70"> s</span>
|
|
</span>
|
|
)}
|
|
<p className={`min-w-0 flex-1 font-mono text-[11px] leading-5 ${automatik.aktiv ? 'text-gold-hell' : 'text-nebel'}`}>{automatik.text}</p>
|
|
{automatik.aktiv && (
|
|
<div className="flex gap-2">
|
|
<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-tinte/15 px-3 py-1 text-[11px] text-dunst hover:text-schnee">
|
|
Stopp
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 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') && (
|
|
<div className={`mt-4 rounded-xl border p-3 ${berichtTon}`}>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<p className="font-mono text-[11px] leading-5">
|
|
<span className="font-semibold">
|
|
{bericht.urteil === 'ruhig' ? 'LAUFWERK RUHIG' : bericht.urteil === 'disc' ? 'VERDACHT: DIE DISC' : bericht.urteil === 'hardware' ? 'VERDACHT: DIE HARDWARE' : 'PROTOKOLL'}
|
|
</span>{' '}
|
|
— {bericht.text}
|
|
</p>
|
|
<button onClick={() => setBerichtOffen(false)} className="flex-none font-mono text-[10.5px] text-nebel hover:text-dunst">
|
|
ausblenden
|
|
</button>
|
|
</div>
|
|
{bericht.letzte.length > 0 && (
|
|
<div className="mt-1.5 space-y-0.5">
|
|
{bericht.letzte.map((z, i) => (
|
|
<p key={i} className="font-mono text-[10.5px] text-nebel">
|
|
{z}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{rip !== undefined && (
|
|
<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">
|
|
<span
|
|
className={`font-mono text-[10.5px] tracking-[0.12em] ${
|
|
rip.phase === 'fehler' ? 'text-rot' : rip.phase === 'fertig' ? (rip.lesefehler ? 'text-warn' : 'text-gruen') : 'text-gold-hell'
|
|
}`}
|
|
>
|
|
{PHASEN_MONO[rip.phase]}
|
|
{rip.lesefehler && rip.phase === 'fertig' ? ' — MIT LESEFEHLERN' : ''}
|
|
</span>
|
|
{ripLaeuft && <span className="font-mono text-[10.5px] text-nebel">{restzeitText(rip.prozent, ripStartMs)}</span>}
|
|
</div>
|
|
{/* 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}
|
|
</p>
|
|
)}
|
|
{rip.rettung !== undefined && (
|
|
<p className="mt-1.5 font-mono text-[10.5px] text-warn">
|
|
Rettungs-Abbild: {rip.rettung.unlesbareSektoren} unlesbare Sektoren an {rip.rettung.stellen} Stellen mit Nullen gefüllt
|
|
</p>
|
|
)}
|
|
|
|
{/* 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 && (
|
|
<div className="mt-3 rounded-xl border border-warn/45 bg-warn/10 p-3">
|
|
<p className="font-mono text-[11px] font-semibold text-warn">Die Disc hat an einer Stelle einen Defekt.</p>
|
|
<p className="mt-1 font-mono text-[11px] leading-5 text-dunst">
|
|
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.
|
|
</p>
|
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
<button
|
|
onClick={() => ripUeberspringen(geraet.id)}
|
|
className="rounded-lg border border-warn/50 bg-warn/15 px-3 py-1 text-[11px] font-semibold text-warn hover:bg-warn/25"
|
|
title="Diesen Titel aufgeben und mit dem nächsten weitermachen"
|
|
>
|
|
Titel überspringen
|
|
</button>
|
|
<span className="font-mono text-[10.5px] text-nebel">oder weiter versuchen — abbrechen geht oben.</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<p className="mt-2 truncate font-mono text-[11px] text-nebel" title={rip.text}>
|
|
> {rip.text}
|
|
</p>
|
|
{rip.dateien.map((datei) => (
|
|
<p key={datei} className="break-all font-mono text-[11px] text-nebel/80">
|
|
{datei}
|
|
</p>
|
|
))}
|
|
|
|
{/* 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 && (
|
|
<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 && (
|
|
<div className="flex-none font-mono text-[42px] font-bold leading-none text-gold-hell">
|
|
{rip.prozent}
|
|
<span className="text-lg text-gold/70">%</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{protokoll.length > 1 && (
|
|
<button onClick={() => setProtokollOffen((o) => !o)} className="mt-2 font-mono text-[10.5px] text-nebel hover:text-dunst">
|
|
{protokollOffen ? '▾ PROTOKOLL AUSBLENDEN' : `▸ PROTOKOLL (${protokoll.length} ZEILEN)`}
|
|
</button>
|
|
)}
|
|
{protokollOffen && (
|
|
<div className="mt-1 max-h-40 space-y-0.5 overflow-y-auto rounded-lg bg-buehne p-2">
|
|
{protokoll.map((zeile, i) => (
|
|
<p key={i} className="break-all font-mono text-[10.5px] leading-4 text-nebel">
|
|
> {zeile}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|