0.12.0-3 Plugin Integration
This commit is contained in:
+117
-8
@@ -28,6 +28,15 @@ function clampPercent(value) {
|
||||
return Math.max(0, Math.min(100, parsed));
|
||||
}
|
||||
|
||||
function normalizeStage(value) {
|
||||
return String(value || '').trim().toUpperCase();
|
||||
}
|
||||
|
||||
function isTerminalStage(value) {
|
||||
const normalized = normalizeStage(value);
|
||||
return normalized === 'FINISHED' || normalized === 'ERROR' || normalized === 'CANCELLED';
|
||||
}
|
||||
|
||||
function formatBytes(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
@@ -297,8 +306,49 @@ function App() {
|
||||
: null;
|
||||
setPipeline((prev) => {
|
||||
const next = { ...prev };
|
||||
const normalizedProgressJobId = normalizeJobId(progressJobId);
|
||||
const progressStage = normalizeStage(payload?.state);
|
||||
const isCdProgressStage = progressStage === 'CD_ANALYZING'
|
||||
|| progressStage === 'CD_RIPPING'
|
||||
|| progressStage === 'CD_ENCODING';
|
||||
const incomingIsTerminal = isTerminalStage(progressStage);
|
||||
const prevActiveJobId = normalizeJobId(prev?.activeJobId || prev?.context?.jobId);
|
||||
const prevJobProgress = normalizedProgressJobId
|
||||
? (prev?.jobProgress?.[normalizedProgressJobId] || null)
|
||||
: null;
|
||||
const prevJobProgressState = normalizeStage(prevJobProgress?.state);
|
||||
const prevJobProgressIsTerminal = isTerminalStage(prevJobProgressState);
|
||||
const matchingCdDriveEntry = normalizedProgressJobId
|
||||
? Object.values(prev?.cdDrives || {}).find((driveState) => normalizeJobId(driveState?.jobId) === normalizedProgressJobId)
|
||||
: null;
|
||||
const matchingCdDriveIsTerminal = isTerminalStage(matchingCdDriveEntry?.state);
|
||||
const hasKnownBinding = Boolean(
|
||||
normalizedProgressJobId
|
||||
&& (
|
||||
prevActiveJobId === normalizedProgressJobId
|
||||
|| prevJobProgress
|
||||
|| matchingCdDriveEntry
|
||||
)
|
||||
);
|
||||
|
||||
// Ignore late/stale progress packets that arrive after a terminal state
|
||||
// or for jobs that are no longer bound in pipeline state.
|
||||
if (
|
||||
normalizedProgressJobId
|
||||
&& !incomingIsTerminal
|
||||
&& (
|
||||
prevJobProgressIsTerminal
|
||||
|| matchingCdDriveIsTerminal
|
||||
|| !hasKnownBinding
|
||||
)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (progressJobId != null) {
|
||||
const previousJobProgress = prev?.jobProgress?.[progressJobId] || {};
|
||||
const previousJobStage = normalizeStage(previousJobProgress?.state);
|
||||
const keepPreviousJobStage = isTerminalStage(previousJobStage) && !incomingIsTerminal;
|
||||
const mergedJobContext = contextPatch
|
||||
? {
|
||||
...(previousJobProgress?.context && typeof previousJobProgress.context === 'object'
|
||||
@@ -313,19 +363,21 @@ function App() {
|
||||
...(prev?.jobProgress || {}),
|
||||
[progressJobId]: {
|
||||
...previousJobProgress,
|
||||
state: payload.state,
|
||||
progress: payload.progress,
|
||||
eta: payload.eta,
|
||||
statusText: payload.statusText,
|
||||
state: keepPreviousJobStage ? previousJobProgress.state : payload.state,
|
||||
progress: keepPreviousJobStage ? previousJobProgress.progress : payload.progress,
|
||||
eta: keepPreviousJobStage ? previousJobProgress.eta : payload.eta,
|
||||
statusText: keepPreviousJobStage ? previousJobProgress.statusText : payload.statusText,
|
||||
...(mergedJobContext !== undefined ? { context: mergedJobContext } : {})
|
||||
}
|
||||
};
|
||||
}
|
||||
if (progressJobId === prev?.activeJobId || progressJobId == null) {
|
||||
next.state = payload.state ?? prev?.state;
|
||||
next.progress = payload.progress ?? prev?.progress;
|
||||
next.eta = payload.eta ?? prev?.eta;
|
||||
next.statusText = payload.statusText ?? prev?.statusText;
|
||||
const previousGlobalStage = normalizeStage(prev?.state);
|
||||
const keepPreviousGlobalStage = isTerminalStage(previousGlobalStage) && !incomingIsTerminal;
|
||||
next.state = keepPreviousGlobalStage ? prev?.state : (payload.state ?? prev?.state);
|
||||
next.progress = keepPreviousGlobalStage ? prev?.progress : (payload.progress ?? prev?.progress);
|
||||
next.eta = keepPreviousGlobalStage ? prev?.eta : (payload.eta ?? prev?.eta);
|
||||
next.statusText = keepPreviousGlobalStage ? prev?.statusText : (payload.statusText ?? prev?.statusText);
|
||||
if (contextPatch) {
|
||||
next.context = {
|
||||
...(prev?.context && typeof prev.context === 'object' ? prev.context : {}),
|
||||
@@ -333,6 +385,63 @@ function App() {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Keep per-drive CD progress in sync with live progress events.
|
||||
// Backend sends frequent PIPELINE_PROGRESS updates, while cdDrives
|
||||
// snapshots are only broadcast on state transitions.
|
||||
if (isCdProgressStage && prev?.cdDrives && typeof prev.cdDrives === 'object') {
|
||||
const cdDrivesEntries = Object.entries(prev.cdDrives);
|
||||
const nextCdDrives = { ...prev.cdDrives };
|
||||
const patchDrive = (driveState) => {
|
||||
const currentDriveStage = normalizeStage(driveState?.state);
|
||||
if (isTerminalStage(currentDriveStage) && !incomingIsTerminal) {
|
||||
return driveState;
|
||||
}
|
||||
const mergedContext = contextPatch
|
||||
? {
|
||||
...(driveState?.context && typeof driveState.context === 'object' ? driveState.context : {}),
|
||||
...contextPatch
|
||||
}
|
||||
: driveState?.context;
|
||||
return {
|
||||
...driveState,
|
||||
state: payload?.state ?? driveState?.state,
|
||||
progress: payload?.progress ?? driveState?.progress,
|
||||
eta: payload?.eta ?? driveState?.eta,
|
||||
statusText: payload?.statusText ?? driveState?.statusText,
|
||||
...(mergedContext !== undefined ? { context: mergedContext } : {})
|
||||
};
|
||||
};
|
||||
|
||||
let updated = false;
|
||||
if (normalizedProgressJobId) {
|
||||
for (const [drivePath, driveState] of cdDrivesEntries) {
|
||||
if (normalizeJobId(driveState?.jobId) === normalizedProgressJobId) {
|
||||
nextCdDrives[drivePath] = patchDrive(driveState);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
const activeCdEntries = cdDrivesEntries.filter(([, driveState]) => {
|
||||
const driveStage = String(driveState?.state || '').trim().toUpperCase();
|
||||
return driveStage === 'CD_ANALYZING'
|
||||
|| driveStage === 'CD_RIPPING'
|
||||
|| driveStage === 'CD_ENCODING';
|
||||
});
|
||||
if (activeCdEntries.length === 1) {
|
||||
const [drivePath, driveState] = activeCdEntries[0];
|
||||
nextCdDrives[drivePath] = patchDrive(driveState);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
next.cdDrives = nextCdDrives;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -826,7 +826,6 @@ export const api = {
|
||||
forceRefresh: options.forceRefresh
|
||||
});
|
||||
},
|
||||
|
||||
// ── User Presets ───────────────────────────────────────────────────────────
|
||||
getUserPresets(mediaType = null, options = {}) {
|
||||
const suffix = mediaType ? `?media_type=${encodeURIComponent(mediaType)}` : '';
|
||||
|
||||
@@ -731,6 +731,7 @@ export default function CdRipConfigPanel({
|
||||
const completedEncodeCount = selectedTrackRows.filter((track) => track.encodeStatus === 'done').length;
|
||||
const liveCurrentTrackPosition = normalizePosition(cdLive?.trackPosition);
|
||||
const lastState = String(context?.lastState || '').trim().toUpperCase();
|
||||
const lastStateLabel = getStatusLabel(lastState);
|
||||
const failureStage = String(context?.stage || '').trim().toUpperCase();
|
||||
const normalizedLivePhase = String(cdLive?.phase || '').trim().toLowerCase();
|
||||
const normalizedStatusText = String(statusText || '').trim().toUpperCase();
|
||||
@@ -897,12 +898,11 @@ export default function CdRipConfigPanel({
|
||||
<div><strong>Pre-Ketten:</strong> {preChainNames.length > 0 ? preChainNames.join(' | ') : '-'}</div>
|
||||
<div><strong>Post-Skripte:</strong> {postScriptNames.length > 0 ? postScriptNames.join(' | ') : '-'}</div>
|
||||
<div><strong>Post-Ketten:</strong> {postChainNames.length > 0 ? postChainNames.join(' | ') : '-'}</div>
|
||||
<div><strong>Tracks fertig:</strong> {completedEncodeCount} / {selectedTrackRows.length || effectiveTrackRows.length}</div>
|
||||
<div><strong>Auswahl:</strong> {selectedTrackNumbers || '-'}</div>
|
||||
<div><strong>Gesamtdauer:</strong> {formatTotalDuration(selectedTrackDurationSec)}</div>
|
||||
<div><strong>Laufwerk:</strong> {devicePath}</div>
|
||||
<div><strong>Output-Pfad:</strong> {outputPath}</div>
|
||||
{lastState ? <div><strong>Letzter Pipeline-State:</strong> {lastState}</div> : null}
|
||||
{lastState ? <div><strong>Letzter Pipeline-State:</strong> {lastStateLabel}</div> : null}
|
||||
{jobId ? <div><strong>Job-ID:</strong> #{jobId}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,16 +94,40 @@ function DriveDevicesEditor({ value, onChange, settingKey }) {
|
||||
try {
|
||||
const parsed = JSON.parse(val || '[]');
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.map((e) => {
|
||||
const normalized = parsed.map((e) => {
|
||||
if (typeof e === 'string') {
|
||||
const p = e.trim();
|
||||
const m = p.match(/sr(\d+)$/);
|
||||
return { path: p, makemkvIndex: m ? Number(m[1]) : 0 };
|
||||
if (!p) return null;
|
||||
return { path: p, makemkvIndex: null };
|
||||
}
|
||||
if (e && typeof e === 'object') {
|
||||
return { path: String(e.path || '').trim(), makemkvIndex: Number.isFinite(Number(e.makemkvIndex)) ? Number(e.makemkvIndex) : 0 };
|
||||
const p = String(e.path || '').trim();
|
||||
if (!p) return null;
|
||||
const idxRaw = Number(e.makemkvIndex);
|
||||
return {
|
||||
path: p,
|
||||
makemkvIndex: Number.isFinite(idxRaw) && idxRaw >= 0 ? Math.trunc(idxRaw) : null
|
||||
};
|
||||
}
|
||||
return { path: '', makemkvIndex: 0 };
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
const used = new Set();
|
||||
let nextAutoIndex = 0;
|
||||
return normalized.map((entry) => {
|
||||
if (entry.makemkvIndex != null) {
|
||||
used.add(entry.makemkvIndex);
|
||||
if (entry.makemkvIndex >= nextAutoIndex) {
|
||||
nextAutoIndex = entry.makemkvIndex + 1;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
while (used.has(nextAutoIndex)) {
|
||||
nextAutoIndex += 1;
|
||||
}
|
||||
const resolved = { path: entry.path, makemkvIndex: nextAutoIndex };
|
||||
used.add(nextAutoIndex);
|
||||
nextAutoIndex += 1;
|
||||
return resolved;
|
||||
});
|
||||
} catch (_e) {
|
||||
return [];
|
||||
@@ -129,8 +153,7 @@ function DriveDevicesEditor({ value, onChange, settingKey }) {
|
||||
onChange={(e) => {
|
||||
const next = [...devices];
|
||||
const p = e.target.value;
|
||||
const m = p.match(/sr(\d+)$/);
|
||||
next[idx] = { ...next[idx], path: p, makemkvIndex: m ? Number(m[1]) : next[idx].makemkvIndex };
|
||||
next[idx] = { ...next[idx], path: p };
|
||||
updateDevices(next);
|
||||
}}
|
||||
className="drive-device-input"
|
||||
|
||||
@@ -6,7 +6,7 @@ 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';
|
||||
import { getProcessStatusLabel, getStatusLabel } from '../utils/statusPresentation';
|
||||
|
||||
const CD_FORMAT_LABELS = {
|
||||
flac: 'FLAC',
|
||||
@@ -26,9 +26,10 @@ function JsonView({ title, value }) {
|
||||
}
|
||||
|
||||
function ScriptResultRow({ result }) {
|
||||
const status = String(result?.status || '').toUpperCase();
|
||||
const isSuccess = status === 'SUCCESS';
|
||||
const isError = status === 'ERROR';
|
||||
const statusCode = String(result?.status || '').toUpperCase();
|
||||
const status = getProcessStatusLabel(statusCode);
|
||||
const isSuccess = statusCode === 'SUCCESS';
|
||||
const isError = statusCode === 'ERROR';
|
||||
const icon = isSuccess ? 'pi-check-circle' : isError ? 'pi-times-circle' : 'pi-minus-circle';
|
||||
const tone = isSuccess ? 'success' : isError ? 'danger' : 'warning';
|
||||
return (
|
||||
|
||||
@@ -197,6 +197,40 @@ function runtimeOutcomeMeta(outcome, status) {
|
||||
return runtimeStatusMeta(status);
|
||||
}
|
||||
|
||||
function driveStateIconMeta(stateLabel) {
|
||||
const normalized = String(stateLabel || '').trim().toUpperCase();
|
||||
if (!normalized || normalized === 'LEER' || normalized === 'IDLE') {
|
||||
return { icon: 'pi-stop', spin: false };
|
||||
}
|
||||
if (normalized === 'DISC_DETECTED') {
|
||||
return { icon: 'pi-check', spin: false };
|
||||
}
|
||||
if (normalized === 'FINISHED') {
|
||||
return { icon: 'pi-check-circle', spin: false };
|
||||
}
|
||||
if (normalized === 'ERROR' || normalized === 'CANCELLED') {
|
||||
return { icon: 'pi-times-circle', spin: false };
|
||||
}
|
||||
if (processingStates.includes(normalized)) {
|
||||
return { icon: 'pi-spinner', spin: true };
|
||||
}
|
||||
if (
|
||||
normalized === 'METADATA_SELECTION'
|
||||
|| normalized === 'CD_METADATA_SELECTION'
|
||||
|| normalized === 'WAITING_FOR_USER_DECISION'
|
||||
) {
|
||||
return { icon: 'pi-list', spin: false };
|
||||
}
|
||||
if (
|
||||
normalized === 'READY_TO_START'
|
||||
|| normalized === 'READY_TO_ENCODE'
|
||||
|| normalized === 'CD_READY_TO_RIP'
|
||||
) {
|
||||
return { icon: 'pi-play', spin: false };
|
||||
}
|
||||
return { icon: 'pi-question-circle', spin: false };
|
||||
}
|
||||
|
||||
function hasRuntimeOutputDetails(item) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return false;
|
||||
@@ -304,6 +338,20 @@ function normalizeQueue(queue) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildCdDriveLifecycleKey(cdDrives) {
|
||||
if (!cdDrives || typeof cdDrives !== 'object') {
|
||||
return '';
|
||||
}
|
||||
const entries = Object.entries(cdDrives)
|
||||
.map(([devicePath, driveState]) => {
|
||||
const state = String(driveState?.state || '').trim().toUpperCase();
|
||||
const jobId = normalizeJobId(driveState?.jobId || driveState?.context?.jobId) || 0;
|
||||
return `${devicePath}|${jobId}|${state}`;
|
||||
})
|
||||
.sort();
|
||||
return entries.join('::');
|
||||
}
|
||||
|
||||
function getQueueActionResult(response) {
|
||||
return response?.result && typeof response.result === 'object' ? response.result : {};
|
||||
}
|
||||
@@ -917,6 +965,10 @@ export default function DashboardPage({
|
||||
() => normalizeHardwareMonitoringPayload(hardwareMonitoring),
|
||||
[hardwareMonitoring]
|
||||
);
|
||||
const cdDriveLifecycleKey = useMemo(
|
||||
() => buildCdDriveLifecycleKey(pipeline?.cdDrives),
|
||||
[pipeline?.cdDrives]
|
||||
);
|
||||
const monitoringSample = monitoringState.sample;
|
||||
const cpuMetrics = monitoringSample?.cpu || null;
|
||||
const memoryMetrics = monitoringSample?.memory || null;
|
||||
@@ -1115,13 +1167,43 @@ export default function DashboardPage({
|
||||
}
|
||||
}, [pipeline?.cdDrives]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cdMetadataDialogVisible) {
|
||||
return;
|
||||
}
|
||||
const dialogJobId = normalizeJobId(cdMetadataDialogContext?.jobId);
|
||||
if (!dialogJobId) {
|
||||
return;
|
||||
}
|
||||
const cdDrives = pipeline?.cdDrives;
|
||||
if (!cdDrives || typeof cdDrives !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
let stillInMetadataSelection = false;
|
||||
for (const drive of Object.values(cdDrives)) {
|
||||
const driveState = String(drive?.state || '').trim().toUpperCase();
|
||||
const ctx = drive?.context && typeof drive.context === 'object' ? drive.context : null;
|
||||
const driveJobId = normalizeJobId(ctx?.jobId || drive?.jobId);
|
||||
if (driveJobId === dialogJobId && driveState === 'CD_METADATA_SELECTION') {
|
||||
stillInMetadataSelection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillInMetadataSelection) {
|
||||
setCdMetadataDialogVisible(false);
|
||||
setCdMetadataDialogContext(null);
|
||||
}
|
||||
}, [cdMetadataDialogVisible, cdMetadataDialogContext?.jobId, pipeline?.cdDrives]);
|
||||
|
||||
useEffect(() => {
|
||||
setQueueState(normalizeQueue(pipeline?.queue));
|
||||
}, [pipeline?.queue]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadDashboardJobs();
|
||||
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, pipeline?.cdDrives, jobsRefreshToken]);
|
||||
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, cdDriveLifecycleKey, jobsRefreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
api.getDetectedDrives()
|
||||
@@ -1553,18 +1635,28 @@ export default function DashboardPage({
|
||||
try {
|
||||
const response = await api.deleteJobFiles(jobId, effectiveTarget);
|
||||
const summary = response?.summary || {};
|
||||
const deletedFiles = effectiveTarget === 'raw'
|
||||
? (summary.raw?.filesDeleted ?? 0)
|
||||
: (summary.movie?.filesDeleted ?? 0);
|
||||
const removedDirs = effectiveTarget === 'raw'
|
||||
? (summary.raw?.dirsRemoved ?? 0)
|
||||
: (summary.movie?.dirsRemoved ?? 0);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: effectiveTarget === 'raw' ? 'RAW gelöscht' : 'Movie gelöscht',
|
||||
detail: `Entfernt: ${deletedFiles} Datei(en), ${removedDirs} Ordner.`,
|
||||
life: 4000
|
||||
});
|
||||
const targetSummary = effectiveTarget === 'raw'
|
||||
? (summary.raw || {})
|
||||
: (summary.movie || {});
|
||||
const deletedFiles = targetSummary.filesDeleted ?? 0;
|
||||
const removedDirs = targetSummary.dirsRemoved ?? 0;
|
||||
const deletedSomething = Boolean(targetSummary.deleted);
|
||||
|
||||
if (!deletedSomething) {
|
||||
toastRef.current?.show({
|
||||
severity: 'warn',
|
||||
summary: effectiveTarget === 'raw' ? 'RAW nicht gelöscht' : 'Movie nicht gelöscht',
|
||||
detail: targetSummary.reason || 'Keine passenden Dateien/Ordner gefunden.',
|
||||
life: 4200
|
||||
});
|
||||
} else {
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: effectiveTarget === 'raw' ? 'RAW gelöscht' : 'Movie gelöscht',
|
||||
detail: `Entfernt: ${deletedFiles} Datei(en), ${removedDirs} Ordner.`,
|
||||
life: 4000
|
||||
});
|
||||
}
|
||||
await loadDashboardJobs();
|
||||
await refreshPipeline();
|
||||
setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null });
|
||||
@@ -2503,6 +2595,8 @@ export default function DashboardPage({
|
||||
const canAnalyzeDrive = cdState
|
||||
? cdState === 'DISC_DETECTED'
|
||||
: (Boolean(pipelineDevice) && pipelineState === 'DISC_DETECTED') || Boolean(drv.detectedDisc);
|
||||
const stateIconMeta = driveStateIconMeta(stateLabel);
|
||||
const discArg = String(drv.discArg || '').trim();
|
||||
|
||||
return (
|
||||
<div key={drivePath} className="drive-list-item">
|
||||
@@ -2510,10 +2604,27 @@ export default function DashboardPage({
|
||||
<div className="drive-list-info">
|
||||
<code className="drive-list-path">{drivePath}</code>
|
||||
{drv.model && <span className="drive-list-model">{drv.model}</span>}
|
||||
{drv.discArg && <span className="drive-list-disc-arg">{drv.discArg}</span>}
|
||||
</div>
|
||||
<div className="drive-list-state">
|
||||
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} className="drive-list-status-tag" />}
|
||||
{stateLabel ? (
|
||||
<span
|
||||
className={`drive-list-status-icon tone-${stateSeverity}`}
|
||||
title={getStatusLabel(stateLabel)}
|
||||
aria-label={`Status: ${getStatusLabel(stateLabel)}`}
|
||||
>
|
||||
<i className={`pi ${stateIconMeta.icon}${stateIconMeta.spin ? ' pi-spin' : ''}`} aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
{isCdDriveLocked ? (
|
||||
<span
|
||||
className="drive-list-lock-indicator"
|
||||
title="Laufwerk ist gesperrt, bis Rip/Encode abgeschlossen ist."
|
||||
aria-label="Laufwerk gesperrt"
|
||||
>
|
||||
<i className="pi pi-lock" aria-hidden="true" />
|
||||
<span>gesperrt</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="drive-list-actions">
|
||||
<Button
|
||||
@@ -2522,6 +2633,7 @@ export default function DashboardPage({
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
className="drive-list-action-btn"
|
||||
loading={isRescanBusy}
|
||||
disabled={isRescanBusy || isDriveActive || isCdDriveLocked}
|
||||
onClick={() => handleRescanDrive(drivePath)}
|
||||
@@ -2535,6 +2647,7 @@ export default function DashboardPage({
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
className="drive-list-action-btn"
|
||||
loading={isAnalyzeBusy}
|
||||
disabled={isAnalyzeBusy}
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
@@ -2544,15 +2657,19 @@ export default function DashboardPage({
|
||||
)}
|
||||
{cdState === 'CD_METADATA_SELECTION' && (
|
||||
<Button
|
||||
label="Metadaten"
|
||||
icon="pi pi-list"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="info"
|
||||
className="drive-list-action-btn"
|
||||
onClick={() => {
|
||||
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
|
||||
setCdMetadataDialogVisible(true);
|
||||
}}
|
||||
disabled={!driveJobId}
|
||||
title="Metadaten auswählen"
|
||||
aria-label="Metadaten auswählen"
|
||||
/>
|
||||
)}
|
||||
{(cdState === 'ERROR' || cdState === 'CANCELLED') && driveJobId && (
|
||||
@@ -2562,6 +2679,7 @@ export default function DashboardPage({
|
||||
rounded
|
||||
size="small"
|
||||
severity="warning"
|
||||
className="drive-list-action-btn"
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
loading={isAnalyzeBusy}
|
||||
disabled={isAnalyzeBusy}
|
||||
@@ -2571,7 +2689,12 @@ export default function DashboardPage({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{discInfo && <div className="drive-list-disc-label">{discInfo}</div>}
|
||||
{(discArg || discInfo) ? (
|
||||
<div className="drive-list-disc-meta">
|
||||
{discArg ? <span className="drive-list-disc-arg">{discArg}</span> : null}
|
||||
{discInfo ? <span className="drive-list-disc-label" title={discInfo}>{discInfo}</span> : null}
|
||||
</div>
|
||||
) : null}
|
||||
{showProgress && (
|
||||
<ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
||||
)}
|
||||
@@ -2663,7 +2786,7 @@ export default function DashboardPage({
|
||||
</div>
|
||||
) : (
|
||||
<Card title="Job Übersicht" subTitle="Kompakte Liste; Klick auf Zeile öffnet die volle Job-Detailansicht mit passenden CTAs">
|
||||
{jobsLoading ? (
|
||||
{jobsLoading && dashboardJobs.length === 0 ? (
|
||||
<p>Jobs werden geladen ...</p>
|
||||
) : dashboardJobs.length === 0 ? (
|
||||
<p>Keine relevanten Jobs im Dashboard (aktive/fortsetzbare Status).</p>
|
||||
|
||||
@@ -823,12 +823,34 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
try {
|
||||
const response = await api.deleteJobFiles(row.id, target);
|
||||
const summary = response.summary || {};
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Dateien gelöscht',
|
||||
detail: `RAW: ${summary.raw?.filesDeleted ?? 0}, ${outputShortLabel}: ${summary.movie?.filesDeleted ?? 0}`,
|
||||
life: 3500
|
||||
});
|
||||
const rawSummary = summary.raw || {};
|
||||
const movieSummary = summary.movie || {};
|
||||
const deletedSomething = target === 'raw'
|
||||
? Boolean(rawSummary.deleted)
|
||||
: target === 'movie'
|
||||
? Boolean(movieSummary.deleted)
|
||||
: Boolean(rawSummary.deleted || movieSummary.deleted);
|
||||
|
||||
if (!deletedSomething) {
|
||||
const reason = target === 'raw'
|
||||
? (rawSummary.reason || 'Keine passenden RAW-Dateien/Ordner gefunden.')
|
||||
: target === 'movie'
|
||||
? (movieSummary.reason || `Keine passenden ${outputShortLabel}-Dateien/Ordner gefunden.`)
|
||||
: (movieSummary.reason || rawSummary.reason || 'Keine passenden Dateien/Ordner gefunden.');
|
||||
toastRef.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Nichts gelöscht',
|
||||
detail: reason,
|
||||
life: 4200
|
||||
});
|
||||
} else {
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Dateien gelöscht',
|
||||
detail: `RAW: ${rawSummary.filesDeleted ?? 0}, ${outputShortLabel}: ${movieSummary.filesDeleted ?? 0}`,
|
||||
life: 3500
|
||||
});
|
||||
}
|
||||
await load();
|
||||
await refreshDetailIfOpen(row.id);
|
||||
} catch (error) {
|
||||
@@ -1617,7 +1639,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
<ul className="history-delete-preview-list">
|
||||
{previewRelatedJobs.map((item) => (
|
||||
<li key={`delete-related-${item.id}`}>
|
||||
<strong>#{item.id}</strong> | {item.title || '-'} | {item.status || '-'} {item.isPrimary ? '(aktuell)' : '(Alt-Eintrag)'}
|
||||
<strong>#{item.id}</strong> | {item.title || '-'} | {getStatusLabel(item.status)} {item.isPrimary ? '(aktuell)' : '(Alt-Eintrag)'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
+90
-17
@@ -1872,7 +1872,7 @@ body {
|
||||
}
|
||||
|
||||
.drive-list-item {
|
||||
padding: 0.4rem 0.6rem;
|
||||
padding: 0.35rem 0.55rem;
|
||||
border: 1px solid var(--rip-border);
|
||||
border-radius: 0.4rem;
|
||||
background: var(--rip-panel-soft);
|
||||
@@ -1882,14 +1882,14 @@ body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.drive-list-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
gap: 0.28rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1906,41 +1906,104 @@ body {
|
||||
.drive-list-model {
|
||||
font-size: 0.75rem;
|
||||
color: var(--rip-muted);
|
||||
max-width: 14rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drive-list-disc-arg {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.72rem;
|
||||
color: var(--rip-muted);
|
||||
opacity: 0.7;
|
||||
opacity: 0.85;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.drive-list-state {
|
||||
justify-self: start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
}
|
||||
|
||||
.drive-list-status-tag.p-tag {
|
||||
max-width: 10rem;
|
||||
padding: 0.15rem 0.45rem;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.2;
|
||||
.drive-list-status-icon {
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
color: var(--rip-muted);
|
||||
}
|
||||
|
||||
.drive-list-status-icon.tone-success {
|
||||
color: #1c8a3a;
|
||||
}
|
||||
|
||||
.drive-list-status-icon.tone-danger {
|
||||
color: #9c2d2d;
|
||||
}
|
||||
|
||||
.drive-list-status-icon.tone-warning {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.drive-list-status-icon.tone-info {
|
||||
color: #24588b;
|
||||
}
|
||||
|
||||
.drive-list-status-icon.tone-secondary {
|
||||
color: var(--rip-muted);
|
||||
}
|
||||
|
||||
.drive-list-lock-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
padding: 0.08rem 0.32rem;
|
||||
border: 1px solid rgba(155, 111, 22, 0.4);
|
||||
border-radius: 999px;
|
||||
background: rgba(251, 233, 194, 0.65);
|
||||
color: #8a5a0a;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.drive-list-lock-indicator .pi {
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
.drive-list-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
gap: 0.16rem;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.drive-list-action-btn.p-button.p-button-icon-only {
|
||||
width: 1.65rem;
|
||||
height: 1.65rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.drive-list-action-btn .p-button-icon {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.drive-list-disc-meta {
|
||||
margin-top: 0.08rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drive-list-disc-label {
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.2rem;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -2903,7 +2966,7 @@ body {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
gap: 0.15rem 0.4rem;
|
||||
}
|
||||
|
||||
@@ -2911,8 +2974,8 @@ body {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.job-path-field-value > span {
|
||||
@@ -2925,6 +2988,16 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.job-path-download-button.p-button.p-button-icon-only {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.job-path-download-button .p-button-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.job-meta-col-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const STATUS_LABELS = {
|
||||
METADATA_SELECTION: 'Metadatenauswahl',
|
||||
WAITING_FOR_USER_DECISION: 'Warte auf Auswahl',
|
||||
READY_TO_START: 'Startbereit',
|
||||
MEDIAINFO_CHECK: 'Mediainfo-Pruefung',
|
||||
MEDIAINFO_CHECK: 'Mediainfo-Prüfung',
|
||||
READY_TO_ENCODE: 'Bereit zum Encodieren',
|
||||
RIPPING: 'Rippen',
|
||||
ENCODING: 'Encodieren',
|
||||
@@ -16,19 +16,82 @@ const STATUS_LABELS = {
|
||||
CD_ANALYZING: 'CD-Analyse',
|
||||
CD_METADATA_SELECTION: 'CD-Metadatenauswahl',
|
||||
CD_READY_TO_RIP: 'CD bereit (Rip/Encode)',
|
||||
CD_RIPPING: 'CD rippen',
|
||||
CD_ENCODING: 'CD encodieren'
|
||||
CD_RIPPING: 'CD-Rippen',
|
||||
CD_ENCODING: 'CD-Encodieren'
|
||||
};
|
||||
|
||||
const PROCESS_STATUS_LABELS = {
|
||||
SUCCESS: 'Erfolgreich',
|
||||
ERROR: 'Fehler',
|
||||
CANCELLED: 'Abgebrochen',
|
||||
RUNNING: 'Laeuft',
|
||||
RUNNING: 'Läuft',
|
||||
STARTED: 'Gestartet',
|
||||
PENDING: 'Ausstehend'
|
||||
PENDING: 'Ausstehend',
|
||||
DONE: 'Erledigt',
|
||||
FAILED: 'Fehlgeschlagen',
|
||||
COMPLETE: 'Abgeschlossen',
|
||||
COMPLETED: 'Abgeschlossen',
|
||||
SKIPPED: 'Übersprungen',
|
||||
OK: 'OK'
|
||||
};
|
||||
|
||||
const FALLBACK_TOKEN_LABELS = {
|
||||
IDLE: 'Bereit',
|
||||
DISC: 'Medium',
|
||||
DETECTED: 'erkannt',
|
||||
READY: 'bereit',
|
||||
TO: 'zu',
|
||||
RIP: 'rippen',
|
||||
RIPPING: 'rippen',
|
||||
ENCODE: 'encodieren',
|
||||
ENCODING: 'encodieren',
|
||||
ANALYZING: 'analyse',
|
||||
METADATA: 'metadaten',
|
||||
SELECTION: 'auswahl',
|
||||
WAITING: 'warte',
|
||||
FOR: 'auf',
|
||||
USER: 'Benutzer',
|
||||
DECISION: 'entscheidung',
|
||||
CHECK: 'prüfung',
|
||||
POST: 'post',
|
||||
SCRIPTS: 'skripte',
|
||||
FINISHED: 'fertig',
|
||||
CANCELLED: 'abgebrochen',
|
||||
ERROR: 'fehler',
|
||||
SUCCESS: 'erfolgreich',
|
||||
RUNNING: 'läuft',
|
||||
STARTED: 'gestartet',
|
||||
PENDING: 'ausstehend',
|
||||
FAILED: 'fehlgeschlagen',
|
||||
SKIPPED: 'übersprungen',
|
||||
CD: 'CD',
|
||||
DVD: 'DVD',
|
||||
RAW: 'RAW'
|
||||
};
|
||||
|
||||
function prettifyUnknownStatus(status) {
|
||||
const raw = String(status || '').trim();
|
||||
if (!raw) {
|
||||
return '-';
|
||||
}
|
||||
if (!raw.includes('_')) {
|
||||
return raw;
|
||||
}
|
||||
const parts = raw
|
||||
.split('_')
|
||||
.map((part) => String(part || '').trim().toUpperCase())
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return raw;
|
||||
}
|
||||
const mapped = parts.map((token) => FALLBACK_TOKEN_LABELS[token] || token.toLowerCase());
|
||||
const joined = mapped.join(' ').trim();
|
||||
if (!joined) {
|
||||
return raw;
|
||||
}
|
||||
return joined.charAt(0).toUpperCase() + joined.slice(1);
|
||||
}
|
||||
|
||||
export function normalizeStatus(status) {
|
||||
return String(status || '').trim().toUpperCase();
|
||||
}
|
||||
@@ -38,7 +101,7 @@ export function getStatusLabel(status, options = {}) {
|
||||
return 'In der Queue';
|
||||
}
|
||||
const normalized = normalizeStatus(status);
|
||||
return STATUS_LABELS[normalized] || (String(status || '').trim() || '-');
|
||||
return STATUS_LABELS[normalized] || prettifyUnknownStatus(status);
|
||||
}
|
||||
|
||||
export function getStatusSeverity(status, options = {}) {
|
||||
@@ -71,7 +134,7 @@ export function getStatusSeverity(status, options = {}) {
|
||||
|
||||
export function getProcessStatusLabel(status) {
|
||||
const normalized = normalizeStatus(status);
|
||||
return PROCESS_STATUS_LABELS[normalized] || (String(status || '').trim() || '-');
|
||||
return PROCESS_STATUS_LABELS[normalized] || prettifyUnknownStatus(status);
|
||||
}
|
||||
|
||||
export const STATUS_FILTER_OPTIONS = [
|
||||
|
||||
Reference in New Issue
Block a user