diff --git a/src/components/editor/EditorSrtPanel.tsx b/src/components/editor/EditorSrtPanel.tsx index 2319e51..44d439d 100644 --- a/src/components/editor/EditorSrtPanel.tsx +++ b/src/components/editor/EditorSrtPanel.tsx @@ -26,6 +26,9 @@ interface TextRange { end: number; } +type CueTimeEdge = "start" | "end"; +const CUE_NUDGE_SECONDS = 0.1; + const SRT_VOICES: SrtVoiceOption[] = [ { id: "narrateur", label: "Narrateur" }, { id: "fermier", label: "Fermier" }, @@ -75,6 +78,21 @@ function formatPreviewTime(totalSeconds: number): string { return `${Math.floor(totalSeconds)}.${Math.floor((totalSeconds % 1) * 10)}s`; } +function parseSrtTime(value: string): number | null { + const match = value.match(/^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/); + if (!match) return null; + + const [, hours, minutes, seconds, milliseconds] = match; + if (!hours || !minutes || !seconds || !milliseconds) return null; + + return ( + Number(hours) * 3600 + + Number(minutes) * 60 + + Number(seconds) + + Number(milliseconds) / 1000 + ); +} + function padTime(value: number): string { return value.toString().padStart(2, "0"); } @@ -190,6 +208,58 @@ function findCueBlockRange( return { start, end }; } +function updateCueTimecode( + content: string, + cueIndex: number, + edge: CueTimeEdge, + time: number, +): string | null { + const range = findCueBlockRange(content, cueIndex); + if (!range) return null; + + const block = content.slice(range.start, range.end); + const lines = block.split("\n"); + const timecodeLine = lines[1]; + if (!timecodeLine) return null; + + const [start, end] = timecodeLine.split(" --> "); + if (!start || !end) return null; + + lines[1] = + edge === "start" + ? `${formatSrtTime(time)} --> ${end}` + : `${start} --> ${formatSrtTime(time)}`; + + return `${content.slice(0, range.start)}${lines.join("\n")}${content.slice(range.end)}`; +} + +function nudgeCueTimecode( + content: string, + cueIndex: number, + delta: number, +): string | null { + const range = findCueBlockRange(content, cueIndex); + if (!range) return null; + + const block = content.slice(range.start, range.end); + const lines = block.split("\n"); + const timecodeLine = lines[1]; + if (!timecodeLine) return null; + + const [start, end] = timecodeLine.split(" --> "); + if (!start || !end) return null; + + const startTime = parseSrtTime(start); + const endTime = parseSrtTime(end); + if (startTime === null || endTime === null) return null; + + const nextStartTime = Math.max(0, startTime + delta); + const nextEndTime = Math.max(nextStartTime + 0.001, endTime + delta); + lines[1] = `${formatSrtTime(nextStartTime)} --> ${formatSrtTime(nextEndTime)}`; + + return `${content.slice(0, range.start)}${lines.join("\n")}${content.slice(range.end)}`; +} + function downloadSrtFile( voice: DialogueVoiceId, language: SubtitleLanguage, @@ -287,6 +357,39 @@ export function EditorSrtPanel(): React.JSX.Element { setStatus(`Cue ${cueIndex} selectionnee dans le SRT.`); } + function handleSetCueTime(cueIndex: number, edge: CueTimeEdge): void { + const updatedContent = updateCueTimecode( + content, + cueIndex, + edge, + audioCurrentTime, + ); + + if (!updatedContent) { + setStatus(`Cue ${cueIndex} introuvable ou timecode invalide.`); + return; + } + + setContent(updatedContent); + setStatus( + `Cue ${cueIndex}: ${edge === "start" ? "debut" : "fin"} place a ${formatSrtTime(audioCurrentTime)}.`, + ); + } + + function handleNudgeCue(cueIndex: number, delta: number): void { + const updatedContent = nudgeCueTimecode(content, cueIndex, delta); + + if (!updatedContent) { + setStatus(`Cue ${cueIndex} introuvable ou timecode invalide.`); + return; + } + + setContent(updatedContent); + setStatus( + `Cue ${cueIndex} decalee de ${delta > 0 ? "+" : ""}${delta.toFixed(1)}s.`, + ); + } + useEffect(() => { let mounted = true; @@ -414,6 +517,46 @@ export function EditorSrtPanel(): React.JSX.Element {
Aucune cue active a ce moment.
)} +