0.12.0 Begin neu Architecture

This commit is contained in:
2026-03-18 15:35:10 +00:00
parent d00191324b
commit 0a9cf6969f
30 changed files with 4570 additions and 183 deletions
+141 -14
View File
@@ -1,9 +1,11 @@
import { useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg';
import { resolveJobPluginExecution } from '../utils/pluginExecution';
import { getStatusLabel } from '../utils/statusPresentation';
const CD_FORMAT_LABELS = {
@@ -465,6 +467,59 @@ function BoolState({ value }) {
);
}
function resolveSelectionStateMeta({ selected, outcome }) {
if (!selected) {
return {
label: 'Nicht ausgewählt',
icon: 'pi-minus-circle',
className: 'track-selection-inline-neutral',
title: 'Nicht ausgewählt'
};
}
if (outcome === 'success') {
return {
label: 'Ausgewählt',
icon: 'pi-check-circle',
className: 'job-step-inline-ok',
title: 'Ausgewählt und erfolgreich'
};
}
if (outcome === 'error') {
return {
label: 'Ausgewählt',
icon: 'pi-times-circle',
className: 'job-step-inline-no',
title: 'Ausgewählt, aber nicht erfolgreich'
};
}
if (outcome === 'cancelled') {
return {
label: 'Ausgewählt',
icon: 'pi-ban',
className: 'job-step-inline-warn',
title: 'Ausgewählt, aber abgebrochen'
};
}
return {
label: 'Ausgewählt',
icon: 'pi-clock',
className: 'track-selection-inline-neutral',
title: 'Ausgewählt'
};
}
function SelectionStateNote({ selected, outcome }) {
const meta = resolveSelectionStateMeta({ selected, outcome });
return (
<small className="track-action-note">
<span className={meta.className} title={meta.title}>
<span>{meta.label}</span>
<i className={`pi ${meta.icon}`} aria-hidden="true" />
</span>
</small>
);
}
function PathField({
label,
value,
@@ -528,17 +583,18 @@ export default function JobDetailDialog({
downloadBusyTarget = null
}) {
const mkDone = Boolean(job?.ripSuccessful) || !job?.makemkvInfo || job?.makemkvInfo?.status === 'SUCCESS';
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(job?.status);
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_RIPPING', 'CD_ENCODING'].includes(job?.status);
const showFinalLog = !running;
const canReencode = !!(job?.rawStatus?.exists && job?.rawStatus?.isEmpty !== true && mkDone && !running);
const mediaType = resolveMediaType(job);
const isCd = mediaType === 'cd';
const isAudiobook = mediaType === 'audiobook';
const canReencode = !!(job?.rawStatus?.exists && job?.rawStatus?.isEmpty !== true && !running
&& (isCd || mkDone));
const canResumeReady = Boolean(
(String(job?.status || '').trim().toUpperCase() === 'READY_TO_ENCODE' || String(job?.last_state || '').trim().toUpperCase() === 'READY_TO_ENCODE')
&& !running
&& typeof onResumeReady === 'function'
);
const mediaType = resolveMediaType(job);
const isCd = mediaType === 'cd';
const isAudiobook = mediaType === 'audiobook';
const hasConfirmedPlan = Boolean(
job?.encodePlan
&& Array.isArray(job?.encodePlan?.titles)
@@ -578,6 +634,7 @@ export default function JobDetailDialog({
const mediaTypeAlt = mediaTypeLabel;
const statusMeta = statusBadgeMeta(job?.status, queueLocked);
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
const pluginExecution = resolveJobPluginExecution(job, null);
const configuredSelection = buildConfiguredScriptAndChainSelection(job);
const hasConfiguredSelection = configuredSelection.preScriptIds.length > 0
|| configuredSelection.postScriptIds.length > 0
@@ -776,6 +833,12 @@ export default function JobDetailDialog({
<i className={`pi ${statusMeta.icon}`} aria-hidden="true" />
</span>
</span>
{pluginExecution ? (
<>
<span className="job-infos-sep">|</span>
<Tag value={pluginExecution.label} severity="warning" title={pluginExecution.title} />
</>
) : null}
<span className="job-infos-sep">|</span>
{isCd ? (
<>
@@ -949,22 +1012,23 @@ export default function JobDetailDialog({
: '-';
const selected = selectedSet ? selectedSet.has(pos) : track.selected !== false;
const encodeResult = encodeResultMap.get(pos);
const encodeLabel = !selected ? 'Nicht übernommen'
: encodeResult ? (encodeResult.success ? 'Erfolgreich' : 'Fehler')
: (job?.ripSuccessful != null ? (job.ripSuccessful ? 'Erfolgreich' : 'Fehler') : 'Ausgewählt');
const encodeClass = !selected ? ''
: encodeResult ? (encodeResult.success ? 'tone-ok' : 'tone-no')
: (job?.ripSuccessful != null ? (job.ripSuccessful ? 'tone-ok' : 'tone-no') : '');
const outcome = !selected
? null
: encodeResult
? (encodeResult.success ? 'success' : 'error')
: (job?.ripSuccessful != null ? (job.ripSuccessful ? 'success' : 'error') : null);
return (
<div key={pos} className="track-item">
<span>#{pos} | {label}{artist ? ` | ${artist}` : ''} | {durLabel}</span>
<small className={`track-action-note ${encodeClass}`}>Encode: {encodeLabel}</small>
<SelectionStateNote selected={selected} outcome={outcome} />
</div>
);
})}
</div>
);
})() : (() => {
const audiobookFormat = String(job?.handbrakeInfo?.format || job?.encodePlan?.format || '').trim().toLowerCase();
const isSplitAudiobook = Boolean(audiobookFormat) && audiobookFormat !== 'm4b';
const chapters = Array.isArray(job.handbrakeInfo?.metadata?.chapters) && job.handbrakeInfo.metadata.chapters.length > 0
? job.handbrakeInfo.metadata.chapters
: (Array.isArray(job.makemkvInfo?.chapters) && job.makemkvInfo.chapters.length > 0
@@ -988,6 +1052,15 @@ export default function JobDetailDialog({
: '-';
const step = stepsByIndex.get(id);
const stepStatus = step ? String(step.status || '').toUpperCase() : null;
const outcome = stepStatus === 'SUCCESS'
? 'success'
: stepStatus === 'ERROR'
? 'error'
: stepStatus === 'CANCELLED'
? 'cancelled'
: (job?.encodeSuccess != null
? (job.encodeSuccess ? 'success' : (String(job?.status || '').trim().toUpperCase() === 'CANCELLED' ? 'cancelled' : 'error'))
: null);
const encodeLabel = stepStatus === 'SUCCESS' ? 'Erfolgreich'
: stepStatus === 'ERROR' ? 'Fehler'
: stepStatus === 'CANCELLED' ? 'Abgebrochen'
@@ -999,7 +1072,11 @@ export default function JobDetailDialog({
return (
<div key={id} className="track-item">
<span>#{id} | {label} | {durLabel}</span>
<small className={`track-action-note ${encodeClass}`}>Encode: {encodeLabel}</small>
{isSplitAudiobook ? (
<SelectionStateNote selected outcome={outcome} />
) : (
<small className={`track-action-note ${encodeClass}`}>Encode: {encodeLabel}</small>
)}
</div>
);
})}
@@ -1076,7 +1153,38 @@ export default function JobDetailDialog({
</div>
) : (
<div className="actions-section">
{!isCd ? (
{isCd ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-play-circle" /> Audio CD</div>
<div className="action-item">
<Button
label="Encode neu starten"
icon="pi pi-sync"
severity="info"
size="small"
onClick={() => onReencode?.(job)}
loading={reencodeBusy}
disabled={!canReencode || typeof onReencode !== 'function'}
/>
<span className="action-desc">Encodiert die vorhandenen WAV-Rohdaten erneut ohne die CD neu zu lesen. Nützlich wenn sich Format oder Metadaten geändert haben.</span>
</div>
{typeof onAssignCdMetadata === 'function' ? (
<div className="action-item">
<Button
label="MusicBrainz neu zuweisen"
icon="pi pi-search"
severity="secondary"
outlined
size="small"
onClick={() => onAssignCdMetadata?.(job)}
loading={cdMetadataAssignBusy}
disabled={running}
/>
<span className="action-desc">Öffnet die MusicBrainz-Suche, um Album-Metadaten (Titel, Interpret, Tracks) neu zuzuweisen.</span>
</div>
) : null}
</div>
) : (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-play-circle" /> Kodierung</div>
{canResumeReady ? (
@@ -1141,6 +1249,25 @@ export default function JobDetailDialog({
}</span>
</div>
</div>
)}
{!isCd && !isAudiobook && typeof onAssignOmdb === 'function' ? (
<div className="actions-group">
<div className="actions-group-label"><i className="pi pi-search" /> Metadaten</div>
<div className="action-item">
<Button
label="OMDB neu zuweisen"
icon="pi pi-search"
severity="secondary"
outlined
size="small"
onClick={() => onAssignOmdb?.(job)}
loading={omdbAssignBusy}
disabled={running}
/>
<span className="action-desc">Öffnet die OMDB-Suche, um Film-Metadaten (Titel, Jahr, Poster, IMDb-ID) neu zuzuweisen.</span>
</div>
</div>
) : null}
<div className="actions-group">