Simplify clip settings with a speed slider and mute toggle

This commit is contained in:
webadderall
2026-09-15 13:52:37 +10:00
parent b3ea7754c9
commit 95a6f38f0b
14 changed files with 43 additions and 196 deletions
+32 -173
View File
@@ -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<string, { volume: number; normalize: boolean }>;
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<BuiltInWallpaper[]>(BUILT_IN_WALLPAPERS);
const [wallpaperPreviewPaths, setWallpaperPreviewPaths] = useState<string[]>([]);
@@ -3019,166 +3011,33 @@ export function SettingsPanel({
</section>
);
const clipSectionContent = (
<section className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<SectionLabel>{tSettings("clip.title", "Clip")}</SectionLabel>
{selectedClipSpeed != null && selectedClipSpeed !== 1 && (
<span className="rounded-full bg-[#06b6d4]/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[#06b6d4]">
{selectedClipSpeed}×
</span>
)}
</div>
<div className="flex items-center gap-3">
<SectionLabel>{tSettings("speed.label", "Speed")}</SectionLabel>
</div>
<div className="grid grid-cols-4 gap-1.5">
{[
{ 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 (
<Button
key={option.speed}
type="button"
onClick={() => onClipSpeedChange?.(option.speed)}
disabled={!supportsPreviewPlaybackRate(option.speed)}
title={
!supportsPreviewPlaybackRate(option.speed)
? tSettings(
"speed.unsupported",
"Not supported for preview on this device",
)
: undefined
}
className={cn(
"h-auto w-full rounded-lg border px-0.5 py-2 text-center shadow-sm transition-all duration-200 ease-out cursor-pointer",
isActive
? "border-[#06b6d4] bg-[#06b6d4] text-white"
: "border-foreground/5 bg-foreground/5 text-muted-foreground hover:bg-foreground/10 hover:border-foreground/10 hover:text-foreground",
)}
>
<span className="text-[10px] font-semibold">{option.label}</span>
</Button>
);
})}
</div>
<div className="mt-2 flex flex-col gap-2 border-t border-foreground/5 pt-3">
<SectionLabel>{tSettings("audio.title", "Audio")}</SectionLabel>
<div className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-1.5">
<div>
<span className="text-[10px] text-muted-foreground">
{tSettings("clip.mute", "Mute")}
</span>
<p className="text-[9px] text-muted-foreground/50 mt-0.5">
{selectedClipMuted
? tSettings("clip.mutedState", "Audio is muted")
: tSettings("clip.unmutedState", "Audio is playing")}
</p>
</div>
<Switch
checked={selectedClipMuted ?? false}
onCheckedChange={(v) => onClipMutedChange?.(v)}
className="data-[state=checked]:bg-[#06b6d4] scale-75"
/>
</div>
{hasClipSourceAudio && (
<div className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-1.5">
<span className="text-[10px] text-muted-foreground">
{tSettings(
"clip.separateClipFromAudio",
"Separate clip from audio",
)}
</span>
<Switch
checked={selectedClipShowSourceAudio ?? false}
onCheckedChange={(v) => onClipShowSourceAudioChange?.(v)}
className="data-[state=checked]:bg-[#06b6d4] scale-75"
/>
</div>
)}
</div>
{selectedClipId && hasClipSourceAudio && sourceAudioTrackMeta.length > 0 && (
<div className="mt-1 flex flex-col gap-3">
{sourceAudioTrackMeta.map((track) => {
const settings = sourceAudioTrackSettings[track.id] ?? {
volume: 1,
normalize: false,
};
return (
<div
key={track.id}
className="rounded-lg border border-foreground/10 bg-foreground/[0.03] px-3 py-2"
>
<div className="mb-2 flex items-center justify-between">
<span className="text-[11px] font-medium text-foreground">
{track.label}
</span>
<button
type="button"
onClick={() => {
onSourceAudioTrackVolumeChange?.(track.id, 1);
onSourceAudioTrackNormalizeChange?.(
track.id,
false,
);
}}
className="text-[10px] text-[#2563EB] transition-opacity hover:opacity-80"
>
{t("common.actions.reset", "Reset")}
</button>
</div>
<div className="mb-2 flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-1.5">
<span className="text-[10px] text-muted-foreground">
{tSettings("audio.normalize", "Normalize")}
</span>
<Switch
checked={settings.normalize}
onCheckedChange={(v) =>
onSourceAudioTrackNormalizeChange?.(track.id, v)
}
className="data-[state=checked]:bg-[#06b6d4] scale-75"
/>
</div>
<SliderControl
label={tSettings("audio.volume", "Volume")}
value={settings.volume}
defaultValue={1}
min={0}
max={1}
step={0.01}
onChange={(v) =>
onSourceAudioTrackVolumeChange?.(track.id, v)
}
formatValue={(v) => `${Math.round(v * 100)}%`}
parseInput={(text) =>
parseFloat(text.replace(/%$/, "")) / 100
}
/>
</div>
);
})}
</div>
)}
<section className="flex flex-col gap-3">
<SectionLabel>{tSettings("clip.title", "Clip")}</SectionLabel>
<SliderControl
label={tSettings("speed.label", "Speed")}
value={selectedClipSpeed ?? 1}
defaultValue={1}
min={0.25}
max={maxClipSpeed}
step={0.25}
onChange={(value) => onClipSpeedChange?.(value)}
formatValue={(value) => `${value}×`}
parseInput={(text) => Number.parseFloat(text)}
/>
<label className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-2">
<span className="text-[11px] text-muted-foreground">
{tSettings("clip.mute", "Mute clip")}
</span>
<Switch
checked={selectedClipMuted ?? false}
onCheckedChange={(muted) => onClipMutedChange?.(muted)}
aria-label={tSettings("clip.mute", "Mute clip")}
className="data-[state=checked]:bg-[#06b6d4] scale-75"
/>
</label>
</section>
);
@@ -342,7 +342,6 @@ export default function VideoEditor() {
activeEffectSection,
appearance,
timeline,
audio,
zoomCommands,
clipCommands,
audioCommands,
@@ -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<typeof useAppearanceState>;
timeline: ReturnType<typeof useTimelineState>;
audio: ReturnType<typeof useVideoEditorAudio>;
zoomCommands: ReturnType<typeof useZoomRegionCommands>;
clipCommands: ReturnType<typeof useClipRegionCommands>;
audioCommands: ReturnType<typeof useAudioRegionCommands>;
@@ -45,7 +43,6 @@ export function useEditorSettingsPanelProps(input: Input): ComponentProps<typeof
activeEffectSection,
appearance,
timeline,
audio,
zoomCommands,
clipCommands,
audioCommands,
@@ -94,17 +91,9 @@ export function useEditorSettingsPanelProps(input: Input): ComponentProps<typeof
selectedClipId: timeline.selectedClipId,
selectedClipSpeed: selectedClip?.speed ?? (timeline.selectedClipId ? 1 : null),
selectedClipMuted: selectedClip?.muted ?? (timeline.selectedClipId ? false : null),
selectedClipShowSourceAudio:
selectedClip?.showSourceAudio ?? (timeline.selectedClipId ? false : null),
onClipSpeedChange: clipCommands.handleClipSpeedChange,
onClipMutedChange: clipCommands.handleClipMutedChange,
onClipShowSourceAudioChange: clipCommands.handleClipShowSourceAudioChange,
onClipDelete: clipCommands.handleClipDelete,
hasClipSourceAudio: timeline.hasClipSourceAudio,
sourceAudioTrackMeta: audio.sourceAudioTrackMeta,
sourceAudioTrackSettings: audio.selectedClipSourceAudioTrackSettings,
onSourceAudioTrackVolumeChange: audio.onSelectedClipSourceAudioTrackVolumeChange,
onSourceAudioTrackNormalizeChange: audio.onSelectedClipSourceAudioTrackNormalizeChange,
selectedAudioId: timeline.selectedAudioId,
selectedAudioVolume: selectedAudio?.volume ?? null,
selectedAudioNormalize:
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Clip",
"mute": "Stummschalten",
"mute": "Clip stummschalten",
"mutedState": "Audio ist stummgeschaltet",
"unmutedState": "Audio wird wiedergegeben",
"separateClipFromAudio": "Clip vom Audio trennen",
+1 -1
View File
@@ -25,7 +25,7 @@
},
"clip": {
"title": "Clip",
"mute": "Mute",
"mute": "Mute clip",
"mutedState": "Audio is muted",
"unmutedState": "Audio is playing",
"separateClipFromAudio": "Separate clip from audio",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Clip",
"mute": "Silenciar",
"mute": "Silenciar clip",
"mutedState": "El audio está silenciado",
"unmutedState": "El audio se está reproduciendo",
"separateClipFromAudio": "Separar clip del audio",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Clip",
"mute": "Sourdine",
"mute": "Couper le son du clip",
"mutedState": "Le son est coupé",
"unmutedState": "Le son est activé",
"separateClipFromAudio": "Séparer le clip de l'audio",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Clip",
"mute": "Disattiva audio",
"mute": "Disattiva audio clip",
"mutedState": "Audio disattivato",
"unmutedState": "Audio in riproduzione",
"separateClipFromAudio": "Separa clip dall'audio",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "클립",
"mute": "음소거",
"mute": "클립 음소거",
"mutedState": "오디오가 음소거됨",
"unmutedState": "오디오가 재생 중",
"separateClipFromAudio": "클립에서 오디오 분리",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Clip",
"mute": "Dempen",
"mute": "Clip dempen",
"mutedState": "Audio is gedempt",
"unmutedState": "Audio wordt afgespeeld",
"separateClipFromAudio": "Clip van audio scheiden",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Clipe",
"mute": "Mudo",
"mute": "Silenciar clipe",
"mutedState": "O áudio está mudo",
"unmutedState": "O áudio está tocando",
"separateClipFromAudio": "Separar áudio do clipe",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "Клип",
"mute": "Без звука",
"mute": "Отключить звук клипа",
"mutedState": "Звук выключен",
"unmutedState": "Звук включен",
"separateClipFromAudio": "Отделить аудио от клипа",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "片段",
"mute": "静音",
"mute": "将片段静音",
"mutedState": "音频已静音",
"unmutedState": "音频正在播放",
"separateClipFromAudio": "将剪辑与音频分离",
+1 -1
View File
@@ -19,7 +19,7 @@
},
"clip": {
"title": "片段",
"mute": "靜音",
"mute": "將片段靜音",
"mutedState": "音訊已靜音",
"unmutedState": "音訊正在播放",
"separateClipFromAudio": "將片段與音訊分離",