From e68c8edc23360fa103554f044bb94c53b8356143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 22 Jul 2026 13:16:11 +0000 Subject: [PATCH] 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 --- .../components/TaskRunnerModal.tsx | 99 ++++++++++++++++--- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx index 6be117b4..83510613 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -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,22 +479,62 @@ const FolderSummary = ({ folder }: FolderSummaryProps) => { ); } + // Mixed layouts — no toggle available: just report the majority run + skipped list. + if (!onKeepAllChange) { + return ( +
+ + + {majorityCount} of {fileCount} videos share this layout and will be converted. + +
+ Different layout — convert these separately: +
    + {skipped.map((f) => ( +
  • + • {f} +
  • + ))} +
+
+
+ ); + } + return ( -
+
- {majorityCount} of {fileCount} videos share this layout and will be converted. + {majorityCount} of {fileCount} videos share a track layout. -
- Different layout — convert these separately: -
    - {skipped.map((f) => ( -
  • - • {f} -
  • - ))} -
+
+ +
+ {!keepAll && ( +
+ Skipped (convert separately): +
    + {skipped.map((f) => ( +
  • + • {f} +
  • + ))} +
+
+ )}
); }; @@ -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 = {}; + 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 {selectedNames.length} selected items
)} - {entryType === 'directory' && folder && !probing && } + {entryType === 'directory' && folder && !probing && ( + 0 ? setKeepAll : undefined} + /> + )} {inputDefs && ( )}