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 084777e1..b356cbd7 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -270,6 +270,19 @@ type TaskInputFormProps = { const trackLabel = (t: { title: string; lang: string; id: number }) => t.title || (t.lang && t.lang !== 'und' && t.lang !== 'unknown' ? t.lang.toUpperCase() : '') || `Track ${t.id + 1}`; +// subtitle_edit input: per-subtitle keep flag + editable label, serialized to JSON in the form value. +type SubtitleEditEntry = { id: number; keep: boolean; label: string }; + +const parseSubtitleSpec = (raw?: string): SubtitleEditEntry[] => { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTracks, subtitleTracks, probing, entryType }: TaskInputFormProps) => { const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key)); if (configurableInputs.length === 0) return null; @@ -277,6 +290,58 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTrack return (
{configurableInputs.map(([key, def]) => { + if (def.type === 'subtitle_edit') { + const tracks = subtitleTracks ?? []; + const byId = new Map(parseSubtitleSpec(values[key]).map((e) => [e.id, e])); + const entryFor = (t: SubtitleTrack): SubtitleEditEntry => byId.get(t.id) ?? { id: t.id, keep: true, label: trackLabel(t) }; + const update = (id: number, patch: Partial) => + onChange(key, JSON.stringify(tracks.map((t) => (t.id === id ? { ...entryFor(t), ...patch } : entryFor(t))))); + const keptCount = tracks.filter((t) => entryFor(t).keep).length; + return ( +
+ {def.description ?? key} + {probing ? ( + + {entryType === 'directory' ? 'Analyzing files…' : 'Probing…'} + + ) : tracks.length === 0 ? ( + No subtitles + ) : ( + <> +
+ {tracks.map((t) => { + const e = entryFor(t); + return ( +
+ update(t.id, { keep: !e.keep })} + className="accent-duck-teal cursor-pointer shrink-0" + /> + + {t.lang || 'und'} + + update(t.id, { label: ev.target.value })} + className="flex-1 min-w-0 px-2 py-1 text-sm rounded-md border border-duck-dark/15 dark:border-foreground/15 bg-background focus:outline-none focus:ring-1 focus:ring-duck-teal disabled:opacity-40" + /> +
+ ); + })} +
+ + Keeping {keptCount} of {tracks.length} + + + )} +
+ ); + } + if (def.type === 'audio_tracks' || def.type === 'subtitle_tracks') { const isAudio = def.type === 'audio_tracks'; const tracks = (isAudio ? audioTracks : subtitleTracks) ?? []; @@ -470,7 +535,7 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa // Track pickers: audio defaults to keep-all, subtitles to keep-none. A folder probes every // video and drives the pickers off the largest matching group (the rest convert separately). const hasTrackPickers = Object.values(defs).some( - (d) => d.type === 'audio_tracks' || d.type === 'subtitle_tracks', + (d) => d.type === 'audio_tracks' || d.type === 'subtitle_tracks' || d.type === 'subtitle_edit', ); if (!hasTrackPickers || !filePath) return; @@ -482,6 +547,8 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa for (const [key, def] of Object.entries(defs)) { if (def.type === 'audio_tracks') next[key] = aud.map((t) => t.id).join(',') || 'none'; if (def.type === 'subtitle_tracks') next[key] = 'none'; + if (def.type === 'subtitle_edit') + next[key] = JSON.stringify(sub.map((t) => ({ id: t.id, keep: true, label: trackLabel(t) }))); } return next; });