diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index a2ee2d9..1ff120f 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -1,5 +1,5 @@ import { cn } from "@/lib/utils"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { getAssetPath } from "@/lib/assetPath"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -7,7 +7,8 @@ import { Button } from "@/components/ui/button"; import { useState } from "react"; import Colorful from '@uiw/react-color-colorful'; import { hsvaToHex } from '@uiw/color-convert'; -import { Trash2, Download, Crop, X, Bug } from "lucide-react"; +import { Trash2, Download, Crop, X, Bug, Upload } from "lucide-react"; +import { toast } from "sonner"; import type { ZoomDepth, CropRegion } from "./types"; import { CropControl } from "./CropControl"; @@ -69,6 +70,8 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, showShadow, onShadowChange, showBlur, onBlurChange, cropRegion, onCropChange, videoElement, onExport }: SettingsPanelProps) { const [wallpaperPaths, setWallpaperPaths] = useState([]); + const [customImages, setCustomImages] = useState([]); + const fileInputRef = useRef(null); useEffect(() => { let mounted = true @@ -94,6 +97,53 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, } }; + const handleImageUpload = (event: React.ChangeEvent) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + 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 reader = new FileReader(); + + reader.onload = (e) => { + const dataUrl = e.target?.result as string; + if (dataUrl) { + setCustomImages(prev => [...prev, dataUrl]); + onWallpaperChange(dataUrl); + toast.success('Custom image uploaded successfully!'); + } + }; + + reader.onerror = () => { + toast.error('Failed to upload image', { + description: 'There was an error reading the file.', + }); + }; + + reader.readAsDataURL(file); + // Reset input so the same file can be selected again + event.target.value = ''; + }; + + 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]); + } + }; + return (
@@ -224,8 +274,54 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
- + + {/* Upload Button */} + + +
+ {/* Custom Images */} + {customImages.map((imageUrl, idx) => { + const isSelected = selected === imageUrl; + return ( +
onWallpaperChange(imageUrl)} + role="button" + > + +
+ ); + })} + + {/* Preset Wallpapers */} {(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path, idx) => { const isSelected = (() => { if (!selected) return false; diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 8c43ca3..0a335e2 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -683,6 +683,12 @@ const VideoPlayback = forwardRef(({ return } + // If it's a data URL (custom uploaded image), use as-is + if (wallpaper.startsWith('data:')) { + if (mounted) setResolvedWallpaper(wallpaper) + return + } + // If it's an absolute web/http or file path, use as-is if (wallpaper.startsWith('http') || wallpaper.startsWith('file://') || wallpaper.startsWith('/')) { // If it's an absolute server path (starts with '/'), resolve via getAssetPath as well @@ -704,7 +710,7 @@ const VideoPlayback = forwardRef(({ return () => { mounted = false } }, [wallpaper]) - const isImageUrl = Boolean(resolvedWallpaper && (resolvedWallpaper.startsWith('file://') || resolvedWallpaper.startsWith('http') || resolvedWallpaper.startsWith('/'))) + const isImageUrl = Boolean(resolvedWallpaper && (resolvedWallpaper.startsWith('file://') || resolvedWallpaper.startsWith('http') || resolvedWallpaper.startsWith('/') || resolvedWallpaper.startsWith('data:'))) const backgroundStyle = isImageUrl ? { backgroundImage: `url(${resolvedWallpaper || ''})` } : { background: resolvedWallpaper || '' };