From e3d4a330dfd814072c414b9cd1496b961df972fb Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sat, 9 May 2026 19:18:16 -0700 Subject: [PATCH] ui revamp --- electron/electron-env.d.ts | 1 + electron/preload.ts | 3 + electron/windows.ts | 7 + src/components/launch/LaunchWindow.module.css | 24 +- src/components/launch/LaunchWindow.tsx | 49 +- .../launch/SourceSelector.module.css | 46 +- src/components/launch/SourceSelector.tsx | 30 +- .../video-editor/AnnotationOverlay.tsx | 5 +- .../video-editor/AnnotationSettingsPanel.tsx | 33 +- .../video-editor/BlurSettingsPanel.tsx | 96 +- src/components/video-editor/SettingsPanel.tsx | 2314 +++++++++-------- src/components/video-editor/VideoEditor.tsx | 552 ++-- src/components/video-editor/VideoPlayback.tsx | 38 +- .../video-editor/projectPersistence.test.ts | 2 +- src/components/video-editor/timeline/Item.tsx | 12 +- .../timeline/ItemGlass.module.css | 46 +- src/components/video-editor/timeline/Row.tsx | 18 +- .../video-editor/timeline/TimelineEditor.tsx | 47 +- src/components/video-editor/types.ts | 2 +- src/index.css | 168 +- src/lib/blurEffects.test.ts | 4 +- src/lib/blurEffects.ts | 10 +- 22 files changed, 1878 insertions(+), 1629 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 744c2c7..1d528cd 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -157,6 +157,7 @@ interface Window { saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>; hudOverlayHide: () => void; hudOverlayClose: () => void; + setHudOverlayIgnoreMouseEvents: (ignore: boolean) => void; showCountdownOverlay: (value: number, runId: number) => Promise; setCountdownOverlayValue: (value: number, runId: number) => Promise; hideCountdownOverlay: (runId: number) => Promise; diff --git a/electron/preload.ts b/electron/preload.ts index 5334a00..5980b4c 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -16,6 +16,9 @@ contextBridge.exposeInMainWorld("electronAPI", { hudOverlayClose: () => { ipcRenderer.send("hud-overlay-close"); }, + setHudOverlayIgnoreMouseEvents: (ignore: boolean) => { + ipcRenderer.send("hud-overlay-ignore-mouse-events", ignore); + }, getSources: async (opts: Electron.SourcesOptions) => { return await ipcRenderer.invoke("get-sources", opts); }, diff --git a/electron/windows.ts b/electron/windows.ts index f94009a..4d4e752 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -24,6 +24,12 @@ ipcMain.on("hud-overlay-hide", () => { } }); +ipcMain.on("hud-overlay-ignore-mouse-events", (_event, ignore: boolean) => { + if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) { + hudOverlayWindow.setIgnoreMouseEvents(ignore, { forward: true }); + } +}); + /** * Creates the always-on-top HUD overlay window centred at the bottom of the * primary display. The window is frameless, transparent, and follows the user @@ -63,6 +69,7 @@ export function createHudOverlayWindow(): BrowserWindow { backgroundThrottling: false, }, }); + win.setIgnoreMouseEvents(true, { forward: true }); // Follow the user across macOS Spaces (virtual desktops). // Without this the HUD stays pinned to the Space it was first opened on. diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index 132fa0a..20b8718 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -44,13 +44,13 @@ position: fixed; right: 0; top: 0; - width: 12rem; - padding: 0.375rem; - border-radius: 0.75rem; - border: 1px solid rgba(255, 255, 255, 0.14); - background: linear-gradient(160deg, rgba(28, 29, 42, 0.98), rgba(18, 19, 28, 0.98)); - box-shadow: 0 20px 45px rgba(0, 0, 0, 0.55); - backdrop-filter: blur(14px); + width: 11rem; + padding: 0.25rem; + border-radius: 0.625rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(8, 9, 12, 0.96); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.48), inset 0 1px 0 rgba(255, 255, 255, 0.045); + backdrop-filter: blur(18px) saturate(140%); pointer-events: auto; box-sizing: border-box; } @@ -60,10 +60,10 @@ display: flex; align-items: center; justify-content: space-between; - padding: 0.5rem 0.625rem; - border-radius: 0.5rem; + padding: 0.425rem 0.5rem; + border-radius: 0.45rem; font-size: 11px; - color: rgba(255, 255, 255, 0.88); + color: rgba(255, 255, 255, 0.72); background: transparent; border: 0; cursor: pointer; @@ -72,12 +72,12 @@ .languageMenuItem:hover, .languageMenuItem:focus-visible { - background: rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.075); color: #ffffff; outline: none; } .languageMenuItemActive { - background: rgba(255, 255, 255, 0.12); + background: rgba(52, 178, 123, 0.14); color: #ffffff; } diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index bffbd9c..6a14fc0 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -62,16 +62,16 @@ function getIcon(name: IconName, className?: string) { } const hudGroupClasses = - "flex items-center gap-0.5 bg-white/5 rounded-full transition-colors duration-150 hover:bg-white/[0.08]"; + "flex items-center gap-0.5 rounded-xl border border-white/[0.07] bg-white/[0.045] transition-colors duration-150 hover:bg-white/[0.075]"; const hudIconBtnClasses = - "flex items-center justify-center p-2 rounded-full transition-all duration-150 cursor-pointer text-white hover:bg-white/10 hover:scale-[1.08] active:scale-95"; + "flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150 cursor-pointer text-white hover:bg-white/10 active:scale-95"; const hudAuxIconBtnClasses = - "flex items-center justify-center p-1.5 rounded-full transition-colors duration-150 text-white/55 hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed"; + "flex h-7 w-7 items-center justify-center rounded-lg transition-colors duration-150 text-white/55 hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed"; const windowBtnClasses = - "flex items-center justify-center p-2 rounded-full transition-all duration-150 cursor-pointer opacity-50 hover:opacity-90 hover:bg-white/[0.08]"; + "flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150 cursor-pointer opacity-50 hover:opacity-90 hover:bg-white/[0.08]"; const hudSidebarClasses = "ml-0.5 pl-1.5 border-l border-white/10 flex items-center gap-0.5"; @@ -87,6 +87,7 @@ export function LaunchWindow() { resolveSystemLocaleSuggestion, } = useI18n(); const suggestedLanguageName = systemLocaleSuggestion ? getLocaleName(systemLocaleSuggestion) : ""; + const activeLanguageLabel = getLocaleName(locale).split(/\s+/)[0] || locale.toUpperCase(); const { recording, @@ -248,6 +249,13 @@ export function LaunchWindow() { return () => cancelAnimationFrame(id); }, [isLanguageMenuOpen]); + useEffect(() => { + window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(true); + return () => { + window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(false); + }; + }, []); + const [selectedSource, setSelectedSource] = useState("Screen"); const [hasSelectedSource, setHasSelectedSource] = useState(false); @@ -320,6 +328,12 @@ export function LaunchWindow() { // recording toolbar widened (issue #305).
{ + const target = event.target as HTMLElement | null; + const shouldCapture = Boolean(target?.closest("[data-hud-interactive='true']")); + window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(!shouldCapture); + }} + onPointerLeave={() => window.electronAPI?.setHudOverlayIgnoreMouseEvents?.(true)} > {systemLocaleSuggestion && (
{/* Mic selector */} {showMicControls && (
setIsMicHovered(true)} onMouseLeave={() => setIsMicHovered(false)} onFocus={() => setIsMicFocused(true)} @@ -409,7 +424,7 @@ export function LaunchWindow() { {/* Webcam selector */} {showWebcamControls && (
setIsWebcamHovered(true)} onMouseLeave={() => setIsWebcamHovered(false)} onFocus={() => setIsWebcamFocused(true)} @@ -485,7 +500,8 @@ export function LaunchWindow() { {/* HUD bar — fixed at bottom center, viewport-relative, never moves */}
{/* Drag handle */}
@@ -494,13 +510,15 @@ export function LaunchWindow() { {/* Source selector */} {/* Audio controls group */} @@ -548,7 +566,7 @@ export function LaunchWindow() { ? paused ? "bg-amber-500/10 hover:bg-amber-500/15" : "bg-red-500/12 hover:bg-red-500/16" - : "bg-white/5 hover:bg-white/[0.08]" + : "bg-white/[0.06] hover:bg-white/[0.10]" }`} onClick={toggleRecording} disabled={!hasSelectedSource && !recording} @@ -624,11 +642,12 @@ export function LaunchWindow() { aria-expanded={isLanguageMenuOpen} aria-haspopup="menu" onClick={() => setIsLanguageMenuOpen((open) => !open)} - className={`h-8 w-8 rounded-lg border border-white/10 bg-white/5 text-white/85 shadow-none transition-colors hover:bg-white/10 ${styles.electronNoDrag}`} + className={`flex h-8 items-center gap-1.5 rounded-lg border border-white/10 bg-white/[0.045] px-2 text-white/85 shadow-none transition-colors hover:bg-white/10 ${styles.electronNoDrag}`} > -
- -
+ + + {activeLanguageLabel} +
diff --git a/src/components/launch/SourceSelector.module.css b/src/components/launch/SourceSelector.module.css index 48d5507..5bd4d96 100644 --- a/src/components/launch/SourceSelector.module.css +++ b/src/components/launch/SourceSelector.module.css @@ -1,8 +1,8 @@ .glassContainer { - background: linear-gradient(135deg, rgba(28, 28, 34, 0.92) 0%, rgba(18, 18, 22, 0.88) 100%); - backdrop-filter: blur(20px) saturate(160%); - -webkit-backdrop-filter: blur(20px) saturate(160%); - border-radius: 30px; + background: linear-gradient(145deg, rgba(13, 14, 17, 0.94) 0%, rgba(8, 9, 12, 0.9) 100%); + backdrop-filter: blur(24px) saturate(150%); + -webkit-backdrop-filter: blur(24px) saturate(150%); + border-radius: 24px; corner-shape: squircle; /* Removed box-shadow here because electron doesn't round corners of the shadow, thereby leaving a square border shadow conflicting with the rounded corners of the SourceSelector. @@ -11,34 +11,36 @@ /* box-shadow: 0 0px 16px 0 rgba(0, 0, 0, 0.32), 0 1px 3px 0 rgba(0, 0, 0, 0.18) inset; */ - border: 1.5px solid rgba(60, 60, 80, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); } .sourceCard { corner-shape: squircle; - border-radius: 20px; - background: linear-gradient(120deg, rgba(38, 38, 48, 0.98) 0%, rgba(24, 24, 32, 0.96) 100%); - border: 1px solid rgba(60, 60, 80, 0.22); - box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.18); + border-radius: 13px; + background: rgba(255, 255, 255, 0.045); + border: 1px solid rgba(255, 255, 255, 0.07); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035); transition: box-shadow 0.2s ease, border-color 0.2s ease, + background-color 0.2s ease, transform 0.2s ease; cursor: pointer; } .sourceCard:hover { - border-color: rgba(120, 120, 160, 0.35); + background: rgba(255, 255, 255, 0.065); + border-color: rgba(255, 255, 255, 0.14); transform: translateY(-1px); - box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.04); } .selected { - border: 1.5px solid #34b27b; - background: linear-gradient(120deg, rgba(52, 178, 123, 0.08) 0%, rgba(38, 38, 48, 0.98) 100%); + border-color: rgba(52, 178, 123, 0.68); + background: linear-gradient(145deg, rgba(52, 178, 123, 0.13), rgba(255, 255, 255, 0.045)); box-shadow: - 0 0 12px rgba(52, 178, 123, 0.15), - 0 0 4px rgba(52, 178, 123, 0.1); + 0 0 0 1px rgba(52, 178, 123, 0.18) inset, + 0 12px 28px rgba(0, 0, 0, 0.22); } .selected:hover { @@ -46,16 +48,16 @@ } .icon { - width: 13px; - height: 13px; + width: 12px; + height: 12px; color: #c7d2fe; } .name { - font-size: 0.8rem; + font-size: 0.72rem; color: #e4e4e7; font-weight: 500; - letter-spacing: 0.01em; + letter-spacing: 0; } .cardText { @@ -65,14 +67,14 @@ /* Checkmark badge */ .checkBadge { - width: 18px; - height: 18px; + width: 17px; + height: 17px; background: #34b27b; border-radius: 9999px; display: flex; align-items: center; justify-content: center; - box-shadow: 0 0 8px rgba(52, 178, 123, 0.4); + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.35); } /* scrollbar */ diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index a2aec55..1a0675a 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -77,24 +77,24 @@ export function SourceSelector() { return (
handleSourceSelect(source)} > -
+
{source.name} {isSelected && ( -
+
- +
)}
-
+
{source.appIcon && ( )} @@ -106,21 +106,21 @@ export function SourceSelector() { return (
-
+
- + {t("sourceSelector.screens", { count: String(screenSources.length) })} {t("sourceSelector.windows", { count: String(windowSources.length) })} @@ -128,14 +128,14 @@ export function SourceSelector() {
{screenSources.map(renderSourceCard)}
{windowSources.map(renderSourceCard)}
@@ -143,18 +143,18 @@ export function SourceSelector() {
-
+
diff --git a/src/components/video-editor/AnnotationOverlay.tsx b/src/components/video-editor/AnnotationOverlay.tsx index f416c32..345423f 100644 --- a/src/components/video-editor/AnnotationOverlay.tsx +++ b/src/components/video-editor/AnnotationOverlay.tsx @@ -82,7 +82,7 @@ export function AnnotationOverlay({ ); const [livePointerPoint, setLivePointerPoint] = useState<{ x: number; y: number } | null>(null); const mosaicCanvasRef = useRef(null); - const blurType = annotation.type === "blur" ? (annotation.blurData?.type ?? "blur") : "blur"; + const blurType = "mosaic"; const blurOverlayColor = annotation.type === "blur" ? getBlurOverlayColor(annotation.blurData) : ""; const mosaicGridOverlayColor = @@ -106,7 +106,7 @@ export function AnnotationOverlay({ const { x, y, width, height } = liveRect; useEffect(() => { - if (annotation.type !== "blur" || blurType !== "mosaic") { + if (annotation.type !== "blur") { return; } void previewFrameVersion; @@ -173,7 +173,6 @@ export function AnnotationOverlay({ ); }, [ annotation, - blurType, containerHeight, containerWidth, height, diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 3f8064e..72e25a8 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -7,7 +7,6 @@ import { ChevronDown, Copy, Image as ImageIcon, - Info, Italic, Trash2, Type, @@ -148,39 +147,39 @@ export function AnnotationSettingsPanel({ }; return ( -
-
-
- {t("annotation.title")} - +
+
+
+ {t("annotation.active")} +
{t("annotation.title")}
{/* Type Selector */} onTypeChange(value as AnnotationType)} - className="mb-6" + className="mb-4" > - + {t("annotation.typeText")} {t("annotation.typeImage")}
- -
-
- - {t("annotation.shortcutsAndTips")} -
-
    -
  • {t("annotation.tipMovePlayhead")}
  • -
  • {t("annotation.tipTabCycle")}
  • -
  • {t("annotation.tipShiftTabCycle")}
  • -
-
); diff --git a/src/components/video-editor/BlurSettingsPanel.tsx b/src/components/video-editor/BlurSettingsPanel.tsx index 09bfe3a..7ead894 100644 --- a/src/components/video-editor/BlurSettingsPanel.tsx +++ b/src/components/video-editor/BlurSettingsPanel.tsx @@ -1,12 +1,5 @@ -import { Info, Trash2 } from "lucide-react"; +import { Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { useScopedT } from "@/contexts/I18nContext"; import { getBlurOverlayColor } from "@/lib/blurEffects"; @@ -19,9 +12,7 @@ import { DEFAULT_BLUR_BLOCK_SIZE, DEFAULT_BLUR_DATA, MAX_BLUR_BLOCK_SIZE, - MAX_BLUR_INTENSITY, MIN_BLUR_BLOCK_SIZE, - MIN_BLUR_INTENSITY, } from "./types"; interface BlurSettingsPanelProps { @@ -49,13 +40,15 @@ export function BlurSettingsPanel({ ]; return ( -
-
-
- {t("annotation.blurShape")} - - {t("annotation.active")} +
+
+
+ + {t("annotation.blurTypeMosaic")} +
+ {t("annotation.typeBlur")} +
@@ -69,6 +62,7 @@ export function BlurSettingsPanel({ const nextBlurData: BlurData = { ...DEFAULT_BLUR_DATA, ...blurRegion.blurData, + type: "mosaic", shape: shape.value, }; onBlurDataChange(nextBlurData); @@ -77,7 +71,7 @@ export function BlurSettingsPanel({ }); }} className={cn( - "h-16 rounded-lg border flex flex-col items-center justify-center transition-all p-2 gap-1", + "h-12 rounded-lg border flex items-center justify-center transition-all p-2 gap-2", isActive ? "bg-[#34B27B] border-[#34B27B]" : "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20", @@ -99,7 +93,7 @@ export function BlurSettingsPanel({ )} /> )} - + {t(`annotation.${shape.labelKey}`)} @@ -107,34 +101,6 @@ export function BlurSettingsPanel({ })}
-
- - -
-
-
+
- {blurRegion.blurData?.type === "mosaic" - ? t("annotation.mosaicBlockSize") - : t("annotation.blurIntensity")} + {t("annotation.mosaicBlockSize")} - {Math.round( - blurRegion.blurData?.type === "mosaic" - ? (blurRegion.blurData?.blockSize ?? DEFAULT_BLUR_BLOCK_SIZE) - : (blurRegion.blurData?.intensity ?? DEFAULT_BLUR_DATA.intensity), - )} + {Math.round(blurRegion.blurData?.blockSize ?? DEFAULT_BLUR_BLOCK_SIZE)} px
{ onBlurDataChange({ ...DEFAULT_BLUR_DATA, ...blurRegion.blurData, - ...(blurRegion.blurData?.type === "mosaic" - ? { blockSize: values[0] } - : { intensity: values[0] }), + type: "mosaic", + blockSize: values[0], }); }} onValueCommit={() => onBlurDataCommit?.()} - min={blurRegion.blurData?.type === "mosaic" ? MIN_BLUR_BLOCK_SIZE : MIN_BLUR_INTENSITY} - max={blurRegion.blurData?.type === "mosaic" ? MAX_BLUR_BLOCK_SIZE : MAX_BLUR_INTENSITY} + min={MIN_BLUR_BLOCK_SIZE} + max={MAX_BLUR_BLOCK_SIZE} step={1} className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" /> @@ -231,16 +187,6 @@ export function BlurSettingsPanel({ {t("annotation.deleteAnnotation")} - -
-
- - {t("annotation.shortcutsAndTips")} -
-
    -
  • {t("annotation.tipMovePlayhead")}
  • -
-
); diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index c625f87..9ef66b1 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -7,8 +7,11 @@ import { FileDown, Film, Image, + LayoutPanelTop, Lock, + MousePointerClick, Palette, + SlidersHorizontal, Sparkles, Star, Trash2, @@ -16,7 +19,7 @@ import { Upload, X, } from "lucide-react"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { type ComponentType, useCallback, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { Accordion, @@ -185,7 +188,7 @@ function ZoomFocusCoordInput({ onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} - className="w-[90px] h-8 rounded-md border border-white/10 bg-white/5 px-2 text-xs text-slate-200 outline-none focus:border-[#34B27B]/50 focus:ring-1 focus:ring-[#34B27B]/30 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none disabled:opacity-50 disabled:cursor-not-allowed" + className="h-7 w-full rounded-md border border-white/10 bg-white/5 px-2 text-[11px] text-slate-200 outline-none focus:border-[#34B27B]/50 focus:ring-1 focus:ring-[#34B27B]/30 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none disabled:opacity-50 disabled:cursor-not-allowed" /> ); } @@ -319,6 +322,8 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [ { depth: 6, label: "5×" }, ]; +type SettingsPanelMode = "background" | "effects" | "layout" | "cursor" | "export"; + export function SettingsPanel({ cursorHighlight, onCursorHighlightChange, @@ -402,6 +407,7 @@ export function SettingsPanel({ onSaveDiagnostic, }: SettingsPanelProps) { const t = useScopedT("settings"); + const [activePanelMode, setActivePanelMode] = useState("background"); // Resolved URLs are for DOM rendering only (backgroundImage). The canonical // `/wallpapers/wallpaperN.jpg` form in WALLPAPER_PATHS is what gets persisted // on click — never the machine-specific file:// URL. @@ -534,6 +540,31 @@ export function SettingsPanel({ const zoomEnabled = Boolean(selectedZoomDepth); const trimEnabled = Boolean(selectedTrimId); + const hasTimelineSelection = Boolean(selectedZoomId || selectedTrimId || selectedSpeedId); + const panelModes: Array<{ + id: SettingsPanelMode; + label: string; + icon: ComponentType<{ className?: string }>; + disabled?: boolean; + }> = [ + { id: "background", label: t("background.title"), icon: Palette }, + { id: "effects", label: t("effects.title"), icon: SlidersHorizontal }, + { id: "layout", label: t("layout.title"), icon: LayoutPanelTop, disabled: !hasWebcam }, + { id: "cursor", label: t("effects.cursorHighlight.title"), icon: MousePointerClick }, + ]; + const exportPanelMode = { + id: "export" as const, + label: exportFormat === "gif" ? t("export.gifButton") : t("export.videoButton"), + icon: Download, + }; + const activeModeLabel = hasTimelineSelection + ? selectedZoomId + ? t("zoom.level") + : selectedSpeedId + ? t("speed.playbackSpeed") + : t("trim.deleteRegion") + : ([...panelModes, exportPanelMode].find((mode) => mode.id === activePanelMode)?.label ?? + t("background.title")); const handleDeleteClick = () => { if (selectedZoomId && onZoomDelete) { @@ -615,6 +646,42 @@ export function SettingsPanel({ const selectedBlur = selectedBlurId ? blurRegions.find((region) => region.id === selectedBlurId) : null; + const commonFooterLinks = ( +
+ + {onSaveDiagnostic && ( + + )} + +
+ ); // If an annotation is selected, show annotation settings instead if ( @@ -625,88 +692,113 @@ export function SettingsPanel({ onAnnotationDelete ) { return ( - onAnnotationContentChange(selectedAnnotation.id, content)} - onTypeChange={(type) => onAnnotationTypeChange(selectedAnnotation.id, type)} - onStyleChange={(style) => onAnnotationStyleChange(selectedAnnotation.id, style)} - onFigureDataChange={ - onAnnotationFigureDataChange - ? (figureData) => onAnnotationFigureDataChange(selectedAnnotation.id, figureData) - : undefined - } - onDuplicate={ - onAnnotationDuplicate ? () => onAnnotationDuplicate(selectedAnnotation.id) : undefined - } - onDelete={() => onAnnotationDelete(selectedAnnotation.id)} - /> +
+
+ onAnnotationContentChange(selectedAnnotation.id, content)} + onTypeChange={(type) => onAnnotationTypeChange(selectedAnnotation.id, type)} + onStyleChange={(style) => onAnnotationStyleChange(selectedAnnotation.id, style)} + onFigureDataChange={ + onAnnotationFigureDataChange + ? (figureData) => onAnnotationFigureDataChange(selectedAnnotation.id, figureData) + : undefined + } + onDuplicate={ + onAnnotationDuplicate ? () => onAnnotationDuplicate(selectedAnnotation.id) : undefined + } + onDelete={() => onAnnotationDelete(selectedAnnotation.id)} + /> +
+
+ {commonFooterLinks} +
+
); } if (selectedBlur && onBlurDataChange && onBlurDelete) { return ( - onBlurDataChange(selectedBlur.id, blurData)} - onBlurDataCommit={onBlurDataCommit} - onDelete={() => onBlurDelete(selectedBlur.id)} - /> +
+
+ onBlurDataChange(selectedBlur.id, blurData)} + onBlurDataCommit={onBlurDataCommit} + onDelete={() => onBlurDelete(selectedBlur.id)} + /> +
+
+ {commonFooterLinks} +
+
); } return ( -
-
-
-
- {t("zoom.level")} -
- {zoomEnabled && selectedZoomDepth && ( - - {selectedZoomCustomScale != null - ? `${selectedZoomCustomScale.toFixed(2)}×` - : ZOOM_DEPTH_OPTIONS.find((o) => o.depth === selectedZoomDepth)?.label} - - )} - -
-
-
- {ZOOM_DEPTH_OPTIONS.map((option) => { - const effectiveScale = - selectedZoomCustomScale ?? - (selectedZoomDepth != null ? ZOOM_DEPTH_SCALES[selectedZoomDepth] : null); - const isActive = effectiveScale === ZOOM_DEPTH_SCALES[option.depth]; - return ( - - ); - })} +
+
+
+ {panelModes.map((mode) => { + const Icon = mode.icon; + const isActive = activePanelMode === mode.id && !hasTimelineSelection; + return ( + + ); + })} + + +
+
+
+ {activeModeLabel} +
{zoomEnabled && ( -
-
- {t("zoom.customScale")} - +
+
+ + {t("zoom.level")} + + {( selectedZoomCustomScale ?? (selectedZoomDepth != null @@ -716,896 +808,941 @@ export function SettingsPanel({ ×
- + {ZOOM_DEPTH_OPTIONS.map((option) => { + const effectiveScale = + selectedZoomCustomScale ?? + (selectedZoomDepth != null ? ZOOM_DEPTH_SCALES[selectedZoomDepth] : null); + const isActive = effectiveScale === ZOOM_DEPTH_SCALES[option.depth]; + return ( + + ); + })} +
+ {zoomEnabled && ( +
+ onZoomCustomScaleChange?.(values[0])} + onValueCommit={() => onZoomCustomScaleCommit?.()} + disabled={!zoomEnabled} + className="relative flex w-full touch-none select-none items-center py-1" + > + + + + + +
+ {MIN_ZOOM_SCALE.toFixed(1)}× + {MAX_ZOOM_SCALE.toFixed(1)}× +
+
+ )} + {zoomEnabled && hasCursorTelemetry && ( +
+ + {t("zoom.focusMode.title")} + +
+ {(["manual", "auto"] as const).map((mode) => { + const isActive = selectedZoomFocusMode === mode; + return ( + + ); + })} +
+
+ )} + {zoomEnabled && + selectedZoomFocusMode !== "auto" && + selectedZoomFocus && + onZoomFocusCoordinateChange && + (() => { + const effectiveZoomScale = + selectedZoomCustomScale ?? (selectedZoomDepth != null ? ZOOM_DEPTH_SCALES[selectedZoomDepth] - : MIN_ZOOM_SCALE), - ]} - onValueChange={(values) => onZoomCustomScaleChange?.(values[0])} - onValueCommit={() => onZoomCustomScaleCommit?.()} - disabled={!zoomEnabled} - className="relative flex w-full touch-none select-none items-center py-1" - > - - - - - -
- {MIN_ZOOM_SCALE.toFixed(1)}× - {MAX_ZOOM_SCALE.toFixed(1)}× -
-
- )} - {!zoomEnabled && ( -

{t("zoom.selectRegion")}

- )} - {zoomEnabled && hasCursorTelemetry && ( -
- - {t("zoom.focusMode.title")} - -
- {(["manual", "auto"] as const).map((mode) => { - const isActive = selectedZoomFocusMode === mode; + : MIN_ZOOM_SCALE); + const bounds = getFocusBoundsForScale(effectiveZoomScale); + const xRange = bounds.maxX - bounds.minX; + const yRange = bounds.maxY - bounds.minY; + const focusToPercentX = (cx: number) => + xRange <= 0 + ? 50 + : Math.max(0, Math.min(100, ((cx - bounds.minX) / xRange) * 100)); + const focusToPercentY = (cy: number) => + yRange <= 0 + ? 50 + : Math.max(0, Math.min(100, ((cy - bounds.minY) / yRange) * 100)); + const percentToFocusX = (p: number) => + xRange <= 0 ? bounds.minX : bounds.minX + (p / 100) * xRange; + const percentToFocusY = (p: number) => + yRange <= 0 ? bounds.minY : bounds.minY + (p / 100) * yRange; return ( - +
+
+ + + onZoomFocusCoordinateChange({ + cx: percentToFocusX(p), + cy: selectedZoomFocus.cy, + }) + } + onCommit={onZoomFocusCoordinateCommit} + /> +
+
+ + + onZoomFocusCoordinateChange({ + cx: selectedZoomFocus.cx, + cy: percentToFocusY(p), + }) + } + onCommit={onZoomFocusCoordinateCommit} + /> +
+
+
); - })} -
- {selectedZoomFocusMode === "auto" && ( -

- {t("zoom.focusMode.autoDescription")} -

- )} -
- )} - {zoomEnabled && - selectedZoomFocusMode !== "auto" && - selectedZoomFocus && - onZoomFocusCoordinateChange && - (() => { - const effectiveZoomScale = - selectedZoomCustomScale ?? - (selectedZoomDepth != null ? ZOOM_DEPTH_SCALES[selectedZoomDepth] : MIN_ZOOM_SCALE); - const bounds = getFocusBoundsForScale(effectiveZoomScale); - const xRange = bounds.maxX - bounds.minX; - const yRange = bounds.maxY - bounds.minY; - const focusToPercentX = (cx: number) => - xRange <= 0 ? 50 : Math.max(0, Math.min(100, ((cx - bounds.minX) / xRange) * 100)); - const focusToPercentY = (cy: number) => - yRange <= 0 ? 50 : Math.max(0, Math.min(100, ((cy - bounds.minY) / yRange) * 100)); - const percentToFocusX = (p: number) => - xRange <= 0 ? bounds.minX : bounds.minX + (p / 100) * xRange; - const percentToFocusY = (p: number) => - yRange <= 0 ? bounds.minY : bounds.minY + (p / 100) * yRange; - return ( -
- - {t("zoom.position.title")} + })()} + {zoomEnabled && ( +
+ + {t("zoom.threeD.title")} -
-
- - - onZoomFocusCoordinateChange({ - cx: percentToFocusX(p), - cy: selectedZoomFocus.cy, - }) - } - onCommit={onZoomFocusCoordinateCommit} - /> -
-
- - - onZoomFocusCoordinateChange({ - cx: selectedZoomFocus.cx, - cy: percentToFocusY(p), - }) - } - onCommit={onZoomFocusCoordinateCommit} - /> -
- - {t("zoom.position.hint")} - -
-
- ); - })()} - {zoomEnabled && ( -
- - {t("zoom.threeD.title")} - -
- {ROTATION_3D_PRESET_ORDER.map((preset) => { - const isActive = selectedZoomRotationPreset === preset; - return ( - - ); - })} -
-
- )} - - {zoomEnabled && ( - - )} -
- - {trimEnabled && ( -
- -
- )} - -
-
- {t("speed.playbackSpeed")} - {selectedSpeedId && selectedSpeedValue && ( - - {SPEED_OPTIONS.find((o) => o.speed === selectedSpeedValue)?.label ?? - `${selectedSpeedValue}×`} - - )} -
-
- {SPEED_OPTIONS.map((option) => { - const isActive = selectedSpeedValue === option.speed; - return ( - - ); - })} -
-
-
- - {t("speed.customPlaybackSpeed")} - - {selectedSpeedId ? ( - onSpeedChange?.(val)} - onError={() => toast.error(t("speed.maxSpeedError"))} - /> - ) : ( -
-
- -- -
- × -
- )} -
-
- {!selectedSpeedId && ( -

{t("speed.selectRegion")}

- )} - {selectedSpeedId && ( - - )} -
- - - {hasWebcam && ( - - -
- - {t("layout.title")} -
-
- -
-
- {t("layout.preset")} -
- -
- {webcamLayoutPreset === "picture-in-picture" && ( -
-
- {t("layout.webcamShape")} -
-
- {( - [ - { value: "rectangle", label: "Rect" }, - { value: "circle", label: "Circle" }, - { value: "square", label: "Square" }, - { value: "rounded", label: "Rounded" }, - ] as Array<{ value: WebcamMaskShape; label: string }> - ).map((shape) => ( - - ))} -
-
- )} - {webcamLayoutPreset === "picture-in-picture" && ( -
-
-
- {t("layout.webcamSize")} -
-
- {webcamSizePreset}% -
-
- onWebcamSizePresetChange?.(values[0])} - onValueCommit={() => onWebcamSizePresetCommit?.()} - min={10} - max={50} - step={1} - className="w-full" - /> -
- )} -
-
- )} - - - -
- - {t("effects.title")} -
-
- -
-
-
- {t("effects.blurBg")} -
- -
-
- -
-
-
-
- {t("effects.motionBlur")} -
- - {motionBlurAmount === 0 ? t("effects.off") : motionBlurAmount.toFixed(2)} - -
- onMotionBlurChange?.(values[0])} - onValueCommit={() => onMotionBlurCommit?.()} - min={0} - max={1} - step={0.01} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" - /> -
-
-
-
- {t("effects.shadow")} -
- - {Math.round(shadowIntensity * 100)}% - -
- onShadowChange?.(values[0])} - onValueCommit={() => onShadowCommit?.()} - min={0} - max={1} - step={0.01} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" - /> -
-
-
-
- {t("effects.roundness")} -
- {borderRadius}px -
- onBorderRadiusChange?.(values[0])} - onValueCommit={() => onBorderRadiusCommit?.()} - min={0} - max={16} - step={0.5} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" - /> -
-
-
-
- {t("effects.padding")} -
- - {webcamLayoutPreset === "vertical-stack" ? "—" : `${padding}%`} - -
- onPaddingChange?.(values[0])} - onValueCommit={() => onPaddingCommit?.()} - min={0} - max={100} - step={1} - disabled={webcamLayoutPreset === "vertical-stack"} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" - /> -
-
- - {cursorHighlight && onCursorHighlightChange && ( -
-
-
- {t("effects.cursorHighlight.title")} -
- -
-
- {(["dot", "ring"] as const).map((style) => ( - - ))} -
-
-
-
- {t("effects.cursorHighlight.size")} -
- - {cursorHighlight.sizePx}px - -
- - onCursorHighlightChange({ - ...cursorHighlight, - sizePx: values[0], - }) - } - min={10} - max={36} - step={1} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" - /> -
- {cursorHighlightSupportsClicks && ( -
-
- {t("effects.cursorHighlight.onlyOnClicks")} -
- -
- )} -
-
- {t("effects.cursorHighlight.color")} -
- - - - - - - onCursorHighlightChange({ - ...cursorHighlight, - color, - }) - } - /> - - -
-
-
-
- {t("effects.cursorHighlight.offsetX")} -
- - {(cursorHighlight.offsetXNorm * 100).toFixed(1)}% - -
- - onCursorHighlightChange({ - ...cursorHighlight, - offsetXNorm: values[0], - }) - } - min={-0.25} - max={0.25} - step={0.005} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" - /> -
-
-
-
- {t("effects.cursorHighlight.offsetY")} -
- - {(cursorHighlight.offsetYNorm * 100).toFixed(1)}% - -
- - onCursorHighlightChange({ - ...cursorHighlight, - offsetYNorm: values[0], - }) - } - min={-0.25} - max={0.25} - step={0.005} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" - /> + ); + })}
)} + {zoomEnabled && ( + + )} +
+ )} + + {trimEnabled && ( +
- - +
+ )} - - -
- - {t("background.title")} + {selectedSpeedId && ( +
+
+ + {t("speed.playbackSpeed")} + + {selectedSpeedId && selectedSpeedValue && ( + + {SPEED_OPTIONS.find((o) => o.speed === selectedSpeedValue)?.label ?? + `${selectedSpeedValue}×`} + + )}
- - - - - - {t("background.image")} - - - {t("background.color")} - - - {t("background.gradient")} - - - -
- - +
+ {SPEED_OPTIONS.map((option) => { + const isActive = selectedSpeedValue === option.speed; + return ( + ); + })} +
+
+ + {t("speed.customPlaybackSpeed")} + + {selectedSpeedId ? ( + onSpeedChange?.(val)} + onError={() => toast.error(t("speed.maxSpeedError"))} + /> + ) : ( +
+
+ -- +
+ × +
+ )} +
+ {selectedSpeedId && ( + + )} +
+ )} -
- {customImages.map((imageUrl, idx) => { - const isSelected = selected === imageUrl; - return ( -
onWallpaperChange(imageUrl)} - role="button" - > + {!hasTimelineSelection && ( + + {hasWebcam && activePanelMode === "layout" && ( + + +
+ + {t("layout.title")} +
+
+ +
+
+ {t("layout.preset")} +
+ +
+ {webcamLayoutPreset === "picture-in-picture" && ( +
+
+ {t("layout.webcamShape")} +
+
+ {( + [ + { value: "rectangle", label: "Rect" }, + { value: "circle", label: "Circle" }, + { value: "square", label: "Square" }, + { value: "rounded", label: "Rounded" }, + ] as Array<{ value: WebcamMaskShape; label: string }> + ).map((shape) => ( + ))} +
+
+ )} + {webcamLayoutPreset === "picture-in-picture" && ( +
+
+
+ {t("layout.webcamSize")} +
+
+ {webcamSizePreset}% +
+
+ onWebcamSizePresetChange?.(values[0])} + onValueCommit={() => onWebcamSizePresetCommit?.()} + min={10} + max={50} + step={1} + className="w-full" + /> +
+ )} +
+
+ )} + + {(activePanelMode === "effects" || activePanelMode === "cursor") && ( + + +
+ {activePanelMode === "cursor" ? ( + + ) : ( + + )} + + {activePanelMode === "cursor" + ? t("effects.cursorHighlight.title") + : t("effects.title")} + +
+
+ + {activePanelMode === "effects" && ( + <> +
+
+
+ {t("effects.blurBg")} +
+ +
+
+ +
+
+
+
+ {t("effects.motionBlur")} +
+ + {motionBlurAmount === 0 + ? t("effects.off") + : motionBlurAmount.toFixed(2)} + +
+ onMotionBlurChange?.(values[0])} + onValueCommit={() => onMotionBlurCommit?.()} + min={0} + max={1} + step={0.01} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+
+
+ {t("effects.shadow")} +
+ + {Math.round(shadowIntensity * 100)}% + +
+ onShadowChange?.(values[0])} + onValueCommit={() => onShadowCommit?.()} + min={0} + max={1} + step={0.01} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+
+
+ {t("effects.roundness")} +
+ + {borderRadius}px + +
+ onBorderRadiusChange?.(values[0])} + onValueCommit={() => onBorderRadiusCommit?.()} + min={0} + max={16} + step={0.5} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+
+
+ {t("effects.padding")} +
+ + {webcamLayoutPreset === "vertical-stack" ? "—" : `${padding}%`} + +
+ onPaddingChange?.(values[0])} + onValueCommit={() => onPaddingCommit?.()} + min={0} + max={100} + step={1} + disabled={webcamLayoutPreset === "vertical-stack"} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+ + )} + + {activePanelMode === "cursor" && cursorHighlight && onCursorHighlightChange && ( +
+
+
+ {t("effects.cursorHighlight.title")} +
+ +
+
+ {(["dot", "ring"] as const).map((style) => ( + + ))} +
+
+
+
+ {t("effects.cursorHighlight.size")} +
+ + {cursorHighlight.sizePx}px + +
+ + onCursorHighlightChange({ + ...cursorHighlight, + sizePx: values[0], + }) + } + min={10} + max={36} + step={1} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+ {cursorHighlightSupportsClicks && ( +
+
+ {t("effects.cursorHighlight.onlyOnClicks")} +
+
- ); - })} - - {WALLPAPER_PATHS.map((canonicalPath, i) => { - const previewUrl = wallpaperPreviewUrls[i] ?? canonicalPath; - const isSelected = selected === canonicalPath; - return ( -
onWallpaperChange(canonicalPath)} - role="button" - /> - ); - })} -
- - - - { - setSelectedColor(color); - onWallpaperChange(color); - }} - /> - - - -
- {GRADIENTS.map((g, idx) => ( + )}
{ - setGradient(g); - onWallpaperChange(g); - }} - role="button" - /> - ))} + className={ + cursorHighlight.enabled ? "" : "opacity-40 pointer-events-none" + } + > +
+ {t("effects.cursorHighlight.color")} +
+ + + + + + + onCursorHighlightChange({ + ...cursorHighlight, + color, + }) + } + /> + + +
+
+
+
+ {t("effects.cursorHighlight.offsetX")} +
+ + {(cursorHighlight.offsetXNorm * 100).toFixed(1)}% + +
+ + onCursorHighlightChange({ + ...cursorHighlight, + offsetXNorm: values[0], + }) + } + min={-0.25} + max={0.25} + step={0.005} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+
+
+ {t("effects.cursorHighlight.offsetY")} +
+ + {(cursorHighlight.offsetYNorm * 100).toFixed(1)}% + +
+ + onCursorHighlightChange({ + ...cursorHighlight, + offsetYNorm: values[0], + }) + } + min={-0.25} + max={0.25} + step={0.005} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+ )} + + + )} + + {activePanelMode === "background" && ( + + +
+ + {t("background.title")}
-
-
- -
-
-
+ + + + + + {t("background.image")} + + + {t("background.color")} + + + {t("background.gradient")} + + + +
+ + + + +
+ {customImages.map((imageUrl, idx) => { + const isSelected = selected === imageUrl; + return ( +
onWallpaperChange(imageUrl)} + role="button" + > + +
+ ); + })} + + {WALLPAPER_PATHS.map((canonicalPath, i) => { + const previewUrl = wallpaperPreviewUrls[i] ?? canonicalPath; + const isSelected = selected === canonicalPath; + return ( +
onWallpaperChange(canonicalPath)} + role="button" + /> + ); + })} +
+ + + + { + setSelectedColor(color); + onWallpaperChange(color); + }} + /> + + + +
+ {GRADIENTS.map((g, idx) => ( +
{ + setGradient(g); + onWallpaperChange(g); + }} + role="button" + /> + ))} +
+ +
+ + + + )} + + )} +
{showCropModal && cropRegion && onCropChange && ( @@ -1731,182 +1868,155 @@ export function SettingsPanel({ )} -
-
- - -
- - {exportFormat === "mp4" && ( -
- - - -
- )} - - {exportFormat === "gif" && ( -
-
-
- {GIF_FRAME_RATES.map((rate) => ( - - ))} -
-
- {Object.entries(GIF_SIZE_PRESETS).map(([key, _preset]) => ( - - ))} -
+
+ {activePanelMode === "export" && !hasTimelineSelection && ( + <> +
+ +
-
- - {gifOutputDimensions.width} × {gifOutputDimensions.height}px - -
- {t("gifSettings.loop")} - + + {exportFormat === "mp4" && ( +
+ + +
-
-
- )} + )} - {unsavedExport && ( - - )} - + {exportFormat === "gif" && ( +
+
+
+ {GIF_FRAME_RATES.map((rate) => ( + + ))} +
+
+ {Object.entries(GIF_SIZE_PRESETS).map(([key, _preset]) => ( + + ))} +
+
+
+ + {gifOutputDimensions.width} × {gifOutputDimensions.height}px + +
+ {t("gifSettings.loop")} + +
+
+
+ )} -
- - {onSaveDiagnostic && ( - + )} + - )} - -
+ + {exportFormat === "gif" ? t("export.gifButton") : t("export.videoButton")} + + + )} + + {commonFooterLinks}
); diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 12832ad..e1f6a60 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -1869,7 +1869,7 @@ export default function VideoEditor() {