From 95a6f38f0b0a19aca672cdcca529f2b1a6da65e7 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:52:37 +1000 Subject: [PATCH 1/8] Simplify clip settings with a speed slider and mute toggle --- src/components/video-editor/SettingsPanel.tsx | 203 +++--------------- src/components/video-editor/VideoEditor.tsx | 1 - .../layout/useEditorSettingsPanelProps.ts | 11 - src/i18n/locales/de/settings.json | 2 +- src/i18n/locales/en/settings.json | 2 +- src/i18n/locales/es/settings.json | 2 +- src/i18n/locales/fr/settings.json | 2 +- src/i18n/locales/it/settings.json | 2 +- src/i18n/locales/ko/settings.json | 2 +- src/i18n/locales/nl/settings.json | 2 +- src/i18n/locales/pt-BR/settings.json | 2 +- src/i18n/locales/ru/settings.json | 2 +- src/i18n/locales/zh-CN/settings.json | 2 +- src/i18n/locales/zh-TW/settings.json | 2 +- 14 files changed, 42 insertions(+), 195 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 8b289e3f5..54df9f1a8 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -523,15 +523,8 @@ interface SettingsPanelProps { selectedClipId?: string | null; selectedClipSpeed?: number | null; selectedClipMuted?: boolean | null; - selectedClipShowSourceAudio?: boolean | null; - hasClipSourceAudio?: boolean; onClipSpeedChange?: (speed: number) => void; onClipMutedChange?: (muted: boolean) => void; - onClipShowSourceAudioChange?: (show: boolean) => void; - sourceAudioTrackMeta?: Array<{ id: string; label: string }>; - sourceAudioTrackSettings?: Record; - onSourceAudioTrackVolumeChange?: (id: string, volume: number) => void; - onSourceAudioTrackNormalizeChange?: (id: string, normalize: boolean) => void; onClipDelete?: (id: string) => void; selectedAudioId?: string | null; selectedAudioVolume?: number | null; @@ -983,15 +976,8 @@ export function SettingsPanel({ selectedClipId, selectedClipSpeed, selectedClipMuted, - selectedClipShowSourceAudio = false, - hasClipSourceAudio = false, onClipSpeedChange, onClipMutedChange, - onClipShowSourceAudioChange, - sourceAudioTrackMeta = [], - sourceAudioTrackSettings = {}, - onSourceAudioTrackVolumeChange, - onSourceAudioTrackNormalizeChange, onClipDelete, selectedAudioId, selectedAudioVolume, @@ -1103,6 +1089,12 @@ export function SettingsPanel({ const { preference: themePreference, setPreference: setThemePreference } = useTheme(); const isBackgroundPanel = panelMode === "background"; const initialEditorPreferences = useMemo(() => loadEditorPreferences(), []); + const maxClipSpeed = useMemo(() => { + for (let speed = 30; speed >= 1; speed -= 0.25) { + if (supportsPreviewPlaybackRate(speed)) return speed; + } + return 1; + }, []); const [builtInWallpapers, setBuiltInWallpapers] = useState(BUILT_IN_WALLPAPERS); const [wallpaperPreviewPaths, setWallpaperPreviewPaths] = useState([]); @@ -3019,166 +3011,33 @@ export function SettingsPanel({ ); - const clipSectionContent = ( -
-
- {tSettings("clip.title", "Clip")} - {selectedClipSpeed != null && selectedClipSpeed !== 1 && ( - - {selectedClipSpeed}× - - )} -
- -
- {tSettings("speed.label", "Speed")} -
-
- {[ - { speed: 0.25, label: "0.25×" }, - { speed: 0.5, label: "0.5×" }, - { speed: 0.75, label: "0.75×" }, - { speed: 1, label: "1×" }, - { speed: 1.25, label: "1.25×" }, - { speed: 1.5, label: "1.5×" }, - { speed: 2, label: "2×" }, - { speed: 2.5, label: "2.5×" }, - { speed: 3, label: "3×" }, - { speed: 4, label: "4×" }, - { speed: 5, label: "5×" }, - { speed: 8, label: "8×" }, - { speed: 10, label: "10×" }, - { speed: 15, label: "15×" }, - { speed: 20, label: "20×" }, - { speed: 30, label: "30×" }, - ].map((option) => { - const isActive = selectedClipSpeed === option.speed; - return ( - - ); - })} -
-
- {tSettings("audio.title", "Audio")} -
-
- - {tSettings("clip.mute", "Mute")} - -

- {selectedClipMuted - ? tSettings("clip.mutedState", "Audio is muted") - : tSettings("clip.unmutedState", "Audio is playing")} -

-
- onClipMutedChange?.(v)} - className="data-[state=checked]:bg-[#06b6d4] scale-75" - /> -
- {hasClipSourceAudio && ( -
- - {tSettings( - "clip.separateClipFromAudio", - "Separate clip from audio", - )} - - onClipShowSourceAudioChange?.(v)} - className="data-[state=checked]:bg-[#06b6d4] scale-75" - /> -
- )} -
- - {selectedClipId && hasClipSourceAudio && sourceAudioTrackMeta.length > 0 && ( -
- {sourceAudioTrackMeta.map((track) => { - const settings = sourceAudioTrackSettings[track.id] ?? { - volume: 1, - normalize: false, - }; - return ( -
-
- - {track.label} - - -
-
- - {tSettings("audio.normalize", "Normalize")} - - - onSourceAudioTrackNormalizeChange?.(track.id, v) - } - className="data-[state=checked]:bg-[#06b6d4] scale-75" - /> -
- - onSourceAudioTrackVolumeChange?.(track.id, v) - } - formatValue={(v) => `${Math.round(v * 100)}%`} - parseInput={(text) => - parseFloat(text.replace(/%$/, "")) / 100 - } - /> -
- ); - })} -
- )} + const clipSectionContent = ( +
+ {tSettings("clip.title", "Clip")} + onClipSpeedChange?.(value)} + formatValue={(value) => `${value}×`} + parseInput={(text) => Number.parseFloat(text)} + /> +
); diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 6d22666a1..636da92c2 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -342,7 +342,6 @@ export default function VideoEditor() { activeEffectSection, appearance, timeline, - audio, zoomCommands, clipCommands, audioCommands, diff --git a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts index 5ea46b498..087611903 100644 --- a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts +++ b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts @@ -1,6 +1,5 @@ import type { ComponentProps, Dispatch, SetStateAction } from "react"; import type { AspectRatio } from "@/utils/aspectRatioUtils"; -import type { useVideoEditorAudio } from "../audio/useVideoEditorAudio"; import type { useAutoCaptionController } from "../captions/useAutoCaptionController"; import type { useAnnotationRegionCommands } from "../hooks/useAnnotationRegionCommands"; import type { useAudioRegionCommands } from "../hooks/useAudioRegionCommands"; @@ -16,7 +15,6 @@ type Input = { activeEffectSection: EditorEffectSection; appearance: ReturnType; timeline: ReturnType; - audio: ReturnType; zoomCommands: ReturnType; clipCommands: ReturnType; audioCommands: ReturnType; @@ -45,7 +43,6 @@ export function useEditorSettingsPanelProps(input: Input): ComponentProps Date: Tue, 15 Sep 2026 17:09:46 +1000 Subject: [PATCH 2/8] Respect device speed bounds in clip controls --- src/components/video-editor/SettingsPanel.tsx | 29 +++++++++++-------- .../videoPlayback/playbackRate.test.ts | 14 +++++++++ .../videoPlayback/playbackRate.ts | 9 ++++++ src/i18n/locales/it/settings.json | 2 +- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 54df9f1a8..ca0cddd95 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -91,7 +91,7 @@ import { } from "./types"; import { fromCursorSwaySliderValue, toCursorSwaySliderValue } from "./videoPlayback/cursorSway"; import { isZeroPadding } from "./videoPlayback/layoutUtils"; -import { supportsPreviewPlaybackRate } from "./videoPlayback/playbackRate"; +import { getPreviewPlaybackRateRange } from "./videoPlayback/playbackRate"; import { cursorSetAssets, getCursorStyleSizeMultiplier, @@ -1089,12 +1089,7 @@ export function SettingsPanel({ const { preference: themePreference, setPreference: setThemePreference } = useTheme(); const isBackgroundPanel = panelMode === "background"; const initialEditorPreferences = useMemo(() => loadEditorPreferences(), []); - const maxClipSpeed = useMemo(() => { - for (let speed = 30; speed >= 1; speed -= 0.25) { - if (supportsPreviewPlaybackRate(speed)) return speed; - } - return 1; - }, []); + const clipSpeedRange = useMemo(getPreviewPlaybackRateRange, []); const [builtInWallpapers, setBuiltInWallpapers] = useState(BUILT_IN_WALLPAPERS); const [wallpaperPreviewPaths, setWallpaperPreviewPaths] = useState([]); @@ -2441,6 +2436,12 @@ export function SettingsPanel({ className="data-[state=checked]:bg-[#2563EB] scale-75" /> + {selectedClipSpeed != null && + (selectedClipSpeed < clipSpeedRange.min || selectedClipSpeed > clipSpeedRange.max) && ( +

+ {selectedClipSpeed}× — {tSettings("speed.unsupported", "Not supported for preview on this device")} +

+ )}
); - - const clipSectionContent = (
{tSettings("clip.title", "Clip")} onClipSpeedChange?.(value)} formatValue={(value) => `${value}×`} parseInput={(text) => Number.parseFloat(text)} /> + {selectedClipSpeed != null && + (selectedClipSpeed < clipSpeedRange.min || selectedClipSpeed > clipSpeedRange.max) && ( +

+ {selectedClipSpeed}× — {tSettings("speed.unsupported", "Not supported for preview on this device")} +

+ )}
); diff --git a/src/components/video-editor/audio/useClipAudioReset.test.ts b/src/components/video-editor/audio/useClipAudioReset.test.ts new file mode 100644 index 000000000..e60131ef1 --- /dev/null +++ b/src/components/video-editor/audio/useClipAudioReset.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import type { SourceAudioTrackSettings } from "./audioTypes"; +import { useClipAudioReset } from "./useClipAudioReset"; + +function harness(selectedClipId: string | null, defaults: SourceAudioTrackSettings = {}) { + let saved = { + selected: { mic: { volume: 0, normalize: true } }, + other: { mic: { volume: 0.4, normalize: true } }, + } as Record; + const initial = saved; + const render = () => + useClipAudioReset({ + selectedClipId, + defaultSourceAudioTrackSettings: defaults, + sourceAudioTrackSettingsByClip: saved, + setSourceAudioTrackSettingsByClip: (update) => { + saved = typeof update === "function" ? update(saved) : update; + }, + }); + return { render, initial, read: () => saved }; +} + +describe("saved clip audio recovery", () => { + it("restores silent saved tracks and clears normalization only for the selected clip", () => { + const state = harness("selected"); + expect(state.render().hasClipAudioOverrides).toBe(true); + state.render().onResetClipAudio(); + expect(state.read().selected.mic).toEqual({ volume: 1, normalize: false }); + expect(state.read().other).toBe(state.initial.other); + expect(state.initial.selected.mic.volume).toBe(0); + expect(state.render().hasClipAudioOverrides).toBe(false); + // These settings are serialized directly in the project, so recovery survives reload. + expect(JSON.parse(JSON.stringify(state.read())).selected.mic.volume).toBe(1); + }); + + it("neutralizes inherited settings without changing defaults or other clips", () => { + const defaults = { system: { volume: 0, normalize: true } }; + const state = harness("new-clip", defaults); + expect(state.render().hasClipAudioOverrides).toBe(true); + state.render().onResetClipAudio(); + expect({ ...defaults, ...state.read()["new-clip"] }).toEqual({ + system: { volume: 1, normalize: false }, + }); + expect(defaults.system.volume).toBe(0); + expect(state.read().selected).toBe(state.initial.selected); + expect(state.render().hasClipAudioOverrides).toBe(false); + }); + + it("does nothing without a selection and hides recovery for untouched clips", () => { + const state = harness(null); + expect(state.render().hasClipAudioOverrides).toBe(false); + state.render().onResetClipAudio(); + expect(state.read()).toBe(state.initial); + expect(harness("new-clip").render().hasClipAudioOverrides).toBe(false); + }); +}); diff --git a/src/components/video-editor/audio/useClipAudioReset.ts b/src/components/video-editor/audio/useClipAudioReset.ts new file mode 100644 index 000000000..38d1cffc7 --- /dev/null +++ b/src/components/video-editor/audio/useClipAudioReset.ts @@ -0,0 +1,42 @@ +import type { useTimelineState } from "../state/useTimelineState"; + +type Input = Pick< + ReturnType, + | "selectedClipId" + | "sourceAudioTrackSettingsByClip" + | "defaultSourceAudioTrackSettings" + | "setSourceAudioTrackSettingsByClip" +>; + +export function useClipAudioReset({ + selectedClipId, + sourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setSourceAudioTrackSettingsByClip, +}: Input) { + const settings = { + ...defaultSourceAudioTrackSettings, + ...(selectedClipId ? sourceAudioTrackSettingsByClip[selectedClipId] : {}), + }; + return { + hasClipAudioOverrides: + selectedClipId !== null && + Object.values(settings).some((setting) => setting.volume !== 1 || setting.normalize), + onResetClipAudio: () => { + if (!selectedClipId) return; + setSourceAudioTrackSettingsByClip((current) => { + const effective = { + ...defaultSourceAudioTrackSettings, + ...current[selectedClipId], + }; + // Explicit neutral values also override inherited project settings. + return { + ...current, + [selectedClipId]: Object.fromEntries( + Object.keys(effective).map((id) => [id, { volume: 1, normalize: false }]), + ), + }; + }); + }, + }; +} diff --git a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts index 087611903..4c4c8c696 100644 --- a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts +++ b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts @@ -1,5 +1,6 @@ import type { ComponentProps, Dispatch, SetStateAction } from "react"; import type { AspectRatio } from "@/utils/aspectRatioUtils"; +import { useClipAudioReset } from "../audio/useClipAudioReset"; import type { useAutoCaptionController } from "../captions/useAutoCaptionController"; import type { useAnnotationRegionCommands } from "../hooks/useAnnotationRegionCommands"; import type { useAudioRegionCommands } from "../hooks/useAudioRegionCommands"; @@ -75,7 +76,10 @@ export function useEditorSettingsPanelProps(input: Input): ComponentProps region.id === timeline.selectedAudioId, ); + const clipAudioReset = useClipAudioReset(timeline); + return { + ...clipAudioReset, panelMode: "editor", activeEffectSection, selected: appearance.wallpaper, diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 276680e20..3dae35b70 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "Clip", "mute": "Clip stummschalten", + "resetAudioSettings": "Audioeinstellungen zurücksetzen", "mutedState": "Audio ist stummgeschaltet", "unmutedState": "Audio wird wiedergegeben", "separateClipFromAudio": "Clip vom Audio trennen", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 9d4582676..9db0e74eb 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -26,6 +26,7 @@ "clip": { "title": "Clip", "mute": "Mute clip", + "resetAudioSettings": "Reset audio settings", "mutedState": "Audio is muted", "unmutedState": "Audio is playing", "separateClipFromAudio": "Separate clip from audio", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index cc2e0a191..5eafdfdde 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "Clip", "mute": "Silenciar clip", + "resetAudioSettings": "Restablecer ajustes de audio", "mutedState": "El audio está silenciado", "unmutedState": "El audio se está reproduciendo", "separateClipFromAudio": "Separar clip del audio", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 480576c78..26283e789 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "Clip", "mute": "Couper le son du clip", + "resetAudioSettings": "Réinitialiser les réglages audio", "mutedState": "Le son est coupé", "unmutedState": "Le son est activé", "separateClipFromAudio": "Séparer le clip de l'audio", diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index 766587d28..b43fdba9a 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "Clip", "mute": "Disattiva l'audio del clip", + "resetAudioSettings": "Ripristina impostazioni audio", "mutedState": "Audio disattivato", "unmutedState": "Audio in riproduzione", "separateClipFromAudio": "Separa clip dall'audio", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index 70713ce45..33488fc0b 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "클립", "mute": "클립 음소거", + "resetAudioSettings": "오디오 설정 초기화", "mutedState": "오디오가 음소거됨", "unmutedState": "오디오가 재생 중", "separateClipFromAudio": "클립에서 오디오 분리", diff --git a/src/i18n/locales/nl/settings.json b/src/i18n/locales/nl/settings.json index 068069a31..9d863cb53 100644 --- a/src/i18n/locales/nl/settings.json +++ b/src/i18n/locales/nl/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "Clip", "mute": "Clip dempen", + "resetAudioSettings": "Audio-instellingen resetten", "mutedState": "Audio is gedempt", "unmutedState": "Audio wordt afgespeeld", "separateClipFromAudio": "Clip van audio scheiden", diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json index 77b23d7ed..7541913a7 100644 --- a/src/i18n/locales/pt-BR/settings.json +++ b/src/i18n/locales/pt-BR/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "Clipe", "mute": "Silenciar clipe", + "resetAudioSettings": "Redefinir configurações de áudio", "mutedState": "O áudio está mudo", "unmutedState": "O áudio está tocando", "separateClipFromAudio": "Separar áudio do clipe", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index a4aebddce..aadb70e02 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "Клип", "mute": "Отключить звук клипа", + "resetAudioSettings": "Сбросить настройки звука", "mutedState": "Звук выключен", "unmutedState": "Звук включен", "separateClipFromAudio": "Отделить аудио от клипа", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index e679d7b68..ba4a68c6a 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "片段", "mute": "将片段静音", + "resetAudioSettings": "重置音频设置", "mutedState": "音频已静音", "unmutedState": "音频正在播放", "separateClipFromAudio": "将剪辑与音频分离", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 0b0fd7973..b7b944030 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -20,6 +20,7 @@ "clip": { "title": "片段", "mute": "將片段靜音", + "resetAudioSettings": "重設音訊設定", "mutedState": "音訊已靜音", "unmutedState": "音訊正在播放", "separateClipFromAudio": "將片段與音訊分離",