DVD Integration
This commit is contained in:
@@ -14,39 +14,41 @@ function normalizeSettingKey(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
const GENERAL_TOOL_KEYS = new Set([
|
||||
'makemkv_command',
|
||||
'makemkv_registration_key',
|
||||
'makemkv_min_length_minutes',
|
||||
'mediainfo_command',
|
||||
'handbrake_command',
|
||||
'handbrake_restart_delete_incomplete_output'
|
||||
]);
|
||||
|
||||
const HANDBRAKE_PRESET_SETTING_KEYS = new Set([
|
||||
'handbrake_preset',
|
||||
'handbrake_preset_bluray',
|
||||
'handbrake_preset_dvd'
|
||||
]);
|
||||
|
||||
function buildToolSections(settings) {
|
||||
const list = Array.isArray(settings) ? settings : [];
|
||||
const definitions = [
|
||||
{
|
||||
id: 'makemkv',
|
||||
title: 'MakeMKV',
|
||||
description: 'Disc-Analyse und Rip-Einstellungen.',
|
||||
match: (key) => key.startsWith('makemkv_')
|
||||
},
|
||||
{
|
||||
id: 'mediainfo',
|
||||
title: 'MediaInfo',
|
||||
description: 'Track-Analyse und zusätzliche mediainfo Parameter.',
|
||||
match: (key) => key.startsWith('mediainfo_')
|
||||
},
|
||||
{
|
||||
id: 'handbrake',
|
||||
title: 'HandBrake',
|
||||
description: 'Preset, Encoding-CLI und HandBrake-Optionen.',
|
||||
match: (key) => key.startsWith('handbrake_')
|
||||
},
|
||||
{
|
||||
id: 'output',
|
||||
title: 'Output',
|
||||
description: 'Container-Format sowie Datei- und Ordnernamen-Template.',
|
||||
match: (key) => key === 'output_extension' || key === 'filename_template' || key === 'output_folder_template'
|
||||
}
|
||||
];
|
||||
|
||||
const buckets = definitions.map((item) => ({
|
||||
...item,
|
||||
const generalBucket = {
|
||||
id: 'general',
|
||||
title: 'General',
|
||||
description: 'Gemeinsame Tool-Settings für alle Medien.',
|
||||
settings: []
|
||||
}));
|
||||
};
|
||||
const blurayBucket = {
|
||||
id: 'bluray',
|
||||
title: 'BluRay',
|
||||
description: 'Profil-spezifische Settings für Blu-ray.',
|
||||
settings: []
|
||||
};
|
||||
const dvdBucket = {
|
||||
id: 'dvd',
|
||||
title: 'DVD',
|
||||
description: 'Profil-spezifische Settings für DVD.',
|
||||
settings: []
|
||||
};
|
||||
const fallbackBucket = {
|
||||
id: 'other',
|
||||
title: 'Weitere Tool-Settings',
|
||||
@@ -56,20 +58,26 @@ function buildToolSections(settings) {
|
||||
|
||||
for (const setting of list) {
|
||||
const key = normalizeSettingKey(setting?.key);
|
||||
let assigned = false;
|
||||
for (const bucket of buckets) {
|
||||
if (bucket.match(key)) {
|
||||
bucket.settings.push(setting);
|
||||
assigned = true;
|
||||
break;
|
||||
}
|
||||
if (GENERAL_TOOL_KEYS.has(key)) {
|
||||
generalBucket.settings.push(setting);
|
||||
continue;
|
||||
}
|
||||
if (!assigned) {
|
||||
fallbackBucket.settings.push(setting);
|
||||
if (key.endsWith('_bluray')) {
|
||||
blurayBucket.settings.push(setting);
|
||||
continue;
|
||||
}
|
||||
if (key.endsWith('_dvd')) {
|
||||
dvdBucket.settings.push(setting);
|
||||
continue;
|
||||
}
|
||||
fallbackBucket.settings.push(setting);
|
||||
}
|
||||
|
||||
const sections = buckets.filter((item) => item.settings.length > 0);
|
||||
const sections = [
|
||||
generalBucket,
|
||||
blurayBucket,
|
||||
dvdBucket
|
||||
].filter((item) => item.settings.length > 0);
|
||||
if (fallbackBucket.settings.length > 0) {
|
||||
sections.push(fallbackBucket);
|
||||
}
|
||||
@@ -96,7 +104,8 @@ function buildSectionsForCategory(categoryName, settings) {
|
||||
}
|
||||
|
||||
function isHandBrakePresetSetting(setting) {
|
||||
return String(setting?.key || '').trim().toLowerCase() === 'handbrake_preset';
|
||||
const key = String(setting?.key || '').trim().toLowerCase();
|
||||
return HANDBRAKE_PRESET_SETTING_KEYS.has(key);
|
||||
}
|
||||
|
||||
export default function DynamicSettingsForm({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from 'primereact/button';
|
||||
import MediaInfoReviewPanel from './MediaInfoReviewPanel';
|
||||
import blurayIndicatorIcon from '../assets/media-bluray.svg';
|
||||
import discIndicatorIcon from '../assets/media-disc.svg';
|
||||
import otherIndicatorIcon from '../assets/media-other.svg';
|
||||
import { getStatusLabel } from '../utils/statusPresentation';
|
||||
|
||||
function JsonView({ title, value }) {
|
||||
@@ -14,9 +15,54 @@ function JsonView({ title, value }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ScriptResultRow({ result }) {
|
||||
const status = String(result?.status || '').toUpperCase();
|
||||
const isSuccess = status === 'SUCCESS';
|
||||
const isError = status === 'ERROR';
|
||||
const isSkipped = status.startsWith('SKIPPED');
|
||||
const icon = isSuccess ? 'pi-check-circle' : isError ? 'pi-times-circle' : 'pi-minus-circle';
|
||||
const tone = isSuccess ? 'success' : isError ? 'danger' : 'warning';
|
||||
return (
|
||||
<div className="script-result-row">
|
||||
<span className={`job-step-inline-${isSuccess ? 'ok' : isError ? 'no' : 'warn'}`}>
|
||||
<i className={`pi ${icon}`} aria-hidden="true" />
|
||||
</span>
|
||||
<span className="script-result-name">{result?.scriptName || result?.chainName || `#${result?.scriptId ?? result?.chainId ?? '?'}`}</span>
|
||||
<span className={`script-result-status tone-${tone}`}>{status}</span>
|
||||
{result?.error ? <span className="script-result-error">{result.error}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScriptSummarySection({ title, summary }) {
|
||||
if (!summary || summary.configured === 0) return null;
|
||||
const results = Array.isArray(summary.results) ? summary.results : [];
|
||||
return (
|
||||
<div className="script-summary-block">
|
||||
<strong>{title}:</strong>
|
||||
<span className="script-summary-counts">
|
||||
{summary.succeeded > 0 ? <span className="tone-success">{summary.succeeded} OK</span> : null}
|
||||
{summary.failed > 0 ? <span className="tone-danger">{summary.failed} Fehler</span> : null}
|
||||
{summary.skipped > 0 ? <span className="tone-warning">{summary.skipped} übersprungen</span> : null}
|
||||
</span>
|
||||
{results.length > 0 ? (
|
||||
<div className="script-result-list">
|
||||
{results.map((r, i) => <ScriptResultRow key={i} result={r} />)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMediaType(job) {
|
||||
const raw = String(job?.mediaType || job?.media_type || '').trim().toLowerCase();
|
||||
return raw === 'bluray' ? 'bluray' : 'disc';
|
||||
if (raw === 'bluray') {
|
||||
return 'bluray';
|
||||
}
|
||||
if (raw === 'dvd' || raw === 'disc') {
|
||||
return 'dvd';
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function statusBadgeMeta(status, queued = false) {
|
||||
@@ -132,9 +178,15 @@ export default function JobDetailDialog({
|
||||
const logLoaded = Boolean(logMeta?.loaded) || Boolean(job?.log);
|
||||
const logTruncated = Boolean(logMeta?.truncated);
|
||||
const mediaType = resolveMediaType(job);
|
||||
const mediaTypeLabel = mediaType === 'bluray' ? 'Blu-ray' : 'Sonstiges Medium';
|
||||
const mediaTypeIcon = mediaType === 'bluray' ? blurayIndicatorIcon : discIndicatorIcon;
|
||||
const mediaTypeAlt = mediaType === 'bluray' ? 'Blu-ray' : 'Disc';
|
||||
const mediaTypeLabel = mediaType === 'bluray'
|
||||
? 'Blu-ray'
|
||||
: (mediaType === 'dvd' ? 'DVD' : 'Sonstiges Medium');
|
||||
const mediaTypeIcon = mediaType === 'bluray'
|
||||
? blurayIndicatorIcon
|
||||
: (mediaType === 'dvd' ? discIndicatorIcon : otherIndicatorIcon);
|
||||
const mediaTypeAlt = mediaType === 'bluray'
|
||||
? 'Blu-ray'
|
||||
: (mediaType === 'dvd' ? 'DVD' : 'Sonstiges Medium');
|
||||
const statusMeta = statusBadgeMeta(job?.status, queueLocked);
|
||||
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
|
||||
|
||||
@@ -267,6 +319,16 @@ export default function JobDetailDialog({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{(job.handbrakeInfo?.preEncodeScripts?.configured > 0 || job.handbrakeInfo?.postEncodeScripts?.configured > 0) ? (
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Skripte</h4>
|
||||
<div className="script-results-grid">
|
||||
<ScriptSummarySection title="Pre-Encode" summary={job.handbrakeInfo?.preEncodeScripts} />
|
||||
<ScriptSummarySection title="Post-Encode" summary={job.handbrakeInfo?.postEncodeScripts} />
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<div className="job-json-grid">
|
||||
<JsonView title="OMDb Info" value={job.omdbInfo} />
|
||||
<JsonView title="MakeMKV Info" value={job.makemkvInfo} />
|
||||
|
||||
@@ -685,27 +685,19 @@ export default function MediaInfoReviewPanel({
|
||||
allowTrackSelection = false,
|
||||
trackSelectionByTitle = {},
|
||||
onTrackSelectionChange = null,
|
||||
availablePostScripts = [],
|
||||
selectedPostEncodeScriptIds = [],
|
||||
allowPostScriptSelection = false,
|
||||
onAddPostEncodeScript = null,
|
||||
onChangePostEncodeScript = null,
|
||||
onRemovePostEncodeScript = null,
|
||||
onReorderPostEncodeScript = null,
|
||||
availablePreScripts = [],
|
||||
selectedPreEncodeScriptIds = [],
|
||||
allowPreScriptSelection = false,
|
||||
onAddPreEncodeScript = null,
|
||||
onChangePreEncodeScript = null,
|
||||
onRemovePreEncodeScript = null,
|
||||
availableScripts = [],
|
||||
availableChains = [],
|
||||
selectedPreEncodeChainIds = [],
|
||||
selectedPostEncodeChainIds = [],
|
||||
allowChainSelection = false,
|
||||
onAddPreEncodeChain = null,
|
||||
onRemovePreEncodeChain = null,
|
||||
onAddPostEncodeChain = null,
|
||||
onRemovePostEncodeChain = null
|
||||
preEncodeItems = [],
|
||||
postEncodeItems = [],
|
||||
allowEncodeItemSelection = false,
|
||||
onAddPreEncodeItem = null,
|
||||
onChangePreEncodeItem = null,
|
||||
onRemovePreEncodeItem = null,
|
||||
onReorderPreEncodeItem = null,
|
||||
onAddPostEncodeItem = null,
|
||||
onChangePostEncodeItem = null,
|
||||
onRemovePostEncodeItem = null,
|
||||
onReorderPostEncodeItem = null
|
||||
}) {
|
||||
if (!review) {
|
||||
return <p>Keine Mediainfo-Daten vorhanden.</p>;
|
||||
@@ -718,30 +710,33 @@ export default function MediaInfoReviewPanel({
|
||||
const totalFiles = Number(review.totalFiles || titles.length || 0);
|
||||
const playlistRecommendation = review.playlistRecommendation || null;
|
||||
const presetLabel = String(presetDisplayValue || review.selectors?.preset || '').trim() || '-';
|
||||
const scriptRows = normalizeScriptIdList(selectedPostEncodeScriptIds);
|
||||
const scriptCatalog = (Array.isArray(availablePostScripts) ? availablePostScripts : [])
|
||||
const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : [])
|
||||
.map((item) => ({
|
||||
id: normalizeScriptId(item?.id),
|
||||
name: String(item?.name || '').trim()
|
||||
}))
|
||||
.filter((item) => item.id !== null && item.name.length > 0);
|
||||
const scriptById = new Map(scriptCatalog.map((item) => [item.id, item]));
|
||||
const canAddScriptRow = allowPostScriptSelection && scriptCatalog.length > 0 && scriptRows.length < scriptCatalog.length;
|
||||
const canReorderScriptRows = allowPostScriptSelection && scriptRows.length > 1;
|
||||
const chainCatalog = (Array.isArray(availableChains) ? availableChains : [])
|
||||
.map((item) => ({ id: Number(item?.id), name: String(item?.name || '').trim() }))
|
||||
.filter((item) => Number.isFinite(item.id) && item.id > 0 && item.name.length > 0);
|
||||
const chainById = new Map(chainCatalog.map((item) => [item.id, item]));
|
||||
|
||||
const handleScriptDrop = (event, targetIndex) => {
|
||||
if (!allowPostScriptSelection || typeof onReorderPostEncodeScript !== 'function') {
|
||||
const makeHandleDrop = (items, onReorder) => (event, targetIndex) => {
|
||||
if (!allowEncodeItemSelection || typeof onReorder !== 'function' || items.length < 2) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const fromText = event.dataTransfer?.getData('text/plain');
|
||||
const fromIndex = Number(fromText);
|
||||
const fromIndex = Number(event.dataTransfer?.getData('text/plain'));
|
||||
if (!Number.isInteger(fromIndex)) {
|
||||
return;
|
||||
}
|
||||
onReorderPostEncodeScript(fromIndex, targetIndex);
|
||||
onReorder(fromIndex, targetIndex);
|
||||
};
|
||||
|
||||
const handlePreDrop = makeHandleDrop(preEncodeItems, onReorderPreEncodeItem);
|
||||
const handlePostDrop = makeHandleDrop(postEncodeItems, onReorderPostEncodeItem);
|
||||
|
||||
return (
|
||||
<div className="media-review-wrap">
|
||||
<div className="media-review-meta">
|
||||
@@ -780,215 +775,196 @@ export default function MediaInfoReviewPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Pre-Encode Scripts */}
|
||||
{(allowPreScriptSelection || normalizeScriptIdList(selectedPreEncodeScriptIds).length > 0) ? (
|
||||
{/* Pre-Encode Items (scripts + chains unified) */}
|
||||
{(allowEncodeItemSelection || preEncodeItems.length > 0) ? (
|
||||
<div className="post-script-box">
|
||||
<h4>Pre-Encode Scripte (optional)</h4>
|
||||
{(Array.isArray(availablePreScripts) ? availablePreScripts : []).length === 0 ? (
|
||||
<small>Keine Scripte konfiguriert. In den Settings unter "Scripte" anlegen.</small>
|
||||
<h4>Pre-Encode Ausführungen (optional)</h4>
|
||||
{scriptCatalog.length === 0 && chainCatalog.length === 0 ? (
|
||||
<small>Keine Skripte oder Ketten konfiguriert. In den Settings anlegen.</small>
|
||||
) : null}
|
||||
{normalizeScriptIdList(selectedPreEncodeScriptIds).length === 0 ? (
|
||||
<small>Keine Pre-Encode Scripte ausgewählt.</small>
|
||||
{preEncodeItems.length === 0 ? (
|
||||
<small>Keine Pre-Encode Ausführungen ausgewählt.</small>
|
||||
) : null}
|
||||
{normalizeScriptIdList(selectedPreEncodeScriptIds).map((scriptId, rowIndex) => {
|
||||
const preCatalog = (Array.isArray(availablePreScripts) ? availablePreScripts : [])
|
||||
.map((item) => ({ id: normalizeScriptId(item?.id), name: String(item?.name || '') }))
|
||||
.filter((item) => item.id !== null);
|
||||
const preById = new Map(preCatalog.map((item) => [item.id, item]));
|
||||
const script = preById.get(scriptId) || null;
|
||||
const selectedElsewhere = new Set(
|
||||
normalizeScriptIdList(selectedPreEncodeScriptIds).filter((_, i) => i !== rowIndex).map((id) => String(id))
|
||||
{preEncodeItems.map((item, rowIndex) => {
|
||||
const isScript = item.type === 'script';
|
||||
const canDrag = allowEncodeItemSelection && preEncodeItems.length > 1;
|
||||
const scriptObj = isScript ? (scriptById.get(normalizeScriptId(item.id)) || null) : null;
|
||||
const chainObj = !isScript ? (chainById.get(Number(item.id)) || null) : null;
|
||||
const name = isScript
|
||||
? (scriptObj?.name || `Skript #${item.id}`)
|
||||
: (chainObj?.name || `Kette #${item.id}`);
|
||||
const usedScriptIds = new Set(
|
||||
preEncodeItems.filter((it, i) => it.type === 'script' && i !== rowIndex).map((it) => String(normalizeScriptId(it.id)))
|
||||
);
|
||||
const options = preCatalog.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
disabled: selectedElsewhere.has(String(item.id))
|
||||
const scriptOptions = scriptCatalog.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
disabled: usedScriptIds.has(String(s.id))
|
||||
}));
|
||||
return (
|
||||
<div key={`pre-script-row-${rowIndex}-${scriptId}`} className={`post-script-row${allowPreScriptSelection ? ' editable' : ''}`}>
|
||||
{allowPreScriptSelection ? (
|
||||
<div
|
||||
key={`pre-item-${rowIndex}-${item.type}-${item.id}`}
|
||||
className={`post-script-row${allowEncodeItemSelection ? ' editable' : ''}`}
|
||||
onDragOver={(event) => {
|
||||
if (!canDrag) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
||||
}}
|
||||
onDrop={(event) => handlePreDrop(event, rowIndex)}
|
||||
>
|
||||
{allowEncodeItemSelection ? (
|
||||
<>
|
||||
<Dropdown
|
||||
value={scriptId}
|
||||
options={options}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
optionDisabled="disabled"
|
||||
onChange={(event) => onChangePreEncodeScript?.(rowIndex, event.value)}
|
||||
className="full-width"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
outlined
|
||||
onClick={() => onRemovePreEncodeScript?.(rowIndex)}
|
||||
<span
|
||||
className={`post-script-drag-handle pi pi-bars${canDrag ? '' : ' disabled'}`}
|
||||
title={canDrag ? 'Ziehen zum Umordnen' : 'Mindestens zwei Einträge zum Umordnen'}
|
||||
draggable={canDrag}
|
||||
onDragStart={(event) => {
|
||||
if (!canDrag) return;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', String(rowIndex));
|
||||
}}
|
||||
/>
|
||||
<i className={`post-script-type-icon pi ${isScript ? 'pi-code' : 'pi-link'}`} title={isScript ? 'Skript' : 'Kette'} />
|
||||
{isScript ? (
|
||||
<Dropdown
|
||||
value={normalizeScriptId(item.id)}
|
||||
options={scriptOptions}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
optionDisabled="disabled"
|
||||
onChange={(event) => onChangePreEncodeItem?.(rowIndex, 'script', event.value)}
|
||||
className="full-width"
|
||||
/>
|
||||
) : (
|
||||
<span className="post-script-chain-name">{name}</span>
|
||||
)}
|
||||
<Button icon="pi pi-times" severity="danger" outlined onClick={() => onRemovePreEncodeItem?.(rowIndex)} />
|
||||
</>
|
||||
) : (
|
||||
<small>{`${rowIndex + 1}. ${script?.name || `Script #${scriptId}`}`}</small>
|
||||
<small><i className={`pi ${isScript ? 'pi-code' : 'pi-link'}`} /> {`${rowIndex + 1}. ${name}`}</small>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{allowPreScriptSelection && (Array.isArray(availablePreScripts) ? availablePreScripts : []).length > normalizeScriptIdList(selectedPreEncodeScriptIds).length ? (
|
||||
<Button
|
||||
label="Pre-Script hinzufügen"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => onAddPreEncodeScript?.()}
|
||||
/>
|
||||
) : null}
|
||||
<small>Diese Scripte werden vor dem Encoding ausgeführt. Bei Fehler wird der Encode abgebrochen.</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Chain Selections */}
|
||||
{(allowChainSelection || selectedPreEncodeChainIds.length > 0 || selectedPostEncodeChainIds.length > 0) ? (
|
||||
<div className="post-script-box">
|
||||
<h4>Skriptketten (optional)</h4>
|
||||
{(Array.isArray(availableChains) ? availableChains : []).length === 0 ? (
|
||||
<small>Keine Skriptketten konfiguriert. In den Settings unter "Skriptketten" anlegen.</small>
|
||||
) : null}
|
||||
{(Array.isArray(availableChains) ? availableChains : []).length > 0 ? (
|
||||
<div className="chain-selection-groups">
|
||||
<div className="chain-selection-group">
|
||||
<strong>Pre-Encode Ketten</strong>
|
||||
{selectedPreEncodeChainIds.length === 0 ? <small>Keine ausgewählt.</small> : null}
|
||||
{selectedPreEncodeChainIds.map((chainId, index) => {
|
||||
const chain = (Array.isArray(availableChains) ? availableChains : []).find((c) => Number(c.id) === chainId);
|
||||
return (
|
||||
<div key={`pre-chain-${index}-${chainId}`} className="post-script-row editable">
|
||||
<small>{`${index + 1}. ${chain?.name || `Kette #${chainId}`}`}</small>
|
||||
{allowChainSelection ? (
|
||||
<Button icon="pi pi-times" severity="danger" outlined onClick={() => onRemovePreEncodeChain?.(index)} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{allowChainSelection ? (
|
||||
<Dropdown
|
||||
value={null}
|
||||
options={(Array.isArray(availableChains) ? availableChains : [])
|
||||
.filter((c) => !selectedPreEncodeChainIds.includes(Number(c.id)))
|
||||
.map((c) => ({ label: c.name, value: c.id }))}
|
||||
onChange={(e) => onAddPreEncodeChain?.(e.value)}
|
||||
placeholder="Kette hinzufügen..."
|
||||
className="chain-add-dropdown"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="chain-selection-group">
|
||||
<strong>Post-Encode Ketten</strong>
|
||||
{selectedPostEncodeChainIds.length === 0 ? <small>Keine ausgewählt.</small> : null}
|
||||
{selectedPostEncodeChainIds.map((chainId, index) => {
|
||||
const chain = (Array.isArray(availableChains) ? availableChains : []).find((c) => Number(c.id) === chainId);
|
||||
return (
|
||||
<div key={`post-chain-${index}-${chainId}`} className="post-script-row editable">
|
||||
<small>{`${index + 1}. ${chain?.name || `Kette #${chainId}`}`}</small>
|
||||
{allowChainSelection ? (
|
||||
<Button icon="pi pi-times" severity="danger" outlined onClick={() => onRemovePostEncodeChain?.(index)} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{allowChainSelection ? (
|
||||
<Dropdown
|
||||
value={null}
|
||||
options={(Array.isArray(availableChains) ? availableChains : [])
|
||||
.filter((c) => !selectedPostEncodeChainIds.includes(Number(c.id)))
|
||||
.map((c) => ({ label: c.name, value: c.id }))}
|
||||
onChange={(e) => onAddPostEncodeChain?.(e.value)}
|
||||
placeholder="Kette hinzufügen..."
|
||||
className="chain-add-dropdown"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{allowEncodeItemSelection ? (
|
||||
<div className="encode-item-add-row">
|
||||
{scriptCatalog.length > preEncodeItems.filter((i) => i.type === 'script').length ? (
|
||||
<Button
|
||||
label="Skript hinzufügen"
|
||||
icon="pi pi-code"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => onAddPreEncodeItem?.('script')}
|
||||
/>
|
||||
) : null}
|
||||
{chainCatalog.length > preEncodeItems.filter((i) => i.type === 'chain').length ? (
|
||||
<Button
|
||||
label="Kette hinzufügen"
|
||||
icon="pi pi-link"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => onAddPreEncodeItem?.('chain')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<small>Ausführung vor dem Encoding, strikt nacheinander. Bei Fehler wird der Encode abgebrochen.</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Post-Encode Items (scripts + chains unified) */}
|
||||
<div className="post-script-box">
|
||||
<h4>Post-Encode Scripte (optional)</h4>
|
||||
{scriptCatalog.length === 0 ? (
|
||||
<small>Keine Scripte konfiguriert. In den Settings unter "Scripte" anlegen.</small>
|
||||
<h4>Post-Encode Ausführungen (optional)</h4>
|
||||
{scriptCatalog.length === 0 && chainCatalog.length === 0 ? (
|
||||
<small>Keine Skripte oder Ketten konfiguriert. In den Settings anlegen.</small>
|
||||
) : null}
|
||||
{scriptRows.length === 0 ? (
|
||||
<small>Keine Post-Encode Scripte ausgewählt.</small>
|
||||
{postEncodeItems.length === 0 ? (
|
||||
<small>Keine Post-Encode Ausführungen ausgewählt.</small>
|
||||
) : null}
|
||||
{scriptRows.map((scriptId, rowIndex) => {
|
||||
const script = scriptById.get(scriptId) || null;
|
||||
const selectedInOtherRows = new Set(
|
||||
scriptRows.filter((id, index) => index !== rowIndex).map((id) => String(id))
|
||||
{postEncodeItems.map((item, rowIndex) => {
|
||||
const isScript = item.type === 'script';
|
||||
const canDrag = allowEncodeItemSelection && postEncodeItems.length > 1;
|
||||
const scriptObj = isScript ? (scriptById.get(normalizeScriptId(item.id)) || null) : null;
|
||||
const chainObj = !isScript ? (chainById.get(Number(item.id)) || null) : null;
|
||||
const name = isScript
|
||||
? (scriptObj?.name || `Skript #${item.id}`)
|
||||
: (chainObj?.name || `Kette #${item.id}`);
|
||||
const usedScriptIds = new Set(
|
||||
postEncodeItems.filter((it, i) => it.type === 'script' && i !== rowIndex).map((it) => String(normalizeScriptId(it.id)))
|
||||
);
|
||||
const options = scriptCatalog.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
disabled: selectedInOtherRows.has(String(item.id))
|
||||
const scriptOptions = scriptCatalog.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
disabled: usedScriptIds.has(String(s.id))
|
||||
}));
|
||||
return (
|
||||
<div
|
||||
key={`post-script-row-${rowIndex}-${scriptId}`}
|
||||
className={`post-script-row${allowPostScriptSelection ? ' editable' : ''}`}
|
||||
key={`post-item-${rowIndex}-${item.type}-${item.id}`}
|
||||
className={`post-script-row${allowEncodeItemSelection ? ' editable' : ''}`}
|
||||
onDragOver={(event) => {
|
||||
if (!canReorderScriptRows) {
|
||||
return;
|
||||
}
|
||||
if (!canDrag) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
||||
}}
|
||||
onDrop={(event) => handleScriptDrop(event, rowIndex)}
|
||||
onDrop={(event) => handlePostDrop(event, rowIndex)}
|
||||
>
|
||||
{allowPostScriptSelection ? (
|
||||
{allowEncodeItemSelection ? (
|
||||
<>
|
||||
<span
|
||||
className={`post-script-drag-handle pi pi-bars${canReorderScriptRows ? '' : ' disabled'}`}
|
||||
title={canReorderScriptRows ? 'Ziehen zum Umordnen' : 'Mindestens zwei Scripte zum Umordnen'}
|
||||
draggable={canReorderScriptRows}
|
||||
className={`post-script-drag-handle pi pi-bars${canDrag ? '' : ' disabled'}`}
|
||||
title={canDrag ? 'Ziehen zum Umordnen' : 'Mindestens zwei Einträge zum Umordnen'}
|
||||
draggable={canDrag}
|
||||
onDragStart={(event) => {
|
||||
if (!canReorderScriptRows) {
|
||||
return;
|
||||
}
|
||||
if (!canDrag) return;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', String(rowIndex));
|
||||
}}
|
||||
/>
|
||||
<Dropdown
|
||||
value={scriptId}
|
||||
options={options}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
optionDisabled="disabled"
|
||||
onChange={(event) => onChangePostEncodeScript?.(rowIndex, event.value)}
|
||||
className="full-width"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
outlined
|
||||
onClick={() => onRemovePostEncodeScript?.(rowIndex)}
|
||||
/>
|
||||
<i className={`post-script-type-icon pi ${isScript ? 'pi-code' : 'pi-link'}`} title={isScript ? 'Skript' : 'Kette'} />
|
||||
{isScript ? (
|
||||
<Dropdown
|
||||
value={normalizeScriptId(item.id)}
|
||||
options={scriptOptions}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
optionDisabled="disabled"
|
||||
onChange={(event) => onChangePostEncodeItem?.(rowIndex, 'script', event.value)}
|
||||
className="full-width"
|
||||
/>
|
||||
) : (
|
||||
<span className="post-script-chain-name">{name}</span>
|
||||
)}
|
||||
<Button icon="pi pi-times" severity="danger" outlined onClick={() => onRemovePostEncodeItem?.(rowIndex)} />
|
||||
</>
|
||||
) : (
|
||||
<small>{`${rowIndex + 1}. ${script?.name || `Script #${scriptId}`}`}</small>
|
||||
<small><i className={`pi ${isScript ? 'pi-code' : 'pi-link'}`} /> {`${rowIndex + 1}. ${name}`}</small>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{canAddScriptRow ? (
|
||||
<Button
|
||||
label="Script hinzufügen"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => onAddPostEncodeScript?.()}
|
||||
/>
|
||||
{allowEncodeItemSelection ? (
|
||||
<div className="encode-item-add-row">
|
||||
{scriptCatalog.length > postEncodeItems.filter((i) => i.type === 'script').length ? (
|
||||
<Button
|
||||
label="Skript hinzufügen"
|
||||
icon="pi pi-code"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => onAddPostEncodeItem?.('script')}
|
||||
/>
|
||||
) : null}
|
||||
{chainCatalog.length > postEncodeItems.filter((i) => i.type === 'chain').length ? (
|
||||
<Button
|
||||
label="Kette hinzufügen"
|
||||
icon="pi pi-link"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => onAddPostEncodeItem?.('chain')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<small>Ausführung erfolgt nur nach erfolgreichem Encode, strikt nacheinander in genau dieser Reihenfolge (Drag-and-Drop möglich).</small>
|
||||
<small>Ausführung nach erfolgreichem Encode, strikt nacheinander (Drag-and-Drop möglich).</small>
|
||||
</div>
|
||||
|
||||
<h4>Titel</h4>
|
||||
|
||||
@@ -78,6 +78,14 @@ function normalizeScriptIdList(values) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function normalizeChainId(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function isBurnedSubtitleTrack(track) {
|
||||
const flags = Array.isArray(track?.subtitlePreviewFlags)
|
||||
? track.subtitlePreviewFlags
|
||||
@@ -225,10 +233,9 @@ export default function PipelineStatusCard({
|
||||
const [presetDisplayMap, setPresetDisplayMap] = useState({});
|
||||
const [scriptCatalog, setScriptCatalog] = useState([]);
|
||||
const [chainCatalog, setChainCatalog] = useState([]);
|
||||
const [selectedPostEncodeScriptIds, setSelectedPostEncodeScriptIds] = useState([]);
|
||||
const [selectedPreEncodeScriptIds, setSelectedPreEncodeScriptIds] = useState([]);
|
||||
const [selectedPostEncodeChainIds, setSelectedPostEncodeChainIds] = useState([]);
|
||||
const [selectedPreEncodeChainIds, setSelectedPreEncodeChainIds] = useState([]);
|
||||
// Unified ordered lists: [{type: 'script'|'chain', id: number}]
|
||||
const [preEncodeItems, setPreEncodeItems] = useState([]);
|
||||
const [postEncodeItems, setPostEncodeItems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -282,20 +289,15 @@ export default function PipelineStatusCard({
|
||||
const fromReview = normalizeTitleId(mediaInfoReview?.encodeInputTitleId);
|
||||
setSelectedEncodeTitleId(fromReview);
|
||||
setTrackSelectionByTitle(buildDefaultTrackSelection(mediaInfoReview));
|
||||
setSelectedPostEncodeScriptIds(
|
||||
normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || [])
|
||||
);
|
||||
setSelectedPreEncodeScriptIds(
|
||||
normalizeScriptIdList(mediaInfoReview?.preEncodeScriptIds || [])
|
||||
);
|
||||
setSelectedPostEncodeChainIds(
|
||||
(Array.isArray(mediaInfoReview?.postEncodeChainIds) ? mediaInfoReview.postEncodeChainIds : [])
|
||||
.map(Number).filter((id) => Number.isFinite(id) && id > 0)
|
||||
);
|
||||
setSelectedPreEncodeChainIds(
|
||||
(Array.isArray(mediaInfoReview?.preEncodeChainIds) ? mediaInfoReview.preEncodeChainIds : [])
|
||||
.map(Number).filter((id) => Number.isFinite(id) && id > 0)
|
||||
);
|
||||
const normChain = (raw) => (Array.isArray(raw) ? raw : []).map(Number).filter((id) => Number.isFinite(id) && id > 0);
|
||||
setPreEncodeItems([
|
||||
...normalizeScriptIdList(mediaInfoReview?.preEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
|
||||
...normChain(mediaInfoReview?.preEncodeChainIds).map((id) => ({ type: 'chain', id }))
|
||||
]);
|
||||
setPostEncodeItems([
|
||||
...normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
|
||||
...normChain(mediaInfoReview?.postEncodeChainIds).map((id) => ({ type: 'chain', id }))
|
||||
]);
|
||||
}, [mediaInfoReview?.encodeInputTitleId, mediaInfoReview?.generatedAt, retryJobId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -454,17 +456,17 @@ export default function PipelineStatusCard({
|
||||
}
|
||||
}
|
||||
: null;
|
||||
const selectedPostScriptIds = normalizeScriptIdList(selectedPostEncodeScriptIds);
|
||||
const selectedPreScriptIds = normalizeScriptIdList(selectedPreEncodeScriptIds);
|
||||
const normalizeChainIdList = (raw) =>
|
||||
(Array.isArray(raw) ? raw : []).map(Number).filter((id) => Number.isFinite(id) && id > 0);
|
||||
const selectedPostScriptIds = postEncodeItems.filter((i) => i.type === 'script').map((i) => i.id);
|
||||
const selectedPreScriptIds = preEncodeItems.filter((i) => i.type === 'script').map((i) => i.id);
|
||||
const selectedPostChainIds = postEncodeItems.filter((i) => i.type === 'chain').map((i) => i.id);
|
||||
const selectedPreChainIds = preEncodeItems.filter((i) => i.type === 'chain').map((i) => i.id);
|
||||
return {
|
||||
encodeTitleId,
|
||||
selectedTrackSelection,
|
||||
selectedPostScriptIds,
|
||||
selectedPreScriptIds,
|
||||
selectedPostChainIds: normalizeChainIdList(selectedPostEncodeChainIds),
|
||||
selectedPreChainIds: normalizeChainIdList(selectedPreEncodeChainIds)
|
||||
selectedPostChainIds,
|
||||
selectedPreChainIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -776,143 +778,219 @@ export default function PipelineStatusCard({
|
||||
};
|
||||
});
|
||||
}}
|
||||
availablePostScripts={scriptCatalog}
|
||||
selectedPostEncodeScriptIds={selectedPostEncodeScriptIds}
|
||||
allowPostScriptSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
||||
onAddPostEncodeScript={() => {
|
||||
setSelectedPostEncodeScriptIds((prev) => {
|
||||
const normalizedCurrent = normalizeScriptIdList(prev);
|
||||
const selectedSet = new Set(normalizedCurrent.map((id) => String(id)));
|
||||
availableScripts={scriptCatalog}
|
||||
availableChains={chainCatalog}
|
||||
preEncodeItems={preEncodeItems}
|
||||
postEncodeItems={postEncodeItems}
|
||||
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
||||
onAddPreEncodeItem={(itemType) => {
|
||||
setPreEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
if (itemType === 'chain') {
|
||||
const selectedSet = new Set(
|
||||
current
|
||||
.filter((item) => item?.type === 'chain')
|
||||
.map((item) => normalizeChainId(item?.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const nextCandidate = (Array.isArray(chainCatalog) ? chainCatalog : [])
|
||||
.map((item) => normalizeChainId(item?.id))
|
||||
.find((id) => id !== null && !selectedSet.has(String(id)));
|
||||
if (nextCandidate === undefined || nextCandidate === null) {
|
||||
return current;
|
||||
}
|
||||
return [...current, { type: 'chain', id: nextCandidate }];
|
||||
}
|
||||
const selectedSet = new Set(
|
||||
current
|
||||
.filter((item) => item?.type === 'script')
|
||||
.map((item) => normalizeScriptId(item?.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const nextCandidate = (Array.isArray(scriptCatalog) ? scriptCatalog : [])
|
||||
.map((item) => normalizeScriptId(item?.id))
|
||||
.find((id) => id !== null && !selectedSet.has(String(id)));
|
||||
if (nextCandidate === undefined || nextCandidate === null) {
|
||||
return normalizedCurrent;
|
||||
return current;
|
||||
}
|
||||
return [...normalizedCurrent, nextCandidate];
|
||||
return [...current, { type: 'script', id: nextCandidate }];
|
||||
});
|
||||
}}
|
||||
onChangePostEncodeScript={(rowIndex, nextScriptId) => {
|
||||
setSelectedPostEncodeScriptIds((prev) => {
|
||||
const normalizedCurrent = normalizeScriptIdList(prev);
|
||||
if (!Number.isFinite(Number(rowIndex)) || rowIndex < 0 || rowIndex >= normalizedCurrent.length) {
|
||||
return normalizedCurrent;
|
||||
onChangePreEncodeItem={(rowIndex, itemType, nextId) => {
|
||||
setPreEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
const index = Number(rowIndex);
|
||||
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
||||
return current;
|
||||
}
|
||||
const normalizedScriptId = normalizeScriptId(nextScriptId);
|
||||
if (normalizedScriptId === null) {
|
||||
return normalizedCurrent;
|
||||
const type = itemType === 'chain' ? 'chain' : 'script';
|
||||
if (type === 'chain') {
|
||||
const normalizedId = normalizeChainId(nextId);
|
||||
if (normalizedId === null) {
|
||||
return current;
|
||||
}
|
||||
const duplicate = current.some((item, idx) =>
|
||||
idx !== index
|
||||
&& item?.type === 'chain'
|
||||
&& String(normalizeChainId(item?.id)) === String(normalizedId)
|
||||
);
|
||||
if (duplicate) {
|
||||
return current;
|
||||
}
|
||||
const next = [...current];
|
||||
next[index] = { type: 'chain', id: normalizedId };
|
||||
return next;
|
||||
}
|
||||
const duplicateAtOtherIndex = normalizedCurrent.some((id, idx) =>
|
||||
idx !== rowIndex && String(id) === String(normalizedScriptId)
|
||||
const normalizedId = normalizeScriptId(nextId);
|
||||
if (normalizedId === null) {
|
||||
return current;
|
||||
}
|
||||
const duplicate = current.some((item, idx) =>
|
||||
idx !== index
|
||||
&& item?.type === 'script'
|
||||
&& String(normalizeScriptId(item?.id)) === String(normalizedId)
|
||||
);
|
||||
if (duplicateAtOtherIndex) {
|
||||
return normalizedCurrent;
|
||||
if (duplicate) {
|
||||
return current;
|
||||
}
|
||||
const next = [...normalizedCurrent];
|
||||
next[rowIndex] = normalizedScriptId;
|
||||
const next = [...current];
|
||||
next[index] = { type: 'script', id: normalizedId };
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onRemovePostEncodeScript={(rowIndex) => {
|
||||
setSelectedPostEncodeScriptIds((prev) => {
|
||||
const normalizedCurrent = normalizeScriptIdList(prev);
|
||||
if (!Number.isFinite(Number(rowIndex)) || rowIndex < 0 || rowIndex >= normalizedCurrent.length) {
|
||||
return normalizedCurrent;
|
||||
onRemovePreEncodeItem={(rowIndex) => {
|
||||
setPreEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
const index = Number(rowIndex);
|
||||
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
||||
return current;
|
||||
}
|
||||
return normalizedCurrent.filter((_, idx) => idx !== rowIndex);
|
||||
return current.filter((_, idx) => idx !== index);
|
||||
});
|
||||
}}
|
||||
onReorderPostEncodeScript={(fromIndex, toIndex) => {
|
||||
setSelectedPostEncodeScriptIds((prev) => {
|
||||
const normalizedCurrent = normalizeScriptIdList(prev);
|
||||
onReorderPreEncodeItem={(fromIndex, toIndex) => {
|
||||
setPreEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
const from = Number(fromIndex);
|
||||
const to = Number(toIndex);
|
||||
if (!Number.isInteger(from) || !Number.isInteger(to)) {
|
||||
return normalizedCurrent;
|
||||
return current;
|
||||
}
|
||||
if (from < 0 || to < 0 || from >= normalizedCurrent.length || to >= normalizedCurrent.length) {
|
||||
return normalizedCurrent;
|
||||
if (from < 0 || to < 0 || from >= current.length || to >= current.length || from === to) {
|
||||
return current;
|
||||
}
|
||||
if (from === to) {
|
||||
return normalizedCurrent;
|
||||
}
|
||||
const next = [...normalizedCurrent];
|
||||
const next = [...current];
|
||||
const [moved] = next.splice(from, 1);
|
||||
next.splice(to, 0, moved);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
availablePreScripts={scriptCatalog}
|
||||
selectedPreEncodeScriptIds={selectedPreEncodeScriptIds}
|
||||
allowPreScriptSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
||||
onAddPreEncodeScript={() => {
|
||||
setSelectedPreEncodeScriptIds((prev) => {
|
||||
const normalizedCurrent = normalizeScriptIdList(prev);
|
||||
const selectedSet = new Set(normalizedCurrent.map((id) => String(id)));
|
||||
onAddPostEncodeItem={(itemType) => {
|
||||
setPostEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
if (itemType === 'chain') {
|
||||
const selectedSet = new Set(
|
||||
current
|
||||
.filter((item) => item?.type === 'chain')
|
||||
.map((item) => normalizeChainId(item?.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const nextCandidate = (Array.isArray(chainCatalog) ? chainCatalog : [])
|
||||
.map((item) => normalizeChainId(item?.id))
|
||||
.find((id) => id !== null && !selectedSet.has(String(id)));
|
||||
if (nextCandidate === undefined || nextCandidate === null) {
|
||||
return current;
|
||||
}
|
||||
return [...current, { type: 'chain', id: nextCandidate }];
|
||||
}
|
||||
const selectedSet = new Set(
|
||||
current
|
||||
.filter((item) => item?.type === 'script')
|
||||
.map((item) => normalizeScriptId(item?.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const nextCandidate = (Array.isArray(scriptCatalog) ? scriptCatalog : [])
|
||||
.map((item) => normalizeScriptId(item?.id))
|
||||
.find((id) => id !== null && !selectedSet.has(String(id)));
|
||||
if (nextCandidate === undefined || nextCandidate === null) {
|
||||
return normalizedCurrent;
|
||||
return current;
|
||||
}
|
||||
return [...normalizedCurrent, nextCandidate];
|
||||
return [...current, { type: 'script', id: nextCandidate }];
|
||||
});
|
||||
}}
|
||||
onChangePreEncodeScript={(rowIndex, nextScriptId) => {
|
||||
setSelectedPreEncodeScriptIds((prev) => {
|
||||
const normalizedCurrent = normalizeScriptIdList(prev);
|
||||
if (!Number.isFinite(Number(rowIndex)) || rowIndex < 0 || rowIndex >= normalizedCurrent.length) {
|
||||
return normalizedCurrent;
|
||||
onChangePostEncodeItem={(rowIndex, itemType, nextId) => {
|
||||
setPostEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
const index = Number(rowIndex);
|
||||
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
||||
return current;
|
||||
}
|
||||
const normalizedScriptId = normalizeScriptId(nextScriptId);
|
||||
if (normalizedScriptId === null) {
|
||||
return normalizedCurrent;
|
||||
const type = itemType === 'chain' ? 'chain' : 'script';
|
||||
if (type === 'chain') {
|
||||
const normalizedId = normalizeChainId(nextId);
|
||||
if (normalizedId === null) {
|
||||
return current;
|
||||
}
|
||||
const duplicate = current.some((item, idx) =>
|
||||
idx !== index
|
||||
&& item?.type === 'chain'
|
||||
&& String(normalizeChainId(item?.id)) === String(normalizedId)
|
||||
);
|
||||
if (duplicate) {
|
||||
return current;
|
||||
}
|
||||
const next = [...current];
|
||||
next[index] = { type: 'chain', id: normalizedId };
|
||||
return next;
|
||||
}
|
||||
if (normalizedCurrent.some((id, idx) => idx !== rowIndex && String(id) === String(normalizedScriptId))) {
|
||||
return normalizedCurrent;
|
||||
const normalizedId = normalizeScriptId(nextId);
|
||||
if (normalizedId === null) {
|
||||
return current;
|
||||
}
|
||||
const next = [...normalizedCurrent];
|
||||
next[rowIndex] = normalizedScriptId;
|
||||
const duplicate = current.some((item, idx) =>
|
||||
idx !== index
|
||||
&& item?.type === 'script'
|
||||
&& String(normalizeScriptId(item?.id)) === String(normalizedId)
|
||||
);
|
||||
if (duplicate) {
|
||||
return current;
|
||||
}
|
||||
const next = [...current];
|
||||
next[index] = { type: 'script', id: normalizedId };
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onRemovePreEncodeScript={(rowIndex) => {
|
||||
setSelectedPreEncodeScriptIds((prev) => {
|
||||
const normalizedCurrent = normalizeScriptIdList(prev);
|
||||
if (!Number.isFinite(Number(rowIndex)) || rowIndex < 0 || rowIndex >= normalizedCurrent.length) {
|
||||
return normalizedCurrent;
|
||||
onRemovePostEncodeItem={(rowIndex) => {
|
||||
setPostEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
const index = Number(rowIndex);
|
||||
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
||||
return current;
|
||||
}
|
||||
return normalizedCurrent.filter((_, idx) => idx !== rowIndex);
|
||||
return current.filter((_, idx) => idx !== index);
|
||||
});
|
||||
}}
|
||||
availableChains={chainCatalog}
|
||||
selectedPreEncodeChainIds={selectedPreEncodeChainIds}
|
||||
selectedPostEncodeChainIds={selectedPostEncodeChainIds}
|
||||
allowChainSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
||||
onAddPreEncodeChain={(chainId) => {
|
||||
setSelectedPreEncodeChainIds((prev) => {
|
||||
const id = Number(chainId);
|
||||
if (!Number.isFinite(id) || id <= 0 || prev.includes(id)) {
|
||||
return prev;
|
||||
onReorderPostEncodeItem={(fromIndex, toIndex) => {
|
||||
setPostEncodeItems((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
const from = Number(fromIndex);
|
||||
const to = Number(toIndex);
|
||||
if (!Number.isInteger(from) || !Number.isInteger(to)) {
|
||||
return current;
|
||||
}
|
||||
return [...prev, id];
|
||||
});
|
||||
}}
|
||||
onRemovePreEncodeChain={(index) => {
|
||||
setSelectedPreEncodeChainIds((prev) => prev.filter((_, i) => i !== index));
|
||||
}}
|
||||
onAddPostEncodeChain={(chainId) => {
|
||||
setSelectedPostEncodeChainIds((prev) => {
|
||||
const id = Number(chainId);
|
||||
if (!Number.isFinite(id) || id <= 0 || prev.includes(id)) {
|
||||
return prev;
|
||||
if (from < 0 || to < 0 || from >= current.length || to >= current.length || from === to) {
|
||||
return current;
|
||||
}
|
||||
return [...prev, id];
|
||||
const next = [...current];
|
||||
const [moved] = next.splice(from, 1);
|
||||
next.splice(to, 0, moved);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onRemovePostEncodeChain={(index) => {
|
||||
setSelectedPostEncodeChainIds((prev) => prev.filter((_, i) => i !== index));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user