task runner: subtitle_edit input type for the subtitle editor
renders each subtitle as a keep checkbox + editable label field, seeded from the file's current tracks. reuses the existing probe / folder-grouping / include plumbing, so it works on a single file or a whole season. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+68
-1
@@ -270,6 +270,19 @@ type TaskInputFormProps = {
|
|||||||
const trackLabel = (t: { title: string; lang: string; id: number }) =>
|
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}`;
|
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 TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTracks, subtitleTracks, probing, entryType }: TaskInputFormProps) => {
|
||||||
const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key));
|
const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key));
|
||||||
if (configurableInputs.length === 0) return null;
|
if (configurableInputs.length === 0) return null;
|
||||||
@@ -277,6 +290,58 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTrack
|
|||||||
return (
|
return (
|
||||||
<div className="px-5 py-3 border-b border-duck-dark/10 flex flex-col gap-3">
|
<div className="px-5 py-3 border-b border-duck-dark/10 flex flex-col gap-3">
|
||||||
{configurableInputs.map(([key, def]) => {
|
{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<SubtitleEditEntry>) =>
|
||||||
|
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 (
|
||||||
|
<div key={key} className="flex flex-col gap-1.5">
|
||||||
|
<span className="text-sm font-medium text-duck-dark dark:text-foreground">{def.description ?? key}</span>
|
||||||
|
{probing ? (
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" /> {entryType === 'directory' ? 'Analyzing files…' : 'Probing…'}
|
||||||
|
</span>
|
||||||
|
) : tracks.length === 0 ? (
|
||||||
|
<span className="text-xs text-duck-dark/50 dark:text-foreground/50">No subtitles</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-1 max-h-72 overflow-y-auto pr-1">
|
||||||
|
{tracks.map((t) => {
|
||||||
|
const e = entryFor(t);
|
||||||
|
return (
|
||||||
|
<div key={t.id} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={e.keep}
|
||||||
|
onChange={() => update(t.id, { keep: !e.keep })}
|
||||||
|
className="accent-duck-teal cursor-pointer shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] font-mono uppercase w-9 shrink-0 text-duck-dark/40 dark:text-foreground/40">
|
||||||
|
{t.lang || 'und'}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={e.label}
|
||||||
|
disabled={!e.keep}
|
||||||
|
onChange={(ev) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-duck-dark/40 dark:text-foreground/40">
|
||||||
|
Keeping {keptCount} of {tracks.length}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (def.type === 'audio_tracks' || def.type === 'subtitle_tracks') {
|
if (def.type === 'audio_tracks' || def.type === 'subtitle_tracks') {
|
||||||
const isAudio = def.type === 'audio_tracks';
|
const isAudio = def.type === 'audio_tracks';
|
||||||
const tracks = (isAudio ? audioTracks : subtitleTracks) ?? [];
|
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
|
// 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).
|
// video and drives the pickers off the largest matching group (the rest convert separately).
|
||||||
const hasTrackPickers = Object.values(defs).some(
|
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;
|
if (!hasTrackPickers || !filePath) return;
|
||||||
|
|
||||||
@@ -482,6 +547,8 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa
|
|||||||
for (const [key, def] of Object.entries(defs)) {
|
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 === 'audio_tracks') next[key] = aud.map((t) => t.id).join(',') || 'none';
|
||||||
if (def.type === 'subtitle_tracks') next[key] = '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;
|
return next;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user