update: gros commit fix editor srt panel 2
This commit is contained in:
@@ -26,6 +26,9 @@ interface TextRange {
|
|||||||
end: number;
|
end: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CueTimeEdge = "start" | "end";
|
||||||
|
const CUE_NUDGE_SECONDS = 0.1;
|
||||||
|
|
||||||
const SRT_VOICES: SrtVoiceOption[] = [
|
const SRT_VOICES: SrtVoiceOption[] = [
|
||||||
{ id: "narrateur", label: "Narrateur" },
|
{ id: "narrateur", label: "Narrateur" },
|
||||||
{ id: "fermier", label: "Fermier" },
|
{ id: "fermier", label: "Fermier" },
|
||||||
@@ -75,6 +78,21 @@ function formatPreviewTime(totalSeconds: number): string {
|
|||||||
return `${Math.floor(totalSeconds)}.${Math.floor((totalSeconds % 1) * 10)}s`;
|
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 {
|
function padTime(value: number): string {
|
||||||
return value.toString().padStart(2, "0");
|
return value.toString().padStart(2, "0");
|
||||||
}
|
}
|
||||||
@@ -190,6 +208,58 @@ function findCueBlockRange(
|
|||||||
return { start, end };
|
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(
|
function downloadSrtFile(
|
||||||
voice: DialogueVoiceId,
|
voice: DialogueVoiceId,
|
||||||
language: SubtitleLanguage,
|
language: SubtitleLanguage,
|
||||||
@@ -287,6 +357,39 @@ export function EditorSrtPanel(): React.JSX.Element {
|
|||||||
setStatus(`Cue ${cueIndex} selectionnee dans le SRT.`);
|
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(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
||||||
@@ -414,6 +517,46 @@ export function EditorSrtPanel(): React.JSX.Element {
|
|||||||
<p>Aucune cue active a ce moment.</p>
|
<p>Aucune cue active a ce moment.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="editor-srt-time-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleSetCueTime(selectedDialogue.subtitleCueIndex, "start")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Set start
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleSetCueTime(selectedDialogue.subtitleCueIndex, "end")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Set end
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleNudgeCue(
|
||||||
|
selectedDialogue.subtitleCueIndex,
|
||||||
|
-CUE_NUDGE_SECONDS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
-100ms
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleNudgeCue(
|
||||||
|
selectedDialogue.subtitleCueIndex,
|
||||||
|
CUE_NUDGE_SECONDS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+100ms
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className="editor-srt-jump-button"
|
className="editor-srt-jump-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1532,6 +1532,28 @@ canvas {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-srt-time-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-srt-time-actions button {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid rgba(125, 211, 252, 0.24);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(125, 211, 252, 0.08);
|
||||||
|
color: #bae6fd;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-srt-time-actions button:hover {
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
background: rgba(125, 211, 252, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.editor-srt-jump-button {
|
.editor-srt-jump-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user