From 3a28cf036565548aeda510743b4c3cd93a37752e Mon Sep 17 00:00:00 2001 From: OrangeChange <95136820+OrangeChange@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:12:35 +0800 Subject: [PATCH 1/2] feat(editor): add independent right-click cursor effects --- src/components/video-editor/SettingsPanel.tsx | 124 ++++++++++++++++-- src/components/video-editor/VideoPlayback.tsx | 17 ++- .../video-editor/editorPreferences.ts | 3 + .../export/buildExportRenderOptions.ts | 1 + .../layout/EditorVideoPreview.tsx | 1 + .../layout/useEditorSettingsPanelProps.ts | 2 + .../useEditorPreferencesPersistence.ts | 2 + .../presets/useVideoEditorPresets.ts | 2 + .../project/useProjectLibraryController.ts | 3 + .../project/useProjectLifecycle.ts | 1 + .../project/useProjectSnapshotModel.ts | 2 + .../video-editor/projectPersistence.test.ts | 33 ++++- .../video-editor/projectPersistence.ts | 7 + .../video-editor/state/useAppearanceState.ts | 6 + src/components/video-editor/types.test.ts | 17 +++ src/components/video-editor/types.ts | 45 +++++++ .../videoPlayback/cursorRenderer.ts | 47 +++++-- src/i18n/locales/de/settings.json | 5 + src/i18n/locales/en/settings.json | 5 + src/i18n/locales/es/settings.json | 5 + src/i18n/locales/fr/settings.json | 5 + src/i18n/locales/it/settings.json | 5 + src/i18n/locales/ko/settings.json | 5 + src/i18n/locales/nl/settings.json | 5 + src/i18n/locales/pt-BR/settings.json | 5 + src/i18n/locales/ru/settings.json | 5 + src/i18n/locales/zh-CN/settings.json | 21 +-- src/i18n/locales/zh-TW/settings.json | 5 + src/lib/exporter/frameRenderer.ts | 4 +- src/lib/exporter/gifExporter.ts | 5 +- src/lib/exporter/modernFrameRenderer.ts | 3 + .../modernVideoExporter.fallback.test.ts | 2 + ...rnVideoExporter.nativeStaticLayout.test.ts | 22 ++++ src/lib/exporter/modernVideoExporter.ts | 8 +- 34 files changed, 393 insertions(+), 35 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 8b289e3f5..56b1f5482 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -52,6 +52,7 @@ import type { AutoCaptionSettings, CaptionCue, CropRegion, + CursorClickEffectProfile, CursorClickEffectStyle, CursorStyle, EditorEffectSection, @@ -589,6 +590,8 @@ interface SettingsPanelProps { onZoomClassicModeChange?: (enabled: boolean) => void; cursorClickEffect?: CursorClickEffectStyle; onCursorClickEffectChange?: (effect: CursorClickEffectStyle) => void; + rightClickEffect?: CursorClickEffectProfile; + onRightClickEffectChange?: (profile?: CursorClickEffectProfile) => void; cursorClickEffectColor?: string; onCursorClickEffectColorChange?: (color: string) => void; cursorClickEffectScale?: number; @@ -1037,6 +1040,8 @@ export function SettingsPanel({ onZoomClassicModeChange, cursorClickEffect = DEFAULT_CURSOR_CLICK_EFFECT, onCursorClickEffectChange, + rightClickEffect, + onRightClickEffectChange, cursorClickEffectColor = DEFAULT_CURSOR_CLICK_EFFECT_COLOR, onCursorClickEffectColorChange, cursorClickEffectScale = DEFAULT_CURSOR_CLICK_EFFECT_SCALE, @@ -1249,6 +1254,38 @@ export function SettingsPanel({ Partial> >({}); const [showCursorClickEffectAdvanced, setShowCursorClickEffectAdvanced] = useState(false); + const [clickEffectTarget, setClickEffectTarget] = useState<"left" | "right">("left"); + const activeClickEffect = + clickEffectTarget === "right" + ? (rightClickEffect?.effect ?? cursorClickEffect) + : cursorClickEffect; + const activeClickEffectColor = + clickEffectTarget === "right" + ? (rightClickEffect?.color ?? cursorClickEffectColor) + : cursorClickEffectColor; + const rightClickFollowsLeft = !rightClickEffect; + const updateActiveClickEffect = (effect: CursorClickEffectStyle) => { + if (clickEffectTarget === "left") { + onCursorClickEffectChange?.(effect); + return; + } + + onRightClickEffectChange?.({ + effect, + color: activeClickEffectColor, + }); + }; + const updateActiveClickEffectColor = (color: string) => { + if (clickEffectTarget === "left") { + onCursorClickEffectColorChange?.(color); + return; + } + + onRightClickEffectChange?.({ + effect: activeClickEffect, + color, + }); + }; const cursorPreviewUrls = builtInCursorPreviewUrls; const showDevMotionControls = import.meta.env.DEV; const cursorStyleOptions = BUILTIN_CURSOR_STYLE_OPTIONS; @@ -1579,6 +1616,7 @@ export function SettingsPanel({ onCursorSpringMassMultiplierChange?.(initialEditorPreferences.cursorSpringMassMultiplier); onCursorClickEffectChange?.(initialEditorPreferences.cursorClickEffect); onCursorClickEffectColorChange?.(initialEditorPreferences.cursorClickEffectColor); + onRightClickEffectChange?.(initialEditorPreferences.rightClickEffect); onCursorClickEffectScaleChange?.(initialEditorPreferences.cursorClickEffectScale); onCursorClickEffectOpacityChange?.(initialEditorPreferences.cursorClickEffectOpacity); onCursorClickEffectDurationMsChange?.(initialEditorPreferences.cursorClickEffectDurationMs); @@ -3311,14 +3349,84 @@ export function SettingsPanel({ formatValue={(v) => `${v.toFixed(2)}×`} parseInput={(text) => parseFloat(text.replace(/×$/, ""))} /> +
+
+ {tSettings("effects.cursorClickEffects.target", "Click button")} +
+ { + if (value === "left" || value === "right") { + setClickEffectTarget(value); + } + }} + className="grid grid-cols-2 gap-1 rounded-lg bg-foreground/[0.04] p-1" + aria-label={tSettings( + "effects.cursorClickEffects.target", + "Click button", + )} + > + + {tSettings("effects.cursorClickEffects.left", "Left click")} + + + {tSettings( + "effects.cursorClickEffects.right", + "Right click", + )} + + + {clickEffectTarget === "right" ? ( +
+
+
+ {tSettings( + "effects.cursorClickEffects.followLeft", + "Follow left click", + )} +
+
+ {tSettings( + "effects.cursorClickEffects.followLeftDescription", + "Use the left-click effect until you customize this profile.", + )} +
+
+ { + onRightClickEffectChange?.( + checked + ? undefined + : { + effect: cursorClickEffect, + color: cursorClickEffectColor, + }, + ); + }} + aria-label={tSettings( + "effects.cursorClickEffects.followLeft", + "Follow left click", + )} + /> +
+ ) : null} +
onCursorClickEffectChange?.(effectId)} + activeEffectId={activeClickEffect} + effectColor={activeClickEffectColor} + onApply={updateActiveClickEffect} showAdvanced={showCursorClickEffectAdvanced} onToggleAdvanced={() => setShowCursorClickEffectAdvanced((current) => !current) @@ -3330,9 +3438,9 @@ export function SettingsPanel({ - onCursorClickEffectColorChange?.(event.target.value) + updateActiveClickEffectColor(event.target.value) } className="sr-only" /> @@ -3346,14 +3454,14 @@ export function SettingsPanel({
{CLICK_EFFECT_COLOR_OPTIONS.map((color) => { const isSelected = - cursorClickEffectColor.toLowerCase() === + activeClickEffectColor.toLowerCase() === color.toLowerCase(); return (