From 5426b6284cde53de963ede211650ccb8a1260638 Mon Sep 17 00:00:00 2001 From: cocoon Date: Sun, 5 Apr 2026 09:16:04 +0000 Subject: [PATCH] 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={