Skript Integration + UI Anpassungen

This commit is contained in:
2026-03-04 21:09:04 +00:00
parent 3b293bb743
commit 3957773854
16 changed files with 2569 additions and 143 deletions

View File

@@ -38,8 +38,8 @@ function buildToolSections(settings) {
{
id: 'output',
title: 'Output',
description: 'Container-Format und Dateinamen-Template.',
match: (key) => key === 'output_extension' || key === 'filename_template'
description: 'Container-Format sowie Datei- und Ordnernamen-Template.',
match: (key) => key === 'output_extension' || key === 'filename_template' || key === 'output_folder_template'
}
];
@@ -95,6 +95,10 @@ function buildSectionsForCategory(categoryName, settings) {
];
}
function isHandBrakePresetSetting(setting) {
return String(setting?.key || '').trim().toLowerCase() === 'handbrake_preset';
}
export default function DynamicSettingsForm({
categories,
values,
@@ -194,11 +198,24 @@ export default function DynamicSettingsForm({
options={setting.options}
optionLabel="label"
optionValue="value"
optionDisabled="disabled"
onChange={(event) => onChange?.(setting.key, event.value)}
/>
) : null}
<small>{setting.description || ''}</small>
{isHandBrakePresetSetting(setting) ? (
<small>
Preset-Erklärung:{' '}
<a
href="https://handbrake.fr/docs/en/latest/technical/official-presets.html"
target="_blank"
rel="noreferrer"
>
HandBrake Official Presets
</a>
</small>
) : null}
{error ? (
<small className="error-text">{error}</small>
) : (

View File

@@ -1,7 +1,8 @@
import { Dialog } from 'primereact/dialog';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import MediaInfoReviewPanel from './MediaInfoReviewPanel';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
function JsonView({ title, value }) {
return (
@@ -12,6 +13,67 @@ function JsonView({ title, value }) {
);
}
function resolveMediaType(job) {
const raw = String(job?.mediaType || job?.media_type || '').trim().toLowerCase();
return raw === 'bluray' ? 'bluray' : 'disc';
}
function statusBadgeMeta(status) {
const normalized = String(status || '').trim().toUpperCase();
if (normalized === 'FINISHED') {
return { label: normalized, icon: 'pi-check-circle', tone: 'success' };
}
if (normalized === 'ERROR') {
return { label: normalized, icon: 'pi-times-circle', tone: 'danger' };
}
if (normalized === 'READY_TO_ENCODE' || normalized === 'READY_TO_START') {
return { label: normalized, icon: 'pi-play-circle', tone: 'info' };
}
if (normalized === 'WAITING_FOR_USER_DECISION') {
return { label: normalized, icon: 'pi-exclamation-circle', tone: 'warning' };
}
if (normalized === 'METADATA_SELECTION') {
return { label: normalized, icon: 'pi-list', tone: 'warning' };
}
if (normalized === 'ANALYZING') {
return { label: normalized, icon: 'pi-search', tone: 'warning' };
}
if (normalized === 'RIPPING') {
return { label: normalized, icon: 'pi-download', tone: 'warning' };
}
if (normalized === 'MEDIAINFO_CHECK') {
return { label: normalized, icon: 'pi-sliders-h', tone: 'warning' };
}
if (normalized === 'ENCODING') {
return { label: normalized, icon: 'pi-cog', tone: 'warning' };
}
return { label: normalized || '-', icon: 'pi-info-circle', tone: 'secondary' };
}
function omdbField(value) {
const raw = String(value || '').trim();
return raw || '-';
}
function omdbRottenTomatoesScore(omdbInfo) {
const ratings = Array.isArray(omdbInfo?.Ratings) ? omdbInfo.Ratings : [];
const entry = ratings.find((item) => String(item?.Source || '').trim().toLowerCase() === 'rotten tomatoes');
return omdbField(entry?.Value);
}
function BoolState({ value }) {
const isTrue = Boolean(value);
return isTrue ? (
<span className="job-step-inline-ok" title="Ja">
<i className="pi pi-check-circle" aria-hidden="true" />
</span>
) : (
<span className="job-step-inline-no" title="Nein">
<i className="pi pi-times-circle" aria-hidden="true" />
</span>
);
}
export default function JobDetailDialog({
visible,
job,
@@ -46,6 +108,12 @@ export default function JobDetailDialog({
const logMeta = job?.logMeta && typeof job.logMeta === 'object' ? job.logMeta : null;
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 statusMeta = statusBadgeMeta(job?.status);
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
return (
<Dialog
@@ -68,22 +136,80 @@ export default function JobDetailDialog({
<div className="poster-large poster-fallback">Kein Poster</div>
)}
<div className="job-meta-grid">
<div className="job-film-info-grid">
<section className="job-meta-block job-meta-block-film">
<h4>Film-Infos</h4>
<div className="job-meta-list">
<div className="job-meta-item">
<strong>Titel:</strong>
<span>{job.title || job.detected_title || '-'}</span>
</div>
<div className="job-meta-item">
<strong>Jahr:</strong>
<span>{job.year || '-'}</span>
</div>
<div className="job-meta-item">
<strong>IMDb:</strong>
<span>{job.imdb_id || '-'}</span>
</div>
<div className="job-meta-item">
<strong>OMDb Match:</strong>
<BoolState value={job.selected_from_omdb} />
</div>
<div className="job-meta-item">
<strong>Medium:</strong>
<span className="job-step-cell">
<img src={mediaTypeIcon} alt={mediaTypeAlt} title={mediaTypeLabel} className="media-indicator-icon" />
<span>{mediaTypeLabel}</span>
</span>
</div>
</div>
</section>
<section className="job-meta-block job-meta-block-film">
<h4>OMDb Details</h4>
<div className="job-meta-list">
<div className="job-meta-item">
<strong>Regisseur:</strong>
<span>{omdbField(omdbInfo?.Director)}</span>
</div>
<div className="job-meta-item">
<strong>Schauspieler:</strong>
<span>{omdbField(omdbInfo?.Actors)}</span>
</div>
<div className="job-meta-item">
<strong>Laufzeit:</strong>
<span>{omdbField(omdbInfo?.Runtime)}</span>
</div>
<div className="job-meta-item">
<strong>Genre:</strong>
<span>{omdbField(omdbInfo?.Genre)}</span>
</div>
<div className="job-meta-item">
<strong>Rotten Tomatoes:</strong>
<span>{omdbRottenTomatoesScore(omdbInfo)}</span>
</div>
<div className="job-meta-item">
<strong>imdbRating:</strong>
<span>{omdbField(omdbInfo?.imdbRating)}</span>
</div>
</div>
</section>
</div>
</div>
<section className="job-meta-block job-meta-block-full">
<h4>Job-Infos</h4>
<div className="job-meta-grid job-meta-grid-compact">
<div>
<strong>Titel:</strong> {job.title || job.detected_title || '-'}
</div>
<div>
<strong>Jahr:</strong> {job.year || '-'}
</div>
<div>
<strong>IMDb:</strong> {job.imdb_id || '-'}
</div>
<div>
<strong>OMDb Match:</strong>{' '}
<Tag value={job.selected_from_omdb ? 'Ja' : 'Nein'} severity={job.selected_from_omdb ? 'success' : 'secondary'} />
</div>
<div>
<strong>Status:</strong> <Tag value={job.status} />
<strong>Aktueller Status:</strong>{' '}
<span
className={`job-status-icon tone-${statusMeta.tone}`}
title={statusMeta.label}
aria-label={statusMeta.label}
>
<i className={`pi ${statusMeta.icon}`} aria-hidden="true" />
</span>
</div>
<div>
<strong>Start:</strong> {job.start_time || '-'}
@@ -101,40 +227,29 @@ export default function JobDetailDialog({
<strong>Encode Input:</strong> {job.encode_input_path || '-'}
</div>
<div>
<strong>Mediainfo bestätigt:</strong> {job.encode_review_confirmed ? 'ja' : 'nein'}
<strong>RAW vorhanden:</strong> <BoolState value={job.rawStatus?.exists} />
</div>
<div>
<strong>RAW vorhanden:</strong> {job.rawStatus?.exists ? 'ja' : 'nein'}
<strong>Movie Datei vorhanden:</strong> <BoolState value={job.outputStatus?.exists} />
</div>
<div>
<strong>RAW leer:</strong> {job.rawStatus?.isEmpty === null ? '-' : job.rawStatus?.isEmpty ? 'ja' : 'nein'}
<strong>Backup erfolgreich:</strong> <BoolState value={job?.backupSuccess} />
</div>
<div>
<strong>Movie Datei vorhanden:</strong> {job.outputStatus?.exists ? 'ja' : 'nein'}
<strong>Encode erfolgreich:</strong> <BoolState value={job?.encodeSuccess} />
</div>
<div>
<strong>Movie-Dir leer:</strong> {job.movieDirStatus?.isEmpty === null ? '-' : job.movieDirStatus?.isEmpty ? 'ja' : 'nein'}
</div>
<div>
<strong>Backup erfolgreich:</strong>{' '}
{job?.backupSuccess ? <span className="job-step-inline-ok"><i className="pi pi-check-circle" aria-hidden="true" /><span>ja</span></span> : 'nein'}
</div>
<div>
<strong>Encode erfolgreich:</strong>{' '}
{job?.encodeSuccess ? <span className="job-step-inline-ok"><i className="pi pi-check-circle" aria-hidden="true" /><span>ja</span></span> : 'nein'}
</div>
<div>
<strong>Fehler:</strong> {job.error_message || '-'}
<div className="job-meta-col-span-2">
<strong>Letzter Fehler:</strong> {job.error_message || '-'}
</div>
</div>
</div>
</section>
<div className="job-json-grid">
<JsonView title="OMDb Info" value={job.omdbInfo} />
<JsonView title="MakeMKV Info" value={job.makemkvInfo} />
<JsonView title="HandBrake Info" value={job.handbrakeInfo} />
<JsonView title="Mediainfo Info" value={job.mediainfoInfo} />
<JsonView title="Encode Plan" value={job.encodePlan} />
<JsonView title="HandBrake Info" value={job.handbrakeInfo} />
</div>
{job.encodePlan ? (

View File

@@ -1,3 +1,6 @@
import { Button } from 'primereact/button';
import { Dropdown } from 'primereact/dropdown';
function formatDuration(minutes) {
const value = Number(minutes || 0);
if (!Number.isFinite(value)) {
@@ -636,15 +639,50 @@ function normalizeTitleId(value) {
return Math.trunc(parsed);
}
function normalizeScriptId(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return Math.trunc(parsed);
}
function normalizeScriptIdList(values) {
const list = Array.isArray(values) ? values : [];
const seen = new Set();
const output = [];
for (const value of list) {
const normalized = normalizeScriptId(value);
if (normalized === null) {
continue;
}
const key = String(normalized);
if (seen.has(key)) {
continue;
}
seen.add(key);
output.push(normalized);
}
return output;
}
export default function MediaInfoReviewPanel({
review,
presetDisplayValue = '',
commandOutputPath = null,
selectedEncodeTitleId = null,
allowTitleSelection = false,
onSelectEncodeTitle = null,
allowTrackSelection = false,
trackSelectionByTitle = {},
onTrackSelectionChange = null
onTrackSelectionChange = null,
availablePostScripts = [],
selectedPostEncodeScriptIds = [],
allowPostScriptSelection = false,
onAddPostEncodeScript = null,
onChangePostEncodeScript = null,
onRemovePostEncodeScript = null,
onReorderPostEncodeScript = null
}) {
if (!review) {
return <p>Keine Mediainfo-Daten vorhanden.</p>;
@@ -656,11 +694,35 @@ export default function MediaInfoReviewPanel({
const processedFiles = Number(review.processedFiles || titles.length || 0);
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 : [])
.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 handleScriptDrop = (event, targetIndex) => {
if (!allowPostScriptSelection || typeof onReorderPostEncodeScript !== 'function') {
return;
}
event.preventDefault();
const fromText = event.dataTransfer?.getData('text/plain');
const fromIndex = Number(fromText);
if (!Number.isInteger(fromIndex)) {
return;
}
onReorderPostEncodeScript(fromIndex, targetIndex);
};
return (
<div className="media-review-wrap">
<div className="media-review-meta">
<div><strong>Preset:</strong> {review.selectors?.preset || '-'}</div>
<div><strong>Preset:</strong> {presetLabel}</div>
<div><strong>Extra Args:</strong> {review.selectors?.extraArgs || '(keine)'}</div>
<div><strong>Preset-Profil:</strong> {review.selectors?.presetProfileSource || '-'}</div>
<div><strong>MIN_LENGTH_MINUTES:</strong> {review.minLengthMinutes}</div>
@@ -695,6 +757,87 @@ export default function MediaInfoReviewPanel({
</div>
) : null}
<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>
) : null}
{scriptRows.length === 0 ? (
<small>Keine Post-Encode Scripte 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))
);
const options = scriptCatalog.map((item) => ({
label: item.name,
value: item.id,
disabled: selectedInOtherRows.has(String(item.id))
}));
return (
<div
key={`post-script-row-${rowIndex}-${scriptId}`}
className={`post-script-row${allowPostScriptSelection ? ' editable' : ''}`}
onDragOver={(event) => {
if (!canReorderScriptRows) {
return;
}
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
}}
onDrop={(event) => handleScriptDrop(event, rowIndex)}
>
{allowPostScriptSelection ? (
<>
<span
className={`post-script-drag-handle pi pi-bars${canReorderScriptRows ? '' : ' disabled'}`}
title={canReorderScriptRows ? 'Ziehen zum Umordnen' : 'Mindestens zwei Scripte zum Umordnen'}
draggable={canReorderScriptRows}
onDragStart={(event) => {
if (!canReorderScriptRows) {
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)}
/>
</>
) : (
<small>{`${rowIndex + 1}. ${script?.name || `Script #${scriptId}`}`}</small>
)}
</div>
);
})}
{canAddScriptRow ? (
<Button
label="Script hinzufügen"
icon="pi pi-plus"
severity="secondary"
outlined
onClick={() => onAddPostEncodeScript?.()}
/>
) : null}
<small>Ausführung erfolgt nur nach erfolgreichem Encode, strikt nacheinander in genau dieser Reihenfolge (Drag-and-Drop möglich).</small>
</div>
<h4>Titel</h4>
<div className="media-title-list">
{titles.length === 0 ? (

View File

@@ -65,6 +65,33 @@ function normalizeTrackIdList(values) {
return output;
}
function normalizeScriptId(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return Math.trunc(parsed);
}
function normalizeScriptIdList(values) {
const list = Array.isArray(values) ? values : [];
const seen = new Set();
const output = [];
for (const value of list) {
const normalized = normalizeScriptId(value);
if (normalized === null) {
continue;
}
const key = String(normalized);
if (seen.has(key)) {
continue;
}
seen.add(key);
output.push(normalized);
}
return output;
}
function buildDefaultTrackSelection(review) {
const titles = Array.isArray(review?.titles) ? review.titles : [];
const selection = {};
@@ -108,6 +135,23 @@ function buildSettingsMap(categories) {
return map;
}
function buildPresetDisplayMap(options) {
const map = {};
const list = Array.isArray(options) ? options : [];
for (const option of list) {
if (!option || option.disabled) {
continue;
}
const value = String(option.value || '').trim();
if (!value) {
continue;
}
const category = String(option.category || '').trim();
map[value] = category ? `${category}/${value}` : value;
}
return map;
}
function sanitizeFileName(input) {
return String(input || 'untitled')
.replace(/[\\/:*?"<>|]/g, '_')
@@ -135,9 +179,10 @@ function buildOutputPathPreview(settings, metadata, fallbackJobId = null) {
const title = metadata?.title || (fallbackJobId ? `job-${fallbackJobId}` : 'job');
const year = metadata?.year || new Date().getFullYear();
const imdbId = metadata?.imdbId || (fallbackJobId ? `job-${fallbackJobId}` : 'noimdb');
const template = settings?.filename_template || '${title} (${year})';
const folderName = sanitizeFileName(renderTemplate('${title} (${year})', { title, year, imdbId }));
const baseName = sanitizeFileName(renderTemplate(template, { title, year, imdbId }));
const fileTemplate = settings?.filename_template || '${title} (${year})';
const folderTemplate = String(settings?.output_folder_template || '').trim() || fileTemplate;
const folderName = sanitizeFileName(renderTemplate(folderTemplate, { title, year, imdbId }));
const baseName = sanitizeFileName(renderTemplate(fileTemplate, { title, year, imdbId }));
const ext = String(settings?.output_extension || 'mkv').trim() || 'mkv';
const root = movieDir.replace(/\/+$/g, '');
return `${root}/${folderName}/${baseName}.${ext}`;
@@ -171,18 +216,43 @@ export default function PipelineStatusCard({
const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
const [settingsMap, setSettingsMap] = useState({});
const [presetDisplayMap, setPresetDisplayMap] = useState({});
const [scriptCatalog, setScriptCatalog] = useState([]);
const [selectedPostEncodeScriptIds, setSelectedPostEncodeScriptIds] = useState([]);
useEffect(() => {
let cancelled = false;
const loadSettings = async () => {
try {
const response = await api.getSettings();
const [settingsResponse, presetsResponse, scriptsResponse] = await Promise.allSettled([
api.getSettings(),
api.getHandBrakePresets(),
api.getScripts()
]);
if (!cancelled) {
setSettingsMap(buildSettingsMap(response?.categories || []));
const categories = settingsResponse.status === 'fulfilled'
? (settingsResponse.value?.categories || [])
: [];
setSettingsMap(buildSettingsMap(categories));
const presetOptions = presetsResponse.status === 'fulfilled'
? (presetsResponse.value?.options || [])
: [];
setPresetDisplayMap(buildPresetDisplayMap(presetOptions));
const scripts = scriptsResponse.status === 'fulfilled'
? (Array.isArray(scriptsResponse.value?.scripts) ? scriptsResponse.value.scripts : [])
: [];
setScriptCatalog(
scripts.map((item) => ({
id: item?.id,
name: item?.name
}))
);
}
} catch (_error) {
if (!cancelled) {
setSettingsMap({});
setPresetDisplayMap({});
setScriptCatalog([]);
}
}
};
@@ -196,6 +266,9 @@ export default function PipelineStatusCard({
const fromReview = normalizeTitleId(mediaInfoReview?.encodeInputTitleId);
setSelectedEncodeTitleId(fromReview);
setTrackSelectionByTitle(buildDefaultTrackSelection(mediaInfoReview));
setSelectedPostEncodeScriptIds(
normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || [])
);
}, [mediaInfoReview?.encodeInputTitleId, mediaInfoReview?.generatedAt, retryJobId]);
useEffect(() => {
@@ -312,6 +385,13 @@ export default function PipelineStatusCard({
() => buildOutputPathPreview(settingsMap, selectedMetadata, retryJobId),
[settingsMap, selectedMetadata, retryJobId]
);
const presetDisplayValue = useMemo(() => {
const preset = String(mediaInfoReview?.selectors?.preset || '').trim();
if (!preset) {
return '';
}
return presetDisplayMap[preset] || preset;
}, [mediaInfoReview?.selectors?.preset, presetDisplayMap]);
const buildSelectedTrackSelectionForCurrentTitle = () => {
const encodeTitleId = normalizeTitleId(selectedEncodeTitleId);
const selectionEntry = encodeTitleId
@@ -329,9 +409,11 @@ export default function PipelineStatusCard({
}
}
: null;
const selectedPostScriptIds = normalizeScriptIdList(selectedPostEncodeScriptIds);
return {
encodeTitleId,
selectedTrackSelection
selectedTrackSelection,
selectedPostScriptIds
};
};
@@ -399,11 +481,16 @@ export default function PipelineStatusCard({
return;
}
const { encodeTitleId, selectedTrackSelection } = buildSelectedTrackSelectionForCurrentTitle();
const {
encodeTitleId,
selectedTrackSelection,
selectedPostScriptIds
} = buildSelectedTrackSelectionForCurrentTitle();
await onStart(retryJobId, {
ensureConfirmed: true,
selectedEncodeTitleId: encodeTitleId,
selectedTrackSelection
selectedTrackSelection,
selectedPostEncodeScriptIds: selectedPostScriptIds
});
}}
loading={busy}
@@ -561,6 +648,7 @@ export default function PipelineStatusCard({
) : null}
<MediaInfoReviewPanel
review={mediaInfoReview}
presetDisplayValue={presetDisplayValue}
commandOutputPath={commandOutputPath}
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
allowTitleSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed}
@@ -594,6 +682,72 @@ export default function PipelineStatusCard({
};
});
}}
availablePostScripts={scriptCatalog}
selectedPostEncodeScriptIds={selectedPostEncodeScriptIds}
allowPostScriptSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed}
onAddPostEncodeScript={() => {
setSelectedPostEncodeScriptIds((prev) => {
const normalizedCurrent = normalizeScriptIdList(prev);
const selectedSet = new Set(normalizedCurrent.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 [...normalizedCurrent, nextCandidate];
});
}}
onChangePostEncodeScript={(rowIndex, nextScriptId) => {
setSelectedPostEncodeScriptIds((prev) => {
const normalizedCurrent = normalizeScriptIdList(prev);
if (!Number.isFinite(Number(rowIndex)) || rowIndex < 0 || rowIndex >= normalizedCurrent.length) {
return normalizedCurrent;
}
const normalizedScriptId = normalizeScriptId(nextScriptId);
if (normalizedScriptId === null) {
return normalizedCurrent;
}
const duplicateAtOtherIndex = normalizedCurrent.some((id, idx) =>
idx !== rowIndex && String(id) === String(normalizedScriptId)
);
if (duplicateAtOtherIndex) {
return normalizedCurrent;
}
const next = [...normalizedCurrent];
next[rowIndex] = normalizedScriptId;
return next;
});
}}
onRemovePostEncodeScript={(rowIndex) => {
setSelectedPostEncodeScriptIds((prev) => {
const normalizedCurrent = normalizeScriptIdList(prev);
if (!Number.isFinite(Number(rowIndex)) || rowIndex < 0 || rowIndex >= normalizedCurrent.length) {
return normalizedCurrent;
}
return normalizedCurrent.filter((_, idx) => idx !== rowIndex);
});
}}
onReorderPostEncodeScript={(fromIndex, toIndex) => {
setSelectedPostEncodeScriptIds((prev) => {
const normalizedCurrent = normalizeScriptIdList(prev);
const from = Number(fromIndex);
const to = Number(toIndex);
if (!Number.isInteger(from) || !Number.isInteger(to)) {
return normalizedCurrent;
}
if (from < 0 || to < 0 || from >= normalizedCurrent.length || to >= normalizedCurrent.length) {
return normalizedCurrent;
}
if (from === to) {
return normalizedCurrent;
}
const next = [...normalizedCurrent];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
return next;
});
}}
/>
</div>
) : null}