From 5426b6284cde53de963ede211650ccb8a1260638 Mon Sep 17 00:00:00 2001 From: cocoon Date: Sun, 5 Apr 2026 09:16:04 +0000 Subject: [PATCH 1/5] feat(editor): duplicate annotations --- .../video-editor/AnnotationSettingsPanel.tsx | 34 ++++++++++++++----- src/components/video-editor/SettingsPanel.tsx | 3 ++ src/components/video-editor/VideoEditor.tsx | 28 +++++++++++++++ 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index b289392e..db3197f9 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -1,6 +1,7 @@ import Block from "@uiw/react-color-block"; import { AlignCenter, + Copy, AlignLeft, AlignRight, Bold, @@ -40,6 +41,7 @@ interface AnnotationSettingsPanelProps { onTypeChange: (type: AnnotationType) => void; onStyleChange: (style: Partial) => void; onFigureDataChange?: (figureData: FigureData) => void; + onDuplicate?: () => void; onDelete: () => void; } @@ -62,6 +64,7 @@ export function AnnotationSettingsPanel({ onTypeChange, onStyleChange, onFigureDataChange, + onDuplicate, onDelete, }: AnnotationSettingsPanelProps) { const t = useScopedT("settings"); @@ -597,15 +600,28 @@ export function AnnotationSettingsPanel({ - +
+ + + +
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 7e556b85..96a61afe 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -140,6 +140,7 @@ interface SettingsPanelProps { onAnnotationTypeChange?: (id: string, type: AnnotationType) => void; onAnnotationStyleChange?: (id: string, style: Partial) => void; onAnnotationFigureDataChange?: (id: string, figureData: FigureData) => void; + onAnnotationDuplicate?: (id: string) => void; onAnnotationDelete?: (id: string) => void; selectedSpeedId?: string | null; selectedSpeedValue?: PlaybackSpeed | null; @@ -213,6 +214,7 @@ export function SettingsPanel({ onAnnotationTypeChange, onAnnotationStyleChange, onAnnotationFigureDataChange, + onAnnotationDuplicate, onAnnotationDelete, selectedSpeedId, selectedSpeedValue, @@ -466,6 +468,7 @@ export function SettingsPanel({ ? (figureData) => onAnnotationFigureDataChange(selectedAnnotation.id, figureData) : undefined } + onDuplicate={onAnnotationDuplicate ? () => onAnnotationDuplicate(selectedAnnotation.id) : undefined} onDelete={() => onAnnotationDelete(selectedAnnotation.id)} /> ); diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 4e5e978a..f489e2d5 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -831,6 +831,33 @@ export default function VideoEditor() { [pushState], ); + const handleAnnotationDuplicate = useCallback( + (id: string) => { + const duplicateId = `annotation-${nextAnnotationIdRef.current++}`; + const duplicateZIndex = nextAnnotationZIndexRef.current++; + pushState((prev) => { + const source = prev.annotationRegions.find((region) => region.id === id); + if (!source) return {}; + + const duplicate: AnnotationRegion = { + ...source, + id: duplicateId, + zIndex: duplicateZIndex, + position: { x: source.position.x + 4, y: source.position.y + 4 }, + size: { ...source.size }, + style: { ...source.style }, + figureData: source.figureData ? { ...source.figureData } : undefined, + }; + + return { annotationRegions: [...prev.annotationRegions, duplicate] }; + }); + setSelectedAnnotationId(duplicateId); + setSelectedZoomId(null); + setSelectedTrimId(null); + }, + [pushState], + ); + const handleAnnotationDelete = useCallback( (id: string) => { pushState((prev) => ({ @@ -1680,6 +1707,7 @@ export default function VideoEditor() { onAnnotationTypeChange={handleAnnotationTypeChange} onAnnotationStyleChange={handleAnnotationStyleChange} onAnnotationFigureDataChange={handleAnnotationFigureDataChange} + onAnnotationDuplicate={handleAnnotationDuplicate} onAnnotationDelete={handleAnnotationDelete} selectedSpeedId={selectedSpeedId} selectedSpeedValue={ From 12f3be02f2902a80be908e4dd3d797900f10df92 Mon Sep 17 00:00:00 2001 From: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:31:37 +0800 Subject: [PATCH 2/5] fix: sort lucide-react imports alphabetically Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> --- src/components/video-editor/AnnotationSettingsPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index db3197f9..2e25830e 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -1,9 +1,9 @@ import Block from "@uiw/react-color-block"; import { AlignCenter, - Copy, AlignLeft, AlignRight, + Copy, Bold, ChevronDown, Image as ImageIcon, From 8b7047365c80d7394ad10964773390ee1a9c5e9e Mon Sep 17 00:00:00 2001 From: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:00:48 +0800 Subject: [PATCH 3/5] style: sort lucide-react imports alphabetically to fix Biome lint --- src/components/video-editor/AnnotationSettingsPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 2e25830e..0b0c174b 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -3,9 +3,9 @@ import { AlignCenter, AlignLeft, AlignRight, - Copy, Bold, ChevronDown, + Copy, Image as ImageIcon, Info, Italic, From 64e011f79889835e703cccdf1f6cf2155975eab0 Mon Sep 17 00:00:00 2001 From: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:01:02 +0800 Subject: [PATCH 4/5] style: wrap long onDuplicate prop to fix Biome formatter --- src/components/video-editor/SettingsPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 96a61afe..d538666b 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -468,7 +468,9 @@ export function SettingsPanel({ ? (figureData) => onAnnotationFigureDataChange(selectedAnnotation.id, figureData) : undefined } - onDuplicate={onAnnotationDuplicate ? () => onAnnotationDuplicate(selectedAnnotation.id) : undefined} + onDuplicate={ + onAnnotationDuplicate ? () => onAnnotationDuplicate(selectedAnnotation.id) : undefined + } onDelete={() => onAnnotationDelete(selectedAnnotation.id)} /> ); From 501c4f20a1fbfae98f738f096bd929607d49ec30 Mon Sep 17 00:00:00 2001 From: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:29:05 +0800 Subject: [PATCH 5/5] fix: remove unused COMPARE_LOCALES variable in i18n-check.mjs to pass Biome lint --- scripts/i18n-check.mjs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index 3fd0331f..699ae9e3 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * Validates that all locale translation files have identical key structures. - * Compares zh-CN and es against the en baseline for every namespace. + * Compares all locale folders (except en) against the en baseline for every namespace. * * Usage: node scripts/i18n-check.mjs */ @@ -11,7 +11,6 @@ import path from "node:path"; const LOCALES_DIR = path.resolve("src/i18n/locales"); const BASE_LOCALE = "en"; -const COMPARE_LOCALES = ["zh-CN", "es"]; function getKeys(obj, prefix = "") { const keys = []; @@ -34,12 +33,19 @@ const namespaces = fs .filter((f) => f.endsWith(".json")) .map((f) => f.replace(".json", "")); +const compareLocales = fs + .readdirSync(LOCALES_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .filter((locale) => locale !== BASE_LOCALE) + .sort((a, b) => a.localeCompare(b)); + for (const namespace of namespaces) { const basePath = path.join(baseDir, `${namespace}.json`); const baseData = JSON.parse(fs.readFileSync(basePath, "utf-8")); const baseKeys = getKeys(baseData); - for (const locale of COMPARE_LOCALES) { + for (const locale of compareLocales) { const localePath = path.join(LOCALES_DIR, locale, `${namespace}.json`); if (!fs.existsSync(localePath)) { @@ -77,6 +83,6 @@ if (hasErrors) { process.exit(1); } else { console.log( - `i18n check PASSED — all ${COMPARE_LOCALES.length} locales match ${BASE_LOCALE} across ${namespaces.length} namespaces.`, + `i18n check PASSED — all ${compareLocales.length} locales match ${BASE_LOCALE} across ${namespaces.length} namespaces.`, ); }