0.12.0-1 Plugins und diverses

This commit is contained in:
2026-03-19 18:57:05 +00:00
parent 0a9cf6969f
commit 24955a956d
33 changed files with 3978 additions and 479 deletions
+411 -105
View File
@@ -12,6 +12,7 @@ import { api } from '../api/client';
import JobDetailDialog from '../components/JobDetailDialog';
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
import CdMetadataDialog from '../components/CdMetadataDialog';
import ReencodeConflictModal from '../components/ReencodeConflictModal';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg';
@@ -32,10 +33,10 @@ const MEDIA_FILTER_OPTIONS = [
];
const SORT_OPTIONS = [
{ label: 'Startzeit: Neu -> Alt', value: '!start_time' },
{ label: 'Startzeit: Alt -> Neu', value: 'start_time' },
{ label: 'Endzeit: Neu -> Alt', value: '!end_time' },
{ label: 'Endzeit: Alt -> Neu', value: 'end_time' },
{ label: 'Startzeit: Neu -> Alt', value: '!sortStartTime' },
{ label: 'Startzeit: Alt -> Neu', value: 'sortStartTime' },
{ label: 'Endzeit: Neu -> Alt', value: '!sortEndTime' },
{ label: 'Endzeit: Alt -> Neu', value: 'sortEndTime' },
{ label: 'Titel: A -> Z', value: 'sortTitle' },
{ label: 'Titel: Z -> A', value: '!sortTitle' },
{ label: 'Medium: A -> Z', value: 'sortMediaType' },
@@ -222,7 +223,11 @@ function resolveCdDetails(row) {
selectedMetadata?.mbId
|| selectedMetadata?.musicBrainzId
|| selectedMetadata?.musicbrainzId
|| selectedMetadata?.musicbrainz_id
|| selectedMetadata?.music_brainz_id
|| selectedMetadata?.musicbrainz
|| selectedMetadata?.mbid
|| row?.imdb_id
|| ''
).trim() || null;
@@ -349,6 +354,24 @@ function formatDateTime(value) {
});
}
function parseSortableTimestamp(value) {
if (!value) {
return null;
}
const ts = Date.parse(value);
return Number.isFinite(ts) ? ts : null;
}
function resolveSortTimestamp(...candidates) {
for (const candidate of candidates) {
const ts = parseSortableTimestamp(candidate);
if (ts != null) {
return ts;
}
}
return 0;
}
export default function HistoryPage({ refreshToken = 0 }) {
const location = useLocation();
const navigate = useNavigate();
@@ -357,8 +380,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
const [status, setStatus] = useState('');
const [mediumFilter, setMediumFilter] = useState('');
const [layout, setLayout] = useState('grid');
const [sortKey, setSortKey] = useState('!start_time');
const [sortField, setSortField] = useState('start_time');
const [sortKey, setSortKey] = useState('!sortStartTime');
const [sortField, setSortField] = useState('sortStartTime');
const [sortOrder, setSortOrder] = useState(-1);
const [selectedJob, setSelectedJob] = useState(null);
const [detailVisible, setDetailVisible] = useState(false);
@@ -372,7 +395,9 @@ export default function HistoryPage({ refreshToken = 0 }) {
const [deleteEntryPreview, setDeleteEntryPreview] = useState(null);
const [deleteEntryPreviewLoading, setDeleteEntryPreviewLoading] = useState(false);
const [deleteEntryTargetBusy, setDeleteEntryTargetBusy] = useState(null);
const [deleteEntrySelectedMoviePaths, setDeleteEntrySelectedMoviePaths] = useState([]);
const [downloadBusyTarget, setDownloadBusyTarget] = useState(null);
const [downloadFolderBusyPath, setDownloadFolderBusyPath] = useState(null);
const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
const [metadataDialogContext, setMetadataDialogContext] = useState(null);
const [omdbAssignBusy, setOmdbAssignBusy] = useState(false);
@@ -381,6 +406,12 @@ export default function HistoryPage({ refreshToken = 0 }) {
const [cdMetadataAssignBusy, setCdMetadataAssignBusy] = useState(false);
const [loading, setLoading] = useState(false);
const [queuedJobIds, setQueuedJobIds] = useState([]);
const [conflictModalVisible, setConflictModalVisible] = useState(false);
const [conflictModalJob, setConflictModalJob] = useState(null);
const [conflictModalFolders, setConflictModalFolders] = useState([]);
const [conflictModalAction, setConflictModalAction] = useState(null); // 'reencode' | 'restart_encode' | 'restart_review' | 'restart_cd_review' | 'delete_output'
const [conflictModalMode, setConflictModalMode] = useState('reencode'); // 'reencode' | 'delete'
const [conflictModalBusy, setConflictModalBusy] = useState(false);
const toastRef = useRef(null);
const queuedJobIdSet = useMemo(() => {
@@ -398,7 +429,9 @@ export default function HistoryPage({ refreshToken = 0 }) {
() => jobs.map((job) => ({
...job,
sortTitle: normalizeSortText(job?.title || job?.detected_title || ''),
sortMediaType: resolveMediaType(job)
sortMediaType: resolveMediaType(job),
sortStartTime: resolveSortTimestamp(job?.start_time, job?.updated_at, job?.created_at, job?.end_time),
sortEndTime: resolveSortTimestamp(job?.end_time, job?.updated_at, job?.created_at, job?.start_time)
})),
[jobs]
);
@@ -470,8 +503,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
const onSortChange = (event) => {
const value = String(event.value || '').trim();
if (!value) {
setSortKey('!start_time');
setSortField('start_time');
setSortKey('!sortStartTime');
setSortField('sortStartTime');
setSortOrder(-1);
return;
}
@@ -507,7 +540,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
setDetailLoading(true);
try {
const response = await api.getJob(jobId, { includeLogs: false });
const response = await api.getJob(jobId, { includeLogs: false, forceRefresh: true });
setSelectedJob(response.job);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Fehler', detail: error.message });
@@ -541,11 +574,242 @@ export default function HistoryPage({ refreshToken = 0 }) {
if (!detailVisible || Number(selectedJob?.id || 0) !== Number(jobId || 0)) {
return;
}
const response = await api.getJob(jobId, { includeLogs: false });
const response = await api.getJob(jobId, { includeLogs: false, forceRefresh: true });
setSelectedJob(response.job);
};
// ── Conflict Modal Helpers ─────────────────────────────────────────────────
const mergeOutputFoldersForJob = (job, folders = []) => {
const normalizedJobId = Number(job?.id || 0);
const merged = [];
const seen = new Set();
const addFolder = (folder) => {
const outputPath = String(folder?.output_path || '').trim();
if (!outputPath || seen.has(outputPath)) {
return;
}
seen.add(outputPath);
merged.push({
id: folder?.id ?? 0,
job_id: folder?.job_id ?? (normalizedJobId || null),
output_path: outputPath,
label: folder?.label || null,
created_at: folder?.created_at || null
});
};
for (const folder of (Array.isArray(folders) ? folders : [])) {
addFolder(folder);
}
const currentPath = String(job?.output_path || '').trim();
if (currentPath && !seen.has(currentPath) && Boolean(job?.outputStatus?.exists)) {
addFolder({
id: 0,
job_id: normalizedJobId,
output_path: currentPath,
label: 'Aktuelle Ausgabe',
created_at: null
});
}
return merged;
};
const loadOutputFoldersForJob = async (job) => {
if (!job?.id) {
return [];
}
try {
const response = await api.getOutputFolders(job.id);
return mergeOutputFoldersForJob(job, response?.folders);
} catch (_error) {
return mergeOutputFoldersForJob(job, []);
}
};
const openConflictModal = async (job, action, mode = 'reencode', preloadedFolders = null) => {
const folders = Array.isArray(preloadedFolders)
? mergeOutputFoldersForJob(job, preloadedFolders)
: await loadOutputFoldersForJob(job);
if (folders.length === 0) {
return false;
}
setConflictModalJob(job);
setConflictModalAction(action);
setConflictModalMode(mode);
setConflictModalFolders(folders);
setConflictModalVisible(true);
return true;
};
const closeConflictModal = () => {
if (conflictModalBusy) return;
setConflictModalVisible(false);
setConflictModalJob(null);
setConflictModalAction(null);
setConflictModalMode('reencode');
setConflictModalFolders([]);
};
const executeHistoryAction = async (job, action, options = {}, executionOptions = {}) => {
const skipConfirm = Boolean(executionOptions?.skipConfirm);
if (action === 'reencode') {
setReencodeBusyJobId(job.id);
try {
await api.reencodeJob(job.id, options);
toastRef.current?.show({ severity: 'success', summary: 'Re-Encode gestartet', detail: 'Job wurde in die Mediainfo-Prüfung gesetzt.', life: 3500 });
await load();
await refreshDetailIfOpen(job.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Re-Encode fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setReencodeBusyJobId(null);
}
} else if (action === 'restart_encode') {
setActionBusy(true);
try {
const response = await api.restartEncodeWithLastSettings(job.id, options);
const result = getQueueActionResult(response);
if (result.queued) {
toastRef.current?.show({ severity: 'info', summary: 'Encode-Neustart in Queue', detail: result.queuePosition > 0 ? `Position ${result.queuePosition}` : 'In der Warteschlange eingeplant.', life: 3500 });
} else {
toastRef.current?.show({ severity: 'success', summary: 'Encode-Neustart gestartet', detail: 'Letzte bestätigte Einstellungen werden verwendet.', life: 3500 });
}
await load();
await refreshDetailIfOpen(job.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Encode-Neustart fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setActionBusy(false);
}
} else if (action === 'restart_review') {
const title = job?.title || job?.detected_title || `Job #${job?.id}`;
if (!skipConfirm) {
const confirmed = window.confirm(`Review für "${title}" neu starten?\nDer Job wird erneut analysiert. Spur- und Skriptauswahl kann danach im Dashboard neu getroffen werden.`);
if (!confirmed) {
return;
}
}
setActionBusy(true);
try {
await api.restartReviewFromRaw(job.id, options);
toastRef.current?.show({
severity: 'success',
summary: 'Review-Neustart',
detail: 'Analyse gestartet. Job ist jetzt im Dashboard verfügbar.',
life: 3500
});
await load();
await refreshDetailIfOpen(job.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Review-Neustart fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setActionBusy(false);
}
} else if (action === 'restart_cd_review') {
const title = job?.title || job?.detected_title || `Job #${job?.id}`;
if (!skipConfirm) {
const confirmed = window.confirm(`CD-Vorprüfung für "${title}" starten?\nMusicBrainz-Suche, Trackauswahl und Ausgabeeinstellungen werden im Dashboard geöffnet.`);
if (!confirmed) {
return;
}
}
setActionBusy(true);
try {
await api.restartCdReviewFromRaw(job.id, options);
toastRef.current?.show({
severity: 'success',
summary: 'CD-Vorprüfung gestartet',
detail: 'Job ist jetzt im Dashboard verfügbar — bitte Metadaten und Einstellungen wählen.',
life: 4000
});
await load();
await refreshDetailIfOpen(job.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'CD-Vorprüfung fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setActionBusy(false);
}
} else if (action === 'delete_output') {
const deleteFolders = Array.isArray(options?.deleteFolders)
? options.deleteFolders.filter((value) => typeof value === 'string' && value.trim())
: [];
if (deleteFolders.length === 0) {
toastRef.current?.show({
severity: 'warn',
summary: 'Keine Ordner gewählt',
detail: 'Es wurden keine Output-Ordner zum Löschen erkannt.',
life: 3500
});
return;
}
setActionBusy(true);
try {
const response = await api.deleteOutputFolders(job.id, deleteFolders);
const result = response?.result && typeof response.result === 'object' ? response.result : {};
const deletedCount = Array.isArray(result.deleted) ? result.deleted.length : 0;
const failedCount = Array.isArray(result.failed) ? result.failed.length : 0;
toastRef.current?.show({
severity: failedCount > 0 ? 'warn' : 'success',
summary: failedCount > 0 ? 'Output teilweise gelöscht' : 'Output gelöscht',
detail: failedCount > 0
? `${deletedCount} Ordner gelöscht, ${failedCount} mit Fehler.`
: `${deletedCount} Ordner gelöscht.`,
life: 4000
});
await load();
await refreshDetailIfOpen(job.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Output löschen fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setActionBusy(false);
}
}
};
const handleConflictKeepBoth = async () => {
const job = conflictModalJob;
const action = conflictModalAction;
setConflictModalBusy(true);
try {
await executeHistoryAction(job, action, { keepBoth: true }, { skipConfirm: true });
closeConflictModal();
} finally {
setConflictModalBusy(false);
}
};
const handleConflictDeleteSelected = async (selectedPaths) => {
const job = conflictModalJob;
const action = conflictModalAction;
const selected = Array.isArray(selectedPaths)
? selectedPaths.filter((value) => typeof value === 'string' && value.trim())
: [];
const allKnownPaths = conflictModalFolders
.map((folder) => String(folder?.output_path || '').trim())
.filter(Boolean);
const deleteFolders = selected.length > 0 ? selected : allKnownPaths;
setConflictModalBusy(true);
try {
const options = deleteFolders.length > 0 ? { deleteFolders } : {};
await executeHistoryAction(job, action, options, { skipConfirm: true });
closeConflictModal();
} finally {
setConflictModalBusy(false);
}
};
// ── File deletion ──────────────────────────────────────────────────────────
const handleDeleteFiles = async (row, target) => {
if (target === 'movie') {
const outputFolders = await loadOutputFoldersForJob(row);
if (outputFolders.length > 1) {
await openConflictModal(row, 'delete_output', 'delete', outputFolders);
return;
}
}
const outputLabel = getOutputLabelForRow(row);
const outputShortLabel = getOutputShortLabelForRow(row);
const label = target === 'raw' ? 'RAW-Dateien' : target === 'movie' ? outputLabel : `RAW + ${outputShortLabel}`;
@@ -608,97 +872,64 @@ export default function HistoryPage({ refreshToken = 0 }) {
}
};
const handleDownloadOutputFolder = async (row, folderPath) => {
const jobId = Number(row?.id || selectedJob?.id || 0);
if (!jobId || !folderPath) return;
setDownloadFolderBusyPath(folderPath);
try {
const response = await api.requestJobArchive(jobId, 'output', { outputPath: folderPath });
const item = response?.item && typeof response.item === 'object' ? response.item : null;
const isReady = String(item?.status || '').trim().toLowerCase() === 'ready';
toastRef.current?.show({
severity: isReady ? 'success' : 'info',
summary: isReady ? 'ZIP bereit' : 'ZIP wird erstellt',
detail: isReady
? 'Output-ZIP ist bereits auf der Downloads-Seite verfügbar.'
: 'Output-ZIP wird im Hintergrund erstellt und erscheint danach auf der Downloads-Seite.',
life: 4000
});
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Download fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setDownloadFolderBusyPath(null);
}
};
const maybeOpenOutputConflictModal = async (row, action) => {
const outputFolders = await loadOutputFoldersForJob(row);
if (outputFolders.length === 0) {
return false;
}
await openConflictModal(row, action, 'reencode', outputFolders);
return true;
};
const handleReencode = async (row) => {
const title = row.title || row.detected_title || `Job #${row.id}`;
const confirmed = window.confirm(`RAW neu encodieren für "${title}" starten?`);
if (!confirmed) {
if (await maybeOpenOutputConflictModal(row, 'reencode')) {
return;
}
setReencodeBusyJobId(row.id);
try {
await api.reencodeJob(row.id);
toastRef.current?.show({
severity: 'success',
summary: 'Re-Encode gestartet',
detail: 'Job wurde in die Mediainfo-Prüfung gesetzt.',
life: 3500
});
await load();
await refreshDetailIfOpen(row.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Re-Encode fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setReencodeBusyJobId(null);
}
await executeHistoryAction(row, 'reencode');
};
const handleRestartEncode = async (row) => {
const title = row.title || row.detected_title || `Job #${row.id}`;
if (row?.encodeSuccess) {
const confirmed = window.confirm(
`Encode für "${title}" ist bereits erfolgreich abgeschlossen. Wirklich erneut encodieren?\n`
+ 'Es wird eine neue Datei mit Kollisionsprüfung angelegt.'
);
if (!confirmed) {
return;
}
}
setActionBusy(true);
try {
const response = await api.restartEncodeWithLastSettings(row.id);
const result = getQueueActionResult(response);
if (result.queued) {
const queuePosition = Number(result?.queuePosition || 0);
toastRef.current?.show({
severity: 'info',
summary: 'Encode-Neustart in Queue',
detail: queuePosition > 0
? `Job wurde auf Position ${queuePosition} eingeplant.`
: 'Job wurde in die Warteschlange eingeplant.',
life: 3500
});
} else {
toastRef.current?.show({
severity: 'success',
summary: 'Encode-Neustart gestartet',
detail: 'Letzte bestätigte Einstellungen werden verwendet.',
life: 3500
});
}
await load();
await refreshDetailIfOpen(row.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Encode-Neustart fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setActionBusy(false);
if (await maybeOpenOutputConflictModal(row, 'restart_encode')) {
return;
}
await executeHistoryAction(row, 'restart_encode');
};
const handleRestartReview = async (row) => {
const title = row?.title || row?.detected_title || `Job #${row?.id}`;
const confirmed = window.confirm(`Review für "${title}" neu starten?\nDer Job wird erneut analysiert. Spur- und Skriptauswahl kann danach im Dashboard neu getroffen werden.`);
if (!confirmed) {
if (await maybeOpenOutputConflictModal(row, 'restart_review')) {
return;
}
await executeHistoryAction(row, 'restart_review');
};
setActionBusy(true);
try {
await api.restartReviewFromRaw(row.id);
toastRef.current?.show({
severity: 'success',
summary: 'Review-Neustart',
detail: 'Analyse gestartet. Job ist jetzt im Dashboard verfügbar.',
life: 3500
});
await load();
await refreshDetailIfOpen(row.id);
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Review-Neustart fehlgeschlagen', detail: error.message, life: 4500 });
} finally {
setActionBusy(false);
const handleRestartCdReview = async (row) => {
if (await maybeOpenOutputConflictModal(row, 'restart_cd_review')) {
return;
}
await executeHistoryAction(row, 'restart_cd_review');
};
const handleRetry = async (row) => {
@@ -725,7 +956,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
});
await load();
if (replacementJobId) {
const detailResponse = await api.getJob(replacementJobId, { includeLogs: false });
const detailResponse = await api.getJob(replacementJobId, { includeLogs: false, forceRefresh: true });
setSelectedJob(detailResponse.job);
setDetailVisible(true);
} else {
@@ -850,6 +1081,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
setDeleteEntryPreview(null);
setDeleteEntryPreviewLoading(false);
setDeleteEntryTargetBusy(null);
setDeleteEntrySelectedMoviePaths([]);
};
const handleDeleteEntry = async (row) => {
@@ -859,12 +1091,20 @@ export default function HistoryPage({ refreshToken = 0 }) {
}
setDeleteEntryDialogRow(row);
setDeleteEntryPreview(null);
setDeleteEntrySelectedMoviePaths([]);
setDeleteEntryDialogVisible(true);
setDeleteEntryPreviewLoading(true);
setDeleteEntryBusy(true);
try {
const response = await api.getJobDeletePreview(jobId, { includeRelated: true });
setDeleteEntryPreview(response?.preview || null);
const preview = response?.preview || null;
setDeleteEntryPreview(preview);
const movieCandidates = Array.isArray(preview?.pathCandidates?.movie) ? preview.pathCandidates.movie : [];
const defaultSelectedMoviePaths = movieCandidates
.filter((item) => Boolean(item?.exists))
.map((item) => String(item?.path || '').trim())
.filter(Boolean);
setDeleteEntrySelectedMoviePaths(defaultSelectedMoviePaths);
} catch (error) {
toastRef.current?.show({
severity: 'error',
@@ -890,11 +1130,27 @@ export default function HistoryPage({ refreshToken = 0 }) {
if (!jobId) {
return;
}
if (
(normalizedTarget === 'movie' || normalizedTarget === 'both')
&& previewMovieExisting.length > 0
&& deleteEntrySelectedMoviePaths.length === 0
) {
toastRef.current?.show({
severity: 'warn',
summary: `${deleteEntryOutputShortLabel}-Ordner auswählen`,
detail: `Bitte mindestens einen ${deleteEntryOutputShortLabel}-Ordner zum Löschen auswählen.`,
life: 3500
});
return;
}
setDeleteEntryBusy(true);
setDeleteEntryTargetBusy(normalizedTarget);
try {
const response = await api.deleteJobEntry(jobId, normalizedTarget, { includeRelated: true });
const response = await api.deleteJobEntry(jobId, normalizedTarget, {
includeRelated: true,
selectedMoviePaths: deleteEntrySelectedMoviePaths
});
const deletedJobIds = Array.isArray(response?.deletedJobIds) ? response.deletedJobIds : [];
const fileSummary = response?.fileSummary || {};
const rawFiles = Number(fileSummary?.raw?.filesDeleted || 0);
@@ -929,6 +1185,24 @@ export default function HistoryPage({ refreshToken = 0 }) {
}
};
const toggleDeleteMoviePathSelection = (moviePath, checked) => {
const normalizedPath = String(moviePath || '').trim();
if (!normalizedPath) {
return;
}
setDeleteEntrySelectedMoviePaths((previous) => {
const nextSet = new Set((Array.isArray(previous) ? previous : []).map((item) => String(item || '').trim()).filter(Boolean));
if (checked) {
nextSet.add(normalizedPath);
} else {
nextSet.delete(normalizedPath);
}
return previewMoviePaths
.map((item) => String(item?.path || '').trim())
.filter((itemPath) => itemPath && nextSet.has(itemPath));
});
};
const handleRemoveFromQueue = async (row) => {
const jobId = normalizeJobId(row?.id || row);
if (!jobId) {
@@ -1202,6 +1476,13 @@ export default function HistoryPage({ refreshToken = 0 }) {
const previewMoviePaths = Array.isArray(deleteEntryPreview?.pathCandidates?.movie) ? deleteEntryPreview.pathCandidates.movie : [];
const previewRawExisting = previewRawPaths.filter((item) => Boolean(item?.exists));
const previewMovieExisting = previewMoviePaths.filter((item) => Boolean(item?.exists));
const previewRawDisplay = previewRawExisting.length > 0 ? previewRawExisting : previewRawPaths.slice(0, 1);
const previewMovieDisplay = previewMovieExisting.length > 0 ? previewMovieExisting : previewMoviePaths.slice(0, 1);
const selectedDeleteMoviePathSet = useMemo(
() => new Set(deleteEntrySelectedMoviePaths.map((item) => String(item || '').trim()).filter(Boolean)),
[deleteEntrySelectedMoviePaths]
);
const movieDeleteSelectionRequired = previewMovieExisting.length > 0 && deleteEntrySelectedMoviePaths.length === 0;
const deleteTargetActionsDisabled = deleteEntryPreviewLoading || Boolean(deleteEntryTargetBusy) || !deleteEntryPreview;
const deleteEntryOutputLabel = getOutputLabelForRow(deleteEntryDialogRow);
const deleteEntryOutputShortLabel = getOutputShortLabelForRow(deleteEntryDialogRow);
@@ -1281,6 +1562,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
logLoadingMode={logLoadingMode}
onRestartEncode={handleRestartEncode}
onRestartReview={handleRestartReview}
onRestartCdReview={handleRestartCdReview}
onReencode={handleReencode}
onRetry={handleRetry}
onAssignOmdb={handleAssignOmdb}
@@ -1288,6 +1570,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
onDeleteFiles={handleDeleteFiles}
onDeleteEntry={handleDeleteEntry}
onDownloadArchive={handleDownloadArchive}
onDownloadOutputFolder={handleDownloadOutputFolder}
onRemoveFromQueue={handleRemoveFromQueue}
isQueued={Boolean(selectedJob?.id && queuedJobIdSet.has(normalizeJobId(selectedJob.id)))}
actionBusy={actionBusy}
@@ -1296,11 +1579,13 @@ export default function HistoryPage({ refreshToken = 0 }) {
reencodeBusy={reencodeBusyJobId === selectedJob?.id}
deleteEntryBusy={deleteEntryBusy}
downloadBusyTarget={downloadBusyTarget}
downloadFolderBusyPath={downloadFolderBusyPath}
onHide={() => {
setDetailVisible(false);
setDetailLoading(false);
setLogLoadingMode(null);
setDownloadBusyTarget(null);
setDownloadFolderBusyPath(null);
}}
/>
@@ -1344,12 +1629,9 @@ export default function HistoryPage({ refreshToken = 0 }) {
<div>
<h4>RAW</h4>
{previewRawPaths.length > 0 ? (() => {
const display = previewRawPaths.filter(p => p.exists).length > 0
? previewRawPaths.filter(p => p.exists)
: previewRawPaths.slice(0, 1);
return (
<ul className="history-delete-preview-list">
{display.map((item) => (
{previewRawDisplay.map((item) => (
<li key={`delete-raw-${item.path}`}>
<span className={item.exists ? 'exists-yes' : 'exists-no'}>
{item.exists ? 'vorhanden' : 'nicht gefunden'}
@@ -1367,17 +1649,30 @@ export default function HistoryPage({ refreshToken = 0 }) {
<div>
<h4>{deleteEntryOutputShortLabel}</h4>
{previewMoviePaths.length > 0 ? (() => {
const display = previewMoviePaths.filter(p => p.exists).length > 0
? previewMoviePaths.filter(p => p.exists)
: previewMoviePaths.slice(0, 1);
return (
<ul className="history-delete-preview-list">
{display.map((item) => (
{previewMovieDisplay.map((item) => (
<li key={`delete-movie-${item.path}`}>
<span className={item.exists ? 'exists-yes' : 'exists-no'}>
{item.exists ? 'vorhanden' : 'nicht gefunden'}
</span>
{' '}| {item.path}
{item.exists ? (
<label className="history-delete-preview-checkbox-row">
<input
type="checkbox"
checked={selectedDeleteMoviePathSet.has(String(item.path || '').trim())}
onChange={(event) => toggleDeleteMoviePathSelection(item.path, Boolean(event.target.checked))}
/>
<span className={item.exists ? 'exists-yes' : 'exists-no'}>
{item.exists ? 'vorhanden' : 'nicht gefunden'}
</span>
<span>| {item.path}</span>
</label>
) : (
<>
<span className={item.exists ? 'exists-yes' : 'exists-no'}>
{item.exists ? 'vorhanden' : 'nicht gefunden'}
</span>
{' '}| {item.path}
</>
)}
</li>
))}
</ul>
@@ -1406,7 +1701,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
outlined
onClick={() => confirmDeleteEntry('movie')}
loading={deleteEntryTargetBusy === 'movie'}
disabled={deleteTargetActionsDisabled}
disabled={deleteTargetActionsDisabled || movieDeleteSelectionRequired}
/>
<Button
label="Beides löschen"
@@ -1414,7 +1709,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
severity="danger"
onClick={() => confirmDeleteEntry('both')}
loading={deleteEntryTargetBusy === 'both'}
disabled={deleteTargetActionsDisabled}
disabled={deleteTargetActionsDisabled || movieDeleteSelectionRequired}
/>
<Button
label="Nur Eintrag löschen"
@@ -1459,6 +1754,17 @@ export default function HistoryPage({ refreshToken = 0 }) {
onFetchRelease={handleMusicBrainzReleaseFetch}
busy={cdMetadataAssignBusy}
/>
<ReencodeConflictModal
visible={conflictModalVisible}
onHide={closeConflictModal}
job={conflictModalJob}
existingFolders={conflictModalFolders}
mode={conflictModalMode}
onKeepBoth={handleConflictKeepBoth}
onDeleteSelected={handleConflictDeleteSelected}
busy={conflictModalBusy}
/>
</div>
);
}