diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 55597aeb2..5065a93ac 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-10 w-full items-center justify-between rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 4e9b838d1..870cde3c4 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -10,7 +10,7 @@ const Switch = React.forwardRef< diff --git a/src/components/video-editor/AnnotationOverlay.tsx b/src/components/video-editor/AnnotationOverlay.tsx index 4bdd44eca..c08621e6b 100644 --- a/src/components/video-editor/AnnotationOverlay.tsx +++ b/src/components/video-editor/AnnotationOverlay.tsx @@ -240,7 +240,7 @@ export function AnnotationOverlay({ bounds="parent" className={cn( "cursor-move transition-all", - isSelected && "ring-2 ring-[#2563EB] ring-offset-2 ring-offset-transparent", + isSelected && "ring-2 ring-primary ring-offset-2 ring-offset-transparent", )} style={{ zIndex: isSelectedBoost ? zIndex + 1000 : zIndex, // Boost selected annotation to ensure it's on top @@ -256,7 +256,7 @@ export function AnnotationOverlay({ width: "12px", height: "12px", backgroundColor: isSelected ? "white" : "transparent", - border: isSelected ? "2px solid #2563EB" : "none", + border: isSelected ? "2px solid hsl(var(--primary))" : "none", borderRadius: "50%", left: "-6px", top: "-6px", @@ -266,7 +266,7 @@ export function AnnotationOverlay({ width: "12px", height: "12px", backgroundColor: isSelected ? "white" : "transparent", - border: isSelected ? "2px solid #2563EB" : "none", + border: isSelected ? "2px solid hsl(var(--primary))" : "none", borderRadius: "50%", right: "-6px", top: "-6px", @@ -276,7 +276,7 @@ export function AnnotationOverlay({ width: "12px", height: "12px", backgroundColor: isSelected ? "white" : "transparent", - border: isSelected ? "2px solid #2563EB" : "none", + border: isSelected ? "2px solid hsl(var(--primary))" : "none", borderRadius: "50%", left: "-6px", bottom: "-6px", @@ -286,7 +286,7 @@ export function AnnotationOverlay({ width: "12px", height: "12px", backgroundColor: isSelected ? "white" : "transparent", - border: isSelected ? "2px solid #2563EB" : "none", + border: isSelected ? "2px solid hsl(var(--primary))" : "none", borderRadius: "50%", right: "-6px", bottom: "-6px", diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index af5326320..fec055454 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -146,7 +146,7 @@ export function AnnotationSettingsPanel({ {t("annotations.settings")} - + {t("annotations.active")} @@ -160,21 +160,21 @@ export function AnnotationSettingsPanel({ {t("annotations.text")} {t("annotations.image")} {t("annotations.blur")} @@ -211,7 +211,7 @@ export function AnnotationSettingsPanel({ onChange={(e) => onContentChange(e.target.value)} placeholder={t("annotations.textPlaceholder")} rows={5} - className="w-full px-3 py-2 bg-foreground/5 border border-foreground/10 rounded-lg text-foreground text-sm placeholder:text-muted-foreground/70 focus:outline-none focus:ring-2 focus:ring-[#2563EB] focus:border-transparent resize-none" + className="w-full px-3 py-2 bg-foreground/5 border border-foreground/10 rounded-lg text-foreground text-sm placeholder:text-muted-foreground/70 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent resize-none" /> @@ -321,7 +321,7 @@ export function AnnotationSettingsPanel({ : "bold", }) } - className="h-8 w-8 data-[state=on]:bg-[#2563EB] data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" + className="h-8 w-8 data-[state=on]:bg-primary data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" > @@ -341,7 +341,7 @@ export function AnnotationSettingsPanel({ : "italic", }) } - className="h-8 w-8 data-[state=on]:bg-[#2563EB] data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" + className="h-8 w-8 data-[state=on]:bg-primary data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" > @@ -362,7 +362,7 @@ export function AnnotationSettingsPanel({ : "underline", }) } - className="h-8 w-8 data-[state=on]:bg-[#2563EB] data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" + className="h-8 w-8 data-[state=on]:bg-primary data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" > @@ -377,7 +377,7 @@ export function AnnotationSettingsPanel({ value="left" aria-label={t("annotations.alignLeft")} onClick={() => onStyleChange({ textAlign: "left" })} - className="h-8 w-8 data-[state=on]:bg-[#2563EB] data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" + className="h-8 w-8 data-[state=on]:bg-primary data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" > @@ -385,7 +385,7 @@ export function AnnotationSettingsPanel({ value="center" aria-label={t("annotations.alignCenter")} onClick={() => onStyleChange({ textAlign: "center" })} - className="h-8 w-8 data-[state=on]:bg-[#2563EB] data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" + className="h-8 w-8 data-[state=on]:bg-primary data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" > @@ -393,7 +393,7 @@ export function AnnotationSettingsPanel({ value="right" aria-label={t("annotations.alignRight")} onClick={() => onStyleChange({ textAlign: "right" })} - className="h-8 w-8 data-[state=on]:bg-[#2563EB] data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" + className="h-8 w-8 data-[state=on]:bg-primary data-[state=on]:text-white text-muted-foreground hover:bg-foreground/5 hover:text-foreground" > @@ -517,7 +517,7 @@ export function AnnotationSettingsPanel({
{isActive ? ( ) : null} @@ -280,13 +280,13 @@ export function ExportSettingsMenu({ })}
{!isLegacyModel && nvidiaCudaExportAvailable ? ( -
+
{tSettings("export.nvidiaCuda.title", "NVIDIA CUDA")} - + {tSettings("export.nvidiaCuda.badge", "Experimental")}
@@ -304,7 +304,7 @@ export function ExportSettingsMenu({ "export.nvidiaCuda.toggle", "Enable experimental NVIDIA CUDA export", )} - className="shrink-0 scale-75 data-[state=checked]:bg-[#2563EB]" + className="shrink-0 scale-75 data-[state=checked]:bg-primary" />
) : null} @@ -331,7 +331,7 @@ export function ExportSettingsMenu({ "export.captionSidecar.toggle", "Export captions sidecar files", )} - className="shrink-0 scale-75 data-[state=checked]:bg-[#2563EB]" + className="shrink-0 scale-75 data-[state=checked]:bg-primary" />
) : null} @@ -442,7 +442,7 @@ export function ExportSettingsMenu({
@@ -453,7 +453,7 @@ export function ExportSettingsMenu({ type="button" size="lg" onClick={onExport} - className="h-11 w-full gap-2 rounded-lg bg-[#2563EB] text-sm font-semibold text-white transition-colors duration-200 hover:bg-[#2563EB]/90" + className="h-11 w-full gap-2 rounded-lg bg-primary text-sm font-semibold text-white transition-colors duration-200 hover:bg-primary/90" > {tSettings("export.exportVideo", undefined, { diff --git a/src/components/video-editor/ExtensionManager.tsx b/src/components/video-editor/ExtensionManager.tsx index 99fdc3eb1..72ffd0cd0 100644 --- a/src/components/video-editor/ExtensionManager.tsx +++ b/src/components/video-editor/ExtensionManager.tsx @@ -2,8 +2,8 @@ import { PuzzlePiece } from "@phosphor-icons/react"; export default function ExtensionManager() { return ( -
-
+
+

Extensions

diff --git a/src/components/video-editor/FormatSelector.tsx b/src/components/video-editor/FormatSelector.tsx index c999eeb32..bddc78a43 100644 --- a/src/components/video-editor/FormatSelector.tsx +++ b/src/components/video-editor/FormatSelector.tsx @@ -50,9 +50,9 @@ export function FormatSelector({ onClick={() => onFormatChange(option.value)} className={cn( "relative flex flex-col items-center gap-2 p-4 rounded-xl border transition-all duration-200", - "focus:outline-none focus:ring-2 focus:ring-[#2563EB]/50 focus:ring-offset-2 focus:ring-offset-editor-dialog", + "focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2 focus:ring-offset-editor-dialog", isSelected - ? "bg-[#2563EB]/10 border-[#2563EB]/50 text-[#2563EB] dark:text-white" + ? "bg-primary/10 border-primary/50 text-primary dark:text-white" : "bg-foreground/5 border-foreground/10 text-muted-foreground hover:bg-foreground/10 hover:border-foreground/20 hover:text-foreground", disabled && "opacity-50 cursor-not-allowed", )} @@ -60,7 +60,7 @@ export function FormatSelector({
{option.icon} @@ -72,7 +72,7 @@ export function FormatSelector({
{isSelected && ( -
+
)} ); diff --git a/src/components/video-editor/KeyboardShortcutsHelp.tsx b/src/components/video-editor/KeyboardShortcutsHelp.tsx index f9c24c636..351912598 100644 --- a/src/components/video-editor/KeyboardShortcutsHelp.tsx +++ b/src/components/video-editor/KeyboardShortcutsHelp.tsx @@ -22,7 +22,7 @@ export function KeyboardShortcutsHelp() { return (
- +
@@ -33,7 +33,7 @@ export function KeyboardShortcutsHelp() { type="button" onClick={openConfig} title={t("keyboardShortcuts.customizeTooltip")} - className="flex items-center gap-1 text-[10px] text-muted-foreground/70 hover:text-[#2563EB] transition-colors" + className="flex items-center gap-1 text-[10px] text-muted-foreground/70 hover:text-primary transition-colors" > {t("keyboardShortcuts.customize")} @@ -44,7 +44,7 @@ export function KeyboardShortcutsHelp() { {SHORTCUT_ACTIONS.map((action) => (
{SHORTCUT_LABELS[action]} - + {formatBinding(shortcuts[action], isMac)}
@@ -55,7 +55,7 @@ export function KeyboardShortcutsHelp() { {t("keyboardShortcuts.panTimeline")} - + {scrollLabels.pan}
@@ -63,7 +63,7 @@ export function KeyboardShortcutsHelp() { {t("keyboardShortcuts.zoomTimeline")} - + {scrollLabels.zoom}
@@ -71,7 +71,7 @@ export function KeyboardShortcutsHelp() { {t("keyboardShortcuts.cycleAnnotations")} - + {t("keyboardShortcuts.tab")}
diff --git a/src/components/video-editor/PlaybackControls.tsx b/src/components/video-editor/PlaybackControls.tsx index 82558a7ac..d6e50ff05 100644 --- a/src/components/video-editor/PlaybackControls.tsx +++ b/src/components/video-editor/PlaybackControls.tsx @@ -68,7 +68,7 @@ export default function PlaybackControls({ {/* Custom Track Background */}
diff --git a/src/components/video-editor/ProjectBrowserDialog.tsx b/src/components/video-editor/ProjectBrowserDialog.tsx index ab71fd6b0..23129bbed 100644 --- a/src/components/video-editor/ProjectBrowserDialog.tsx +++ b/src/components/video-editor/ProjectBrowserDialog.tsx @@ -219,7 +219,7 @@ export default function ProjectBrowserDialog({ )} {entry.isCurrent ? (
- + Current
@@ -301,7 +301,7 @@ export default function ProjectBrowserDialog({ )} {entry.isCurrent ? (
- + Current
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 8b289e3f5..21988a4a0 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -5,6 +5,7 @@ import { Trash as Trash2, UploadSimple as Upload, X, + CornersOut, BoundingBox, Crop, Camera, Monitor, Sun, Moon, Globe, Sparkle, Keyboard, MagnifyingGlassPlus, Play, SpeakerHigh, Scissors, Gauge, VideoCamera, TextAa, Eye, ArrowsClockwise, ArrowsLeftRight, ArrowsOutCardinal, Bug, DownloadSimple, Trash, Subtitles, } from "@phosphor-icons/react"; import { AnimatePresence, LayoutGroup, motion } from "motion/react"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -187,11 +188,43 @@ function getBackgroundTabForWallpaper(value: string): BackgroundTab { return "image"; } -function SectionLabel({ children }: { children: React.ReactNode }) { +function SectionLabel({ + title, + icon: Icon, + subtitle, + children, + className +}: { + title?: React.ReactNode; + icon?: React.ElementType; + subtitle?: string; + children?: React.ReactNode; + className?: string; +}) { + if (!Icon && !title) { + return ( +

+ {children} +

+ ); + } return ( -

- {children} -

+
+ {Icon && ( +
+ +
+ )} +
+

{title || children}

+ {subtitle &&

{subtitle}

} +
+
); } @@ -279,7 +312,7 @@ function MotionPresetCards({ }) { return (
-
{title}
+
{MOTION_PRESET_ORDER.map((presetId) => { const Icon = presetId === "focused" ? CursorClick : PresentationChart; @@ -292,9 +325,9 @@ function MotionPresetCards({ onClick={() => onApply(presetId)} className={cn( "rounded-xl border px-3 py-3 text-left transition-all", - "border-foreground/10 bg-foreground/[0.03] hover:border-foreground/20 hover:bg-foreground/[0.06]", + "border-transparent bg-foreground/[0.03] hover:border-transparent hover:bg-foreground/[0.06]", isActive && - "border-[#2563EB]/70 bg-[#2563EB]/12 shadow-[inset_0_0_0_1px_rgba(37,99,235,0.15)]", + "border-primary/70 bg-primary/12 shadow-[inset_0_0_0_1px_rgba(37,99,235,0.15)]", )} >
@@ -302,7 +335,7 @@ function MotionPresetCards({ className={cn( "mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-foreground/10 bg-black/10 text-muted-foreground", isActive && - "border-[#2563EB]/30 bg-[#2563EB]/10 text-[#75A6FF]", + "border-primary/30 bg-primary/10 text-[#75A6FF]", )} > @@ -440,12 +473,12 @@ function CursorClickEffectCards({ return (
-
{title}
+ @@ -1764,7 +1797,7 @@ export function SettingsPanel({
-
+
{( [ { value: "image", label: tSettings("background.image") }, @@ -1784,7 +1817,7 @@ export function SettingsPanel({ {isActive ? ( @@ -2119,12 +2149,12 @@ export function SettingsPanel({ />
- {tSettings("effects.padding")} +
@@ -2219,12 +2249,12 @@ export function SettingsPanel({ const cropSectionContent = (
- {tSettings("sections.crop", "Crop")} + {isCropped ? ( @@ -2280,259 +2310,323 @@ export function SettingsPanel({ ); const captionsSectionContent = ( -
-
-
- {tSettings("sections.captions", "Captions")} +
+
+
+
-
- {tSettings("captions.enabled", "Show")} +
+
-
-
- -
-
-
- {tSettings("captions.language", "Language")} +
+ {/* Model & Language Card */} +
+ {/* Select Model row */} +
+
+
+ +
+
+ {tSettings("captions.selectModel", "Select Model")} + Choose Whisper model +
+
+ +
+
+ {/* Language row */} +
+
+
+ +
+
+ {tSettings("captions.language", "Language")} + Detection language +
+
+
-
-
-
+ + {/* Download progress bar */} + {whisperModelDownloadStatus === "downloading" && ( +
+
+ {tSettings("captions.downloading", "Downloading...")} {Math.round(whisperModelDownloadProgress)}% +
+
+
+
+
+ )} + + {/* Action buttons */} +
+
{whisperModelDownloadStatus === "downloading" ? ( - + + {tSettings("captions.downloading", "Downloading...")} {Math.round(whisperModelDownloadProgress)}% + ) : whisperModelPath ? ( - + ) : ( - + )} - +
-
-
- - {isGeneratingCaptions ? ( -
-
- {tSettings( - "captions.generatingStatus", - "Generating captions. This can take a moment.", - )} -
-
-
- ) : null} +
- {whisperModelDownloadStatus === "downloading" ? ( -
-
+ {isGeneratingCaptions && ( +
+
+ {tSettings( + "captions.generatingStatus", + "Generating captions. This can take a moment.", + )} +
+
- ) : null} + )}
+
-
-
- {tSettings("captions.animation", "Animation")} +
+
+
+
+ +
+
+ {tSettings("captions.animation", "Animation")} + Caption entry style +
+
+
- -
-
-
- {tSettings("captions.timelineQuickAdd", "Hover to add on timeline")} +
+ +
+
+
+
+ +
+
+ {tSettings("captions.textColor", "Text color")} + Caption font color +
+
+ + updateAutoCaptionSettings({ textColor: event.target.value }) + } + className="h-8 w-10 rounded-lg cursor-pointer bg-transparent" + />
- - updateAutoCaptionSettings({ timelineQuickAdd }) - } - aria-label={tSettings( - "captions.timelineQuickAdd", - "Hover to add on timeline", - )} - className="data-[state=checked]:bg-[#2563EB] scale-75" - />
-
); @@ -2541,36 +2635,41 @@ export function SettingsPanel({ const settingsSectionContent = (
- {t("editor.theme.appearance", "Appearance")} -
+ +
{( [ - { value: "light", label: t("editor.theme.light", "Light") }, - { value: "dark", label: t("editor.theme.dark", "Dark") }, - { value: "system", label: t("editor.theme.system", "System") }, + { value: "light", label: t("editor.theme.light", "Light"), icon: Sun }, + { value: "dark", label: t("editor.theme.dark", "Dark"), icon: Moon }, + { value: "system", label: t("editor.theme.system", "System"), icon: Monitor }, ] as const - ).map((option) => ( - - ))} + ).map((option) => { + const Icon = option.icon; + const isSelected = themePreference === option.value; + return ( + + ); + })}
- {t("common.app.language", "Language")} + {hasUnsavedChanges ? ( - + ) : null} {projectDisplayName} diff --git a/src/components/video-editor/layout/EditorPresetMenu.tsx b/src/components/video-editor/layout/EditorPresetMenu.tsx index 1c360a553..d2f4e6cd2 100644 --- a/src/components/video-editor/layout/EditorPresetMenu.tsx +++ b/src/components/video-editor/layout/EditorPresetMenu.tsx @@ -69,7 +69,7 @@ export function EditorPresetMenu({ t, presets }: Props) { @@ -93,7 +93,7 @@ export function EditorPresetMenu({ t, presets }: Props) { className={cn( "flex items-center gap-2 rounded-xl border px-2 py-2 text-sm transition-colors", isActive - ? "border-[#2563EB]/20 bg-[#2563EB]/10 text-foreground" + ? "border-primary/20 bg-primary/10 text-foreground" : "border-foreground/8 bg-foreground/[0.03] text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground", )} > @@ -104,7 +104,7 @@ export function EditorPresetMenu({ t, presets }: Props) { > {preset.name} {isActive ? ( - + ) : null} + + - - {getAspectRatioLabel(aspectRatio)} - - - - - ( + setAspectRatio(ratio)} + className="flex cursor-pointer items-center justify-between gap-3 text-muted-foreground hover:bg-foreground/10 hover:text-foreground" + > + {getAspectRatioLabel(ratio)} + {aspectRatio === ratio ? ( + + ) : null} + + ))} + + +
+ -
+ + {t("settings.crop.title")} + {isCropped ? ( + + ) : null} + +
+ )}
+ {isFullscreen && ( + + )}
-
-
-
- - + {!isFullscreen && ( +
+
+ + + + + + { + const nextTrack = + timeline.annotationRegions.length > 0 + ? Math.max( + ...timeline.annotationRegions.map( + (region) => region.trackIndex ?? 0, + ), + ) + 1 + : 0; + timelineRef.current?.addAnnotation(nextTrack); + }} + className="cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground" + > + {t("timeline.annotation.label")} + + { + const nextTrack = + timeline.audioRegions.length > 0 + ? Math.max( + ...timeline.audioRegions.map( + (region) => region.trackIndex ?? 0, + ), + ) + 1 + : 0; + timelineRef.current?.addAudio(nextTrack); + }} + className="cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground" + > + {t("timeline.audio.label")} + + + +
- - - { - const nextTrack = - timeline.annotationRegions.length > 0 - ? Math.max( - ...timeline.annotationRegions.map( - (region) => region.trackIndex ?? 0, - ), - ) + 1 - : 0; - timelineRef.current?.addAnnotation(nextTrack); - }} - className="cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground" + + - - -
+ + +
-
-
- - {formatTime(projection.timelinePlayheadTime)} - - - - - - {formatTime(projection.timelineDuration)} - -
-
+
+
+ + {formatTime(projection.timelinePlayheadTime)} + + + + + + {formatTime(projection.timelineDuration)} + +
+
-
-
- -
-
0 - ? `max(calc(${previewVolume * 100}% - 6px), 1.2rem)` - : 0, - }} - /> -
- - {Math.round(previewVolume * 100)}% - - setPreviewVolume(Number(event.target.value))} - className="absolute inset-0 h-full w-full cursor-ew-resize opacity-0" - /> +
+
+ +
+
0 + ? `max(calc(${previewVolume * 100}% - 6px), 1.2rem)` + : 0, + }} + /> +
+ + {Math.round(previewVolume * 100)}% + + setPreviewVolume(Number(event.target.value))} + className="absolute inset-0 h-full w-full cursor-ew-resize opacity-0" + /> +
+
+
-
+ )}
); diff --git a/src/components/video-editor/layout/EditorShell.tsx b/src/components/video-editor/layout/EditorShell.tsx index e85928280..ac91ee2c2 100644 --- a/src/components/video-editor/layout/EditorShell.tsx +++ b/src/components/video-editor/layout/EditorShell.tsx @@ -145,48 +145,54 @@ export function EditorShell(props: Props) { ); return ( -
- - -
-
- +
+
+ +
+
+ +
+
+
+ +
- +
+ +
{editorDialogs} -
- {sections.map((section) => { +
+
+ {sections.map((section, index) => { const isActive = activeSection === section.id; return ( -
- + {isActive ? ( + + {/* Top Inverted Curve */} + {index !== 0 && ( + + + + )} + {/* Bottom Inverted Curve */} + {index !== sections.length - 1 && ( + + + + )} + + ) : null} + +
); })} -
- +
{activeSection === "extensions" ? ( diff --git a/src/components/video-editor/timeline/ItemGlass.module.css b/src/components/video-editor/timeline/ItemGlass.module.css index 1ad9fdd0c..764ced3db 100644 --- a/src/components/video-editor/timeline/ItemGlass.module.css +++ b/src/components/video-editor/timeline/ItemGlass.module.css @@ -15,8 +15,8 @@ .glassGreen.selected { background: #1c3e89; - border-color: #2563eb; - box-shadow: inset 0 0 0 1.5px #2563eb; + border-color: hsl(var(--primary)); + box-shadow: inset 0 0 0 1.5px hsl(var(--primary)); z-index: 10; } @@ -125,8 +125,8 @@ .glassCyan.selected { background: #1c3e89; - border-color: #2563eb; - box-shadow: inset 0 0 0 1.5px #2563eb; + border-color: hsl(var(--primary)); + box-shadow: inset 0 0 0 1.5px hsl(var(--primary)); z-index: 10; } @@ -206,8 +206,8 @@ } :global(:root:not(.dark)) .glassGreen.selected { background: linear-gradient(180deg, #93c5fd 0%, #3b82f6 100%); - border-color: #2563eb; - box-shadow: inset 0 0 0 1.5px #2563eb; + border-color: hsl(var(--primary)); + box-shadow: inset 0 0 0 1.5px hsl(var(--primary)); } :global(:root:not(.dark)) .glassRed { @@ -276,8 +276,8 @@ } :global(:root:not(.dark)) .glassCyan.selected { background: linear-gradient(180deg, #93c5fd 0%, #3b82f6 100%); - border-color: #2563eb; - box-shadow: inset 0 0 0 1.5px #2563eb; + border-color: hsl(var(--primary)); + box-shadow: inset 0 0 0 1.5px hsl(var(--primary)); } :global(:root:not(.dark)) .glassDarkGreen { diff --git a/src/components/video-editor/timeline/components/axis/TimelineAxis.tsx b/src/components/video-editor/timeline/components/axis/TimelineAxis.tsx index a285515c3..557ef6305 100644 --- a/src/components/video-editor/timeline/components/axis/TimelineAxis.tsx +++ b/src/components/video-editor/timeline/components/axis/TimelineAxis.tsx @@ -91,7 +91,7 @@ export default function TimelineAxis({ videoDurationMs, currentTimeMs }: Timelin className={cn( "text-[10px] font-medium tabular-nums tracking-tight", Math.abs(marker.time - currentTimeMs) < 1 - ? "text-[#2563EB]" + ? "text-primary" : "text-foreground/40", )} > diff --git a/src/components/video-editor/timeline/components/markers/KeyframeMarkers.tsx b/src/components/video-editor/timeline/components/markers/KeyframeMarkers.tsx index e35ff5122..5f22b8275 100644 --- a/src/components/video-editor/timeline/components/markers/KeyframeMarkers.tsx +++ b/src/components/video-editor/timeline/components/markers/KeyframeMarkers.tsx @@ -75,7 +75,7 @@ const KeyframeMarkers: React.FC = ({ return (
{ e.stopPropagation(); @@ -98,7 +98,7 @@ export default function PlaybackCursor({ className="absolute -top-1 left-1/2 -translate-x-1/2 hover:scale-125 transition-transform" style={{ width: "16px", height: "16px" }} > -
+
@@ -89,7 +89,7 @@ export default function TimelineToolbar({ onClick={onSuggestZooms} variant="ghost" size="icon" - className="h-7 w-7 text-muted-foreground hover:text-[#2563EB] hover:bg-[#2563EB]/10 transition-all" + className="h-7 w-7 text-muted-foreground hover:text-primary hover:bg-primary/10 transition-all" title={suggestZoomsLabel} aria-label={suggestZoomsLabel} > @@ -150,7 +150,7 @@ export default function TimelineToolbar({ > {getAspectRatioLabel(ratio)} {aspectRatio === ratio && ( - + )} ))} @@ -165,7 +165,7 @@ export default function TimelineToolbar({ onCustomAspectWidthChange(event.target.value.replace(/\D/g, "")) } onKeyDown={onCustomAspectRatioKeyDown} - className="w-12 h-7 rounded border border-foreground/10 bg-foreground/5 px-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-[#2563EB]" + className="w-12 h-7 rounded border border-foreground/10 bg-foreground/5 px-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary" aria-label="Custom aspect width" /> : @@ -179,7 +179,7 @@ export default function TimelineToolbar({ ) } onKeyDown={onCustomAspectRatioKeyDown} - className="w-12 h-7 rounded border border-foreground/10 bg-foreground/5 px-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-[#2563EB]" + className="w-12 h-7 rounded border border-foreground/10 bg-foreground/5 px-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary" aria-label="Custom aspect height" />
@@ -206,25 +206,25 @@ export default function TimelineToolbar({ > {cropLabel} - {isCropped ? : null} + {isCropped ? : null}
- + Side Scroll Pan - + {scrollLabels.pan} Pan - + {scrollLabels.zoom} Zoom diff --git a/src/index.css b/src/index.css index afbb98aa0..00d090cb2 100644 --- a/src/index.css +++ b/src/index.css @@ -268,3 +268,11 @@ transform: translateX(270%); } } + +/* Preview Fullscreen mode — hide all other editor UI */ +body.preview-fullscreen [data-editor-header], +body.preview-fullscreen [data-editor-sidebar], +body.preview-fullscreen [data-editor-timeline], +body.preview-fullscreen [data-editor-announcement] { + display: none !important; +} diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 629f25210..6c8e4210f 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -440,9 +440,7 @@ export class FrameRenderer { private retainedBackgroundTextureFrame: VideoFrame | null = null; private retainedWebcamSourceFrame: VideoFrame | null = null; private retainedWebcamTextureFrame: VideoFrame | null = null; - private retainedSceneBitmapTimestamp: number | null = null; private retainedSceneBitmap: ImageBitmap | null = null; - private retainedBackgroundBitmapTimestamp: number | null = null; private retainedBackgroundBitmap: ImageBitmap | null = null; private cleanupWebcamSource: (() => void) | null = null; @@ -623,8 +621,14 @@ export class FrameRenderer { }; const preferredRenderBackend = this.config.preferredRenderBackend; + + // Linux WebGPU in PixiJS v8/Electron has upstream driver issues (especially + // on Intel integrated graphics) leading to _resourceType BindGroup crashes. + // Force WebGL on Linux for stability unless they specifically force WebGPU. + const isLinux = typeof navigator !== "undefined" && navigator.userAgent.toLowerCase().includes("linux"); + const backendOrder: ExportRenderBackend[] = - preferredRenderBackend === "webgl" + preferredRenderBackend === "webgl" || (isLinux && preferredRenderBackend !== "webgpu") ? ["webgl", "webgpu"] : preferredRenderBackend === "webgpu" ? ["webgpu", "webgl"] @@ -739,7 +743,8 @@ export class FrameRenderer { const previousTexture = layer.sprite.texture; layer.sprite.texture = nextTexture; layer.textureSource = nextTexture.source as unknown as MutableVideoTextureSource; - previousTexture.destroy(true); + const isSameSource = previousTexture.source === nextTexture.source; + previousTexture.destroy(!isSameSource); } else { layer.sprite = new Sprite(nextTexture); layer.container.addChild(layer.sprite); @@ -854,13 +859,11 @@ export class FrameRenderer { if (kind === "scene") { this.retainedSceneBitmap?.close(); this.retainedSceneBitmap = null; - this.retainedSceneBitmapTimestamp = null; return; } this.retainedBackgroundBitmap?.close(); this.retainedBackgroundBitmap = null; - this.retainedBackgroundBitmapTimestamp = null; } private async resolveDetachedVideoFrameSource( @@ -869,70 +872,9 @@ export class FrameRenderer { fallbackWidth: number, fallbackHeight: number, ): Promise { - if (this.rendererBackend !== "webgpu" || typeof createImageBitmap !== "function") { - return this.stageVideoFrameForTexture(frame, kind, fallbackWidth, fallbackHeight); - } - - const cachedTimestamp = - kind === "scene" - ? this.retainedSceneBitmapTimestamp - : this.retainedBackgroundBitmapTimestamp; - const cachedBitmap = - kind === "scene" ? this.retainedSceneBitmap : this.retainedBackgroundBitmap; - if (cachedTimestamp === frame.timestamp && cachedBitmap) { - return cachedBitmap; - } - - try { - const bitmap = await createImageBitmap(frame); - this.closeRetainedBitmap(kind); - if (kind === "scene") { - this.retainedSceneBitmap = bitmap; - this.retainedSceneBitmapTimestamp = frame.timestamp; - } else { - this.retainedBackgroundBitmap = bitmap; - this.retainedBackgroundBitmapTimestamp = frame.timestamp; - } - return bitmap; - } catch (error) { - console.warn( - `[ModernFrameRenderer] Failed to detach ${kind} VideoFrame to ImageBitmap, falling back to retained VideoFrame:`, - error, - ); - return this.stageVideoFrameForTexture(frame, kind, fallbackWidth, fallbackHeight); - } + return this.stageVideoFrameForTexture(frame, kind, fallbackWidth, fallbackHeight); } - private resolveRetainedVideoFrameSource( - frame: VideoFrame, - kind: "scene" | "background" | "webcam", - fallbackWidth: number, - fallbackHeight: number, - ): CanvasImageSource | VideoFrame { - if (this.rendererBackend !== "webgpu") { - return frame; - } - - const state = this.getRetainedVideoFrameState(kind); - if (state.sourceFrame === frame && state.textureFrame) { - return state.textureFrame; - } - - try { - const retainedFrame = new VideoFrame(frame, { - timestamp: frame.timestamp, - }); - this.closeRetainedVideoFrame(kind); - this.setRetainedVideoFrameState(kind, frame, retainedFrame); - return retainedFrame; - } catch (error) { - console.warn( - `[ModernFrameRenderer] Failed to retain ${kind} VideoFrame, falling back to staging canvas:`, - error, - ); - return this.stageVideoFrameOnCanvas(frame, kind, fallbackWidth, fallbackHeight); - } - } private ensureVideoFrameStagingCanvas( kind: "scene" | "background" | "webcam", @@ -1011,17 +953,9 @@ export class FrameRenderer { fallbackWidth: number, fallbackHeight: number, ): CanvasImageSource | VideoFrame { - // Keep webcam uploads on the older canvas-staged path. The newer - // retained-VideoFrame upload path is fine for the main scene/background, - // but it has produced unstable webcam overlays in Lightning exports. - if (kind === "webcam") { - return this.stageVideoFrameOnCanvas(frame, kind, fallbackWidth, fallbackHeight); - } - - if (this.rendererBackend === "webgpu") { - return this.resolveRetainedVideoFrameSource(frame, kind, fallbackWidth, fallbackHeight); - } - + // Use canvas staging for all video frames. The retained-VideoFrame + // upload path has proven unstable across various WebGPU environments + // (especially on Linux), leading to _resourceType undefined crashes. return this.stageVideoFrameOnCanvas(frame, kind, fallbackWidth, fallbackHeight); } @@ -1032,7 +966,8 @@ export class FrameRenderer { const nextTexture = this.createTextureFromSource(source); const previousTexture = sprite.texture; sprite.texture = nextTexture; - previousTexture.destroy(true); + const isSameSource = previousTexture.source === nextTexture.source; + previousTexture.destroy(!isSameSource); return nextTexture.source as unknown as MutableVideoTextureSource; } @@ -1671,7 +1606,8 @@ export class FrameRenderer { const previousTexture = this.captionSprite.texture; this.captionSprite.texture = nextTexture; this.captionTextureSource = nextTexture.source as unknown as MutableVideoTextureSource; - previousTexture.destroy(true); + const isSameSource = previousTexture.source === nextTexture.source; + previousTexture.destroy(!isSameSource); } else { this.captionSprite = new Sprite(nextTexture); this.captionSprite.anchor.set(0.5);