From 2704c280bd3d7951800ba992f708a8cff378e3af Mon Sep 17 00:00:00 2001 From: LorenzoLancia Date: Wed, 22 Apr 2026 22:26:45 +0200 Subject: [PATCH 1/4] feat: add copy and paste support for components in timeline --- src/components/video-editor/VideoEditor.tsx | 242 ++++++++++++++++++ .../video-editor/timeline/TimelineEditor.tsx | 49 +++- 2 files changed, 290 insertions(+), 1 deletion(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 6d21d13c9..8e5020557 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -76,6 +76,33 @@ import { import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback"; import { TRANSITION_WINDOW_MS, ZOOM_IN_TRANSITION_WINDOW_MS } from "./videoPlayback/constants"; +type TimelineClipboardItem = + | { kind: "zoom"; region: ZoomRegion } + | { kind: "trim"; region: TrimRegion } + | { kind: "speed"; region: SpeedRegion } + | { kind: "annotation"; region: AnnotationRegion } + | { kind: "blur"; region: AnnotationRegion }; + +function cloneAnnotationRegion(region: AnnotationRegion): AnnotationRegion { + return { + ...region, + position: { ...region.position }, + size: { ...region.size }, + style: { ...region.style }, + figureData: region.figureData ? { ...region.figureData } : undefined, + blurData: region.blurData + ? { + ...region.blurData, + freehandPoints: region.blurData.freehandPoints?.map((point) => ({ ...point })), + } + : undefined, + }; +} + +function spansOverlap(startA: number, endA: number, startB: number, endB: number): boolean { + return startA < endB && endA > startB; +} + export default function VideoEditor() { const { state: editorState, @@ -144,6 +171,7 @@ export default function VideoEditor() { format: string; } | null>(null); const [isFullscreen, setIsFullscreen] = useState(false); + const [timelineClipboard, setTimelineClipboard] = useState(null); const playerContainerRef = useRef(null); const videoPlaybackRef = useRef(null); @@ -155,6 +183,7 @@ export default function VideoEditor() { const { shortcuts, isMac } = useShortcuts(); const t = useScopedT("editor"); const ts = useScopedT("settings"); + const tt = useScopedT("timeline"); const availableLocales = getAvailableLocales(); const { locale, setLocale } = useI18n(); @@ -1013,6 +1042,209 @@ export default function VideoEditor() { [pushState], ); + const handleCopySelectedTimelineItem = useCallback(() => { + if (selectedAnnotationId) { + const region = annotationOnlyRegions.find((item) => item.id === selectedAnnotationId); + if (region) { + setTimelineClipboard({ kind: "annotation", region: cloneAnnotationRegion(region) }); + } + return; + } + + if (selectedBlurId) { + const region = blurRegions.find((item) => item.id === selectedBlurId); + if (region) { + setTimelineClipboard({ kind: "blur", region: cloneAnnotationRegion(region) }); + } + return; + } + + if (selectedZoomId) { + const region = zoomRegions.find((item) => item.id === selectedZoomId); + if (region) { + setTimelineClipboard({ kind: "zoom", region: { ...region, focus: { ...region.focus } } }); + } + return; + } + + if (selectedTrimId) { + const region = trimRegions.find((item) => item.id === selectedTrimId); + if (region) { + setTimelineClipboard({ kind: "trim", region: { ...region } }); + } + return; + } + + if (selectedSpeedId) { + const region = speedRegions.find((item) => item.id === selectedSpeedId); + if (region) { + setTimelineClipboard({ kind: "speed", region: { ...region } }); + } + } + }, [ + selectedAnnotationId, + selectedBlurId, + selectedZoomId, + selectedTrimId, + selectedSpeedId, + annotationOnlyRegions, + blurRegions, + zoomRegions, + trimRegions, + speedRegions, + ]); + + const handlePasteTimelineItem = useCallback(() => { + if (!timelineClipboard) return; + + const totalMs = Math.max(0, Math.round(duration * 1000)); + if (totalMs <= 0) return; + + const sourceDuration = Math.max( + 1, + timelineClipboard.region.endMs - timelineClipboard.region.startMs, + ); + const pastedDuration = Math.min(sourceDuration, totalMs); + const targetStart = Math.min( + Math.max(0, Math.round(currentTime * 1000)), + Math.max(0, totalMs - pastedDuration), + ); + const targetEnd = Math.min(totalMs, targetStart + pastedDuration); + + if (timelineClipboard.kind === "annotation" || timelineClipboard.kind === "blur") { + const id = `annotation-${nextAnnotationIdRef.current++}`; + const zIndex = nextAnnotationZIndexRef.current++; + const source = cloneAnnotationRegion(timelineClipboard.region); + const pasted: AnnotationRegion = { + ...source, + id, + startMs: targetStart, + endMs: targetEnd, + zIndex, + position: { + x: Math.min(96, source.position.x + 4), + y: Math.min(96, source.position.y + 4), + }, + }; + + pushState((prev) => ({ + annotationRegions: [...prev.annotationRegions, pasted], + })); + + if (timelineClipboard.kind === "blur") { + setSelectedBlurId(id); + setSelectedAnnotationId(null); + } else { + setSelectedAnnotationId(id); + setSelectedBlurId(null); + } + setSelectedZoomId(null); + setSelectedTrimId(null); + setSelectedSpeedId(null); + return; + } + + if (timelineClipboard.kind === "zoom") { + const hasConflict = zoomRegions.some((region) => + spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), + ); + if (hasConflict) { + toast.error(tt("errors.cannotPlaceZoom"), { + description: tt("errors.zoomExistsAtLocation"), + }); + return; + } + + const id = `zoom-${nextZoomIdRef.current++}`; + pushState((prev) => ({ + zoomRegions: [ + ...prev.zoomRegions, + { + ...timelineClipboard.region, + id, + startMs: targetStart, + endMs: targetEnd, + focus: { ...timelineClipboard.region.focus }, + }, + ], + })); + setSelectedZoomId(id); + setSelectedTrimId(null); + setSelectedAnnotationId(null); + setSelectedBlurId(null); + setSelectedSpeedId(null); + return; + } + + if (timelineClipboard.kind === "trim") { + const hasConflict = trimRegions.some((region) => + spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), + ); + if (hasConflict) { + toast.error(tt("errors.cannotPlaceTrim"), { + description: tt("errors.trimExistsAtLocation"), + }); + return; + } + + const id = `trim-${nextTrimIdRef.current++}`; + pushState((prev) => ({ + trimRegions: [ + ...prev.trimRegions, + { + ...timelineClipboard.region, + id, + startMs: targetStart, + endMs: targetEnd, + }, + ], + })); + setSelectedTrimId(id); + setSelectedZoomId(null); + setSelectedAnnotationId(null); + setSelectedBlurId(null); + setSelectedSpeedId(null); + return; + } + + const hasConflict = speedRegions.some((region) => + spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), + ); + if (hasConflict) { + toast.error(tt("errors.cannotPlaceSpeed"), { + description: tt("errors.speedExistsAtLocation"), + }); + return; + } + + const id = `speed-${nextSpeedIdRef.current++}`; + pushState((prev) => ({ + speedRegions: [ + ...prev.speedRegions, + { + ...timelineClipboard.region, + id, + startMs: targetStart, + endMs: targetEnd, + }, + ], + })); + setSelectedSpeedId(id); + setSelectedZoomId(null); + setSelectedTrimId(null); + setSelectedAnnotationId(null); + setSelectedBlurId(null); + }, [ + timelineClipboard, + duration, + currentTime, + pushState, + zoomRegions, + trimRegions, + speedRegions, + tt, + ]); + const handleAnnotationDelete = useCallback( (id: string) => { pushState((prev) => ({ @@ -1921,6 +2153,16 @@ export default function VideoEditor() { onBlurDelete={handleAnnotationDelete} selectedBlurId={selectedBlurId} onSelectBlur={handleSelectBlur} + canCopySelectedItem={ + !!selectedZoomId || + !!selectedTrimId || + !!selectedAnnotationId || + !!selectedBlurId || + !!selectedSpeedId + } + canPasteTimelineItem={!!timelineClipboard} + onCopySelectedItem={handleCopySelectedTimelineItem} + onPasteTimelineItem={handlePasteTimelineItem} aspectRatio={aspectRatio} onAspectRatioChange={(ar) => pushState({ diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 81e621823..b4e837b51 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -87,6 +87,10 @@ interface TimelineEditorProps { onSpeedDelete?: (id: string) => void; selectedSpeedId?: string | null; onSelectSpeed?: (id: string | null) => void; + canCopySelectedItem?: boolean; + canPasteTimelineItem?: boolean; + onCopySelectedItem?: () => void; + onPasteTimelineItem?: () => void; aspectRatio: AspectRatio; onAspectRatioChange: (aspectRatio: AspectRatio) => void; } @@ -806,6 +810,10 @@ export default function TimelineEditor({ onSpeedDelete, selectedSpeedId, onSelectSpeed, + canCopySelectedItem = false, + canPasteTimelineItem = false, + onCopySelectedItem, + onPasteTimelineItem, aspectRatio, onAspectRatioChange, }: TimelineEditorProps) { @@ -1234,7 +1242,42 @@ export default function TimelineEditor({ useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + const target = e.target; + const isEditableTarget = + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target instanceof HTMLSelectElement || + (target instanceof HTMLElement && target.isContentEditable); + if (isEditableTarget) { + return; + } + + const mod = isMac ? e.metaKey : e.ctrlKey; + const key = e.key.toLowerCase(); + + if ( + mod && + !e.shiftKey && + !e.altKey && + key === "c" && + canCopySelectedItem && + onCopySelectedItem + ) { + e.preventDefault(); + onCopySelectedItem(); + return; + } + + if ( + mod && + !e.shiftKey && + !e.altKey && + key === "v" && + canPasteTimelineItem && + onPasteTimelineItem + ) { + e.preventDefault(); + onPasteTimelineItem(); return; } @@ -1326,6 +1369,10 @@ export default function TimelineEditor({ onSelectAnnotation, keyShortcuts, isMac, + canCopySelectedItem, + canPasteTimelineItem, + onCopySelectedItem, + onPasteTimelineItem, ]); const clampedRange = useMemo(() => { From 80a3b1e4d3988627171ab0ba578f0bd35eb35821 Mon Sep 17 00:00:00 2001 From: LorenzoLancia Date: Thu, 23 Apr 2026 22:15:45 +0200 Subject: [PATCH 2/4] fix: clone position from original --- src/components/video-editor/VideoEditor.tsx | 203 ++++++++---------- .../timelineClipboardUtils.test.ts | 85 ++++++++ .../video-editor/timelineClipboardUtils.ts | 38 ++++ 3 files changed, 218 insertions(+), 108 deletions(-) create mode 100644 src/components/video-editor/timelineClipboardUtils.test.ts create mode 100644 src/components/video-editor/timelineClipboardUtils.ts diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 8e5020557..8b4658e80 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -52,6 +52,11 @@ import { } from "./projectPersistence"; import { SettingsPanel } from "./SettingsPanel"; import TimelineEditor from "./timeline/TimelineEditor"; +import { + cloneAnnotationRegion, + getPastedAnnotationPosition, + spansOverlap, +} from "./timelineClipboardUtils"; import { type AnnotationRegion, type BlurData, @@ -83,26 +88,6 @@ type TimelineClipboardItem = | { kind: "annotation"; region: AnnotationRegion } | { kind: "blur"; region: AnnotationRegion }; -function cloneAnnotationRegion(region: AnnotationRegion): AnnotationRegion { - return { - ...region, - position: { ...region.position }, - size: { ...region.size }, - style: { ...region.style }, - figureData: region.figureData ? { ...region.figureData } : undefined, - blurData: region.blurData - ? { - ...region.blurData, - freehandPoints: region.blurData.freehandPoints?.map((point) => ({ ...point })), - } - : undefined, - }; -} - -function spansOverlap(startA: number, endA: number, startB: number, endB: number): boolean { - return startA < endB && endA > startB; -} - export default function VideoEditor() { const { state: editorState, @@ -1105,12 +1090,44 @@ export default function VideoEditor() { timelineClipboard.region.endMs - timelineClipboard.region.startMs, ); const pastedDuration = Math.min(sourceDuration, totalMs); - const targetStart = Math.min( - Math.max(0, Math.round(currentTime * 1000)), - Math.max(0, totalMs - pastedDuration), - ); + const currentTimeMs = Math.max(0, Math.round(currentTime * 1000)); + const targetStart = Math.min(currentTimeMs, Math.max(0, totalMs - pastedDuration)); const targetEnd = Math.min(totalMs, targetStart + pastedDuration); + function clearTimelineSelection() { + setSelectedZoomId(null); + setSelectedTrimId(null); + setSelectedAnnotationId(null); + setSelectedBlurId(null); + setSelectedSpeedId(null); + } + + function pasteSpanRegion(config: { + existingRegions: T[]; + createId: () => string; + createRegion: (id: string) => T; + pushRegion: (region: T) => void; + selectRegion: (id: string) => void; + errorTitle: string; + errorDescription: string; + }) { + const hasConflict = config.existingRegions.some((region) => + spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), + ); + if (hasConflict) { + toast.error(config.errorTitle, { + description: config.errorDescription, + }); + return true; + } + + const id = config.createId(); + config.pushRegion(config.createRegion(id)); + clearTimelineSelection(); + config.selectRegion(id); + return true; + } + if (timelineClipboard.kind === "annotation" || timelineClipboard.kind === "blur") { const id = `annotation-${nextAnnotationIdRef.current++}`; const zIndex = nextAnnotationZIndexRef.current++; @@ -1121,10 +1138,7 @@ export default function VideoEditor() { startMs: targetStart, endMs: targetEnd, zIndex, - position: { - x: Math.min(96, source.position.x + 4), - y: Math.min(96, source.position.y + 4), - }, + position: getPastedAnnotationPosition(source.position, source.size), }; pushState((prev) => ({ @@ -1145,95 +1159,68 @@ export default function VideoEditor() { } if (timelineClipboard.kind === "zoom") { - const hasConflict = zoomRegions.some((region) => - spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), - ); - if (hasConflict) { - toast.error(tt("errors.cannotPlaceZoom"), { - description: tt("errors.zoomExistsAtLocation"), - }); - return; - } - - const id = `zoom-${nextZoomIdRef.current++}`; - pushState((prev) => ({ - zoomRegions: [ - ...prev.zoomRegions, - { - ...timelineClipboard.region, - id, - startMs: targetStart, - endMs: targetEnd, - focus: { ...timelineClipboard.region.focus }, - }, - ], - })); - setSelectedZoomId(id); - setSelectedTrimId(null); - setSelectedAnnotationId(null); - setSelectedBlurId(null); - setSelectedSpeedId(null); - return; - } - - if (timelineClipboard.kind === "trim") { - const hasConflict = trimRegions.some((region) => - spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), - ); - if (hasConflict) { - toast.error(tt("errors.cannotPlaceTrim"), { - description: tt("errors.trimExistsAtLocation"), - }); - return; - } - - const id = `trim-${nextTrimIdRef.current++}`; - pushState((prev) => ({ - trimRegions: [ - ...prev.trimRegions, - { - ...timelineClipboard.region, - id, - startMs: targetStart, - endMs: targetEnd, - }, - ], - })); - setSelectedTrimId(id); - setSelectedZoomId(null); - setSelectedAnnotationId(null); - setSelectedBlurId(null); - setSelectedSpeedId(null); - return; - } - - const hasConflict = speedRegions.some((region) => - spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), - ); - if (hasConflict) { - toast.error(tt("errors.cannotPlaceSpeed"), { - description: tt("errors.speedExistsAtLocation"), + pasteSpanRegion({ + existingRegions: zoomRegions, + createId: () => `zoom-${nextZoomIdRef.current++}`, + createRegion: (id) => ({ + ...timelineClipboard.region, + id, + startMs: targetStart, + endMs: targetEnd, + focus: { ...timelineClipboard.region.focus }, + }), + pushRegion: (region) => { + pushState((prev) => ({ + zoomRegions: [...prev.zoomRegions, region], + })); + }, + selectRegion: setSelectedZoomId, + errorTitle: tt("errors.cannotPlaceZoom"), + errorDescription: tt("errors.zoomExistsAtLocation"), }); return; } - const id = `speed-${nextSpeedIdRef.current++}`; - pushState((prev) => ({ - speedRegions: [ - ...prev.speedRegions, - { + if (timelineClipboard.kind === "trim") { + pasteSpanRegion({ + existingRegions: trimRegions, + createId: () => `trim-${nextTrimIdRef.current++}`, + createRegion: (id) => ({ ...timelineClipboard.region, id, startMs: targetStart, endMs: targetEnd, + }), + pushRegion: (region) => { + pushState((prev) => ({ + trimRegions: [...prev.trimRegions, region], + })); }, - ], - })); - setSelectedSpeedId(id); - setSelectedZoomId(null); - setSelectedTrimId(null); - setSelectedAnnotationId(null); - setSelectedBlurId(null); + selectRegion: setSelectedTrimId, + errorTitle: tt("errors.cannotPlaceTrim"), + errorDescription: tt("errors.trimExistsAtLocation"), + }); + return; + } + + pasteSpanRegion({ + existingRegions: speedRegions, + createId: () => `speed-${nextSpeedIdRef.current++}`, + createRegion: (id) => ({ + ...timelineClipboard.region, + id, + startMs: targetStart, + endMs: targetEnd, + }), + pushRegion: (region) => { + pushState((prev) => ({ + speedRegions: [...prev.speedRegions, region], + })); + }, + selectRegion: setSelectedSpeedId, + errorTitle: tt("errors.cannotPlaceSpeed"), + errorDescription: tt("errors.speedExistsAtLocation"), + }); }, [ timelineClipboard, duration, diff --git a/src/components/video-editor/timelineClipboardUtils.test.ts b/src/components/video-editor/timelineClipboardUtils.test.ts new file mode 100644 index 000000000..6660c4b92 --- /dev/null +++ b/src/components/video-editor/timelineClipboardUtils.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { + cloneAnnotationRegion, + getPastedAnnotationPosition, + spansOverlap, +} from "./timelineClipboardUtils"; +import type { AnnotationRegion } from "./types"; + +function createAnnotationRegion(overrides: Partial = {}): AnnotationRegion { + return { + id: "annotation-1", + startMs: 100, + endMs: 600, + type: "blur", + content: "", + position: { x: 10, y: 15 }, + size: { width: 30, height: 20 }, + style: { + color: "#fff", + backgroundColor: "transparent", + fontSize: 32, + fontFamily: "Inter", + fontWeight: "bold", + fontStyle: "normal", + textDecoration: "none", + textAlign: "center", + }, + zIndex: 1, + blurData: { + type: "mosaic", + shape: "freehand", + color: "white", + intensity: 12, + blockSize: 8, + freehandPoints: [ + { x: 10, y: 20 }, + { x: 30, y: 40 }, + ], + }, + ...overrides, + }; +} + +describe("timelineClipboardUtils", () => { + it("deep clones nested annotation data", () => { + const original = createAnnotationRegion(); + const cloned = cloneAnnotationRegion(original); + + expect(cloned).toEqual(original); + expect(cloned).not.toBe(original); + expect(cloned.position).not.toBe(original.position); + expect(cloned.size).not.toBe(original.size); + expect(cloned.style).not.toBe(original.style); + expect(cloned.blurData).not.toBe(original.blurData); + expect(cloned.blurData?.freehandPoints).not.toBe(original.blurData?.freehandPoints); + expect(cloned.blurData?.freehandPoints?.[0]).not.toBe(original.blurData?.freehandPoints?.[0]); + }); + + it("detects true overlaps but not adjacent spans", () => { + expect(spansOverlap(100, 200, 150, 250)).toBe(true); + expect(spansOverlap(100, 200, 200, 300)).toBe(false); + expect(spansOverlap(100, 200, 0, 100)).toBe(false); + }); + + it("preserves pasted annotation positions when they are already in bounds", () => { + expect(getPastedAnnotationPosition({ x: 10, y: 15 }, { width: 30, height: 20 })).toEqual({ + x: 10, + y: 15, + }); + }); + + it("clamps pasted annotation positions when the source would overflow its bounds", () => { + expect(getPastedAnnotationPosition({ x: 94, y: 93 }, { width: 12, height: 9 })).toEqual({ + x: 88, + y: 91, + }); + }); + + it("pins oversized pasted annotations to the visible origin", () => { + expect(getPastedAnnotationPosition({ x: 50, y: 50 }, { width: 140, height: 120 })).toEqual({ + x: 0, + y: 0, + }); + }); +}); diff --git a/src/components/video-editor/timelineClipboardUtils.ts b/src/components/video-editor/timelineClipboardUtils.ts new file mode 100644 index 000000000..37e5a7a5c --- /dev/null +++ b/src/components/video-editor/timelineClipboardUtils.ts @@ -0,0 +1,38 @@ +import type { AnnotationPosition, AnnotationRegion, AnnotationSize } from "./types"; + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +export function cloneAnnotationRegion(region: AnnotationRegion): AnnotationRegion { + return { + ...region, + position: { ...region.position }, + size: { ...region.size }, + style: { ...region.style }, + figureData: region.figureData ? { ...region.figureData } : undefined, + blurData: region.blurData + ? { + ...region.blurData, + freehandPoints: region.blurData.freehandPoints?.map((point) => ({ ...point })), + } + : undefined, + }; +} + +export function spansOverlap(startA: number, endA: number, startB: number, endB: number): boolean { + return startA < endB && endA > startB; +} + +export function getPastedAnnotationPosition( + position: AnnotationPosition, + size: AnnotationSize, +): AnnotationPosition { + const maxX = Math.max(0, 100 - size.width); + const maxY = Math.max(0, 100 - size.height); + + return { + x: clamp(position.x, 0, maxX), + y: clamp(position.y, 0, maxY), + }; +} From dca91b4f8616b44c8f903d8d22505ba17e6ac7bb Mon Sep 17 00:00:00 2001 From: Lorenzo Lancia Date: Fri, 24 Apr 2026 17:39:33 +0200 Subject: [PATCH 3/4] fix: copy/paste notification --- package-lock.json | 1282 +------------------ src/components/video-editor/VideoEditor.tsx | 11 + src/hooks/useClipboardFeedback.ts | 30 + src/i18n/locales/en/timeline.json | 4 + src/i18n/locales/es/timeline.json | 38 +- src/i18n/locales/fr/timeline.json | 32 +- src/i18n/locales/ko-KR/timeline.json | 76 +- src/i18n/locales/tr/timeline.json | 82 +- src/i18n/locales/zh-CN/timeline.json | 82 +- src/i18n/locales/zh-TW/timeline.json | 82 +- 10 files changed, 270 insertions(+), 1449 deletions(-) create mode 100644 src/hooks/useClipboardFeedback.ts diff --git a/package-lock.json b/package-lock.json index ba40beb4d..998f8f3cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4993,1125 +4993,7 @@ } } }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", - "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", - "dev": true, - "dependencies": { - "@vitest/spy": "4.0.16", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/browser-playwright/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@vitest/browser-playwright/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@vitest/browser-playwright/node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/browser/node_modules/@vitest/mocker": { + "node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": { "version": "4.0.16", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", @@ -6137,85 +5019,32 @@ } } }, - "node_modules/@vitest/browser/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/@vitest/browser/node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/@vitest/browser/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12.0.0" + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { - "picomatch": { + "msw": { + "optional": true + }, + "vite": { "optional": true } } }, - "node_modules/@vitest/browser/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@vitest/browser/node_modules/pixelmatch": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", @@ -6237,83 +5066,6 @@ "node": ">=14.19.0" } }, - "node_modules/@vitest/browser/node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/@vitest/expect": { "version": "4.0.16", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 8b4658e80..74cf6c56b 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/dialog"; import { useI18n, useScopedT } from "@/contexts/I18nContext"; import { useShortcuts } from "@/contexts/ShortcutsContext"; +import { useClipboardFeedback } from "@/hooks/useClipboardFeedback"; import { INITIAL_EDITOR_STATE, useEditorHistory } from "@/hooks/useEditorHistory"; import { type Locale } from "@/i18n/config"; import { getAvailableLocales, getLocaleName } from "@/i18n/loader"; @@ -166,6 +167,7 @@ export default function VideoEditor() { const nextSpeedIdRef = useRef(1); const { shortcuts, isMac } = useShortcuts(); + const { notifyCopied, notifyPasted } = useClipboardFeedback(); const t = useScopedT("editor"); const ts = useScopedT("settings"); const tt = useScopedT("timeline"); @@ -1032,6 +1034,7 @@ export default function VideoEditor() { const region = annotationOnlyRegions.find((item) => item.id === selectedAnnotationId); if (region) { setTimelineClipboard({ kind: "annotation", region: cloneAnnotationRegion(region) }); + notifyCopied(); } return; } @@ -1040,6 +1043,7 @@ export default function VideoEditor() { const region = blurRegions.find((item) => item.id === selectedBlurId); if (region) { setTimelineClipboard({ kind: "blur", region: cloneAnnotationRegion(region) }); + notifyCopied(); } return; } @@ -1048,6 +1052,7 @@ export default function VideoEditor() { const region = zoomRegions.find((item) => item.id === selectedZoomId); if (region) { setTimelineClipboard({ kind: "zoom", region: { ...region, focus: { ...region.focus } } }); + notifyCopied(); } return; } @@ -1056,6 +1061,7 @@ export default function VideoEditor() { const region = trimRegions.find((item) => item.id === selectedTrimId); if (region) { setTimelineClipboard({ kind: "trim", region: { ...region } }); + notifyCopied(); } return; } @@ -1064,6 +1070,7 @@ export default function VideoEditor() { const region = speedRegions.find((item) => item.id === selectedSpeedId); if (region) { setTimelineClipboard({ kind: "speed", region: { ...region } }); + notifyCopied(); } } }, [ @@ -1077,6 +1084,7 @@ export default function VideoEditor() { zoomRegions, trimRegions, speedRegions, + notifyCopied, ]); const handlePasteTimelineItem = useCallback(() => { @@ -1125,6 +1133,7 @@ export default function VideoEditor() { config.pushRegion(config.createRegion(id)); clearTimelineSelection(); config.selectRegion(id); + notifyPasted(); return true; } @@ -1155,6 +1164,7 @@ export default function VideoEditor() { setSelectedZoomId(null); setSelectedTrimId(null); setSelectedSpeedId(null); + notifyPasted(); return; } @@ -1230,6 +1240,7 @@ export default function VideoEditor() { trimRegions, speedRegions, tt, + notifyPasted, ]); const handleAnnotationDelete = useCallback( diff --git a/src/hooks/useClipboardFeedback.ts b/src/hooks/useClipboardFeedback.ts new file mode 100644 index 000000000..9fcde509a --- /dev/null +++ b/src/hooks/useClipboardFeedback.ts @@ -0,0 +1,30 @@ +import { useCallback } from "react"; +import { toast } from "sonner"; +import { useScopedT } from "@/contexts/I18nContext"; + +export type ClipboardAction = "copy" | "paste"; + +export function useClipboardFeedback() { + const t = useScopedT("timeline"); + + const notifyClipboardAction = useCallback( + (action: ClipboardAction) => { + toast.success(action === "copy" ? t("feedback.copied") : t("feedback.pasted")); + }, + [t], + ); + + const notifyCopied = useCallback(() => { + notifyClipboardAction("copy"); + }, [notifyClipboardAction]); + + const notifyPasted = useCallback(() => { + notifyClipboardAction("paste"); + }, [notifyClipboardAction]); + + return { + notifyClipboardAction, + notifyCopied, + notifyPasted, + }; +} diff --git a/src/i18n/locales/en/timeline.json b/src/i18n/locales/en/timeline.json index b4d5bd8fb..4320869fd 100644 --- a/src/i18n/locales/en/timeline.json +++ b/src/i18n/locales/en/timeline.json @@ -49,5 +49,9 @@ "success": { "addedZoomSuggestions": "Added {{count}} cursor-based zoom suggestion", "addedZoomSuggestionsPlural": "Added {{count}} cursor-based zoom suggestions" + }, + "feedback": { + "copied": "Copied!", + "pasted": "Pasted!" } } diff --git a/src/i18n/locales/es/timeline.json b/src/i18n/locales/es/timeline.json index 12a83b047..cbc4408d5 100644 --- a/src/i18n/locales/es/timeline.json +++ b/src/i18n/locales/es/timeline.json @@ -3,16 +3,16 @@ "addZoom": "Agregar zoom (Z)", "suggestZooms": "Sugerir zooms desde el cursor", "addTrim": "Agregar recorte (T)", - "addAnnotation": "Agregar anotación (A)", + "addAnnotation": "Agregar anotación (A)", "addSpeed": "Agregar velocidad (S)", "addBlur": "Agregar desenfoque (B)" }, "hints": { "pressZoom": "Presiona Z para agregar zoom", "pressTrim": "Presiona T para agregar recorte", - "pressAnnotation": "Presiona A para agregar anotación", + "pressAnnotation": "Presiona A para agregar anotación", "pressSpeed": "Presiona S para agregar velocidad", - "pressBlur": "Presiona B para agregar una región de desenfoque" + "pressBlur": "Presiona B para agregar una región de desenfoque" }, "labels": { "pan": "Desplazar", @@ -20,9 +20,9 @@ "zoomItem": "Zoom {{index}}", "trimItem": "Recorte {{index}}", "speedItem": "Velocidad {{index}}", - "annotationItem": "Anotación", + "annotationItem": "Anotación", "imageItem": "Imagen", - "emptyText": "Texto vacío", + "emptyText": "Texto vacío", "blurItem": "Desenfoque {{index}}" }, "emptyState": { @@ -30,24 +30,28 @@ "dragAndDrop": "Arrastra y suelta un video para comenzar a editar" }, "errors": { - "cannotPlaceZoom": "No se puede colocar el zoom aquí", - "zoomExistsAtLocation": "Ya existe un zoom en esta ubicación o no hay suficiente espacio disponible.", - "zoomSuggestionUnavailable": "El controlador de sugerencias de zoom no está disponible", - "noCursorTelemetry": "No hay telemetría de cursor disponible", + "cannotPlaceZoom": "No se puede colocar el zoom aquí", + "zoomExistsAtLocation": "Ya existe un zoom en esta ubicación o no hay suficiente espacio disponible.", + "zoomSuggestionUnavailable": "El controlador de sugerencias de zoom no está disponible", + "noCursorTelemetry": "No hay telemetría de cursor disponible", "noCursorTelemetryDescription": "Graba una captura de pantalla primero para generar sugerencias basadas en el cursor.", - "noUsableTelemetry": "No hay telemetría de cursor utilizable", - "noUsableTelemetryDescription": "La grabación no incluye suficientes datos de movimiento del cursor.", + "noUsableTelemetry": "No hay telemetría de cursor utilizable", + "noUsableTelemetryDescription": "La grabación no incluye suficientes datos de movimiento del cursor.", "noDwellMoments": "No se encontraron momentos claros de pausa del cursor", - "noDwellMomentsDescription": "Intenta una grabación con pausas más lentas del cursor en acciones importantes.", + "noDwellMomentsDescription": "Intenta una grabación con pausas más lentas del cursor en acciones importantes.", "noAutoZoomSlots": "No hay espacios de auto-zoom disponibles", "noAutoZoomSlotsDescription": "Los puntos de pausa detectados se superponen con regiones de zoom existentes.", - "cannotPlaceTrim": "No se puede colocar el recorte aquí", - "trimExistsAtLocation": "Ya existe un recorte en esta ubicación o no hay suficiente espacio disponible.", - "cannotPlaceSpeed": "No se puede colocar la velocidad aquí", - "speedExistsAtLocation": "Ya existe una región de velocidad en esta ubicación o no hay suficiente espacio disponible." + "cannotPlaceTrim": "No se puede colocar el recorte aquí", + "trimExistsAtLocation": "Ya existe un recorte en esta ubicación o no hay suficiente espacio disponible.", + "cannotPlaceSpeed": "No se puede colocar la velocidad aquí", + "speedExistsAtLocation": "Ya existe una región de velocidad en esta ubicación o no hay suficiente espacio disponible." }, "success": { - "addedZoomSuggestions": "Se agregó {{count}} sugerencia de zoom basada en el cursor", + "addedZoomSuggestions": "Se agregó {{count}} sugerencia de zoom basada en el cursor", "addedZoomSuggestionsPlural": "Se agregaron {{count}} sugerencias de zoom basadas en el cursor" + }, + "feedback": { + "copied": "Copied!", + "pasted": "Pasted!" } } diff --git a/src/i18n/locales/fr/timeline.json b/src/i18n/locales/fr/timeline.json index 5985ea673..294f340f0 100644 --- a/src/i18n/locales/fr/timeline.json +++ b/src/i18n/locales/fr/timeline.json @@ -1,7 +1,7 @@ { "buttons": { "addZoom": "Ajouter un zoom (Z)", - "suggestZooms": "Suggérer des zooms depuis le curseur", + "suggestZooms": "Suggérer des zooms depuis le curseur", "addTrim": "Ajouter une coupe (T)", "addAnnotation": "Ajouter une annotation (A)", "addSpeed": "Ajouter une vitesse (S)", @@ -26,28 +26,32 @@ "blurItem": "Flou {{index}}" }, "emptyState": { - "noVideo": "Aucune vidéo chargée", - "dragAndDrop": "Glissez-déposez une vidéo pour commencer à éditer" + "noVideo": "Aucune vidéo chargée", + "dragAndDrop": "Glissez-déposez une vidéo pour commencer à éditer" }, "errors": { "cannotPlaceZoom": "Impossible de placer le zoom ici", - "zoomExistsAtLocation": "Un zoom existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant.", + "zoomExistsAtLocation": "Un zoom existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant.", "zoomSuggestionUnavailable": "Gestionnaire de suggestions de zoom non disponible", - "noCursorTelemetry": "Aucune télémétrie de curseur disponible", - "noCursorTelemetryDescription": "Enregistrez d\u0027abord un screencast pour générer des suggestions basées sur le curseur.", - "noUsableTelemetry": "Aucune télémétrie de curseur utilisable", - "noUsableTelemetryDescription": "L\u0027enregistrement ne contient pas suffisamment de données de mouvement du curseur.", - "noDwellMoments": "Aucun moment de pause du curseur trouvé", + "noCursorTelemetry": "Aucune télémétrie de curseur disponible", + "noCursorTelemetryDescription": "Enregistrez d\u0027abord un screencast pour générer des suggestions basées sur le curseur.", + "noUsableTelemetry": "Aucune télémétrie de curseur utilisable", + "noUsableTelemetryDescription": "L\u0027enregistrement ne contient pas suffisamment de données de mouvement du curseur.", + "noDwellMoments": "Aucun moment de pause du curseur trouvé", "noDwellMomentsDescription": "Essayez un enregistrement avec des pauses plus lentes du curseur sur les actions importantes.", "noAutoZoomSlots": "Aucun emplacement de zoom automatique disponible", - "noAutoZoomSlotsDescription": "Les points de pause détectés chevauchent des régions de zoom existantes.", + "noAutoZoomSlotsDescription": "Les points de pause détectés chevauchent des régions de zoom existantes.", "cannotPlaceTrim": "Impossible de placer la coupe ici", - "trimExistsAtLocation": "Une coupe existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant.", + "trimExistsAtLocation": "Une coupe existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant.", "cannotPlaceSpeed": "Impossible de placer la vitesse ici", - "speedExistsAtLocation": "Une région de vitesse existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant." + "speedExistsAtLocation": "Une région de vitesse existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant." }, "success": { - "addedZoomSuggestions": "{{count}} suggestion de zoom basée sur le curseur ajoutée", - "addedZoomSuggestionsPlural": "{{count}} suggestions de zoom basées sur le curseur ajoutées" + "addedZoomSuggestions": "{{count}} suggestion de zoom basée sur le curseur ajoutée", + "addedZoomSuggestionsPlural": "{{count}} suggestions de zoom basées sur le curseur ajoutées" + }, + "feedback": { + "copied": "Copied!", + "pasted": "Pasted!" } } diff --git a/src/i18n/locales/ko-KR/timeline.json b/src/i18n/locales/ko-KR/timeline.json index 167c26ffe..ee67a9fc7 100644 --- a/src/i18n/locales/ko-KR/timeline.json +++ b/src/i18n/locales/ko-KR/timeline.json @@ -1,50 +1,54 @@ { "buttons": { - "addZoom": "줌 추가 (Z)", - "suggestZooms": "커서 기반 줌 제안", - "addTrim": "트림 추가 (T)", - "addAnnotation": "주석 추가 (A)", - "addSpeed": "속도 추가 (S)" + "addZoom": "줌 추가 (Z)", + "suggestZooms": "커서 기반 줌 제안", + "addTrim": "트림 추가 (T)", + "addAnnotation": "주석 추가 (A)", + "addSpeed": "속도 추가 (S)" }, "hints": { - "pressZoom": "Z를 눌러 줌 추가", - "pressTrim": "T를 눌러 트림 추가", - "pressAnnotation": "A를 눌러 주석 추가", - "pressSpeed": "S를 눌러 속도 추가" + "pressZoom": "Z를 눌러 줌 추가", + "pressTrim": "T를 눌러 트림 추가", + "pressAnnotation": "A를 눌러 주석 추가", + "pressSpeed": "S를 눌러 속도 추가" }, "labels": { - "pan": "이동", - "zoom": "줌", - "zoomItem": "줌 {{index}}", - "trimItem": "트림 {{index}}", - "speedItem": "속도 {{index}}", - "annotationItem": "주석", - "imageItem": "이미지", - "emptyText": "빈 텍스트" + "pan": "이동", + "zoom": "줌", + "zoomItem": "줌 {{index}}", + "trimItem": "트림 {{index}}", + "speedItem": "속도 {{index}}", + "annotationItem": "주석", + "imageItem": "이미지", + "emptyText": "빈 텍스트" }, "emptyState": { - "noVideo": "불러온 비디오 없음", - "dragAndDrop": "비디오를 드래그 앤 드롭해서 편집을 시작하세요" + "noVideo": "불러온 비디오 없음", + "dragAndDrop": "비디오를 드래그 앤 드롭해서 편집을 시작하세요" }, "errors": { - "cannotPlaceZoom": "이 위치에 줌을 추가할 수 없습니다", - "zoomExistsAtLocation": "이 위치에 이미 줌이 있거나 공간이 부족합니다.", - "zoomSuggestionUnavailable": "줌 제안 기능을 사용할 수 없습니다", - "noCursorTelemetry": "커서 데이터가 없습니다", - "noCursorTelemetryDescription": "커서 기반 제안을 생성하려면 먼저 화면을 녹화해 주세요.", - "noUsableTelemetry": "사용 가능한 커서 데이터가 없습니다", - "noUsableTelemetryDescription": "녹화에 충분한 커서 이동 데이터가 포함되어 있지 않습니다.", - "noDwellMoments": "명확한 커서 정지 구간을 찾을 수 없습니다", - "noDwellMomentsDescription": "중요한 동작에서 커서를 천천히 멈추며 녹화해 보세요.", - "noAutoZoomSlots": "자동 줌 슬롯이 없습니다", - "noAutoZoomSlotsDescription": "감지된 정지 지점이 기존 줌 구간과 겹칩니다.", - "cannotPlaceTrim": "이 위치에 트림을 추가할 수 없습니다", - "trimExistsAtLocation": "이 위치에 이미 트림이 있거나 공간이 부족합니다.", - "cannotPlaceSpeed": "이 위치에 속도를 추가할 수 없습니다", - "speedExistsAtLocation": "이 위치에 이미 속도 구간이 있거나 공간이 부족합니다." + "cannotPlaceZoom": "이 위치에 줌을 추가할 수 없습니다", + "zoomExistsAtLocation": "이 위치에 이미 줌이 있거나 공간이 부족합니다.", + "zoomSuggestionUnavailable": "줌 제안 기능을 사용할 수 없습니다", + "noCursorTelemetry": "커서 데이터가 없습니다", + "noCursorTelemetryDescription": "커서 기반 제안을 생성하려면 먼저 화면을 녹화해 주세요.", + "noUsableTelemetry": "사용 가능한 커서 데이터가 없습니다", + "noUsableTelemetryDescription": "녹화에 충분한 커서 이동 데이터가 포함되어 있지 않습니다.", + "noDwellMoments": "명확한 커서 ì •ì§€ 구간을 찾을 수 없습니다", + "noDwellMomentsDescription": "중요한 동작에서 커서를 천천히 멈추며 녹화해 보세요.", + "noAutoZoomSlots": "자동 줌 슬롯이 없습니다", + "noAutoZoomSlotsDescription": "감지된 ì •ì§€ 지점이 기존 줌 구간과 겹칩니다.", + "cannotPlaceTrim": "이 위치에 트림을 추가할 수 없습니다", + "trimExistsAtLocation": "이 위치에 이미 트림이 있거나 공간이 부족합니다.", + "cannotPlaceSpeed": "이 위치에 속도를 추가할 수 없습니다", + "speedExistsAtLocation": "이 위치에 이미 속도 구간이 있거나 공간이 부족합니다." }, "success": { - "addedZoomSuggestions": "커서 기반 줌 제안 {{count}}개가 추가되었습니다", - "addedZoomSuggestionsPlural": "커서 기반 줌 제안 {{count}}개가 추가되었습니다" + "addedZoomSuggestions": "커서 기반 줌 제안 {{count}}개가 추가되었습니다", + "addedZoomSuggestionsPlural": "커서 기반 줌 제안 {{count}}개가 추가되었습니다" + }, + "feedback": { + "copied": "Copied!", + "pasted": "Pasted!" } } diff --git a/src/i18n/locales/tr/timeline.json b/src/i18n/locales/tr/timeline.json index 294640bff..761d82ff5 100644 --- a/src/i18n/locales/tr/timeline.json +++ b/src/i18n/locales/tr/timeline.json @@ -1,53 +1,57 @@ { "buttons": { - "addZoom": "Yakınlaştırma Ekle (Z)", - "suggestZooms": "İmleçten Yakınlaştırma Öner", - "addTrim": "Kırpma Ekle (T)", - "addAnnotation": "Açıklama Ekle (A)", - "addSpeed": "Hız Ekle (S)", - "addBlur": "Bulanık ekle (B)" + "addZoom": "YakınlaÅŸtırma Ekle (Z)", + "suggestZooms": "İmleçten YakınlaÅŸtırma Öner", + "addTrim": "Kırpma Ekle (T)", + "addAnnotation": "Açıklama Ekle (A)", + "addSpeed": "Hız Ekle (S)", + "addBlur": "Bulanık ekle (B)" }, "hints": { - "pressZoom": "Yakınlaştırma eklemek için Z tuşuna basın", - "pressTrim": "Kırpma eklemek için T tuşuna basın", - "pressAnnotation": "Açıklama eklemek için A tuşuna basın", - "pressSpeed": "Hız eklemek için S tuşuna basın", - "pressBlur": "Bulanık bölge eklemek için B tuşuna basın" + "pressZoom": "YakınlaÅŸtırma eklemek için Z tuÅŸuna basın", + "pressTrim": "Kırpma eklemek için T tuÅŸuna basın", + "pressAnnotation": "Açıklama eklemek için A tuÅŸuna basın", + "pressSpeed": "Hız eklemek için S tuÅŸuna basın", + "pressBlur": "Bulanık bölge eklemek için B tuÅŸuna basın" }, "labels": { - "pan": "Kaydır", - "zoom": "Yakınlaştır", - "zoomItem": "Yakınlaştırma {{index}}", - "trimItem": "Kırpma {{index}}", - "speedItem": "Hız {{index}}", - "annotationItem": "Açıklama", - "imageItem": "Görüntü", - "emptyText": "Boş metin", - "blurItem": "Bulanık {{index}}" + "pan": "Kaydır", + "zoom": "YakınlaÅŸtır", + "zoomItem": "YakınlaÅŸtırma {{index}}", + "trimItem": "Kırpma {{index}}", + "speedItem": "Hız {{index}}", + "annotationItem": "Açıklama", + "imageItem": "Görüntü", + "emptyText": "BoÅŸ metin", + "blurItem": "Bulanık {{index}}" }, "emptyState": { - "noVideo": "Video Yüklenmedi", - "dragAndDrop": "Düzenlemeye başlamak için bir video sürükleyip bırakın" + "noVideo": "Video Yüklenmedi", + "dragAndDrop": "Düzenlemeye baÅŸlamak için bir video sürükleyip bırakın" }, "errors": { - "cannotPlaceZoom": "Buraya yakınlaştırma yerleştirilemiyor", - "zoomExistsAtLocation": "Bu konumda zaten bir yakınlaştırma var veya yeterli alan yok.", - "zoomSuggestionUnavailable": "Yakınlaştırma öneri işleyicisi kullanılamıyor", - "noCursorTelemetry": "İmleç telemetrisi mevcut değil", - "noCursorTelemetryDescription": "İmleç tabanlı öneriler oluşturmak için önce bir ekran kaydı yapın.", - "noUsableTelemetry": "Kullanılabilir imleç telemetrisi yok", - "noUsableTelemetryDescription": "Kayıt yeterli imleç hareketi verisi içermiyor.", - "noDwellMoments": "Belirgin imleç bekleme anları bulunamadı", - "noDwellMomentsDescription": "Önemli işlemlerde daha yavaş imleç duraklamaları olan bir kayıt deneyin.", - "noAutoZoomSlots": "Otomatik yakınlaştırma alanı yok", - "noAutoZoomSlotsDescription": "Algılanan bekleme noktaları mevcut yakınlaştırma bölgeleriyle çakışıyor.", - "cannotPlaceTrim": "Buraya kırpma yerleştirilemiyor", - "trimExistsAtLocation": "Bu konumda zaten bir kırpma var veya yeterli alan yok.", - "cannotPlaceSpeed": "Buraya hız yerleştirilemiyor", - "speedExistsAtLocation": "Bu konumda zaten bir hız bölgesi var veya yeterli alan yok." + "cannotPlaceZoom": "Buraya yakınlaÅŸtırma yerleÅŸtirilemiyor", + "zoomExistsAtLocation": "Bu konumda zaten bir yakınlaÅŸtırma var veya yeterli alan yok.", + "zoomSuggestionUnavailable": "YakınlaÅŸtırma öneri iÅŸleyicisi kullanılamıyor", + "noCursorTelemetry": "İmleç telemetrisi mevcut deÄŸil", + "noCursorTelemetryDescription": "İmleç tabanlı öneriler oluÅŸturmak için önce bir ekran kaydı yapın.", + "noUsableTelemetry": "Kullanılabilir imleç telemetrisi yok", + "noUsableTelemetryDescription": "Kayıt yeterli imleç hareketi verisi içermiyor.", + "noDwellMoments": "Belirgin imleç bekleme anları bulunamadı", + "noDwellMomentsDescription": "Önemli iÅŸlemlerde daha yavaÅŸ imleç duraklamaları olan bir kayıt deneyin.", + "noAutoZoomSlots": "Otomatik yakınlaÅŸtırma alanı yok", + "noAutoZoomSlotsDescription": "Algılanan bekleme noktaları mevcut yakınlaÅŸtırma bölgeleriyle çakışıyor.", + "cannotPlaceTrim": "Buraya kırpma yerleÅŸtirilemiyor", + "trimExistsAtLocation": "Bu konumda zaten bir kırpma var veya yeterli alan yok.", + "cannotPlaceSpeed": "Buraya hız yerleÅŸtirilemiyor", + "speedExistsAtLocation": "Bu konumda zaten bir hız bölgesi var veya yeterli alan yok." }, "success": { - "addedZoomSuggestions": "{{count}} imleç tabanlı yakınlaştırma önerisi eklendi", - "addedZoomSuggestionsPlural": "{{count}} imleç tabanlı yakınlaştırma önerisi eklendi" + "addedZoomSuggestions": "{{count}} imleç tabanlı yakınlaÅŸtırma önerisi eklendi", + "addedZoomSuggestionsPlural": "{{count}} imleç tabanlı yakınlaÅŸtırma önerisi eklendi" + }, + "feedback": { + "copied": "Copied!", + "pasted": "Pasted!" } } diff --git a/src/i18n/locales/zh-CN/timeline.json b/src/i18n/locales/zh-CN/timeline.json index 7841dcb56..def3d87d2 100644 --- a/src/i18n/locales/zh-CN/timeline.json +++ b/src/i18n/locales/zh-CN/timeline.json @@ -1,53 +1,57 @@ { "buttons": { - "addZoom": "添加缩放 (Z)", - "suggestZooms": "根据光标建议缩放", - "addTrim": "添加剪辑 (T)", - "addAnnotation": "添加标注 (A)", - "addSpeed": "添加速度 (S)", - "addBlur": "添加模糊 (B)" + "addZoom": "添加缩放 (Z)", + "suggestZooms": "根据光标建议缩放", + "addTrim": "添加剪辑 (T)", + "addAnnotation": "添加标注 (A)", + "addSpeed": "添加速度 (S)", + "addBlur": "添加模糊 (B)" }, "hints": { - "pressZoom": "按 Z 添加缩放", - "pressTrim": "按 T 添加剪辑", - "pressAnnotation": "按 A 添加标注", - "pressSpeed": "按 S 添加速度", - "pressBlur": "按 B 添加模糊区域" + "pressZoom": "按 Z 添加缩放", + "pressTrim": "按 T 添加剪辑", + "pressAnnotation": "按 A 添加标注", + "pressSpeed": "按 S 添加速度", + "pressBlur": "按 B 添加模糊区域" }, "labels": { - "pan": "平移", - "zoom": "缩放", - "zoomItem": "缩放 {{index}}", - "trimItem": "剪辑 {{index}}", - "speedItem": "速度 {{index}}", - "annotationItem": "标注", - "imageItem": "图片", - "emptyText": "空文本", - "blurItem": "模糊 {{index}}" + "pan": "平移", + "zoom": "缩放", + "zoomItem": "缩放 {{index}}", + "trimItem": "剪辑 {{index}}", + "speedItem": "速度 {{index}}", + "annotationItem": "标注", + "imageItem": "图片", + "emptyText": "空文本", + "blurItem": "模糊 {{index}}" }, "emptyState": { - "noVideo": "未加载视频", - "dragAndDrop": "拖放视频以开始编辑" + "noVideo": "未加载视频", + "dragAndDrop": "拖放视频以开始编辑" }, "errors": { - "cannotPlaceZoom": "无法在此处放置缩放", - "zoomExistsAtLocation": "此位置已存在缩放或没有足够的空间。", - "zoomSuggestionUnavailable": "缩放建议处理器不可用", - "noCursorTelemetry": "无可用的光标遥测数据", - "noCursorTelemetryDescription": "请先录制一段屏幕录像以生成基于光标的建议。", - "noUsableTelemetry": "无可用的光标遥测数据", - "noUsableTelemetryDescription": "录制内容没有包含足够的光标移动数据。", - "noDwellMoments": "未找到明确的光标停留时刻", - "noDwellMomentsDescription": "请尝试在重要操作上进行较慢光标停留的录制。", - "noAutoZoomSlots": "无可用的自动缩放位置", - "noAutoZoomSlotsDescription": "检测到的停留点与现有缩放区域重叠。", - "cannotPlaceTrim": "无法在此处放置剪辑", - "trimExistsAtLocation": "此位置已存在剪辑或没有足够的空间。", - "cannotPlaceSpeed": "无法在此处放置速度", - "speedExistsAtLocation": "此位置已存在速度区域或没有足够的空间。" + "cannotPlaceZoom": "无法在此处放置缩放", + "zoomExistsAtLocation": "此位置已存在缩放或没有足够的空间。", + "zoomSuggestionUnavailable": "缩放建议处理器不可用", + "noCursorTelemetry": "无可用的光标遥测数据", + "noCursorTelemetryDescription": "请先录制一段屏幕录像以生成基于光标的建议。", + "noUsableTelemetry": "无可用的光标遥测数据", + "noUsableTelemetryDescription": "录制内容没有包含足够的光标移动数据。", + "noDwellMoments": "未找到明确的光标停留时刻", + "noDwellMomentsDescription": "请尝试在重要操作上进行较慢光标停留的录制。", + "noAutoZoomSlots": "无可用的自动缩放位置", + "noAutoZoomSlotsDescription": "检测到的停留点与现有缩放区域重叠。", + "cannotPlaceTrim": "无法在此处放置剪辑", + "trimExistsAtLocation": "此位置已存在剪辑或没有足够的空间。", + "cannotPlaceSpeed": "无法在此处放置速度", + "speedExistsAtLocation": "此位置已存在速度区域或没有足够的空间。" }, "success": { - "addedZoomSuggestions": "已添加 {{count}} 个基于光标的缩放建议", - "addedZoomSuggestionsPlural": "已添加 {{count}} 个基于光标的缩放建议" + "addedZoomSuggestions": "已添加 {{count}} 个基于光标的缩放建议", + "addedZoomSuggestionsPlural": "已添加 {{count}} 个基于光标的缩放建议" + }, + "feedback": { + "copied": "Copied!", + "pasted": "Pasted!" } } diff --git a/src/i18n/locales/zh-TW/timeline.json b/src/i18n/locales/zh-TW/timeline.json index 52457d61f..783f6548b 100644 --- a/src/i18n/locales/zh-TW/timeline.json +++ b/src/i18n/locales/zh-TW/timeline.json @@ -1,53 +1,57 @@ { "buttons": { - "addZoom": "新增縮放 (Z)", - "suggestZooms": "根據游標建議縮放", - "addTrim": "新增剪輯 (T)", - "addAnnotation": "新增標註 (A)", - "addSpeed": "新增速度 (S)", - "addBlur": "新增模糊 (B)" + "addZoom": "新增縮放 (Z)", + "suggestZooms": "根據游標建議縮放", + "addTrim": "新增剪輯 (T)", + "addAnnotation": "新增標註 (A)", + "addSpeed": "新增速度 (S)", + "addBlur": "新增模糊 (B)" }, "hints": { - "pressZoom": "按 Z 新增縮放", - "pressTrim": "按 T 新增剪輯", - "pressAnnotation": "按 A 新增標註", - "pressSpeed": "按 S 新增速度", - "pressBlur": "按 B 新增模糊區域" + "pressZoom": "按 Z 新增縮放", + "pressTrim": "按 T 新增剪輯", + "pressAnnotation": "按 A 新增標註", + "pressSpeed": "按 S 新增速度", + "pressBlur": "按 B 新增模糊區域" }, "labels": { - "pan": "平移", - "zoom": "縮放", - "zoomItem": "縮放 {{index}}", - "trimItem": "剪輯 {{index}}", - "speedItem": "速度 {{index}}", - "annotationItem": "標註", - "imageItem": "圖片", - "emptyText": "空文字", - "blurItem": "模糊 {{index}}" + "pan": "平移", + "zoom": "縮放", + "zoomItem": "縮放 {{index}}", + "trimItem": "剪輯 {{index}}", + "speedItem": "速度 {{index}}", + "annotationItem": "標註", + "imageItem": "圖片", + "emptyText": "空文字", + "blurItem": "模糊 {{index}}" }, "emptyState": { - "noVideo": "未載入影片", - "dragAndDrop": "拖放影片以開始編輯" + "noVideo": "未載入影片", + "dragAndDrop": "拖放影片以開始編輯" }, "errors": { - "cannotPlaceZoom": "無法在此處放置縮放", - "zoomExistsAtLocation": "此位置已存在縮放或沒有足夠的空間。", - "zoomSuggestionUnavailable": "縮放建議處理器不可用", - "noCursorTelemetry": "無可用的游標遙測資料", - "noCursorTelemetryDescription": "請先錄製一段螢幕錄影以產生基於游標的建議。", - "noUsableTelemetry": "無可用的游標遙測資料", - "noUsableTelemetryDescription": "錄製內容沒有包含足夠的游標移動資料。", - "noDwellMoments": "未找到明確的游標停留時刻", - "noDwellMomentsDescription": "請嘗試在重要操作上進行較慢游標停留的錄製。", - "noAutoZoomSlots": "無可用的自動縮放位置", - "noAutoZoomSlotsDescription": "偵測到的停留點與現有縮放區域重疊。", - "cannotPlaceTrim": "無法在此處放置剪輯", - "trimExistsAtLocation": "此位置已存在剪輯或沒有足夠的空間。", - "cannotPlaceSpeed": "無法在此處放置速度", - "speedExistsAtLocation": "此位置已存在速度區域或沒有足夠的空間。" + "cannotPlaceZoom": "無法在此處放置縮放", + "zoomExistsAtLocation": "此位置已存在縮放或沒有足夠的空間。", + "zoomSuggestionUnavailable": "縮放建議處理器不可用", + "noCursorTelemetry": "無可用的游標遙測資料", + "noCursorTelemetryDescription": "請先錄製一段螢幕錄影以產生基於游標的建議。", + "noUsableTelemetry": "無可用的游標遙測資料", + "noUsableTelemetryDescription": "錄製內容沒有包含足夠的游標移動資料。", + "noDwellMoments": "未找到明確的游標停留時刻", + "noDwellMomentsDescription": "請嘗試在重要操作上進行較慢游標停留的錄製。", + "noAutoZoomSlots": "無可用的自動縮放位置", + "noAutoZoomSlotsDescription": "偵測到的停留點與現有縮放區域重疊。", + "cannotPlaceTrim": "無法在此處放置剪輯", + "trimExistsAtLocation": "此位置已存在剪輯或沒有足夠的空間。", + "cannotPlaceSpeed": "無法在此處放置速度", + "speedExistsAtLocation": "此位置已存在速度區域或沒有足夠的空間。" }, "success": { - "addedZoomSuggestions": "已新增 {{count}} 個基於游標的縮放建議", - "addedZoomSuggestionsPlural": "已新增 {{count}} 個基於游標的縮放建議" + "addedZoomSuggestions": "已新增 {{count}} 個基於游標的縮放建議", + "addedZoomSuggestionsPlural": "已新增 {{count}} 個基於游標的縮放建議" + }, + "feedback": { + "copied": "Copied!", + "pasted": "Pasted!" } } From 42468c13b6aef1ea93f2bd400223c08f64eaf711 Mon Sep 17 00:00:00 2001 From: Lorenzo Lancia Date: Fri, 24 Apr 2026 18:09:42 +0200 Subject: [PATCH 4/4] fix --- src/components/video-editor/VideoEditor.tsx | 7 +- src/i18n/locales/es/timeline.json | 38 +++++----- src/i18n/locales/fr/timeline.json | 32 ++++---- src/i18n/locales/ko-KR/timeline.json | 76 +++++++++---------- src/i18n/locales/tr/timeline.json | 82 ++++++++++----------- src/i18n/locales/zh-CN/timeline.json | 82 ++++++++++----------- src/i18n/locales/zh-TW/timeline.json | 82 ++++++++++----------- 7 files changed, 199 insertions(+), 200 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 74cf6c56b..9bcfe0b01 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -1118,7 +1118,7 @@ export default function VideoEditor() { selectRegion: (id: string) => void; errorTitle: string; errorDescription: string; - }) { + }): void { const hasConflict = config.existingRegions.some((region) => spansOverlap(region.startMs, region.endMs, targetStart, targetEnd), ); @@ -1126,7 +1126,7 @@ export default function VideoEditor() { toast.error(config.errorTitle, { description: config.errorDescription, }); - return true; + return; } const id = config.createId(); @@ -1134,7 +1134,6 @@ export default function VideoEditor() { clearTimelineSelection(); config.selectRegion(id); notifyPasted(); - return true; } if (timelineClipboard.kind === "annotation" || timelineClipboard.kind === "blur") { @@ -2158,7 +2157,7 @@ export default function VideoEditor() { !!selectedBlurId || !!selectedSpeedId } - canPasteTimelineItem={!!timelineClipboard} + canPasteTimelineItem={!!timelineClipboard && duration > 0} onCopySelectedItem={handleCopySelectedTimelineItem} onPasteTimelineItem={handlePasteTimelineItem} aspectRatio={aspectRatio} diff --git a/src/i18n/locales/es/timeline.json b/src/i18n/locales/es/timeline.json index cbc4408d5..52a59c6a8 100644 --- a/src/i18n/locales/es/timeline.json +++ b/src/i18n/locales/es/timeline.json @@ -3,16 +3,16 @@ "addZoom": "Agregar zoom (Z)", "suggestZooms": "Sugerir zooms desde el cursor", "addTrim": "Agregar recorte (T)", - "addAnnotation": "Agregar anotación (A)", + "addAnnotation": "Agregar anotación (A)", "addSpeed": "Agregar velocidad (S)", "addBlur": "Agregar desenfoque (B)" }, "hints": { "pressZoom": "Presiona Z para agregar zoom", "pressTrim": "Presiona T para agregar recorte", - "pressAnnotation": "Presiona A para agregar anotación", + "pressAnnotation": "Presiona A para agregar anotación", "pressSpeed": "Presiona S para agregar velocidad", - "pressBlur": "Presiona B para agregar una región de desenfoque" + "pressBlur": "Presiona B para agregar una región de desenfoque" }, "labels": { "pan": "Desplazar", @@ -20,9 +20,9 @@ "zoomItem": "Zoom {{index}}", "trimItem": "Recorte {{index}}", "speedItem": "Velocidad {{index}}", - "annotationItem": "Anotación", + "annotationItem": "Anotación", "imageItem": "Imagen", - "emptyText": "Texto vacío", + "emptyText": "Texto vacío", "blurItem": "Desenfoque {{index}}" }, "emptyState": { @@ -30,28 +30,28 @@ "dragAndDrop": "Arrastra y suelta un video para comenzar a editar" }, "errors": { - "cannotPlaceZoom": "No se puede colocar el zoom aquí", - "zoomExistsAtLocation": "Ya existe un zoom en esta ubicación o no hay suficiente espacio disponible.", - "zoomSuggestionUnavailable": "El controlador de sugerencias de zoom no está disponible", - "noCursorTelemetry": "No hay telemetría de cursor disponible", + "cannotPlaceZoom": "No se puede colocar el zoom aquí", + "zoomExistsAtLocation": "Ya existe un zoom en esta ubicación o no hay suficiente espacio disponible.", + "zoomSuggestionUnavailable": "El controlador de sugerencias de zoom no está disponible", + "noCursorTelemetry": "No hay telemetría de cursor disponible", "noCursorTelemetryDescription": "Graba una captura de pantalla primero para generar sugerencias basadas en el cursor.", - "noUsableTelemetry": "No hay telemetría de cursor utilizable", - "noUsableTelemetryDescription": "La grabación no incluye suficientes datos de movimiento del cursor.", + "noUsableTelemetry": "No hay telemetría de cursor utilizable", + "noUsableTelemetryDescription": "La grabación no incluye suficientes datos de movimiento del cursor.", "noDwellMoments": "No se encontraron momentos claros de pausa del cursor", - "noDwellMomentsDescription": "Intenta una grabación con pausas más lentas del cursor en acciones importantes.", + "noDwellMomentsDescription": "Intenta una grabación con pausas más lentas del cursor en acciones importantes.", "noAutoZoomSlots": "No hay espacios de auto-zoom disponibles", "noAutoZoomSlotsDescription": "Los puntos de pausa detectados se superponen con regiones de zoom existentes.", - "cannotPlaceTrim": "No se puede colocar el recorte aquí", - "trimExistsAtLocation": "Ya existe un recorte en esta ubicación o no hay suficiente espacio disponible.", - "cannotPlaceSpeed": "No se puede colocar la velocidad aquí", - "speedExistsAtLocation": "Ya existe una región de velocidad en esta ubicación o no hay suficiente espacio disponible." + "cannotPlaceTrim": "No se puede colocar el recorte aquí", + "trimExistsAtLocation": "Ya existe un recorte en esta ubicación o no hay suficiente espacio disponible.", + "cannotPlaceSpeed": "No se puede colocar la velocidad aquí", + "speedExistsAtLocation": "Ya existe una región de velocidad en esta ubicación o no hay suficiente espacio disponible." }, "success": { - "addedZoomSuggestions": "Se agregó {{count}} sugerencia de zoom basada en el cursor", + "addedZoomSuggestions": "Se agregó {{count}} sugerencia de zoom basada en el cursor", "addedZoomSuggestionsPlural": "Se agregaron {{count}} sugerencias de zoom basadas en el cursor" }, "feedback": { - "copied": "Copied!", - "pasted": "Pasted!" + "copied": "¡Copiado!", + "pasted": "¡Pegado!" } } diff --git a/src/i18n/locales/fr/timeline.json b/src/i18n/locales/fr/timeline.json index 294f340f0..001516bd5 100644 --- a/src/i18n/locales/fr/timeline.json +++ b/src/i18n/locales/fr/timeline.json @@ -1,7 +1,7 @@ { "buttons": { "addZoom": "Ajouter un zoom (Z)", - "suggestZooms": "Suggérer des zooms depuis le curseur", + "suggestZooms": "Suggérer des zooms depuis le curseur", "addTrim": "Ajouter une coupe (T)", "addAnnotation": "Ajouter une annotation (A)", "addSpeed": "Ajouter une vitesse (S)", @@ -26,32 +26,32 @@ "blurItem": "Flou {{index}}" }, "emptyState": { - "noVideo": "Aucune vidéo chargée", - "dragAndDrop": "Glissez-déposez une vidéo pour commencer à éditer" + "noVideo": "Aucune vidéo chargée", + "dragAndDrop": "Glissez-déposez une vidéo pour commencer à éditer" }, "errors": { "cannotPlaceZoom": "Impossible de placer le zoom ici", - "zoomExistsAtLocation": "Un zoom existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant.", + "zoomExistsAtLocation": "Un zoom existe déjà à cet emplacement ou l'espace disponible est insuffisant.", "zoomSuggestionUnavailable": "Gestionnaire de suggestions de zoom non disponible", - "noCursorTelemetry": "Aucune télémétrie de curseur disponible", - "noCursorTelemetryDescription": "Enregistrez d\u0027abord un screencast pour générer des suggestions basées sur le curseur.", - "noUsableTelemetry": "Aucune télémétrie de curseur utilisable", - "noUsableTelemetryDescription": "L\u0027enregistrement ne contient pas suffisamment de données de mouvement du curseur.", - "noDwellMoments": "Aucun moment de pause du curseur trouvé", + "noCursorTelemetry": "Aucune télémétrie de curseur disponible", + "noCursorTelemetryDescription": "Enregistrez d'abord un screencast pour générer des suggestions basées sur le curseur.", + "noUsableTelemetry": "Aucune télémétrie de curseur utilisable", + "noUsableTelemetryDescription": "L'enregistrement ne contient pas suffisamment de données de mouvement du curseur.", + "noDwellMoments": "Aucun moment de pause du curseur trouvé", "noDwellMomentsDescription": "Essayez un enregistrement avec des pauses plus lentes du curseur sur les actions importantes.", "noAutoZoomSlots": "Aucun emplacement de zoom automatique disponible", - "noAutoZoomSlotsDescription": "Les points de pause détectés chevauchent des régions de zoom existantes.", + "noAutoZoomSlotsDescription": "Les points de pause détectés chevauchent des régions de zoom existantes.", "cannotPlaceTrim": "Impossible de placer la coupe ici", - "trimExistsAtLocation": "Une coupe existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant.", + "trimExistsAtLocation": "Une coupe existe déjà à cet emplacement ou l'espace disponible est insuffisant.", "cannotPlaceSpeed": "Impossible de placer la vitesse ici", - "speedExistsAtLocation": "Une région de vitesse existe déjà à cet emplacement ou l\u0027espace disponible est insuffisant." + "speedExistsAtLocation": "Une région de vitesse existe déjà à cet emplacement ou l'espace disponible est insuffisant." }, "success": { - "addedZoomSuggestions": "{{count}} suggestion de zoom basée sur le curseur ajoutée", - "addedZoomSuggestionsPlural": "{{count}} suggestions de zoom basées sur le curseur ajoutées" + "addedZoomSuggestions": "{{count}} suggestion de zoom basée sur le curseur ajoutée", + "addedZoomSuggestionsPlural": "{{count}} suggestions de zoom basées sur le curseur ajoutées" }, "feedback": { - "copied": "Copied!", - "pasted": "Pasted!" + "copied": "Copié !", + "pasted": "Collé !" } } diff --git a/src/i18n/locales/ko-KR/timeline.json b/src/i18n/locales/ko-KR/timeline.json index ee67a9fc7..b18067c2c 100644 --- a/src/i18n/locales/ko-KR/timeline.json +++ b/src/i18n/locales/ko-KR/timeline.json @@ -1,54 +1,54 @@ { "buttons": { - "addZoom": "줌 추가 (Z)", - "suggestZooms": "커서 기반 줌 제안", - "addTrim": "트림 추가 (T)", - "addAnnotation": "주석 추가 (A)", - "addSpeed": "속도 추가 (S)" + "addZoom": "줌 추가 (Z)", + "suggestZooms": "커서 기반 줌 제안", + "addTrim": "트림 추가 (T)", + "addAnnotation": "주석 추가 (A)", + "addSpeed": "속도 추가 (S)" }, "hints": { - "pressZoom": "Z를 눌러 줌 추가", - "pressTrim": "T를 눌러 트림 추가", - "pressAnnotation": "A를 눌러 주석 추가", - "pressSpeed": "S를 눌러 속도 추가" + "pressZoom": "Z를 눌러 줌 추가", + "pressTrim": "T를 눌러 트림 추가", + "pressAnnotation": "A를 눌러 주석 추가", + "pressSpeed": "S를 눌러 속도 추가" }, "labels": { - "pan": "이동", - "zoom": "줌", - "zoomItem": "줌 {{index}}", - "trimItem": "트림 {{index}}", - "speedItem": "속도 {{index}}", - "annotationItem": "주석", - "imageItem": "이미지", - "emptyText": "빈 텍스트" + "pan": "이동", + "zoom": "줌", + "zoomItem": "줌 {{index}}", + "trimItem": "트림 {{index}}", + "speedItem": "속도 {{index}}", + "annotationItem": "주석", + "imageItem": "이미지", + "emptyText": "빈 텍스트" }, "emptyState": { - "noVideo": "불러온 비디오 없음", - "dragAndDrop": "비디오를 드래그 앤 드롭해서 편집을 시작하세요" + "noVideo": "불러온 비디오 없음", + "dragAndDrop": "비디오를 드래그 앤 드롭해서 편집을 시작하세요" }, "errors": { - "cannotPlaceZoom": "이 위치에 줌을 추가할 수 없습니다", - "zoomExistsAtLocation": "이 위치에 이미 줌이 있거나 공간이 부족합니다.", - "zoomSuggestionUnavailable": "줌 제안 기능을 사용할 수 없습니다", - "noCursorTelemetry": "커서 데이터가 없습니다", - "noCursorTelemetryDescription": "커서 기반 제안을 생성하려면 먼저 화면을 녹화해 주세요.", - "noUsableTelemetry": "사용 가능한 커서 데이터가 없습니다", - "noUsableTelemetryDescription": "녹화에 충분한 커서 이동 데이터가 포함되어 있지 않습니다.", - "noDwellMoments": "명확한 커서 ì •ì§€ 구간을 찾을 수 없습니다", - "noDwellMomentsDescription": "중요한 동작에서 커서를 천천히 멈추며 녹화해 보세요.", - "noAutoZoomSlots": "자동 줌 슬롯이 없습니다", - "noAutoZoomSlotsDescription": "감지된 ì •ì§€ 지점이 기존 줌 구간과 겹칩니다.", - "cannotPlaceTrim": "이 위치에 트림을 추가할 수 없습니다", - "trimExistsAtLocation": "이 위치에 이미 트림이 있거나 공간이 부족합니다.", - "cannotPlaceSpeed": "이 위치에 속도를 추가할 수 없습니다", - "speedExistsAtLocation": "이 위치에 이미 속도 구간이 있거나 공간이 부족합니다." + "cannotPlaceZoom": "이 위치에 줌을 추가할 수 없습니다", + "zoomExistsAtLocation": "이 위치에 이미 줌이 있거나 공간이 부족합니다.", + "zoomSuggestionUnavailable": "줌 제안 기능을 사용할 수 없습니다", + "noCursorTelemetry": "커서 데이터가 없습니다", + "noCursorTelemetryDescription": "커서 기반 제안을 생성하려면 먼저 화면을 녹화해 주세요.", + "noUsableTelemetry": "사용 가능한 커서 데이터가 없습니다", + "noUsableTelemetryDescription": "녹화에 충분한 커서 이동 데이터가 포함되어 있지 않습니다.", + "noDwellMoments": "명확한 커서 정지 구간을 찾을 수 없습니다", + "noDwellMomentsDescription": "중요한 동작에서 커서를 천천히 멈추며 녹화해 보세요.", + "noAutoZoomSlots": "자동 줌 슬롯이 없습니다", + "noAutoZoomSlotsDescription": "감지된 정지 지점이 기존 줌 구간과 겹칩니다.", + "cannotPlaceTrim": "이 위치에 트림을 추가할 수 없습니다", + "trimExistsAtLocation": "이 위치에 이미 트림이 있거나 공간이 부족합니다.", + "cannotPlaceSpeed": "이 위치에 속도를 추가할 수 없습니다", + "speedExistsAtLocation": "이 위치에 이미 속도 구간이 있거나 공간이 부족합니다." }, "success": { - "addedZoomSuggestions": "커서 기반 줌 제안 {{count}}개가 추가되었습니다", - "addedZoomSuggestionsPlural": "커서 기반 줌 제안 {{count}}개가 추가되었습니다" + "addedZoomSuggestions": "커서 기반 줌 제안 {{count}}개가 추가되었습니다", + "addedZoomSuggestionsPlural": "커서 기반 줌 제안 {{count}}개가 추가되었습니다" }, "feedback": { - "copied": "Copied!", - "pasted": "Pasted!" + "copied": "복사됨!", + "pasted": "붙여넣음!" } } diff --git a/src/i18n/locales/tr/timeline.json b/src/i18n/locales/tr/timeline.json index 761d82ff5..e19368c24 100644 --- a/src/i18n/locales/tr/timeline.json +++ b/src/i18n/locales/tr/timeline.json @@ -1,57 +1,57 @@ { "buttons": { - "addZoom": "YakınlaÅŸtırma Ekle (Z)", - "suggestZooms": "İmleçten YakınlaÅŸtırma Öner", - "addTrim": "Kırpma Ekle (T)", - "addAnnotation": "Açıklama Ekle (A)", - "addSpeed": "Hız Ekle (S)", - "addBlur": "Bulanık ekle (B)" + "addZoom": "Yakınlaştırma Ekle (Z)", + "suggestZooms": "İmleçten Yakınlaştırma Öner", + "addTrim": "Kırpma Ekle (T)", + "addAnnotation": "Açıklama Ekle (A)", + "addSpeed": "Hız Ekle (S)", + "addBlur": "Bulanık ekle (B)" }, "hints": { - "pressZoom": "YakınlaÅŸtırma eklemek için Z tuÅŸuna basın", - "pressTrim": "Kırpma eklemek için T tuÅŸuna basın", - "pressAnnotation": "Açıklama eklemek için A tuÅŸuna basın", - "pressSpeed": "Hız eklemek için S tuÅŸuna basın", - "pressBlur": "Bulanık bölge eklemek için B tuÅŸuna basın" + "pressZoom": "Yakınlaştırma eklemek için Z tuşuna basın", + "pressTrim": "Kırpma eklemek için T tuşuna basın", + "pressAnnotation": "Açıklama eklemek için A tuşuna basın", + "pressSpeed": "Hız eklemek için S tuşuna basın", + "pressBlur": "Bulanık bölge eklemek için B tuşuna basın" }, "labels": { - "pan": "Kaydır", - "zoom": "YakınlaÅŸtır", - "zoomItem": "YakınlaÅŸtırma {{index}}", - "trimItem": "Kırpma {{index}}", - "speedItem": "Hız {{index}}", - "annotationItem": "Açıklama", - "imageItem": "Görüntü", - "emptyText": "BoÅŸ metin", - "blurItem": "Bulanık {{index}}" + "pan": "Kaydır", + "zoom": "Yakınlaştır", + "zoomItem": "Yakınlaştırma {{index}}", + "trimItem": "Kırpma {{index}}", + "speedItem": "Hız {{index}}", + "annotationItem": "Açıklama", + "imageItem": "Görüntü", + "emptyText": "Boş metin", + "blurItem": "Bulanık {{index}}" }, "emptyState": { - "noVideo": "Video Yüklenmedi", - "dragAndDrop": "Düzenlemeye baÅŸlamak için bir video sürükleyip bırakın" + "noVideo": "Video Yüklenmedi", + "dragAndDrop": "Düzenlemeye başlamak için bir video sürükleyip bırakın" }, "errors": { - "cannotPlaceZoom": "Buraya yakınlaÅŸtırma yerleÅŸtirilemiyor", - "zoomExistsAtLocation": "Bu konumda zaten bir yakınlaÅŸtırma var veya yeterli alan yok.", - "zoomSuggestionUnavailable": "YakınlaÅŸtırma öneri iÅŸleyicisi kullanılamıyor", - "noCursorTelemetry": "İmleç telemetrisi mevcut deÄŸil", - "noCursorTelemetryDescription": "İmleç tabanlı öneriler oluÅŸturmak için önce bir ekran kaydı yapın.", - "noUsableTelemetry": "Kullanılabilir imleç telemetrisi yok", - "noUsableTelemetryDescription": "Kayıt yeterli imleç hareketi verisi içermiyor.", - "noDwellMoments": "Belirgin imleç bekleme anları bulunamadı", - "noDwellMomentsDescription": "Önemli iÅŸlemlerde daha yavaÅŸ imleç duraklamaları olan bir kayıt deneyin.", - "noAutoZoomSlots": "Otomatik yakınlaÅŸtırma alanı yok", - "noAutoZoomSlotsDescription": "Algılanan bekleme noktaları mevcut yakınlaÅŸtırma bölgeleriyle çakışıyor.", - "cannotPlaceTrim": "Buraya kırpma yerleÅŸtirilemiyor", - "trimExistsAtLocation": "Bu konumda zaten bir kırpma var veya yeterli alan yok.", - "cannotPlaceSpeed": "Buraya hız yerleÅŸtirilemiyor", - "speedExistsAtLocation": "Bu konumda zaten bir hız bölgesi var veya yeterli alan yok." + "cannotPlaceZoom": "Buraya yakınlaştırma yerleştirilemiyor", + "zoomExistsAtLocation": "Bu konumda zaten bir yakınlaştırma var veya yeterli alan yok.", + "zoomSuggestionUnavailable": "Yakınlaştırma öneri işleyicisi kullanılamıyor", + "noCursorTelemetry": "İmleç telemetrisi mevcut değil", + "noCursorTelemetryDescription": "İmleç tabanlı öneriler oluşturmak için önce bir ekran kaydı yapın.", + "noUsableTelemetry": "Kullanılabilir imleç telemetrisi yok", + "noUsableTelemetryDescription": "Kayıt yeterli imleç hareketi verisi içermiyor.", + "noDwellMoments": "Belirgin imleç bekleme anları bulunamadı", + "noDwellMomentsDescription": "Önemli işlemlerde daha yavaş imleç duraklamaları olan bir kayıt deneyin.", + "noAutoZoomSlots": "Otomatik yakınlaştırma alanı yok", + "noAutoZoomSlotsDescription": "Algılanan bekleme noktaları mevcut yakınlaştırma bölgeleriyle çakışıyor.", + "cannotPlaceTrim": "Buraya kırpma yerleştirilemiyor", + "trimExistsAtLocation": "Bu konumda zaten bir kırpma var veya yeterli alan yok.", + "cannotPlaceSpeed": "Buraya hız yerleştirilemiyor", + "speedExistsAtLocation": "Bu konumda zaten bir hız bölgesi var veya yeterli alan yok." }, "success": { - "addedZoomSuggestions": "{{count}} imleç tabanlı yakınlaÅŸtırma önerisi eklendi", - "addedZoomSuggestionsPlural": "{{count}} imleç tabanlı yakınlaÅŸtırma önerisi eklendi" + "addedZoomSuggestions": "{{count}} imleç tabanlı yakınlaştırma önerisi eklendi", + "addedZoomSuggestionsPlural": "{{count}} imleç tabanlı yakınlaştırma önerisi eklendi" }, "feedback": { - "copied": "Copied!", - "pasted": "Pasted!" + "copied": "Kopyalandı!", + "pasted": "Yapıştırıldı!" } } diff --git a/src/i18n/locales/zh-CN/timeline.json b/src/i18n/locales/zh-CN/timeline.json index def3d87d2..ab35e5085 100644 --- a/src/i18n/locales/zh-CN/timeline.json +++ b/src/i18n/locales/zh-CN/timeline.json @@ -1,57 +1,57 @@ { "buttons": { - "addZoom": "添加缩放 (Z)", - "suggestZooms": "根据光标建议缩放", - "addTrim": "添加剪辑 (T)", - "addAnnotation": "添加标注 (A)", - "addSpeed": "添加速度 (S)", - "addBlur": "添加模糊 (B)" + "addZoom": "添加缩放 (Z)", + "suggestZooms": "根据光标建议缩放", + "addTrim": "添加剪辑 (T)", + "addAnnotation": "添加标注 (A)", + "addSpeed": "添加速度 (S)", + "addBlur": "添加模糊 (B)" }, "hints": { - "pressZoom": "按 Z 添加缩放", - "pressTrim": "按 T 添加剪辑", - "pressAnnotation": "按 A 添加标注", - "pressSpeed": "按 S 添加速度", - "pressBlur": "按 B 添加模糊区域" + "pressZoom": "按 Z 添加缩放", + "pressTrim": "按 T 添加剪辑", + "pressAnnotation": "按 A 添加标注", + "pressSpeed": "按 S 添加速度", + "pressBlur": "按 B 添加模糊区域" }, "labels": { - "pan": "平移", - "zoom": "缩放", - "zoomItem": "缩放 {{index}}", - "trimItem": "剪辑 {{index}}", - "speedItem": "速度 {{index}}", - "annotationItem": "标注", - "imageItem": "图片", - "emptyText": "空文本", - "blurItem": "模糊 {{index}}" + "pan": "平移", + "zoom": "缩放", + "zoomItem": "缩放 {{index}}", + "trimItem": "剪辑 {{index}}", + "speedItem": "速度 {{index}}", + "annotationItem": "标注", + "imageItem": "图片", + "emptyText": "空文本", + "blurItem": "模糊 {{index}}" }, "emptyState": { - "noVideo": "未加载视频", - "dragAndDrop": "拖放视频以开始编辑" + "noVideo": "未加载视频", + "dragAndDrop": "拖放视频以开始编辑" }, "errors": { - "cannotPlaceZoom": "无法在此处放置缩放", - "zoomExistsAtLocation": "此位置已存在缩放或没有足够的空间。", - "zoomSuggestionUnavailable": "缩放建议处理器不可用", - "noCursorTelemetry": "无可用的光标遥测数据", - "noCursorTelemetryDescription": "请先录制一段屏幕录像以生成基于光标的建议。", - "noUsableTelemetry": "无可用的光标遥测数据", - "noUsableTelemetryDescription": "录制内容没有包含足够的光标移动数据。", - "noDwellMoments": "未找到明确的光标停留时刻", - "noDwellMomentsDescription": "请尝试在重要操作上进行较慢光标停留的录制。", - "noAutoZoomSlots": "无可用的自动缩放位置", - "noAutoZoomSlotsDescription": "检测到的停留点与现有缩放区域重叠。", - "cannotPlaceTrim": "无法在此处放置剪辑", - "trimExistsAtLocation": "此位置已存在剪辑或没有足够的空间。", - "cannotPlaceSpeed": "无法在此处放置速度", - "speedExistsAtLocation": "此位置已存在速度区域或没有足够的空间。" + "cannotPlaceZoom": "无法在此处放置缩放", + "zoomExistsAtLocation": "此位置已存在缩放或没有足够的空间。", + "zoomSuggestionUnavailable": "缩放建议处理器不可用", + "noCursorTelemetry": "无可用的光标遥测数据", + "noCursorTelemetryDescription": "请先录制一段屏幕录像以生成基于光标的建议。", + "noUsableTelemetry": "无可用的光标遥测数据", + "noUsableTelemetryDescription": "录制内容没有包含足够的光标移动数据。", + "noDwellMoments": "未找到明确的光标停留时刻", + "noDwellMomentsDescription": "请尝试在重要操作上进行较慢光标停留的录制。", + "noAutoZoomSlots": "无可用的自动缩放位置", + "noAutoZoomSlotsDescription": "检测到的停留点与现有缩放区域重叠。", + "cannotPlaceTrim": "无法在此处放置剪辑", + "trimExistsAtLocation": "此位置已存在剪辑或没有足够的空间。", + "cannotPlaceSpeed": "无法在此处放置速度", + "speedExistsAtLocation": "此位置已存在速度区域或没有足够的空间。" }, "success": { - "addedZoomSuggestions": "已添加 {{count}} 个基于光标的缩放建议", - "addedZoomSuggestionsPlural": "已添加 {{count}} 个基于光标的缩放建议" + "addedZoomSuggestions": "已添加 {{count}} 个基于光标的缩放建议", + "addedZoomSuggestionsPlural": "已添加 {{count}} 个基于光标的缩放建议" }, "feedback": { - "copied": "Copied!", - "pasted": "Pasted!" + "copied": "已复制!", + "pasted": "已粘贴!" } } diff --git a/src/i18n/locales/zh-TW/timeline.json b/src/i18n/locales/zh-TW/timeline.json index 783f6548b..667250542 100644 --- a/src/i18n/locales/zh-TW/timeline.json +++ b/src/i18n/locales/zh-TW/timeline.json @@ -1,57 +1,57 @@ { "buttons": { - "addZoom": "新增縮放 (Z)", - "suggestZooms": "根據游標建議縮放", - "addTrim": "新增剪輯 (T)", - "addAnnotation": "新增標註 (A)", - "addSpeed": "新增速度 (S)", - "addBlur": "新增模糊 (B)" + "addZoom": "新增縮放 (Z)", + "suggestZooms": "根據游標建議縮放", + "addTrim": "新增剪輯 (T)", + "addAnnotation": "新增標註 (A)", + "addSpeed": "新增速度 (S)", + "addBlur": "新增模糊 (B)" }, "hints": { - "pressZoom": "按 Z 新增縮放", - "pressTrim": "按 T 新增剪輯", - "pressAnnotation": "按 A 新增標註", - "pressSpeed": "按 S 新增速度", - "pressBlur": "按 B 新增模糊區域" + "pressZoom": "按 Z 新增縮放", + "pressTrim": "按 T 新增剪輯", + "pressAnnotation": "按 A 新增標註", + "pressSpeed": "按 S 新增速度", + "pressBlur": "按 B 新增模糊區域" }, "labels": { - "pan": "平移", - "zoom": "縮放", - "zoomItem": "縮放 {{index}}", - "trimItem": "剪輯 {{index}}", - "speedItem": "速度 {{index}}", - "annotationItem": "標註", - "imageItem": "圖片", - "emptyText": "空文字", - "blurItem": "模糊 {{index}}" + "pan": "平移", + "zoom": "縮放", + "zoomItem": "縮放 {{index}}", + "trimItem": "剪輯 {{index}}", + "speedItem": "速度 {{index}}", + "annotationItem": "標註", + "imageItem": "圖片", + "emptyText": "空文字", + "blurItem": "模糊 {{index}}" }, "emptyState": { - "noVideo": "未載入影片", - "dragAndDrop": "拖放影片以開始編輯" + "noVideo": "未載入影片", + "dragAndDrop": "拖放影片以開始編輯" }, "errors": { - "cannotPlaceZoom": "無法在此處放置縮放", - "zoomExistsAtLocation": "此位置已存在縮放或沒有足夠的空間。", - "zoomSuggestionUnavailable": "縮放建議處理器不可用", - "noCursorTelemetry": "無可用的游標遙測資料", - "noCursorTelemetryDescription": "請先錄製一段螢幕錄影以產生基於游標的建議。", - "noUsableTelemetry": "無可用的游標遙測資料", - "noUsableTelemetryDescription": "錄製內容沒有包含足夠的游標移動資料。", - "noDwellMoments": "未找到明確的游標停留時刻", - "noDwellMomentsDescription": "請嘗試在重要操作上進行較慢游標停留的錄製。", - "noAutoZoomSlots": "無可用的自動縮放位置", - "noAutoZoomSlotsDescription": "偵測到的停留點與現有縮放區域重疊。", - "cannotPlaceTrim": "無法在此處放置剪輯", - "trimExistsAtLocation": "此位置已存在剪輯或沒有足夠的空間。", - "cannotPlaceSpeed": "無法在此處放置速度", - "speedExistsAtLocation": "此位置已存在速度區域或沒有足夠的空間。" + "cannotPlaceZoom": "無法在此處放置縮放", + "zoomExistsAtLocation": "此位置已存在縮放或沒有足夠的空間。", + "zoomSuggestionUnavailable": "縮放建議處理器不可用", + "noCursorTelemetry": "無可用的游標遙測資料", + "noCursorTelemetryDescription": "請先錄製一段螢幕錄影以產生基於游標的建議。", + "noUsableTelemetry": "無可用的游標遙測資料", + "noUsableTelemetryDescription": "錄製內容沒有包含足夠的游標移動資料。", + "noDwellMoments": "未找到明確的游標停留時刻", + "noDwellMomentsDescription": "請嘗試在重要操作上進行較慢游標停留的錄製。", + "noAutoZoomSlots": "無可用的自動縮放位置", + "noAutoZoomSlotsDescription": "偵測到的停留點與現有縮放區域重疊。", + "cannotPlaceTrim": "無法在此處放置剪輯", + "trimExistsAtLocation": "此位置已存在剪輯或沒有足夠的空間。", + "cannotPlaceSpeed": "無法在此處放置速度", + "speedExistsAtLocation": "此位置已存在速度區域或沒有足夠的空間。" }, "success": { - "addedZoomSuggestions": "已新增 {{count}} 個基於游標的縮放建議", - "addedZoomSuggestionsPlural": "已新增 {{count}} 個基於游標的縮放建議" + "addedZoomSuggestions": "已新增 {{count}} 個基於游標的縮放建議", + "addedZoomSuggestionsPlural": "已新增 {{count}} 個基於游標的縮放建議" }, "feedback": { - "copied": "Copied!", - "pasted": "Pasted!" + "copied": "已複製!", + "pasted": "已貼上!" } }