task runner: "convert all, keep every track" option for mixed-layout folders
when a folder has mixed audio/subtitle layouts, the mismatch banner now offers a toggle: convert the matching group (pick tracks, skip the rest) OR convert every video keeping all audio + all subtitles (no picking, nothing skipped). the second mode hides the pickers and clears the track-selection inputs so run.sh keeps everything. only shown for tasks with track pickers (Convert Video). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+73
-4
@@ -265,6 +265,7 @@ type TaskInputFormProps = {
|
||||
subtitleTracks?: SubtitleTrack[];
|
||||
probing?: boolean;
|
||||
entryType?: 'file' | 'directory';
|
||||
hideTrackPickers?: boolean;
|
||||
};
|
||||
|
||||
const trackLabel = (t: { title: string; lang: string; id: number }) =>
|
||||
@@ -283,7 +284,7 @@ const parseSubtitleSpec = (raw?: string): SubtitleEditEntry[] => {
|
||||
}
|
||||
};
|
||||
|
||||
const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTracks, subtitleTracks, probing, entryType }: TaskInputFormProps) => {
|
||||
const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTracks, subtitleTracks, probing, entryType, hideTrackPickers }: TaskInputFormProps) => {
|
||||
const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key));
|
||||
if (configurableInputs.length === 0) return null;
|
||||
|
||||
@@ -343,6 +344,7 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTrack
|
||||
}
|
||||
|
||||
if (def.type === 'audio_tracks' || def.type === 'subtitle_tracks') {
|
||||
if (hideTrackPickers) return null; // "convert all, keep every track" mode — selection doesn't apply
|
||||
const isAudio = def.type === 'audio_tracks';
|
||||
const tracks = (isAudio ? audioTracks : subtitleTracks) ?? [];
|
||||
const selected = new Set(
|
||||
@@ -451,9 +453,13 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTrack
|
||||
|
||||
type FolderSummaryProps = {
|
||||
folder: { fileCount: number; majorityCount: number; skipped: string[] };
|
||||
keepAll?: boolean;
|
||||
// When provided (mixed-layout folder with track pickers), offers the "convert all, keep every
|
||||
// track" escape hatch instead of only converting the matching group.
|
||||
onKeepAllChange?: (v: boolean) => void;
|
||||
};
|
||||
|
||||
const FolderSummary = ({ folder }: FolderSummaryProps) => {
|
||||
const FolderSummary = ({ folder, keepAll = false, onKeepAllChange }: FolderSummaryProps) => {
|
||||
const { fileCount, majorityCount, skipped } = folder;
|
||||
|
||||
if (fileCount === 0) {
|
||||
@@ -473,6 +479,8 @@ const FolderSummary = ({ folder }: FolderSummaryProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Mixed layouts — no toggle available: just report the majority run + skipped list.
|
||||
if (!onKeepAllChange) {
|
||||
return (
|
||||
<div className="px-5 py-3 border-b border-duck-dark/10 flex flex-col gap-1.5">
|
||||
<span className="flex items-center gap-2 text-sm text-amber-600 dark:text-amber-500">
|
||||
@@ -491,6 +499,44 @@ const FolderSummary = ({ folder }: FolderSummaryProps) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-5 py-3 border-b border-duck-dark/10 flex flex-col gap-2">
|
||||
<span className="flex items-center gap-2 text-sm text-amber-600 dark:text-amber-500">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
{majorityCount} of {fileCount} videos share a track layout.
|
||||
</span>
|
||||
<div className="flex flex-col gap-1.5 text-sm text-duck-dark dark:text-foreground">
|
||||
<label className="flex items-start gap-2 cursor-pointer">
|
||||
<input type="radio" checked={!keepAll} onChange={() => onKeepAllChange(false)} className="accent-duck-teal cursor-pointer mt-0.5" />
|
||||
<span>
|
||||
Convert the {majorityCount} matching
|
||||
<span className="text-xs text-duck-dark/50 dark:text-foreground/50"> — pick tracks below; the other {skipped.length} are skipped</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-start gap-2 cursor-pointer">
|
||||
<input type="radio" checked={keepAll} onChange={() => onKeepAllChange(true)} className="accent-duck-teal cursor-pointer mt-0.5" />
|
||||
<span>
|
||||
Convert all {fileCount} — keep every track
|
||||
<span className="text-xs text-duck-dark/50 dark:text-foreground/50"> — nothing skipped</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{!keepAll && (
|
||||
<div className="text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||
<span>Skipped (convert separately):</span>
|
||||
<ul className="mt-1 max-h-20 overflow-y-auto flex flex-col gap-0.5 pl-1">
|
||||
{skipped.map((f) => (
|
||||
<li key={f} className="truncate">
|
||||
• {f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Script-mode runner ──
|
||||
@@ -519,6 +565,8 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa
|
||||
const [folder, setFolder] = useState<{ fileCount: number; majorityCount: number; skipped: string[] } | null>(null);
|
||||
// Newline-separated folder-relative paths to restrict a batch to the majority group (empty = all).
|
||||
const [includeFiles, setIncludeFiles] = useState('');
|
||||
// Mixed-layout escape hatch: convert every video keeping all tracks (no picking, nothing skipped).
|
||||
const [keepAll, setKeepAll] = useState(false);
|
||||
|
||||
// Fetch task detail to get input definitions
|
||||
useEffect(() => {
|
||||
@@ -638,8 +686,22 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
// Folder has a track-selection picker (convert-video style) → the "keep every track" escape hatch applies.
|
||||
const hasSelectablePickers = Object.values(inputDefs ?? {}).some(
|
||||
(d) => d.type === 'audio_tracks' || d.type === 'subtitle_tracks',
|
||||
);
|
||||
|
||||
const handleRun = () => {
|
||||
const allInputs = { ...formValues, ...(includeFiles ? { include: includeFiles } : {}), ...autoInputs };
|
||||
// "Keep every track" mode: clear track selections (empty = keep all) and convert everything.
|
||||
const overrides: Record<string, string> = {};
|
||||
if (keepAll && inputDefs) {
|
||||
for (const [key, def] of Object.entries(inputDefs)) {
|
||||
if (def.type === 'audio_tracks' || def.type === 'subtitle_tracks') overrides[key] = '';
|
||||
}
|
||||
}
|
||||
// Include list: keepAll on a whole folder = all files (none); on a multi-selection = all selected.
|
||||
const inc = keepAll ? (selectedNames && selectedNames.length > 1 ? selectedNames.join('\n') : '') : includeFiles;
|
||||
const allInputs = { ...formValues, ...overrides, ...(inc ? { include: inc } : {}), ...autoInputs };
|
||||
runner.run(taskDirName, allInputs, cwd);
|
||||
};
|
||||
|
||||
@@ -651,7 +713,13 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa
|
||||
Running on <span className="font-medium">{selectedNames.length}</span> selected items
|
||||
</div>
|
||||
)}
|
||||
{entryType === 'directory' && folder && !probing && <FolderSummary folder={folder} />}
|
||||
{entryType === 'directory' && folder && !probing && (
|
||||
<FolderSummary
|
||||
folder={folder}
|
||||
keepAll={keepAll}
|
||||
onKeepAllChange={hasSelectablePickers && folder.skipped.length > 0 ? setKeepAll : undefined}
|
||||
/>
|
||||
)}
|
||||
{inputDefs && (
|
||||
<TaskInputForm
|
||||
inputDefs={inputDefs}
|
||||
@@ -662,6 +730,7 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa
|
||||
subtitleTracks={subtitleTracks}
|
||||
probing={probing}
|
||||
entryType={entryType}
|
||||
hideTrackPickers={keepAll}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user