Skip to content

Commit 56d3d59

Browse files
Merge pull request #342 from kuishou68/cocoon/feature-duplicate-annotation
feat(editor): duplicate annotations
2 parents e85d07b + 0ec1835 commit 56d3d59

3 files changed

Lines changed: 58 additions & 9 deletions

File tree

src/components/video-editor/AnnotationSettingsPanel.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
AlignRight,
66
Bold,
77
ChevronDown,
8+
Copy,
89
Image as ImageIcon,
910
Info,
1011
Italic,
@@ -45,6 +46,7 @@ interface AnnotationSettingsPanelProps {
4546
onTypeChange: (type: AnnotationType) => void;
4647
onStyleChange: (style: Partial<AnnotationRegion["style"]>) => void;
4748
onFigureDataChange?: (figureData: FigureData) => void;
49+
onDuplicate?: () => void;
4850
onDelete: () => void;
4951
}
5052

@@ -67,6 +69,7 @@ export function AnnotationSettingsPanel({
6769
onTypeChange,
6870
onStyleChange,
6971
onFigureDataChange,
72+
onDuplicate,
7073
onDelete,
7174
}: AnnotationSettingsPanelProps) {
7275
const t = useScopedT("settings");
@@ -602,15 +605,28 @@ export function AnnotationSettingsPanel({
602605
</TabsContent>
603606
</Tabs>
604607

605-
<Button
606-
onClick={onDelete}
607-
variant="destructive"
608-
size="sm"
609-
className="w-full gap-2 bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 hover:border-red-500/30 transition-all mt-4"
610-
>
611-
<Trash2 className="w-4 h-4" />
612-
{t("annotation.deleteAnnotation")}
613-
</Button>
608+
<div className="mt-4 grid grid-cols-2 gap-2">
609+
<Button
610+
onClick={() => onDuplicate?.()}
611+
variant="outline"
612+
size="sm"
613+
disabled={!onDuplicate}
614+
className="w-full gap-2 bg-white/5 text-slate-200 border border-white/10 hover:bg-white/10 hover:border-white/20 transition-all"
615+
>
616+
<Copy className="w-4 h-4" />
617+
Duplicate
618+
</Button>
619+
620+
<Button
621+
onClick={onDelete}
622+
variant="destructive"
623+
size="sm"
624+
className="w-full gap-2 bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 hover:border-red-500/30 transition-all"
625+
>
626+
<Trash2 className="w-4 h-4" />
627+
{t("annotation.deleteAnnotation")}
628+
</Button>
629+
</div>
614630

615631
<div className="mt-6 p-3 bg-white/5 rounded-lg border border-white/5">
616632
<div className="flex items-center gap-2 mb-2 text-slate-300">

src/components/video-editor/SettingsPanel.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ interface SettingsPanelProps {
210210
onAnnotationTypeChange?: (id: string, type: AnnotationType) => void;
211211
onAnnotationStyleChange?: (id: string, style: Partial<AnnotationRegion["style"]>) => void;
212212
onAnnotationFigureDataChange?: (id: string, figureData: FigureData) => void;
213+
onAnnotationDuplicate?: (id: string) => void;
213214
onAnnotationDelete?: (id: string) => void;
214215
selectedBlurId?: string | null;
215216
blurRegions?: AnnotationRegion[];
@@ -301,6 +302,7 @@ export function SettingsPanel({
301302
onAnnotationTypeChange,
302303
onAnnotationStyleChange,
303304
onAnnotationFigureDataChange,
305+
onAnnotationDuplicate,
304306
onAnnotationDelete,
305307
selectedBlurId,
306308
blurRegions = [],
@@ -569,6 +571,9 @@ export function SettingsPanel({
569571
? (figureData) => onAnnotationFigureDataChange(selectedAnnotation.id, figureData)
570572
: undefined
571573
}
574+
onDuplicate={
575+
onAnnotationDuplicate ? () => onAnnotationDuplicate(selectedAnnotation.id) : undefined
576+
}
572577
onDelete={() => onAnnotationDelete(selectedAnnotation.id)}
573578
/>
574579
);

src/components/video-editor/VideoEditor.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,33 @@ export default function VideoEditor() {
987987
[pushState],
988988
);
989989

990+
const handleAnnotationDuplicate = useCallback(
991+
(id: string) => {
992+
const duplicateId = `annotation-${nextAnnotationIdRef.current++}`;
993+
const duplicateZIndex = nextAnnotationZIndexRef.current++;
994+
pushState((prev) => {
995+
const source = prev.annotationRegions.find((region) => region.id === id);
996+
if (!source) return {};
997+
998+
const duplicate: AnnotationRegion = {
999+
...source,
1000+
id: duplicateId,
1001+
zIndex: duplicateZIndex,
1002+
position: { x: source.position.x + 4, y: source.position.y + 4 },
1003+
size: { ...source.size },
1004+
style: { ...source.style },
1005+
figureData: source.figureData ? { ...source.figureData } : undefined,
1006+
};
1007+
1008+
return { annotationRegions: [...prev.annotationRegions, duplicate] };
1009+
});
1010+
setSelectedAnnotationId(duplicateId);
1011+
setSelectedZoomId(null);
1012+
setSelectedTrimId(null);
1013+
},
1014+
[pushState],
1015+
);
1016+
9901017
const handleAnnotationDelete = useCallback(
9911018
(id: string) => {
9921019
pushState((prev) => ({
@@ -1993,6 +2020,7 @@ export default function VideoEditor() {
19932020
onAnnotationTypeChange={handleAnnotationTypeChange}
19942021
onAnnotationStyleChange={handleAnnotationStyleChange}
19952022
onAnnotationFigureDataChange={handleAnnotationFigureDataChange}
2023+
onAnnotationDuplicate={handleAnnotationDuplicate}
19962024
onAnnotationDelete={handleAnnotationDelete}
19972025
selectedBlurId={selectedBlurId}
19982026
blurRegions={blurRegions}

0 commit comments

Comments
 (0)