From a188063fe208e49339aad05e416c5f9dda430338 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sun, 8 Nov 2026 11:03:10 +1100 Subject: [PATCH] Fix Recordly 2.0 part 1 follow-up review findings Preserve existing PR history and integrate validated second-cycle fixes. --- .../video-editor/captionLayout.test.ts | 68 ++++++++++++++++++- src/components/video-editor/captionLayout.ts | 17 ++--- src/i18n/locales/de/timeline.json | 7 +- src/i18n/locales/en/timeline.json | 7 +- src/i18n/locales/es/timeline.json | 7 +- src/i18n/locales/fr/billing.json | 2 +- src/i18n/locales/fr/timeline.json | 7 +- src/i18n/locales/it/timeline.json | 7 +- src/i18n/locales/ja/timeline.json | 7 +- src/i18n/locales/ko/timeline.json | 7 +- src/i18n/locales/nl/timeline.json | 7 +- src/i18n/locales/pt-BR/timeline.json | 7 +- src/i18n/locales/zh-CN/timeline.json | 7 +- src/i18n/locales/zh-TW/common.json | 2 +- src/i18n/locales/zh-TW/timeline.json | 7 +- src/i18n/locales/zh-TW/ui.json | 2 +- 16 files changed, 140 insertions(+), 28 deletions(-) diff --git a/src/components/video-editor/captionLayout.test.ts b/src/components/video-editor/captionLayout.test.ts index 59a6b16c..a2ac0b30 100644 --- a/src/components/video-editor/captionLayout.test.ts +++ b/src/components/video-editor/captionLayout.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { buildActiveCaptionLayout, flattenCaptionWords } from "./captionLayout"; +import { + buildActiveCaptionLayout, + flattenCaptionWords, + prepareCaptionLayout, +} from "./captionLayout"; import { type CaptionCue, DEFAULT_AUTO_CAPTION_SETTINGS } from "./types"; describe("flattenCaptionWords", () => { @@ -80,6 +84,68 @@ describe("current-word caption layout", () => { } }); + it.each([ + "chronological", + "reversed", + ] as const)("keeps the visible overlapping cue and its highlighted word together for %s saved cues", (order) => { + const next: CaptionCue = { + id: "b", + startMs: 800, + endMs: 1800, + text: "next phrase", + words: [ + { text: "next", startMs: 800, endMs: 1200 }, + { text: "phrase", startMs: 1400, endMs: 1800, leadingSpace: true }, + ], + }; + const cues = order === "chronological" ? [cue, next] : [next, cue]; + const options = { + cues, + settings: { + ...DEFAULT_AUTO_CAPTION_SETTINGS, + maxRows: 1, + animationStyle: "none" as const, + }, + maxWidthPx: 1000, + measureText: (text: string) => text.length * 10, + }; + const sample = prepareCaptionLayout(options); + expect(sample(799)?.cue.id).toBe("a"); + for (const timeMs of [800, 900, 999, 1000]) { + const active = sample(timeMs)!; + expect(active.editTarget.text).toBe("next phrase"); + expect(active.cue.id).toBe("b"); + const emphasized = active.visibleLines + .flatMap((line) => line.words) + .filter((word) => word.state === "active"); + expect(emphasized.map((word) => word.cueId)).toEqual(["b"]); + expect(emphasized[0].index).toBe(active.activeWordIndex); + expect(active.activeWordProgress).toBeCloseTo((timeMs - 800) / 400); + expect(buildActiveCaptionLayout({ ...options, timeMs })).toEqual(active); + } + expect(sample(799)?.cue.id).toBe("a"); + expect(sample(900)?.cue.id).toBe("b"); + expect(sample(1800)).toBeNull(); + }); + + it("keeps an overlapping cue lead-in unhighlighted until its own first word", () => { + const next: CaptionCue = { + id: "b", + startMs: 800, + endMs: 1800, + text: "next", + words: [{ text: "next", startMs: 880, endMs: 1700 }], + }; + for (const timeMs of [800, 850, 879]) { + const active = layout(timeMs, [cue, next])!; + expect(active.editTarget.text).toBe("next"); + expect(active.cue.id).toBe("b"); + expect(active.activeWordIndex).toBe(-1); + expect(active.visibleLines[0].words[0].state).toBe("upcoming"); + } + expect(layout(880, [cue, next])?.visibleLines[0].words[0].state).toBe("active"); + }); + it("finishes Pop in 90ms while Fade retains its 180ms entrance", () => { const animated = (animationStyle: "pop" | "fade", timeMs: number) => buildActiveCaptionLayout({ diff --git a/src/components/video-editor/captionLayout.ts b/src/components/video-editor/captionLayout.ts index 11c167c6..018fa749 100644 --- a/src/components/video-editor/captionLayout.ts +++ b/src/components/video-editor/captionLayout.ts @@ -125,16 +125,6 @@ function splitCaptionWords(cue: CaptionCue) { })); } -function getActiveCaptionCue(cues: CaptionCue[], timeMs: number) { - for (const cue of cues) { - if (timeMs >= cue.startMs && timeMs < cue.endMs) { - return cue; - } - } - - return null; -} - function isWithinCaptionCoverage(sorted: CaptionCue[], timeMs: number) { for (let index = 0; index < sorted.length; index += 1) { const cue = sorted[index]; @@ -559,11 +549,12 @@ export function prepareCaptionLayout( prepared = preparePage(pages[visiblePageIndex]); preparedPages.set(visiblePageIndex, prepared); } - const currentCue = getActiveCaptionCue(options.cues, timeMs); + // The visible page owns the cue through overlaps and bridged gaps. + const currentCue = prepared.cue; // Hold emphasis through acoustic gaps; a cue lead-in cannot highlight its predecessor. let activeWordIndex = -1; sourceWords.forEach((word, index) => { - if (currentCue && word.cueId !== currentCue.id) return; + if (word.cueId !== currentCue.id) return; if ( timeMs >= word.startMs && (activeWordIndex < 0 || word.startMs >= sourceWords[activeWordIndex].startMs) @@ -597,7 +588,7 @@ export function prepareCaptionLayout( ); return { fontBaselinePx: options.fontBaselinePx, - cue: currentCue ?? prepared.cue, + cue: currentCue, blockKey: prepared.blockKey, visibleLines, hasWordTimings: prepared.hasWordTimings, diff --git a/src/i18n/locales/de/timeline.json b/src/i18n/locales/de/timeline.json index 3a15827f..4dc41991 100644 --- a/src/i18n/locales/de/timeline.json +++ b/src/i18n/locales/de/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "Geschwindigkeit hinzufügen (S)", "resizeLeft": "Größe links anpassen", - "resizeRight": "Größe rechts anpassen" + "resizeRight": "Größe rechts anpassen", + "hints": { + "splitClip": "Drücke {{binding}}, um den Clip zu teilen", + "addAnnotation": "Drücke {{binding}}, um eine Anmerkung hinzuzufügen", + "addAudio": "Klicke auf das Musiksymbol, um Audio hinzuzufügen" + } } diff --git a/src/i18n/locales/en/timeline.json b/src/i18n/locales/en/timeline.json index 86cc447b..653fb618 100644 --- a/src/i18n/locales/en/timeline.json +++ b/src/i18n/locales/en/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "Add Speed (S)", "resizeLeft": "Resize left", - "resizeRight": "Resize right" + "resizeRight": "Resize right", + "hints": { + "splitClip": "Press {{binding}} to split clip", + "addAnnotation": "Press {{binding}} to add annotation", + "addAudio": "Click music icon to add audio" + } } diff --git a/src/i18n/locales/es/timeline.json b/src/i18n/locales/es/timeline.json index 08421f85..be1f01cb 100644 --- a/src/i18n/locales/es/timeline.json +++ b/src/i18n/locales/es/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "Agregar Velocidad (S)", "resizeLeft": "Redimensionar izquierda", - "resizeRight": "Redimensionar derecha" + "resizeRight": "Redimensionar derecha", + "hints": { + "splitClip": "Pulsa {{binding}} para dividir el clip", + "addAnnotation": "Pulsa {{binding}} para añadir una anotación", + "addAudio": "Haz clic en el icono de música para añadir audio" + } } diff --git a/src/i18n/locales/fr/billing.json b/src/i18n/locales/fr/billing.json index c996e757..58184ef4 100644 --- a/src/i18n/locales/fr/billing.json +++ b/src/i18n/locales/fr/billing.json @@ -31,7 +31,7 @@ "commercialRequirement": "Un abonnement Recordly Cloud actif est requis pour un usage commercial.", "featureShare": "Partagez des vidéos en haute définition 1080p", "featureStorage": "Stockage cloud et bande passante illimités", - "featureWeeklyUploads": "{{count}} nouveaux envois sur 7 jours glissants", + "featureWeeklyUploads": "Jusqu’à {{count}} nouveaux envois par période glissante de 7 jours", "featureUploadFallback": "Importez des enregistrements plus volumineux", "featureLinks": "Les liens restent accessibles tant que Cloud est actif", "featureExisting": "Conservez vos enregistrements cloud tant que Cloud est actif", diff --git a/src/i18n/locales/fr/timeline.json b/src/i18n/locales/fr/timeline.json index 2413f377..cccf8cd0 100644 --- a/src/i18n/locales/fr/timeline.json +++ b/src/i18n/locales/fr/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "Ajouter une vitesse (S)", "resizeLeft": "Redimensionner vers la gauche", - "resizeRight": "Redimensionner vers la droite" + "resizeRight": "Redimensionner vers la droite", + "hints": { + "splitClip": "Appuyez sur {{binding}} pour scinder le clip", + "addAnnotation": "Appuyez sur {{binding}} pour ajouter une annotation", + "addAudio": "Cliquez sur l’icône de musique pour ajouter de l’audio" + } } diff --git a/src/i18n/locales/it/timeline.json b/src/i18n/locales/it/timeline.json index d39098d0..e3878d11 100644 --- a/src/i18n/locales/it/timeline.json +++ b/src/i18n/locales/it/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "Aggiungi velocità (S)", "resizeLeft": "Ridimensiona a sinistra", - "resizeRight": "Ridimensiona a destra" + "resizeRight": "Ridimensiona a destra", + "hints": { + "splitClip": "Premi {{binding}} per dividere la clip", + "addAnnotation": "Premi {{binding}} per aggiungere un’annotazione", + "addAudio": "Fai clic sull’icona della musica per aggiungere audio" + } } diff --git a/src/i18n/locales/ja/timeline.json b/src/i18n/locales/ja/timeline.json index a516f361..fa467284 100644 --- a/src/i18n/locales/ja/timeline.json +++ b/src/i18n/locales/ja/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "速度変更を追加 (S)", "resizeLeft": "左端を調整", - "resizeRight": "右端を調整" + "resizeRight": "右端を調整", + "hints": { + "splitClip": "{{binding}} キーでクリップを分割", + "addAnnotation": "{{binding}} キーで注釈を追加", + "addAudio": "音楽アイコンをクリックして音声を追加" + } } diff --git a/src/i18n/locales/ko/timeline.json b/src/i18n/locales/ko/timeline.json index ccb9be80..e9af4ff9 100644 --- a/src/i18n/locales/ko/timeline.json +++ b/src/i18n/locales/ko/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "속도 추가 (S)", "resizeLeft": "왼쪽 크기 조절", - "resizeRight": "오른쪽 크기 조절" + "resizeRight": "오른쪽 크기 조절", + "hints": { + "splitClip": "{{binding}} 키를 눌러 클립 분할", + "addAnnotation": "{{binding}} 키를 눌러 주석 추가", + "addAudio": "음악 아이콘을 클릭하여 오디오 추가" + } } diff --git a/src/i18n/locales/nl/timeline.json b/src/i18n/locales/nl/timeline.json index 0147195f..3af8a3fc 100644 --- a/src/i18n/locales/nl/timeline.json +++ b/src/i18n/locales/nl/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "Snelheid toevoegen (S)", "resizeLeft": "Links verkleinen", - "resizeRight": "Rechts verkleinen" + "resizeRight": "Rechts verkleinen", + "hints": { + "splitClip": "Druk op {{binding}} om de clip te splitsen", + "addAnnotation": "Druk op {{binding}} om een annotatie toe te voegen", + "addAudio": "Klik op het muziekpictogram om audio toe te voegen" + } } diff --git a/src/i18n/locales/pt-BR/timeline.json b/src/i18n/locales/pt-BR/timeline.json index 84689d56..8803480e 100644 --- a/src/i18n/locales/pt-BR/timeline.json +++ b/src/i18n/locales/pt-BR/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "Adicionar velocidade (S)", "resizeLeft": "Redimensionar para a esquerda", - "resizeRight": "Redimensionar para a direita" + "resizeRight": "Redimensionar para a direita", + "hints": { + "splitClip": "Pressione {{binding}} para dividir o clipe", + "addAnnotation": "Pressione {{binding}} para adicionar uma anotação", + "addAudio": "Clique no ícone de música para adicionar áudio" + } } diff --git a/src/i18n/locales/zh-CN/timeline.json b/src/i18n/locales/zh-CN/timeline.json index 54d64867..21f9300d 100644 --- a/src/i18n/locales/zh-CN/timeline.json +++ b/src/i18n/locales/zh-CN/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "添加变速 (S)", "resizeLeft": "向左调整", - "resizeRight": "向右调整" + "resizeRight": "向右调整", + "hints": { + "splitClip": "按 {{binding}} 拆分片段", + "addAnnotation": "按 {{binding}} 添加注释", + "addAudio": "点击音乐图标添加音频" + } } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index ab48a801..e5233892 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -4,7 +4,7 @@ "editorTitle": "Recordly 編輯器", "subtitle": "螢幕錄製與編輯", "language": "語言", - "manageRecordings": "打開錄製影像文件夾" + "manageRecordings": "開啟錄製資料夾" }, "actions": { "cancel": "取消", diff --git a/src/i18n/locales/zh-TW/timeline.json b/src/i18n/locales/zh-TW/timeline.json index ab4c4506..29ea40be 100644 --- a/src/i18n/locales/zh-TW/timeline.json +++ b/src/i18n/locales/zh-TW/timeline.json @@ -38,5 +38,10 @@ }, "addSpeed": "新增速度(S)", "resizeLeft": "向左調整大小", - "resizeRight": "向右調整大小" + "resizeRight": "向右調整大小", + "hints": { + "splitClip": "按 {{binding}} 分割片段", + "addAnnotation": "按 {{binding}} 新增註解", + "addAudio": "點選音樂圖示以新增音訊" + } } diff --git a/src/i18n/locales/zh-TW/ui.json b/src/i18n/locales/zh-TW/ui.json index 03bcf2f7..11776ba3 100644 --- a/src/i18n/locales/zh-TW/ui.json +++ b/src/i18n/locales/zh-TW/ui.json @@ -230,7 +230,7 @@ "projectDuplicateHelp": "使用相同來源檔案,且儲存的編輯器設定完全一致。保留目前開啟的專案,或最近儲存的副本。編輯內容不同的專案會保留。", "unusedHelp": "未被此媒體庫中的專案及其配套軌道參照的來源檔案。無法檢查未儲存的編輯或媒體庫以外的專案。", "title": "清理媒體庫", - "scopeHelp": "搜尋、檔案夾和時間篩選仍然有效。請先檢視建議再選取;此處不會刪除任何內容。", + "scopeHelp": "搜尋、資料夾和時間篩選仍然有效。請先檢視建議再選取;此處不會刪除任何內容。", "find": "尋找", "all": "所有項目", "unused": "未用於已儲存專案",