+ const cropPixelX = cropRegion.x * 100;
+ const cropPixelY = cropRegion.y * 100;
+ const cropPixelWidth = cropRegion.width * 100;
+ const cropPixelHeight = cropRegion.height * 100;
+ const videoAspectRatio = videoElement
+ ? videoElement.videoWidth / videoElement.videoHeight
+ : 16 / 9;
+ const isVideoPortrait = videoAspectRatio < 1;
+ const maxContainerWidth = isVideoPortrait ? "40vw" : "75vw";
+ const maxContainerHeight = "75vh";
-
handlePointerDown(e, 'top')}
- />
+ return (
+
+
+
-
handlePointerDown(e, 'bottom')}
- />
+
+
+
-
handlePointerDown(e, 'left')}
- />
+
handlePointerDown(e, "top")}
+ />
-
handlePointerDown(e, 'right')}
- />
-
-
- );
+
handlePointerDown(e, "bottom")}
+ />
+
+
handlePointerDown(e, "left")}
+ />
+
+
handlePointerDown(e, "right")}
+ />
+
+
+ );
}
diff --git a/src/components/video-editor/ExportDialog.tsx b/src/components/video-editor/ExportDialog.tsx
index 7a77e25..7c00271 100644
--- a/src/components/video-editor/ExportDialog.tsx
+++ b/src/components/video-editor/ExportDialog.tsx
@@ -1,273 +1,271 @@
-import { useEffect, useState } from 'react';
-import { X, Download, Loader2 } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import type { ExportProgress } from '@/lib/exporter';
-import { toast } from 'sonner'; // Add this import
-
+import { Download, Loader2, X } from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner"; // Add this import
+import { Button } from "@/components/ui/button";
+import type { ExportProgress } from "@/lib/exporter";
interface ExportDialogProps {
- isOpen: boolean;
- onClose: () => void;
- progress: ExportProgress | null;
- isExporting: boolean;
- error: string | null;
- onCancel?: () => void;
- exportFormat?: 'mp4' | 'gif';
- exportedFilePath?: string;
+ isOpen: boolean;
+ onClose: () => void;
+ progress: ExportProgress | null;
+ isExporting: boolean;
+ error: string | null;
+ onCancel?: () => void;
+ exportFormat?: "mp4" | "gif";
+ exportedFilePath?: string;
}
export function ExportDialog({
- isOpen,
- onClose,
- progress,
- isExporting,
- error,
- onCancel,
- exportFormat = 'mp4',
- exportedFilePath, // Add this line
+ isOpen,
+ onClose,
+ progress,
+ isExporting,
+ error,
+ onCancel,
+ exportFormat = "mp4",
+ exportedFilePath, // Add this line
}: ExportDialogProps) {
- const [showSuccess, setShowSuccess] = useState(false);
+ const [showSuccess, setShowSuccess] = useState(false);
- // Reset showSuccess when a new export starts or dialog reopens
- useEffect(() => {
- if (isExporting) {
- setShowSuccess(false);
- }
- }, [isExporting]);
+ // Reset showSuccess when a new export starts or dialog reopens
+ useEffect(() => {
+ if (isExporting) {
+ setShowSuccess(false);
+ }
+ }, [isExporting]);
- // Reset showSuccess when dialog opens fresh
- useEffect(() => {
- if (isOpen && !isExporting && !progress) {
- setShowSuccess(false);
- }
- }, [isOpen, isExporting, progress]);
+ // Reset showSuccess when dialog opens fresh
+ useEffect(() => {
+ if (isOpen && !isExporting && !progress) {
+ setShowSuccess(false);
+ }
+ }, [isOpen, isExporting, progress]);
- useEffect(() => {
- if (!isExporting && progress && progress.percentage >= 100 && !error) {
- setShowSuccess(true);
- const timer = setTimeout(() => {
- setShowSuccess(false);
- onClose();
- }, 2000);
- return () => clearTimeout(timer);
- }
- }, [isExporting, progress, error, onClose]);
+ useEffect(() => {
+ if (!isExporting && progress && progress.percentage >= 100 && !error) {
+ setShowSuccess(true);
+ const timer = setTimeout(() => {
+ setShowSuccess(false);
+ onClose();
+ }, 2000);
+ return () => clearTimeout(timer);
+ }
+ }, [isExporting, progress, error, onClose]);
- if (!isOpen) return null;
+ if (!isOpen) return null;
- const formatLabel = exportFormat === 'gif' ? 'GIF' : 'Video';
-
- // Determine if we're in the compiling phase (frames done but still exporting)
- const isCompiling = isExporting && progress && progress.percentage >= 100 && exportFormat === 'gif';
- const isFinalizing = progress?.phase === 'finalizing';
- const renderProgress = progress?.renderProgress;
-
- // Get status message based on phase
- const getStatusMessage = () => {
- if (error) return 'Please try again';
- if (isCompiling || isFinalizing) {
- if (renderProgress !== undefined && renderProgress > 0) {
- return `Compiling GIF... ${renderProgress}%`;
- }
- return 'Compiling GIF... This may take a while';
- }
- return 'This may take a moment...';
- };
+ const formatLabel = exportFormat === "gif" ? "GIF" : "Video";
- // Get title based on phase
- const getTitle = () => {
- if (error) return 'Export Failed';
- if (isCompiling || isFinalizing) return 'Compiling GIF';
- return `Exporting ${formatLabel}`;
- };
+ // Determine if we're in the compiling phase (frames done but still exporting)
+ const isCompiling =
+ isExporting && progress && progress.percentage >= 100 && exportFormat === "gif";
+ const isFinalizing = progress?.phase === "finalizing";
+ const renderProgress = progress?.renderProgress;
- const handleClickShowInFolder = async () => {
- if (exportedFilePath) {
- try {
- const result = await window.electronAPI.revealInFolder(exportedFilePath);
- if (!result.success) {
- const errorMessage = result.error || result.message || 'Failed to reveal item in folder.';
- console.error('Failed to reveal in folder:', errorMessage);
- toast.error(errorMessage);
- }
- } catch (err) {
- const errorMessage = String(err);
- console.error('Error calling revealInFolder IPC:', errorMessage);
- toast.error(`Error revealing in folder: ${errorMessage}`);
- }
- }
- };
+ // Get status message based on phase
+ const getStatusMessage = () => {
+ if (error) return "Please try again";
+ if (isCompiling || isFinalizing) {
+ if (renderProgress !== undefined && renderProgress > 0) {
+ return `Compiling GIF... ${renderProgress}%`;
+ }
+ return "Compiling GIF... This may take a while";
+ }
+ return "This may take a moment...";
+ };
- return (
- <>
-
-
-
-
- {showSuccess ? (
- <>
-
-
-
-
- Export Complete
- Your {formatLabel.toLowerCase()} is ready
- {exportedFilePath && (
-
- )}
- {exportedFilePath && (
-
- {exportedFilePath.split('/').pop()}
-
- )}
-
- >
- ) : (
- <>
- {isExporting ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
- {getTitle()}
-
-
- {getStatusMessage()}
-
-
- >
- )}
-
- {!isExporting && (
-
- )}
-
+ // Get title based on phase
+ const getTitle = () => {
+ if (error) return "Export Failed";
+ if (isCompiling || isFinalizing) return "Compiling GIF";
+ return `Exporting ${formatLabel}`;
+ };
- {error && (
-
- )}
+ const handleClickShowInFolder = async () => {
+ if (exportedFilePath) {
+ try {
+ const result = await window.electronAPI.revealInFolder(exportedFilePath);
+ if (!result.success) {
+ const errorMessage = result.error || result.message || "Failed to reveal item in folder.";
+ console.error("Failed to reveal in folder:", errorMessage);
+ toast.error(errorMessage);
+ }
+ } catch (err) {
+ const errorMessage = String(err);
+ console.error("Error calling revealInFolder IPC:", errorMessage);
+ toast.error(`Error revealing in folder: ${errorMessage}`);
+ }
+ }
+ };
- {isExporting && progress && (
-
-
-
- {isCompiling || isFinalizing ? 'Compiling' : 'Rendering Frames'}
-
- {isCompiling || isFinalizing ? (
- renderProgress !== undefined && renderProgress > 0 ? (
- `${renderProgress}%`
- ) : (
-
-
- Processing...
-
- )
- ) : (
- `${progress.percentage.toFixed(0)}%`
- )}
-
-
-
- {isCompiling || isFinalizing ? (
- // Show render progress if available, otherwise animated indeterminate bar
- renderProgress !== undefined && renderProgress > 0 ? (
-
- ) : (
-
- )
- ) : (
-
- )}
-
-
+
+ )
+ ) : (
+
+ )}
+
+
-
-
-
- {isCompiling || isFinalizing ? 'Status' : 'Format'}
-
-
- {isCompiling || isFinalizing ? 'Compiling...' : formatLabel}
-
-
-
-
Frames
-
- {progress.currentFrame} / {progress.totalFrames}
-
-
-
+
+
+
+ {isCompiling || isFinalizing ? "Status" : "Format"}
+
+
+ {isCompiling || isFinalizing ? "Compiling..." : formatLabel}
+
+
+
+
+ Frames
+
+
+ {progress.currentFrame} / {progress.totalFrames}
+
+
+
- {onCancel && (
-
-
-
- )}
-
- )}
+ {onCancel && (
+
+
+
+ )}
+
+ )}
- {showSuccess && (
-
-
- {formatLabel} saved successfully!
-
-
- )}
-
- >
- );
+ {showSuccess && (
+
+
{formatLabel} saved successfully!
+
+ )}
+
+ >
+ );
}
diff --git a/src/components/video-editor/FormatSelector.tsx b/src/components/video-editor/FormatSelector.tsx
index 4d13e9a..9714856 100644
--- a/src/components/video-editor/FormatSelector.tsx
+++ b/src/components/video-editor/FormatSelector.tsx
@@ -1,77 +1,77 @@
-import { Film, Image } from 'lucide-react';
-import { cn } from '@/lib/utils';
-import type { ExportFormat } from '@/lib/exporter/types';
+import { Film, Image } from "lucide-react";
+import type { ExportFormat } from "@/lib/exporter/types";
+import { cn } from "@/lib/utils";
interface FormatSelectorProps {
- selectedFormat: ExportFormat;
- onFormatChange: (format: ExportFormat) => void;
- disabled?: boolean;
+ selectedFormat: ExportFormat;
+ onFormatChange: (format: ExportFormat) => void;
+ disabled?: boolean;
}
interface FormatOption {
- value: ExportFormat;
- label: string;
- description: string;
- icon: React.ReactNode;
+ value: ExportFormat;
+ label: string;
+ description: string;
+ icon: React.ReactNode;
}
const formatOptions: FormatOption[] = [
- {
- value: 'mp4',
- label: 'MP4 Video',
- description: 'High quality video file',
- icon:
,
- },
- {
- value: 'gif',
- label: 'GIF Animation',
- description: 'Animated image for sharing',
- icon:
,
- },
+ {
+ value: "mp4",
+ label: "MP4 Video",
+ description: "High quality video file",
+ icon:
,
+ },
+ {
+ value: "gif",
+ label: "GIF Animation",
+ description: "Animated image for sharing",
+ icon:
,
+ },
];
export function FormatSelector({
- selectedFormat,
- onFormatChange,
- disabled = false,
+ selectedFormat,
+ onFormatChange,
+ disabled = false,
}: FormatSelectorProps) {
- return (
-
- {formatOptions.map((option) => {
- const isSelected = selectedFormat === option.value;
- return (
-
- );
- })}
-
- );
+ return (
+
+ {formatOptions.map((option) => {
+ const isSelected = selectedFormat === option.value;
+ return (
+
+ );
+ })}
+
+ );
}
diff --git a/src/components/video-editor/GifOptionsPanel.tsx b/src/components/video-editor/GifOptionsPanel.tsx
index ef1b6bd..f350d46 100644
--- a/src/components/video-editor/GifOptionsPanel.tsx
+++ b/src/components/video-editor/GifOptionsPanel.tsx
@@ -1,110 +1,111 @@
-import { Switch } from '@/components/ui/switch';
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import { GIF_FRAME_RATES, GIF_SIZE_PRESETS, type GifFrameRate, type GifSizePreset } from '@/lib/exporter/types';
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import {
+ GIF_FRAME_RATES,
+ GIF_SIZE_PRESETS,
+ type GifFrameRate,
+ type GifSizePreset,
+} from "@/lib/exporter/types";
interface GifOptionsPanelProps {
- frameRate: GifFrameRate;
- onFrameRateChange: (rate: GifFrameRate) => void;
- loop: boolean;
- onLoopChange: (loop: boolean) => void;
- sizePreset: GifSizePreset;
- onSizePresetChange: (preset: GifSizePreset) => void;
- outputDimensions: { width: number; height: number };
- disabled?: boolean;
+ frameRate: GifFrameRate;
+ onFrameRateChange: (rate: GifFrameRate) => void;
+ loop: boolean;
+ onLoopChange: (loop: boolean) => void;
+ sizePreset: GifSizePreset;
+ onSizePresetChange: (preset: GifSizePreset) => void;
+ outputDimensions: { width: number; height: number };
+ disabled?: boolean;
}
export function GifOptionsPanel({
- frameRate,
- onFrameRateChange,
- loop,
- onLoopChange,
- sizePreset,
- onSizePresetChange,
- outputDimensions,
- disabled = false,
+ frameRate,
+ onFrameRateChange,
+ loop,
+ onLoopChange,
+ sizePreset,
+ onSizePresetChange,
+ outputDimensions,
+ disabled = false,
}: GifOptionsPanelProps) {
- const sizePresetOptions = Object.entries(GIF_SIZE_PRESETS).map(([key, value]) => ({
- value: key as GifSizePreset,
- label: value.label,
- }));
+ const sizePresetOptions = Object.entries(GIF_SIZE_PRESETS).map(([key, value]) => ({
+ value: key as GifSizePreset,
+ label: value.label,
+ }));
- return (
-
- {/* Frame Rate */}
-
-
-
-
+ return (
+
+ {/* Frame Rate */}
+
+
+
+
- {/* Size Preset */}
-
-
-
-
- Output: {outputDimensions.width} × {outputDimensions.height}px
-
-
+ {/* Size Preset */}
+
+
+
+
+ Output: {outputDimensions.width} × {outputDimensions.height}px
+
+
- {/* Loop Toggle */}
-
-
-
-
GIF will play continuously
-
-
-
-
- );
+ {/* Loop Toggle */}
+
+
+
+
GIF will play continuously
+
+
+
+
+ );
}
diff --git a/src/components/video-editor/KeyboardShortcutsHelp.tsx b/src/components/video-editor/KeyboardShortcutsHelp.tsx
index b48896f..8ffe412 100644
--- a/src/components/video-editor/KeyboardShortcutsHelp.tsx
+++ b/src/components/video-editor/KeyboardShortcutsHelp.tsx
@@ -1,65 +1,74 @@
import { HelpCircle, Settings2 } from "lucide-react";
-import { useState, useEffect } from "react";
-import { formatShortcut } from "@/utils/platformUtils";
+import { useEffect, useState } from "react";
import { useShortcuts } from "@/contexts/ShortcutsContext";
-import { formatBinding, SHORTCUT_LABELS, SHORTCUT_ACTIONS } from "@/lib/shortcuts";
+import { formatBinding, SHORTCUT_ACTIONS, SHORTCUT_LABELS } from "@/lib/shortcuts";
+import { formatShortcut } from "@/utils/platformUtils";
export function KeyboardShortcutsHelp() {
- const { shortcuts, isMac, openConfig } = useShortcuts();
+ const { shortcuts, isMac, openConfig } = useShortcuts();
- const [scrollLabels, setScrollLabels] = useState({ pan: 'Shift + Ctrl + Scroll', zoom: 'Ctrl + Scroll' });
+ const [scrollLabels, setScrollLabels] = useState({
+ pan: "Shift + Ctrl + Scroll",
+ zoom: "Ctrl + Scroll",
+ });
- useEffect(() => {
- Promise.all([
- formatShortcut(['shift', 'mod', 'Scroll']),
- formatShortcut(['mod', 'Scroll']),
- ]).then(([pan, zoom]) => setScrollLabels({ pan, zoom }));
- }, []);
+ useEffect(() => {
+ Promise.all([
+ formatShortcut(["shift", "mod", "Scroll"]),
+ formatShortcut(["mod", "Scroll"]),
+ ]).then(([pan, zoom]) => setScrollLabels({ pan, zoom }));
+ }, []);
- return (
-
-
+ return (
+
+
-
-
- Keyboard Shortcuts
-
-
+
+
+ Keyboard Shortcuts
+
+
-
- {SHORTCUT_ACTIONS.map((action) => (
-
- {SHORTCUT_LABELS[action]}
-
- {formatBinding(shortcuts[action], isMac)}
-
-
- ))}
+
+ {SHORTCUT_ACTIONS.map((action) => (
+
+ {SHORTCUT_LABELS[action]}
+
+ {formatBinding(shortcuts[action], isMac)}
+
+
+ ))}
-
-
- Pan Timeline
- {scrollLabels.pan}
-
-
- Zoom Timeline
- {scrollLabels.zoom}
-
-
- Cycle Annotations
- Tab
-
-
-
-
-
- );
+
+
+ Pan Timeline
+
+ {scrollLabels.pan}
+
+
+
+ Zoom Timeline
+
+ {scrollLabels.zoom}
+
+
+
+ Cycle Annotations
+
+ Tab
+
+
+
+
+
+
+ );
}
diff --git a/src/components/video-editor/PlaybackControls.tsx b/src/components/video-editor/PlaybackControls.tsx
index 7c776dd..b580bf8 100644
--- a/src/components/video-editor/PlaybackControls.tsx
+++ b/src/components/video-editor/PlaybackControls.tsx
@@ -1,92 +1,89 @@
-import { Button } from "../ui/button";
-import { Play, Pause } from "lucide-react";
+import { Pause, Play } from "lucide-react";
import { cn } from "@/lib/utils";
+import { Button } from "../ui/button";
interface PlaybackControlsProps {
- isPlaying: boolean;
- currentTime: number;
- duration: number;
- onTogglePlayPause: () => void;
- onSeek: (time: number) => void;
+ isPlaying: boolean;
+ currentTime: number;
+ duration: number;
+ onTogglePlayPause: () => void;
+ onSeek: (time: number) => void;
}
export default function PlaybackControls({
- isPlaying,
- currentTime,
- duration,
- onTogglePlayPause,
- onSeek,
+ isPlaying,
+ currentTime,
+ duration,
+ onTogglePlayPause,
+ onSeek,
}: PlaybackControlsProps) {
- function formatTime(seconds: number) {
- if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return '0:00';
- const mins = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- return `${mins}:${secs.toString().padStart(2, '0')}`;
- }
+ function formatTime(seconds: number) {
+ if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return "0:00";
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+ }
- function handleSeekChange(e: React.ChangeEvent
) {
- onSeek(parseFloat(e.target.value));
- }
+ function handleSeekChange(e: React.ChangeEvent) {
+ onSeek(parseFloat(e.target.value));
+ }
- const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
+ const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
- return (
-
-
-
-
- {formatTime(currentTime)}
-
-
-
- {/* Custom Track Background */}
-
-
- {/* Interactive Input */}
-
-
- {/* Custom Thumb (visual only, follows progress) */}
-
-
-
-
- {formatTime(duration)}
-
-
- );
+ return (
+
+
+
+
+ {formatTime(currentTime)}
+
+
+
+ {/* Custom Track Background */}
+
+
+ {/* Interactive Input */}
+
+
+ {/* Custom Thumb (visual only, follows progress) */}
+
+
+
+
+ {formatTime(duration)}
+
+
+ );
}
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx
index 7007e30..3e80507 100644
--- a/src/components/video-editor/SettingsPanel.tsx
+++ b/src/components/video-editor/SettingsPanel.tsx
@@ -1,802 +1,912 @@
-import { cn } from "@/lib/utils";
-import { useEffect, useRef } from "react";
-import { getAssetPath } from "@/lib/assetPath";
+import Block from "@uiw/react-color-block";
+import {
+ Bug,
+ Crop,
+ Download,
+ Film,
+ FolderOpen,
+ Image,
+ Palette,
+ Save,
+ Sparkles,
+ Star,
+ Trash2,
+ Upload,
+ X,
+} from "lucide-react";
+import { useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Button } from "@/components/ui/button";
-import { useState } from "react";
-import Block from '@uiw/react-color-block';
-import { Trash2, Download, Crop, X, Bug, Upload, Star, Film, Image, Sparkles, Palette, Save, FolderOpen } from "lucide-react";
-import { toast } from "sonner";
-import type { ZoomDepth, CropRegion, AnnotationRegion, AnnotationType, PlaybackSpeed } from "./types";
-import { SPEED_OPTIONS } from "./types";
+import { getAssetPath } from "@/lib/assetPath";
+import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
+import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
+import { cn } from "@/lib/utils";
+import { type AspectRatio } from "@/utils/aspectRatioUtils";
+import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
import { CropControl } from "./CropControl";
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp";
-import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
-import { type AspectRatio } from "@/utils/aspectRatioUtils";
-import type { ExportQuality, ExportFormat, GifFrameRate, GifSizePreset } from "@/lib/exporter";
-import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
+import type {
+ AnnotationRegion,
+ AnnotationType,
+ CropRegion,
+ PlaybackSpeed,
+ ZoomDepth,
+} from "./types";
+import { SPEED_OPTIONS } from "./types";
const WALLPAPER_COUNT = 18;
-const WALLPAPER_RELATIVE = Array.from({ length: WALLPAPER_COUNT }, (_, i) => `wallpapers/wallpaper${i + 1}.jpg`);
+const WALLPAPER_RELATIVE = Array.from(
+ { length: WALLPAPER_COUNT },
+ (_, i) => `wallpapers/wallpaper${i + 1}.jpg`,
+);
const GRADIENTS = [
- "linear-gradient( 111.6deg, rgba(114,167,232,1) 9.4%, rgba(253,129,82,1) 43.9%, rgba(253,129,82,1) 54.8%, rgba(249,202,86,1) 86.3% )",
- "linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%)",
- "radial-gradient( circle farthest-corner at 3.2% 49.6%, rgba(80,12,139,0.87) 0%, rgba(161,10,144,0.72) 83.6% )",
- "linear-gradient( 111.6deg, rgba(0,56,68,1) 0%, rgba(163,217,185,1) 51.5%, rgba(231, 148, 6, 1) 88.6% )",
- "linear-gradient( 107.7deg, rgba(235,230,44,0.55) 8.4%, rgba(252,152,15,1) 90.3% )",
- "linear-gradient( 91deg, rgba(72,154,78,1) 5.2%, rgba(251,206,70,1) 95.9% )",
- "radial-gradient( circle farthest-corner at 10% 20%, rgba(2,37,78,1) 0%, rgba(4,56,126,1) 19.7%, rgba(85,245,221,1) 100.2% )",
- "linear-gradient( 109.6deg, rgba(15,2,2,1) 11.2%, rgba(36,163,190,1) 91.1% )",
- "linear-gradient(135deg, #FBC8B4, #2447B1)",
- "linear-gradient(109.6deg, #F635A6, #36D860)",
- "linear-gradient(90deg, #FF0101, #4DFF01)",
- "linear-gradient(315deg, #EC0101, #5044A9)",
- "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%)",
- "linear-gradient(to top, #a18cd1 0%, #fbc2eb 100%)",
- "linear-gradient(to right, #ff8177 0%, #ff867a 0%, #ff8c7f 21%, #f99185 52%, #cf556c 78%, #b12a5b 100%)",
- "linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)",
- "linear-gradient(to right, #4facfe 0%, #00f2fe 100%)",
- "linear-gradient(to top, #fcc5e4 0%, #fda34b 15%, #ff7882 35%, #c8699e 52%, #7046aa 71%, #0c1db8 87%, #020f75 100%)",
- "linear-gradient(to right, #fa709a 0%, #fee140 100%)",
- "linear-gradient(to top, #30cfd0 0%, #330867 100%)",
- "linear-gradient(to top, #c471f5 0%, #fa71cd 100%)",
- "linear-gradient(to right, #f78ca0 0%, #f9748f 19%, #fd868c 60%, #fe9a8b 100%)",
- "linear-gradient(to top, #48c6ef 0%, #6f86d6 100%)",
- "linear-gradient(to right, #0acffe 0%, #495aff 100%)",
+ "linear-gradient( 111.6deg, rgba(114,167,232,1) 9.4%, rgba(253,129,82,1) 43.9%, rgba(253,129,82,1) 54.8%, rgba(249,202,86,1) 86.3% )",
+ "linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%)",
+ "radial-gradient( circle farthest-corner at 3.2% 49.6%, rgba(80,12,139,0.87) 0%, rgba(161,10,144,0.72) 83.6% )",
+ "linear-gradient( 111.6deg, rgba(0,56,68,1) 0%, rgba(163,217,185,1) 51.5%, rgba(231, 148, 6, 1) 88.6% )",
+ "linear-gradient( 107.7deg, rgba(235,230,44,0.55) 8.4%, rgba(252,152,15,1) 90.3% )",
+ "linear-gradient( 91deg, rgba(72,154,78,1) 5.2%, rgba(251,206,70,1) 95.9% )",
+ "radial-gradient( circle farthest-corner at 10% 20%, rgba(2,37,78,1) 0%, rgba(4,56,126,1) 19.7%, rgba(85,245,221,1) 100.2% )",
+ "linear-gradient( 109.6deg, rgba(15,2,2,1) 11.2%, rgba(36,163,190,1) 91.1% )",
+ "linear-gradient(135deg, #FBC8B4, #2447B1)",
+ "linear-gradient(109.6deg, #F635A6, #36D860)",
+ "linear-gradient(90deg, #FF0101, #4DFF01)",
+ "linear-gradient(315deg, #EC0101, #5044A9)",
+ "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%)",
+ "linear-gradient(to top, #a18cd1 0%, #fbc2eb 100%)",
+ "linear-gradient(to right, #ff8177 0%, #ff867a 0%, #ff8c7f 21%, #f99185 52%, #cf556c 78%, #b12a5b 100%)",
+ "linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)",
+ "linear-gradient(to right, #4facfe 0%, #00f2fe 100%)",
+ "linear-gradient(to top, #fcc5e4 0%, #fda34b 15%, #ff7882 35%, #c8699e 52%, #7046aa 71%, #0c1db8 87%, #020f75 100%)",
+ "linear-gradient(to right, #fa709a 0%, #fee140 100%)",
+ "linear-gradient(to top, #30cfd0 0%, #330867 100%)",
+ "linear-gradient(to top, #c471f5 0%, #fa71cd 100%)",
+ "linear-gradient(to right, #f78ca0 0%, #f9748f 19%, #fd868c 60%, #fe9a8b 100%)",
+ "linear-gradient(to top, #48c6ef 0%, #6f86d6 100%)",
+ "linear-gradient(to right, #0acffe 0%, #495aff 100%)",
];
interface SettingsPanelProps {
- selected: string;
- onWallpaperChange: (path: string) => void;
- selectedZoomDepth?: ZoomDepth | null;
- onZoomDepthChange?: (depth: ZoomDepth) => void;
- selectedZoomId?: string | null;
- onZoomDelete?: (id: string) => void;
- selectedTrimId?: string | null;
- onTrimDelete?: (id: string) => void;
- shadowIntensity?: number;
- onShadowChange?: (intensity: number) => void;
- showBlur?: boolean;
- onBlurChange?: (showBlur: boolean) => void;
- motionBlurEnabled?: boolean;
- onMotionBlurChange?: (enabled: boolean) => void;
- borderRadius?: number;
- onBorderRadiusChange?: (radius: number) => void;
- padding?: number;
- onPaddingChange?: (padding: number) => void;
- cropRegion?: CropRegion;
- onCropChange?: (region: CropRegion) => void;
- aspectRatio: AspectRatio;
- videoElement?: HTMLVideoElement | null;
- exportQuality?: ExportQuality;
- onExportQualityChange?: (quality: ExportQuality) => void;
- // Export format settings
- exportFormat?: ExportFormat;
- onExportFormatChange?: (format: ExportFormat) => void;
- gifFrameRate?: GifFrameRate;
- onGifFrameRateChange?: (rate: GifFrameRate) => void;
- gifLoop?: boolean;
- onGifLoopChange?: (loop: boolean) => void;
- gifSizePreset?: GifSizePreset;
- onGifSizePresetChange?: (preset: GifSizePreset) => void;
- gifOutputDimensions?: { width: number; height: number };
- onSaveProject?: () => void;
- onLoadProject?: () => void;
- onExport?: () => void;
- selectedAnnotationId?: string | null;
- annotationRegions?: AnnotationRegion[];
- onAnnotationContentChange?: (id: string, content: string) => void;
- onAnnotationTypeChange?: (id: string, type: AnnotationType) => void;
- onAnnotationStyleChange?: (id: string, style: Partial) => void;
- onAnnotationFigureDataChange?: (id: string, figureData: any) => void;
- onAnnotationDelete?: (id: string) => void;
- selectedSpeedId?: string | null;
- selectedSpeedValue?: PlaybackSpeed | null;
- onSpeedChange?: (speed: PlaybackSpeed) => void;
- onSpeedDelete?: (id: string) => void;
+ selected: string;
+ onWallpaperChange: (path: string) => void;
+ selectedZoomDepth?: ZoomDepth | null;
+ onZoomDepthChange?: (depth: ZoomDepth) => void;
+ selectedZoomId?: string | null;
+ onZoomDelete?: (id: string) => void;
+ selectedTrimId?: string | null;
+ onTrimDelete?: (id: string) => void;
+ shadowIntensity?: number;
+ onShadowChange?: (intensity: number) => void;
+ showBlur?: boolean;
+ onBlurChange?: (showBlur: boolean) => void;
+ motionBlurEnabled?: boolean;
+ onMotionBlurChange?: (enabled: boolean) => void;
+ borderRadius?: number;
+ onBorderRadiusChange?: (radius: number) => void;
+ padding?: number;
+ onPaddingChange?: (padding: number) => void;
+ cropRegion?: CropRegion;
+ onCropChange?: (region: CropRegion) => void;
+ aspectRatio: AspectRatio;
+ videoElement?: HTMLVideoElement | null;
+ exportQuality?: ExportQuality;
+ onExportQualityChange?: (quality: ExportQuality) => void;
+ // Export format settings
+ exportFormat?: ExportFormat;
+ onExportFormatChange?: (format: ExportFormat) => void;
+ gifFrameRate?: GifFrameRate;
+ onGifFrameRateChange?: (rate: GifFrameRate) => void;
+ gifLoop?: boolean;
+ onGifLoopChange?: (loop: boolean) => void;
+ gifSizePreset?: GifSizePreset;
+ onGifSizePresetChange?: (preset: GifSizePreset) => void;
+ gifOutputDimensions?: { width: number; height: number };
+ onSaveProject?: () => void;
+ onLoadProject?: () => void;
+ onExport?: () => void;
+ selectedAnnotationId?: string | null;
+ annotationRegions?: AnnotationRegion[];
+ onAnnotationContentChange?: (id: string, content: string) => void;
+ onAnnotationTypeChange?: (id: string, type: AnnotationType) => void;
+ onAnnotationStyleChange?: (id: string, style: Partial) => void;
+ onAnnotationFigureDataChange?: (id: string, figureData: any) => void;
+ onAnnotationDelete?: (id: string) => void;
+ selectedSpeedId?: string | null;
+ selectedSpeedValue?: PlaybackSpeed | null;
+ onSpeedChange?: (speed: PlaybackSpeed) => void;
+ onSpeedDelete?: (id: string) => void;
}
export default SettingsPanel;
const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [
- { depth: 1, label: "1.25×" },
- { depth: 2, label: "1.5×" },
- { depth: 3, label: "1.8×" },
- { depth: 4, label: "2.2×" },
- { depth: 5, label: "3.5×" },
- { depth: 6, label: "5×" },
+ { depth: 1, label: "1.25×" },
+ { depth: 2, label: "1.5×" },
+ { depth: 3, label: "1.8×" },
+ { depth: 4, label: "2.2×" },
+ { depth: 5, label: "3.5×" },
+ { depth: 6, label: "5×" },
];
-export function SettingsPanel({
- selected,
- onWallpaperChange,
- selectedZoomDepth,
- onZoomDepthChange,
- selectedZoomId,
- onZoomDelete,
- selectedTrimId,
- onTrimDelete,
- shadowIntensity = 0,
- onShadowChange,
- showBlur,
- onBlurChange,
- motionBlurEnabled = false,
- onMotionBlurChange,
- borderRadius = 0,
- onBorderRadiusChange,
- padding = 50,
- onPaddingChange,
- cropRegion,
- onCropChange,
- aspectRatio,
- videoElement,
- exportQuality = 'good',
- onExportQualityChange,
- exportFormat = 'mp4',
- onExportFormatChange,
- gifFrameRate = 15,
- onGifFrameRateChange,
- gifLoop = true,
- onGifLoopChange,
- gifSizePreset = 'medium',
- onGifSizePresetChange,
- gifOutputDimensions = { width: 1280, height: 720 },
- onSaveProject,
- onLoadProject,
- onExport,
- selectedAnnotationId,
- annotationRegions = [],
- onAnnotationContentChange,
- onAnnotationTypeChange,
- onAnnotationStyleChange,
- onAnnotationFigureDataChange,
- onAnnotationDelete,
- selectedSpeedId,
- selectedSpeedValue,
- onSpeedChange,
- onSpeedDelete,
+export function SettingsPanel({
+ selected,
+ onWallpaperChange,
+ selectedZoomDepth,
+ onZoomDepthChange,
+ selectedZoomId,
+ onZoomDelete,
+ selectedTrimId,
+ onTrimDelete,
+ shadowIntensity = 0,
+ onShadowChange,
+ showBlur,
+ onBlurChange,
+ motionBlurEnabled = false,
+ onMotionBlurChange,
+ borderRadius = 0,
+ onBorderRadiusChange,
+ padding = 50,
+ onPaddingChange,
+ cropRegion,
+ onCropChange,
+ aspectRatio,
+ videoElement,
+ exportQuality = "good",
+ onExportQualityChange,
+ exportFormat = "mp4",
+ onExportFormatChange,
+ gifFrameRate = 15,
+ onGifFrameRateChange,
+ gifLoop = true,
+ onGifLoopChange,
+ gifSizePreset = "medium",
+ onGifSizePresetChange,
+ gifOutputDimensions = { width: 1280, height: 720 },
+ onSaveProject,
+ onLoadProject,
+ onExport,
+ selectedAnnotationId,
+ annotationRegions = [],
+ onAnnotationContentChange,
+ onAnnotationTypeChange,
+ onAnnotationStyleChange,
+ onAnnotationFigureDataChange,
+ onAnnotationDelete,
+ selectedSpeedId,
+ selectedSpeedValue,
+ onSpeedChange,
+ onSpeedDelete,
}: SettingsPanelProps) {
- const [wallpaperPaths, setWallpaperPaths] = useState([]);
- const [customImages, setCustomImages] = useState([]);
- const fileInputRef = useRef(null);
+ const [wallpaperPaths, setWallpaperPaths] = useState([]);
+ const [customImages, setCustomImages] = useState([]);
+ const fileInputRef = useRef(null);
- useEffect(() => {
- let mounted = true
- ;(async () => {
- try {
- const resolved = await Promise.all(WALLPAPER_RELATIVE.map(p => getAssetPath(p)))
- if (mounted) setWallpaperPaths(resolved)
- } catch (err) {
- if (mounted) setWallpaperPaths(WALLPAPER_RELATIVE.map(p => `/${p}`))
- }
- })()
- return () => { mounted = false }
- }, [])
- const colorPalette = [
- '#FF0000', '#FFD700', '#00FF00', '#FFFFFF', '#0000FF', '#FF6B00',
- '#9B59B6', '#E91E63', '#00BCD4', '#FF5722', '#8BC34A', '#FFC107',
- '#34B27B', '#000000', '#607D8B', '#795548',
- ];
-
- const [selectedColor, setSelectedColor] = useState('#ADADAD');
- const [gradient, setGradient] = useState(GRADIENTS[0]);
- const [showCropDropdown, setShowCropDropdown] = useState(false);
+ useEffect(() => {
+ let mounted = true;
+ (async () => {
+ try {
+ const resolved = await Promise.all(WALLPAPER_RELATIVE.map((p) => getAssetPath(p)));
+ if (mounted) setWallpaperPaths(resolved);
+ } catch {
+ if (mounted) setWallpaperPaths(WALLPAPER_RELATIVE.map((p) => `/${p}`));
+ }
+ })();
+ return () => {
+ mounted = false;
+ };
+ }, []);
+ const colorPalette = [
+ "#FF0000",
+ "#FFD700",
+ "#00FF00",
+ "#FFFFFF",
+ "#0000FF",
+ "#FF6B00",
+ "#9B59B6",
+ "#E91E63",
+ "#00BCD4",
+ "#FF5722",
+ "#8BC34A",
+ "#FFC107",
+ "#34B27B",
+ "#000000",
+ "#607D8B",
+ "#795548",
+ ];
- const zoomEnabled = Boolean(selectedZoomDepth);
- const trimEnabled = Boolean(selectedTrimId);
-
- const handleDeleteClick = () => {
- if (selectedZoomId && onZoomDelete) {
- onZoomDelete(selectedZoomId);
- }
- };
+ const [selectedColor, setSelectedColor] = useState("#ADADAD");
+ const [gradient, setGradient] = useState(GRADIENTS[0]);
+ const [showCropDropdown, setShowCropDropdown] = useState(false);
- const handleTrimDeleteClick = () => {
- if (selectedTrimId && onTrimDelete) {
- onTrimDelete(selectedTrimId);
- }
- };
+ const zoomEnabled = Boolean(selectedZoomDepth);
+ const trimEnabled = Boolean(selectedTrimId);
- const handleImageUpload = (event: React.ChangeEvent) => {
- const files = event.target.files;
- if (!files || files.length === 0) return;
+ const handleDeleteClick = () => {
+ if (selectedZoomId && onZoomDelete) {
+ onZoomDelete(selectedZoomId);
+ }
+ };
- const file = files[0];
-
- // Validate file type - only allow JPG/JPEG
- const validTypes = ['image/jpeg', 'image/jpg'];
- if (!validTypes.includes(file.type)) {
- toast.error('Invalid file type', {
- description: 'Please upload a JPG or JPEG image file.',
- });
- event.target.value = '';
- return;
- }
+ const handleTrimDeleteClick = () => {
+ if (selectedTrimId && onTrimDelete) {
+ onTrimDelete(selectedTrimId);
+ }
+ };
- const reader = new FileReader();
+ const handleImageUpload = (event: React.ChangeEvent) => {
+ const files = event.target.files;
+ if (!files || files.length === 0) return;
- reader.onload = (e) => {
- const dataUrl = e.target?.result as string;
- if (dataUrl) {
- setCustomImages(prev => [...prev, dataUrl]);
- onWallpaperChange(dataUrl);
- toast.success('Custom image uploaded successfully!');
- }
- };
+ const file = files[0];
- reader.onerror = () => {
- toast.error('Failed to upload image', {
- description: 'There was an error reading the file.',
- });
- };
+ // Validate file type - only allow JPG/JPEG
+ const validTypes = ["image/jpeg", "image/jpg"];
+ if (!validTypes.includes(file.type)) {
+ toast.error("Invalid file type", {
+ description: "Please upload a JPG or JPEG image file.",
+ });
+ event.target.value = "";
+ return;
+ }
- reader.readAsDataURL(file);
- // Reset input so the same file can be selected again
- event.target.value = '';
- };
+ const reader = new FileReader();
- const handleRemoveCustomImage = (imageUrl: string, event: React.MouseEvent) => {
- event.stopPropagation();
- setCustomImages(prev => prev.filter(img => img !== imageUrl));
- // If the removed image was selected, clear selection
- if (selected === imageUrl) {
- onWallpaperChange(wallpaperPaths[0] || WALLPAPER_RELATIVE[0]);
- }
- };
+ reader.onload = (e) => {
+ const dataUrl = e.target?.result as string;
+ if (dataUrl) {
+ setCustomImages((prev) => [...prev, dataUrl]);
+ onWallpaperChange(dataUrl);
+ toast.success("Custom image uploaded successfully!");
+ }
+ };
- // Find selected annotation
- const selectedAnnotation = selectedAnnotationId
- ? annotationRegions.find(a => a.id === selectedAnnotationId)
- : null;
+ reader.onerror = () => {
+ toast.error("Failed to upload image", {
+ description: "There was an error reading the file.",
+ });
+ };
- // If an annotation is selected, show annotation settings instead
- if (selectedAnnotation && onAnnotationContentChange && onAnnotationTypeChange && onAnnotationStyleChange && 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}
- onDelete={() => onAnnotationDelete(selectedAnnotation.id)}
- />
- );
- }
+ reader.readAsDataURL(file);
+ // Reset input so the same file can be selected again
+ event.target.value = "";
+ };
- return (
-
-
-
-
-
Zoom Level
-
- {zoomEnabled && selectedZoomDepth && (
-
- {ZOOM_DEPTH_OPTIONS.find(o => o.depth === selectedZoomDepth)?.label}
-
- )}
-
-
-
-
- {ZOOM_DEPTH_OPTIONS.map((option) => {
- const isActive = selectedZoomDepth === option.depth;
- return (
-
- );
- })}
-
- {!zoomEnabled && (
-
Select a zoom region to adjust
- )}
- {zoomEnabled && (
-
- )}
-
+ const handleRemoveCustomImage = (imageUrl: string, event: React.MouseEvent) => {
+ event.stopPropagation();
+ setCustomImages((prev) => prev.filter((img) => img !== imageUrl));
+ // If the removed image was selected, clear selection
+ if (selected === imageUrl) {
+ onWallpaperChange(wallpaperPaths[0] || WALLPAPER_RELATIVE[0]);
+ }
+ };
- {trimEnabled && (
-
-
-
- )}
+ // Find selected annotation
+ const selectedAnnotation = selectedAnnotationId
+ ? annotationRegions.find((a) => a.id === selectedAnnotationId)
+ : null;
-
-
- Playback Speed
- {selectedSpeedId && selectedSpeedValue && (
-
- {SPEED_OPTIONS.find(o => o.speed === selectedSpeedValue)?.label ?? `${selectedSpeedValue}×`}
-
- )}
-
-
- {SPEED_OPTIONS.map((option) => {
- const isActive = selectedSpeedValue === option.speed;
- return (
-
- );
- })}
-
- {!selectedSpeedId && (
-
Select a speed region to adjust
- )}
- {selectedSpeedId && (
-
- )}
-
+ // If an annotation is selected, show annotation settings instead
+ if (
+ selectedAnnotation &&
+ onAnnotationContentChange &&
+ onAnnotationTypeChange &&
+ onAnnotationStyleChange &&
+ 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
+ }
+ onDelete={() => onAnnotationDelete(selectedAnnotation.id)}
+ />
+ );
+ }
-
-
-
-
-
- Video Effects
-
-
-
-
-
-
-
-
-
Shadow
-
{Math.round(shadowIntensity * 100)}%
-
-
onShadowChange?.(values[0])}
- 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"
- />
-
-
-
-
Roundness
-
{borderRadius}px
-
-
onBorderRadiusChange?.(values[0])}
- 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"
- />
-
-
-
-
onPaddingChange?.(values[0])}
- min={0}
- max={100}
- step={1}
- className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
- />
-
-
+ return (
+
+
+
+
+
Zoom Level
+
+ {zoomEnabled && selectedZoomDepth && (
+
+ {ZOOM_DEPTH_OPTIONS.find((o) => o.depth === selectedZoomDepth)?.label}
+
+ )}
+
+
+
+
+ {ZOOM_DEPTH_OPTIONS.map((option) => {
+ const isActive = selectedZoomDepth === option.depth;
+ return (
+
+ );
+ })}
+
+ {!zoomEnabled && (
+
+ Select a zoom region to adjust
+
+ )}
+ {zoomEnabled && (
+
+ )}
+
-
-
-
+ {trimEnabled && (
+
+
+
+ )}
-
-
-
-
-
-
-
- Image
- Color
- Gradient
-
-
-
-
-
-
+
+
+ Playback Speed
+ {selectedSpeedId && selectedSpeedValue && (
+
+ {SPEED_OPTIONS.find((o) => o.speed === selectedSpeedValue)?.label ??
+ `${selectedSpeedValue}×`}
+
+ )}
+
+
+ {SPEED_OPTIONS.map((option) => {
+ const isActive = selectedSpeedValue === option.speed;
+ return (
+
+ );
+ })}
+
+ {!selectedSpeedId && (
+
+ Select a speed region to adjust
+
+ )}
+ {selectedSpeedId && (
+
+ )}
+
-
- {customImages.map((imageUrl, idx) => {
- const isSelected = selected === imageUrl;
- return (
-
onWallpaperChange(imageUrl)}
- role="button"
- >
-
-
- );
- })}
+
+
+
+
+
+ Video Effects
+
+
+
+
- {(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path) => {
- const isSelected = (() => {
- if (!selected) return false;
- if (selected === path) return true;
- try {
- const clean = (s: string) => s.replace(/^file:\/\//, '').replace(/^\//, '')
- if (clean(selected).endsWith(clean(path))) return true;
- if (clean(path).endsWith(clean(selected))) return true;
- } catch {}
- return false;
- })();
- return (
- onWallpaperChange(path)}
- role="button"
- />
- )
- })}
-
-
-
-
-
- {
- setSelectedColor(color.hex);
- onWallpaperChange(color.hex);
- }}
- style={{
- width: '100%',
- borderRadius: '8px',
- }}
- />
-
-
-
-
-
- {GRADIENTS.map((g, idx) => (
-
{ setGradient(g); onWallpaperChange(g); }}
- role="button"
- />
- ))}
-
-
-
-
-
-
-
-
+
+
+
+
Shadow
+
+ {Math.round(shadowIntensity * 100)}%
+
+
+
onShadowChange?.(values[0])}
+ 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"
+ />
+
+
+
+
Roundness
+
{borderRadius}px
+
+
onBorderRadiusChange?.(values[0])}
+ 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"
+ />
+
+
+
+
onPaddingChange?.(values[0])}
+ min={0}
+ max={100}
+ step={1}
+ className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
+ />
+
+
- {showCropDropdown && cropRegion && onCropChange && (
- <>
- setShowCropDropdown(false)}
- />
-
-
-
-
Crop Video
-
Drag on each side to adjust the crop area
-
-
-
-
-
-
-
-
- >
- )}
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Image
+
+
+ Color
+
+
+ Gradient
+
+
- {exportFormat === 'mp4' && (
-
-
-
-
-
- )}
+
+
+