0.12.0-16 misc fixes

This commit is contained in:
2026-03-30 14:37:23 +00:00
parent 76b020d5c0
commit 5284dd2a43
30 changed files with 6955 additions and 896 deletions
+374 -115
View File
@@ -300,6 +300,18 @@ function resolveMediaType(job) {
if (['audiobook', 'audio_book', 'audio book', 'book'].includes(raw)) {
return 'audiobook';
}
if (raw === 'converter') {
return 'converter';
}
}
if (String(encodePlan?.mediaProfile || '').trim().toLowerCase() === 'converter') {
return 'converter';
}
if (String(job?.media_type || '').trim().toLowerCase() === 'converter') {
return 'converter';
}
if (resolveConverterMediaType(job)) {
return 'converter';
}
const statusCandidates = [job?.status, job?.last_state, job?.makemkvInfo?.lastState];
if (statusCandidates.some((v) => String(v || '').trim().toUpperCase().startsWith('CD_'))) {
@@ -325,6 +337,26 @@ function resolveMediaType(job) {
return 'other';
}
function resolveConverterMediaType(job) {
const encodePlan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : null;
const candidates = [
encodePlan?.converterMediaType,
job?.converterMediaType,
job?.makemkvInfo?.converterMediaType,
job?.mediainfoInfo?.converterMediaType
];
for (const candidate of candidates) {
const raw = String(candidate || '').trim().toLowerCase();
if (!raw) {
continue;
}
if (raw === 'audio' || raw === 'video' || raw === 'iso') {
return raw;
}
}
return null;
}
function resolveCdDetails(job) {
const encodePlan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : {};
const makemkvInfo = job?.makemkvInfo && typeof job.makemkvInfo === 'object' ? job.makemkvInfo : {};
@@ -595,10 +627,12 @@ export default function JobDetailDialog({
onDownloadArchive,
onDownloadOutputFolder,
onRemoveFromQueue,
onCancel,
isQueued = false,
omdbAssignBusy = false,
cdMetadataAssignBusy = false,
actionBusy = false,
cancelBusy = false,
reencodeBusy = false,
deleteEntryBusy = false,
downloadBusyTarget = null,
@@ -609,11 +643,19 @@ export default function JobDetailDialog({
const isUnencodedOrphanImport = Boolean(
job?.makemkvInfo?.source === 'orphan_raw_import' && !job?.handbrakeInfo
);
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_RIPPING', 'CD_ENCODING'].includes(job?.status);
const statusUpper = String(job?.status || '').trim().toUpperCase();
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_RIPPING', 'CD_ENCODING'].includes(statusUpper);
const softCancelable = ['READY_TO_START', 'READY_TO_ENCODE', 'METADATA_SELECTION', 'WAITING_FOR_USER_DECISION', 'CD_METADATA_SELECTION', 'CD_READY_TO_RIP'].includes(statusUpper);
const showCancelAction = (running || softCancelable) && typeof onCancel === 'function';
const showFinalLog = !running;
const mediaType = resolveMediaType(job);
const isCd = mediaType === 'cd';
const isAudiobook = mediaType === 'audiobook';
const isConverter = mediaType === 'converter';
const converterMediaType = isConverter ? (resolveConverterMediaType(job) || 'video') : null;
const converterMediaTypeLabel = converterMediaType === 'audio'
? 'Audio'
: (converterMediaType === 'iso' ? 'ISO (Video)' : 'Video');
const cdRawPathCandidates = [
job?.raw_path,
job?.makemkvInfo?.rawPath,
@@ -704,6 +746,62 @@ export default function JobDetailDialog({
&& mediaType !== 'audiobook'
&& typeof onRestartReview === 'function'
);
const converterPlan = isConverter && job?.encodePlan && typeof job.encodePlan === 'object'
? job.encodePlan
: {};
const converterMetadata = isConverter && converterPlan?.metadata && typeof converterPlan.metadata === 'object'
? converterPlan.metadata
: {};
const converterInputPaths = isConverter
? (
Array.isArray(converterPlan?.inputPaths) && converterPlan.inputPaths.length > 0
? converterPlan.inputPaths
: [converterPlan?.inputPath || job?.raw_path]
)
.map((value) => String(value || '').trim())
.filter(Boolean)
: [];
const converterTrackList = isConverter && Array.isArray(converterPlan?.tracks)
? converterPlan.tracks
: [];
const converterOutputFormat = String(
converterPlan?.outputFormat
|| job?.handbrakeInfo?.format
|| converterPlan?.format
|| ''
).trim().toLowerCase();
const converterPresetLabel = String(
converterPlan?.userPreset?.name
|| converterPlan?.userPreset?.handbrakePreset
|| ''
).trim() || null;
const converterAudioQualityLabel = isConverter && converterMediaType === 'audio'
? (() => {
const opts = converterPlan?.audioFormatOptions && typeof converterPlan.audioFormatOptions === 'object'
? converterPlan.audioFormatOptions
: {};
if (converterOutputFormat === 'flac') {
return `Kompression ${Number(opts?.flacCompression ?? 5)}`;
}
if (converterOutputFormat === 'mp3') {
const mode = String(opts?.mp3Mode || 'cbr').trim().toLowerCase();
if (mode === 'vbr') {
return `VBR V${Number(opts?.mp3Quality ?? 4)}`;
}
return `CBR ${Number(opts?.mp3Bitrate ?? 192)} kbps`;
}
if (converterOutputFormat === 'aac') {
return `${Number(opts?.aacBitrate ?? 256)} kbps`;
}
if (converterOutputFormat === 'opus') {
return `${Number(opts?.opusBitrate ?? 160)} kbps`;
}
if (converterOutputFormat === 'ogg') {
return `Qualität ${Number(opts?.oggQuality ?? 6)}`;
}
return converterOutputFormat ? converterOutputFormat.toUpperCase() : '-';
})()
: null;
const canDeleteEntry = !running && typeof onDeleteEntry === 'function';
const queueLocked = Boolean(isQueued && job?.id);
const logCount = Number(job?.log_count || 0);
@@ -719,7 +817,7 @@ export default function JobDetailDialog({
? 'DVD'
: isCd
? 'Audio CD'
: (isAudiobook ? 'Audiobook' : 'Sonstiges Medium');
: (isAudiobook ? 'Audiobook' : (isConverter ? `Converter ${converterMediaTypeLabel}` : 'Sonstiges Medium'));
const mediaTypeIcon = mediaType === 'bluray'
? blurayIndicatorIcon
: mediaType === 'dvd'
@@ -742,6 +840,11 @@ export default function JobDetailDialog({
const canDownloadOutput = Boolean(job?.output_path && job?.outputStatus?.exists && typeof onDownloadArchive === 'function');
const outputFolders = Array.isArray(job?.outputFolders) ? job.outputFolders : [];
const hasAnyOutputFolder = outputFolders.length > 0 || Boolean(job?.outputStatus?.exists);
const canShowGeneralEncodeActions = canResumeReady
|| typeof onRestartEncode === 'function'
|| typeof onRestartReview === 'function'
|| typeof onReencode === 'function';
const useAudioPosterLayout = isCd || isAudiobook || (isConverter && converterMediaType === 'audio');
return (
<Dialog
@@ -759,9 +862,9 @@ export default function JobDetailDialog({
<div className="job-head-row">
{job.poster_url && job.poster_url !== 'N/A' ? (
<img src={job.poster_url} alt={job.title || 'Poster'} className={isCd || isAudiobook ? 'poster-large-audio' : 'poster-large'} />
<img src={job.poster_url} alt={job.title || 'Poster'} className={useAudioPosterLayout ? 'poster-large-audio' : 'poster-large'} />
) : (
<div className={`${isCd || isAudiobook ? 'poster-large-audio' : 'poster-large'} poster-fallback`}>{isCd || isAudiobook ? 'Kein Cover' : 'Kein Poster'}</div>
<div className={`${useAudioPosterLayout ? 'poster-large-audio' : 'poster-large'} poster-fallback`}>{useAudioPosterLayout ? 'Kein Cover' : 'Kein Poster'}</div>
)}
<div className="job-film-info-grid">
@@ -812,6 +915,70 @@ export default function JobDetailDialog({
</div>
</div>
</section>
) : isConverter ? (
<section className="job-meta-block job-meta-block-film">
<h4>{converterMediaType === 'audio' ? 'Converter Audio' : 'Converter Video'}</h4>
<div className="job-meta-list">
<div className="job-meta-item">
<strong>Titel:</strong>
<span>{job.title || converterMetadata?.albumTitle || (job?.id ? `Job #${job.id}` : '-')}</span>
</div>
<div className="job-meta-item">
<strong>Typ:</strong>
<span>{converterMediaTypeLabel}</span>
</div>
<div className="job-meta-item">
<strong>Eingaben:</strong>
<span>{converterInputPaths.length > 0 ? `${converterInputPaths.length} Datei${converterInputPaths.length !== 1 ? 'en' : ''}` : '-'}</span>
</div>
<div className="job-meta-item">
<strong>Format:</strong>
<span>{converterOutputFormat ? converterOutputFormat.toUpperCase() : '-'}</span>
</div>
{converterMediaType === 'audio' ? (
<>
<div className="job-meta-item">
<strong>Album:</strong>
<span>{converterMetadata?.albumTitle || job?.title || (job?.id ? `Job #${job.id}` : '-')}</span>
</div>
<div className="job-meta-item">
<strong>Interpret:</strong>
<span>{converterMetadata?.albumArtist || '-'}</span>
</div>
<div className="job-meta-item">
<strong>Jahr:</strong>
<span>{converterMetadata?.albumYear || job?.year || '-'}</span>
</div>
<div className="job-meta-item">
<strong>Tracks:</strong>
<span>{converterTrackList.length > 0 ? converterTrackList.length : converterInputPaths.length || '-'}</span>
</div>
<div className="job-meta-item">
<strong>Qualität:</strong>
<span>{converterAudioQualityLabel || '-'}</span>
</div>
</>
) : (
<>
<div className="job-meta-item">
<strong>Preset:</strong>
<span>{converterPresetLabel || '-'}</span>
</div>
<div className="job-meta-item">
<strong>HandBrake Titel:</strong>
<span>{converterPlan?.handBrakeTitleId || '-'}</span>
</div>
</>
)}
<div className="job-meta-item">
<strong>Medium:</strong>
<span className="job-step-cell">
<img src={mediaTypeIcon} alt={mediaTypeAlt} title={mediaTypeLabel} className="media-indicator-icon" />
<span>{mediaTypeLabel}</span>
</span>
</div>
</div>
</section>
) : (
<>
<section className="job-meta-block job-meta-block-film">
@@ -937,7 +1104,7 @@ export default function JobDetailDialog({
</>
) : (
<>
<span><strong>{isAudiobook ? 'Import:' : 'Backup:'}</strong> <BoolState value={job?.backupSuccess} /></span>
<span><strong>{isAudiobook || isConverter ? 'Import:' : 'Backup:'}</strong> <BoolState value={job?.backupSuccess} /></span>
<span className="job-infos-sep">|</span>
<span><strong>Encode:</strong> <BoolState value={job?.encodeSuccess} /></span>
</>
@@ -950,7 +1117,7 @@ export default function JobDetailDialog({
<div><strong>Ende:</strong> {job.end_time || '-'}</div>
{/* Zeile 3+4: Pfade */}
<PathField
label={isCd ? 'WAV:' : 'RAW:'}
label={isCd ? 'WAV:' : (isConverter ? 'Input:' : 'RAW:')}
value={isCd ? (job.raw_path || job.output_path) : job.raw_path}
onDownload={canDownloadRaw ? () => onDownloadArchive?.(job, 'raw') : null}
downloadDisabled={!canDownloadRaw}
@@ -990,7 +1157,72 @@ export default function JobDetailDialog({
</div>
</section>
{!isCd && !isAudiobook && (hasConfiguredSelection || encodePlanUserPreset || job.encodePlan?.minLengthMinutes != null || Array.isArray(job.encodePlan?.titles)) ? (
{isConverter ? (
<section className="job-meta-block job-meta-block-full">
<h4>Encode-Konfiguration</h4>
<div className="job-meta-grid job-meta-grid-compact">
<div>
<strong>Typ:</strong> {converterMediaTypeLabel}
</div>
<div>
<strong>Format:</strong> {converterOutputFormat ? converterOutputFormat.toUpperCase() : '-'}
</div>
<div>
<strong>Eingabe-Modus:</strong> {converterPlan?.isFolder ? 'Ordner' : (converterPlan?.isSharedAudio ? 'Gemeinsam' : 'Einzeln')}
</div>
<div>
<strong>Eingabe-Dateien:</strong> {converterInputPaths.length > 0 ? converterInputPaths.length : '-'}
</div>
<div>
<strong>Preset:</strong> {converterPresetLabel || '-'}
</div>
<div>
<strong>HandBrake Titel:</strong> {converterPlan?.handBrakeTitleId || '-'}
</div>
</div>
{converterMediaType === 'audio' ? (
<div className="job-meta-grid job-meta-grid-compact">
<div>
<strong>Album:</strong> {converterMetadata?.albumTitle || job?.title || (job?.id ? `Job #${job.id}` : '-')}
</div>
<div>
<strong>Interpret:</strong> {converterMetadata?.albumArtist || '-'}
</div>
<div>
<strong>Jahr:</strong> {converterMetadata?.albumYear || job?.year || '-'}
</div>
<div>
<strong>Qualität:</strong> {converterAudioQualityLabel || '-'}
</div>
</div>
) : null}
{converterInputPaths.length > 0 ? (
<div className="track-group">
{converterInputPaths.map((inputPath, index) => (
<div key={`${inputPath}-${index}`} className="track-item">
<span>#{index + 1} | {inputPath}</span>
</div>
))}
</div>
) : null}
{converterMediaType === 'audio' && converterTrackList.length > 0 ? (
<div className="track-group">
{converterTrackList.map((track, index) => {
const position = Number(track?.position) > 0 ? Math.trunc(Number(track.position)) : (index + 1);
const title = String(track?.title || '').trim() || `Track ${position}`;
const artist = String(track?.artist || '').trim();
return (
<div key={`${position}-${title}`} className="track-item">
<span>#{position} | {artist ? `${artist} - ` : ''}{title}</span>
</div>
);
})}
</div>
) : null}
</section>
) : null}
{!isCd && !isAudiobook && !isConverter && (hasConfiguredSelection || encodePlanUserPreset || job.encodePlan?.minLengthMinutes != null || Array.isArray(job.encodePlan?.titles)) ? (
<section className="job-meta-block job-meta-block-full">
<h4>Encode-Konfiguration</h4>
{/* Zeile 1: Preset + Mindestlaufzeit */}
@@ -1193,13 +1425,13 @@ export default function JobDetailDialog({
<section className="job-meta-block job-meta-block-full">
<h4>Logs</h4>
<div className="job-json-grid">
{!isCd && !isAudiobook ? <JsonView title="OMDb Info" value={job.omdbInfo} /> : null}
{!isCd && !isAudiobook && !isConverter ? <JsonView title="OMDb Info" value={job.omdbInfo} /> : null}
{isCd ? <JsonView title="MusicBrainz" value={job.makemkvInfo?.selectedMetadata ?? null} /> : null}
{isAudiobook ? <JsonView title="Metadaten" value={job.handbrakeInfo?.metadata ?? job.makemkvInfo?.selectedMetadata ?? null} /> : null}
<JsonView title={isCd ? 'cdparanoia Info' : (isAudiobook ? 'Audiobook Info' : 'MakeMKV Info')} value={job.makemkvInfo} />
<JsonView title={isCd ? 'cdparanoia Info' : (isAudiobook ? 'Audiobook Info' : (isConverter ? 'Converter Analyze Info' : 'MakeMKV Info'))} value={job.makemkvInfo} />
{!isCd && !isAudiobook ? <JsonView title="Mediainfo Info" value={job.mediainfoInfo} /> : null}
<JsonView title={isCd ? 'Rip-Plan' : 'Encode Plan'} value={job.encodePlan} />
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : 'HandBrake Info')} value={job.handbrakeInfo} />
<JsonView title={isCd ? 'Rip-Plan' : (isConverter ? 'Converter Plan' : 'Encode Plan')} value={job.encodePlan} />
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : (isConverter ? 'Converter Encode Info' : 'HandBrake Info'))} value={job.handbrakeInfo} />
</div>
</section>
@@ -1257,6 +1489,27 @@ export default function JobDetailDialog({
</div>
) : (
<div className="actions-section">
{showCancelAction ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-ban" /> {running ? 'Laufender Job' : 'Wartender Job'}</div>
<div className="action-item">
<Button
label="Job abbrechen"
icon="pi pi-times"
severity="warning"
size="small"
onClick={() => onCancel?.(job)}
loading={cancelBusy}
/>
<span className="action-desc">
{running
? 'Bricht den aktuell laufenden Job sofort ab.'
: `Bricht den wartenden Job im Status ${statusUpper || '-'} ab.`}
</span>
</div>
</div>
) : null}
{isCd ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-play-circle" /> Audio CD</div>
@@ -1304,73 +1557,77 @@ export default function JobDetailDialog({
</div>
</div>
) : (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-play-circle" /> Kodierung</div>
{canResumeReady ? (
<div className="action-item">
<Button
label="Im Dashboard öffnen"
icon="pi pi-window-maximize"
severity="info"
outlined
size="small"
onClick={() => onResumeReady?.(job)}
loading={actionBusy}
/>
<span className="action-desc">Öffnet den wartenden Job im Dashboard zur Weiterverarbeitung.</span>
</div>
) : null}
{typeof onRestartEncode === 'function' ? (
<div className="action-item">
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
size="small"
onClick={() => onRestartEncode?.(job)}
loading={actionBusy}
disabled={!canRestartEncode}
/>
<span className="action-desc">Startet {isAudiobook ? 'FFmpeg' : 'HandBrake'} mit den zuletzt gespeicherten Einstellungen direkt neu ohne Review.</span>
</div>
) : null}
{typeof onRestartReview === 'function' ? (
<div className="action-item">
<Button
label={isAudiobook ? 'Kapitel-Auswahl öffnen' : 'Spur-Auswahl öffnen'}
icon="pi pi-list"
severity="info"
outlined
size="small"
onClick={() => onRestartReview?.(job)}
loading={actionBusy}
disabled={!canRestartReview || isUnencodedOrphanImport}
/>
<span className="action-desc">{isAudiobook
? 'Öffnet die Kapitel-Auswahl erneut, um Kapitel anzupassen bevor der Encode startet.'
: 'Öffnet die Spur- und Preset-Auswahl erneut, um Einstellungen vor dem Encode zu ändern.'
}</span>
</div>
) : null}
<div className="action-item">
<Button
label="Neustart"
icon="pi pi-sync"
severity="info"
size="small"
onClick={() => isUnencodedOrphanImport ? onRestartReview?.(job) : onReencode?.(job)}
loading={isUnencodedOrphanImport ? actionBusy : reencodeBusy}
disabled={isUnencodedOrphanImport ? !canRestartReview : (!canReencode || typeof onReencode !== 'function')}
/>
<span className="action-desc">{isAudiobook
? 'Liest die AAX-Datei neu ein und öffnet danach die Kapitel-Auswahl. Sinnvoll wenn sich die Quelldatei geändert hat.'
: 'Analysiert die Quelldatei neu (MediaInfo, Playlist-Check) und öffnet dann die Spur-Auswahl. Unterschied zu „Spur-Auswahl öffnen": der Analyse-Schritt wird komplett wiederholt.'
}</span>
canShowGeneralEncodeActions ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-play-circle" /> Kodierung</div>
{canResumeReady ? (
<div className="action-item">
<Button
label="Im Dashboard öffnen"
icon="pi pi-window-maximize"
severity="info"
outlined
size="small"
onClick={() => onResumeReady?.(job)}
loading={actionBusy}
/>
<span className="action-desc">Öffnet den wartenden Job im Dashboard zur Weiterverarbeitung.</span>
</div>
) : null}
{typeof onRestartEncode === 'function' ? (
<div className="action-item">
<Button
label="Encode neu starten"
icon="pi pi-play"
severity="success"
size="small"
onClick={() => onRestartEncode?.(job)}
loading={actionBusy}
disabled={!canRestartEncode}
/>
<span className="action-desc">Startet {isAudiobook ? 'FFmpeg' : 'HandBrake'} mit den zuletzt gespeicherten Einstellungen direkt neu ohne Review.</span>
</div>
) : null}
{typeof onRestartReview === 'function' ? (
<div className="action-item">
<Button
label={isAudiobook ? 'Kapitel-Auswahl öffnen' : 'Spur-Auswahl öffnen'}
icon="pi pi-list"
severity="info"
outlined
size="small"
onClick={() => onRestartReview?.(job)}
loading={actionBusy}
disabled={!canRestartReview || isUnencodedOrphanImport}
/>
<span className="action-desc">{isAudiobook
? 'Öffnet die Kapitel-Auswahl erneut, um Kapitel anzupassen bevor der Encode startet.'
: 'Öffnet die Spur- und Preset-Auswahl erneut, um Einstellungen vor dem Encode zu ändern.'
}</span>
</div>
) : null}
{typeof onReencode === 'function' ? (
<div className="action-item">
<Button
label="Neustart"
icon="pi pi-sync"
severity="info"
size="small"
onClick={() => isUnencodedOrphanImport ? onRestartReview?.(job) : onReencode?.(job)}
loading={isUnencodedOrphanImport ? actionBusy : reencodeBusy}
disabled={isUnencodedOrphanImport ? !canRestartReview : !canReencode}
/>
<span className="action-desc">{isAudiobook
? 'Liest die AAX-Datei neu ein und öffnet danach die Kapitel-Auswahl. Sinnvoll wenn sich die Quelldatei geändert hat.'
: 'Analysiert die Quelldatei neu (MediaInfo, Playlist-Check) und öffnet dann die Spur-Auswahl. Unterschied zu „Spur-Auswahl öffnen": der Analyse-Schritt wird komplett wiederholt.'
}</span>
</div>
) : null}
</div>
</div>
) : null
)}
{!isCd && !isAudiobook && typeof onAssignOmdb === 'function' ? (
{!isCd && !isAudiobook && !isConverter && typeof onAssignOmdb === 'function' ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-search" /> Metadaten</div>
<div className="action-item">
@@ -1389,47 +1646,49 @@ export default function JobDetailDialog({
</div>
) : null}
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-trash" /> Dateien löschen</div>
<div className="action-item">
<Button
label="RAW löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'raw')}
loading={actionBusy}
disabled={!job.rawStatus?.exists || typeof onDeleteFiles !== 'function'}
/>
<span className="action-desc">Löscht die Quelldateien nach dem Rip ({isCd ? 'Audio-Rohdaten' : isAudiobook ? 'AAX/MP3-Quelldatei' : 'MKV-Datei von MakeMKV'}).</span>
{typeof onDeleteFiles === 'function' ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-trash" /> Dateien löschen</div>
<div className="action-item">
<Button
label="RAW löschen"
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'raw')}
loading={actionBusy}
disabled={!job.rawStatus?.exists}
/>
<span className="action-desc">Löscht die Quelldateien nach dem Rip ({isCd ? 'Audio-Rohdaten' : isAudiobook ? 'AAX/MP3-Quelldatei' : isConverter ? 'Quelldatei aus dem Converter-Import' : 'MKV-Datei von MakeMKV'}).</span>
</div>
<div className="action-item">
<Button
label={isCd ? 'Audio löschen' : (isAudiobook ? 'Ausgabe löschen' : (isConverter ? 'Output löschen' : 'Movie löschen'))}
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'movie')}
loading={actionBusy}
disabled={!hasAnyOutputFolder}
/>
<span className="action-desc">Löscht {isCd ? 'die fertig gerippten Audiodateien' : isAudiobook ? 'die fertig konvertierte Audiobook-Ausgabe' : isConverter ? 'die fertig konvertierte Ausgabe' : 'die fertig encodierte Filmdatei'}.</span>
</div>
<div className="action-item">
<Button
label="Beides löschen"
icon="pi pi-times"
severity="danger"
size="small"
onClick={() => onDeleteFiles?.(job, 'both')}
loading={actionBusy}
disabled={!job.rawStatus?.exists && !job.outputStatus?.exists}
/>
<span className="action-desc">Löscht RAW-Quelldateien und Ausgabe gemeinsam.</span>
</div>
</div>
<div className="action-item">
<Button
label={isCd ? 'Audio löschen' : (isAudiobook ? 'Ausgabe löschen' : 'Movie löschen')}
icon="pi pi-trash"
severity="warning"
outlined
size="small"
onClick={() => onDeleteFiles?.(job, 'movie')}
loading={actionBusy}
disabled={!hasAnyOutputFolder || typeof onDeleteFiles !== 'function'}
/>
<span className="action-desc">Löscht {isCd ? 'die fertig gerippten Audiodateien' : isAudiobook ? 'die fertig konvertierte Audiobook-Ausgabe' : 'die fertig encodierte Filmdatei'}.</span>
</div>
<div className="action-item">
<Button
label="Beides löschen"
icon="pi pi-times"
severity="danger"
size="small"
onClick={() => onDeleteFiles?.(job, 'both')}
loading={actionBusy}
disabled={(!job.rawStatus?.exists && !job.outputStatus?.exists) || typeof onDeleteFiles !== 'function'}
/>
<span className="action-desc">Löscht RAW-Quelldateien und Ausgabe gemeinsam.</span>
</div>
</div>
) : null}
</div>
)}