mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
Simplify clip settings with a speed slider and mute toggle
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"clip": {
|
||||
"title": "클립",
|
||||
"mute": "음소거",
|
||||
"mute": "클립 음소거",
|
||||
"mutedState": "오디오가 음소거됨",
|
||||
"unmutedState": "오디오가 재생 중",
|
||||
"separateClipFromAudio": "클립에서 오디오 분리",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"clip": {
|
||||
"title": "Клип",
|
||||
"mute": "Без звука",
|
||||
"mute": "Отключить звук клипа",
|
||||
"mutedState": "Звук выключен",
|
||||
"unmutedState": "Звук включен",
|
||||
"separateClipFromAudio": "Отделить аудио от клипа",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"clip": {
|
||||
"title": "片段",
|
||||
"mute": "静音",
|
||||
"mute": "将片段静音",
|
||||
"mutedState": "音频已静音",
|
||||
"unmutedState": "音频正在播放",
|
||||
"separateClipFromAudio": "将剪辑与音频分离",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"clip": {
|
||||
"title": "片段",
|
||||
"mute": "靜音",
|
||||
"mute": "將片段靜音",
|
||||
"mutedState": "音訊已靜音",
|
||||
"unmutedState": "音訊正在播放",
|
||||
"separateClipFromAudio": "將片段與音訊分離",
|
||||
|
||||
Reference in New Issue
Block a user