ui improvements & more wallpapers

This commit is contained in:
Siddharth
2025-11-20 13:27:39 -07:00
parent 6081747b7d
commit c6dbf1fa67
18 changed files with 357 additions and 308 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

+4 -4
View File
@@ -158,7 +158,7 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
<div
className={cn(
"absolute h-[3px] cursor-ns-resize z-20 pointer-events-auto bg-green-500"
"absolute h-[3px] cursor-ns-resize z-20 pointer-events-auto bg-[#34B27B]"
)}
style={{
left: `${cropPixelX}%`,
@@ -173,7 +173,7 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
<div
className={cn(
"absolute h-[3px] cursor-ns-resize z-20 pointer-events-auto bg-green-500"
"absolute h-[3px] cursor-ns-resize z-20 pointer-events-auto bg-[#34B27B]"
)}
style={{
left: `${cropPixelX}%`,
@@ -188,7 +188,7 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
<div
className={cn(
"absolute w-[3px] cursor-ew-resize z-20 pointer-events-auto bg-green-500"
"absolute w-[3px] cursor-ew-resize z-20 pointer-events-auto bg-[#34B27B]"
)}
style={{
left: `${cropPixelX}%`,
@@ -203,7 +203,7 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
<div
className={cn(
"absolute w-[3px] cursor-ew-resize z-20 pointer-events-auto bg-green-500"
"absolute w-[3px] cursor-ew-resize z-20 pointer-events-auto bg-[#34B27B]"
)}
style={{
left: `${cropPixelX + cropPixelWidth}%`,
+54 -35
View File
@@ -38,29 +38,41 @@ export function ExportDialog({
return (
<>
<div
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 animate-in fade-in duration-200"
className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 animate-in fade-in duration-200"
onClick={isExporting ? undefined : onClose}
/>
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 bg-[#23232a] rounded-2xl shadow-2xl border border-[#3a3a42] p-8 w-[90vw] max-w-md animate-in zoom-in-95 duration-200">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[60] bg-[#09090b] rounded-2xl shadow-2xl border border-white/10 p-8 w-[90vw] max-w-md animate-in zoom-in-95 duration-200">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
{showSuccess ? (
<>
<div className="w-10 h-10 rounded-full bg-[#34B27B] flex items-center justify-center">
<Download className="w-6 h-6 text-white" />
<div className="w-12 h-12 rounded-full bg-[#34B27B]/20 flex items-center justify-center ring-1 ring-[#34B27B]/50">
<Download className="w-6 h-6 text-[#34B27B]" />
</div>
<div>
<span className="text-xl font-bold text-slate-200 block">Export Complete</span>
<span className="text-sm text-slate-400">Your video is ready</span>
</div>
<span className="text-xl font-bold text-slate-200">Export Complete!</span>
</>
) : (
<>
{isExporting ? (
<Loader2 className="w-6 h-6 text-[#34B27B] animate-spin" />
<div className="w-12 h-12 rounded-full bg-[#34B27B]/10 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-[#34B27B] animate-spin" />
</div>
) : (
<Download className="w-6 h-6 text-[#34B27B]" />
<div className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center border border-white/10">
<Download className="w-6 h-6 text-slate-200" />
</div>
)}
<span className="text-xl font-bold text-slate-200">
{error ? 'Export Failed' : isExporting ? 'Exporting Video' : 'Export Video'}
</span>
<div>
<span className="text-xl font-bold text-slate-200 block">
{error ? 'Export Failed' : isExporting ? 'Exporting Video' : 'Export Video'}
</span>
<span className="text-sm text-slate-400">
{error ? 'Please try again' : isExporting ? 'This may take a moment...' : 'Ready to start'}
</span>
</div>
</>
)}
</div>
@@ -69,7 +81,7 @@ export function ExportDialog({
variant="ghost"
size="icon"
onClick={onClose}
className="hover:bg-[#34B27B]/20 text-slate-200"
className="hover:bg-white/10 text-slate-400 hover:text-white rounded-full"
>
<X className="w-5 h-5" />
</Button>
@@ -77,40 +89,53 @@ export function ExportDialog({
</div>
{error && (
<div className="mb-6">
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4">
<p className="text-sm text-red-400">{error}</p>
<div className="mb-6 animate-in slide-in-from-top-2">
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-4 flex items-start gap-3">
<div className="p-1 bg-red-500/20 rounded-full">
<X className="w-3 h-3 text-red-400" />
</div>
<p className="text-sm text-red-400 leading-relaxed">{error}</p>
</div>
</div>
)}
{isExporting && progress && (
<div className="space-y-4">
<div className="space-y-6">
<div className="space-y-2">
<div className="flex justify-between text-sm text-slate-400">
<div className="flex justify-between text-xs font-medium text-slate-400 uppercase tracking-wider">
<span>Progress</span>
<span className="font-mono">{progress.percentage.toFixed(1)}%</span>
<span className="font-mono text-slate-200">{progress.percentage.toFixed(0)}%</span>
</div>
<div className="h-3 bg-[#18181b] rounded-full overflow-hidden border border-[#34B27B]/30">
<div className="h-2 bg-white/5 rounded-full overflow-hidden border border-white/5">
<div
className="h-full bg-gradient-to-r from-[#34B27B] to-[#2a9964] transition-all duration-300 ease-out"
className="h-full bg-[#34B27B] shadow-[0_0_10px_rgba(52,178,123,0.3)] transition-all duration-300 ease-out"
style={{ width: `${Math.min(progress.percentage, 100)}%` }}
/>
</div>
</div>
<div className="text-sm">
<div className="text-slate-400 mb-1">Frame</div>
<div className="text-slate-200 font-mono text-lg">
{progress.currentFrame} / {progress.totalFrames}
<div className="grid grid-cols-2 gap-4">
<div className="bg-white/5 rounded-xl p-3 border border-white/5">
<div className="text-[10px] text-slate-500 uppercase tracking-wider mb-1">Current Frame</div>
<div className="text-slate-200 font-mono text-lg font-medium">
{progress.currentFrame} <span className="text-slate-500 text-sm">/ {progress.totalFrames}</span>
</div>
</div>
<div className="bg-white/5 rounded-xl p-3 border border-white/5">
<div className="text-[10px] text-slate-500 uppercase tracking-wider mb-1">Status</div>
<div className="text-slate-200 font-medium text-sm flex items-center gap-2 h-[28px]">
<span className="w-2 h-2 rounded-full bg-[#34B27B] animate-pulse" />
Processing
</div>
</div>
</div>
{onCancel && (
<div className="pt-4 border-t border-[#3a3a42]">
<div className="pt-2">
<Button
onClick={onCancel}
className="w-full py-3 bg-[#34B27B] text-white hover:bg-[#34B27B]/80 transition-all"
variant="destructive"
className="w-full py-6 bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 hover:border-red-500/30 transition-all rounded-xl"
>
Cancel Export
</Button>
@@ -120,14 +145,8 @@ export function ExportDialog({
)}
{showSuccess && (
<div className="text-center py-8">
<p className="text-lg text-slate-200">Video saved successfully!</p>
</div>
)}
{!isExporting && !error && !showSuccess && (
<div className="text-center py-4">
<p className="text-slate-400">Ready to export your video</p>
<div className="text-center py-4 animate-in zoom-in-95">
<p className="text-lg text-slate-200 font-medium">Video saved successfully!</p>
</div>
)}
</div>
@@ -1,5 +1,6 @@
import { Button } from "../ui/button";
import { MdPlayArrow, MdPause } from "react-icons/md";
import { Play, Pause } from "lucide-react";
import { cn } from "@/lib/utils";
interface PlaybackControlsProps {
isPlaying: boolean;
@@ -27,36 +28,63 @@ export default function PlaybackControls({
onSeek(parseFloat(e.target.value));
}
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
<div className="flex items-center gap-4 px-4 rounded-xl py-3">
<div className="flex items-center gap-4 px-6 py-3 rounded-full bg-black/60 backdrop-blur-md border border-white/10 shadow-xl transition-all duration-300 hover:bg-black/70 hover:border-white/20">
<Button
onClick={onTogglePlayPause}
size="icon"
className="w-8 h-8 rounded-full bg-transparent text-slate-200 hover:bg-[#18181b] transition-colors border border-white"
className={cn(
"w-10 h-10 rounded-full transition-all duration-200 border border-white/10",
isPlaying
? "bg-white/10 text-white hover:bg-white/20"
: "bg-white text-black hover:bg-white/90 hover:scale-105 shadow-[0_0_15px_rgba(255,255,255,0.3)]"
)}
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? (
<MdPause width={18} height={18} />
<Pause className="w-4 h-4 fill-current" />
) : (
<MdPlayArrow width={18} height={18} />
<Play className="w-4 h-4 fill-current ml-0.5" />
)}
</Button>
<span className="text-xs text-slate-400 font-mono">
<span className="text-xs font-medium text-slate-300 tabular-nums w-[40px] text-right">
{formatTime(currentTime)}
</span>
<input
type="range"
min="0"
max={duration}
value={currentTime}
onChange={handleSeekChange}
step="0.01"
className="flex-1 h-2 rounded-full transition-all duration-[33ms] custom-playback-range"
style={{
background: `linear-gradient(to right, #34B27B 0%, #34B27B ${(currentTime / duration) * 100}%, #23232a ${(currentTime / duration) * 100}%, #23232a 100%)`,
}}
/>
<span className="text-xs text-slate-400 font-mono">
<div className="flex-1 relative h-8 flex items-center group">
{/* Custom Track Background */}
<div className="absolute left-0 right-0 h-1 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-[#34B27B] rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
{/* Interactive Input */}
<input
type="range"
min="0"
max={duration || 100}
value={currentTime}
onChange={handleSeekChange}
step="0.01"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
{/* Custom Thumb (visual only, follows progress) */}
<div
className="absolute w-3 h-3 bg-white rounded-full shadow-lg pointer-events-none group-hover:scale-125 transition-transform duration-100"
style={{
left: `${progress}%`,
transform: 'translateX(-50%)'
}}
/>
</div>
<span className="text-xs font-medium text-slate-500 tabular-nums w-[40px]">
{formatTime(duration)}
</span>
</div>
+147 -125
View File
@@ -11,7 +11,7 @@ import { Trash2, Download, Crop, X, Bug } from "lucide-react";
import type { ZoomDepth, CropRegion } from "./types";
import { CropControl } from "./CropControl";
const WALLPAPER_COUNT = 12;
const WALLPAPER_COUNT = 23;
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% )",
@@ -26,6 +26,19 @@ const GRADIENTS = [
"linear-gradient(109.6deg, #F635A6, #36D860)",
"linear-gradient(90deg, #FF0101, #4DFF01)",
"linear-gradient(315deg, #EC0101, #5044A9)",
// New Gradients
"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 {
@@ -83,13 +96,13 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
};
return (
<div className="flex-[3] min-w-0 bg-[#18181b] border border-[#23232a] rounded-xl p-8 flex flex-col shadow-lg">
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-slate-200">Zoom Level</span>
<div className="flex-[3] min-w-0 bg-[#09090b] border border-white/5 rounded-2xl p-6 flex flex-col shadow-xl h-full overflow-y-auto custom-scrollbar">
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-slate-200">Zoom Level</span>
{zoomEnabled && selectedZoomDepth && (
<span className="text-xs uppercase tracking-wide text-slate-400/80">
Active · {ZOOM_DEPTH_OPTIONS.find(o => o.depth === selectedZoomDepth)?.label}
<span className="text-[10px] uppercase tracking-wider font-medium text-[#34B27B] bg-[#34B27B]/10 px-2 py-1 rounded-full">
{ZOOM_DEPTH_OPTIONS.find(o => o.depth === selectedZoomDepth)?.label} Active
</span>
)}
</div>
@@ -103,74 +116,75 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
disabled={!zoomEnabled}
onClick={() => onZoomDepthChange?.(option.depth)}
className={cn(
"h-auto w-full rounded-xl border px-2 py-3 text-center shadow-lg transition-all flex flex-col items-center justify-center gap-1",
"duration-150 ease-in-out",
zoomEnabled ? "opacity-100" : "opacity-60",
"h-auto w-full rounded-xl border px-1 py-3 text-center shadow-sm transition-all flex flex-col items-center justify-center gap-1.5",
"duration-200 ease-out",
zoomEnabled ? "opacity-100 cursor-pointer" : "opacity-40 cursor-not-allowed",
isActive
? "border-[#34B27B] bg-white text-black shadow-[#34B27B]/20 scale-105"
: "border-[#23232a] bg-[#23232a] text-slate-200 hover:border-[#34B27B] hover:scale-105"
? "border-[#34B27B] bg-[#34B27B] text-white shadow-[#34B27B]/20 scale-105 ring-2 ring-[#34B27B]/20"
: "border-white/5 bg-white/5 text-slate-400 hover:bg-white/10 hover:border-white/10 hover:text-slate-200"
)}
style={isActive ? { background: '#fff', color: '#111' } : undefined}
>
<span className={cn("text-sm font-semibold tracking-tight", isActive ? "text-black" : "text-slate-200")}>{option.label}</span>
<span className={cn("text-sm font-semibold tracking-tight")}>{option.label}</span>
</Button>
);
})}
</div>
{!zoomEnabled && (
<p className="text-xs text-slate-400/80 mt-2">Select a zoom item in the timeline to adjust its depth.</p>
<p className="text-xs text-slate-500 mt-3 text-center">Select a zoom region in the timeline to adjust depth.</p>
)}
{zoomEnabled && (
<Button
onClick={handleDeleteClick}
variant="destructive"
size="sm"
className="mt-3 w-full gap-2 bg-[#34B27B] text-white border-none hover:bg-[#34B27B]/80"
className="mt-4 w-full gap-2 bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 hover:border-red-500/30 transition-all"
>
<Trash2 className="w-4 h-4" />
Delete Zoom
Delete Zoom Region
</Button>
)}
</div>
<div className="mb-6">
<div className="flex flex-col gap-3 mb-4">
<div className="flex items-center gap-2">
<Switch
checked={showShadow}
onCheckedChange={onShadowChange}
/>
<div className="text-sm text-slate-200">Shadow</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={showBlur}
onCheckedChange={onBlurChange}
/>
<div className="text-sm text-slate-200">Blur Background</div>
</div>
<div className="mb-8 space-y-4">
<div className="flex items-center justify-between p-3 rounded-xl bg-white/5 border border-white/5">
<div className="text-sm font-medium text-slate-200">Drop Shadow</div>
<Switch
checked={showShadow}
onCheckedChange={onShadowChange}
className="data-[state=checked]:bg-[#34B27B]"
/>
</div>
<div className="flex items-center justify-between p-3 rounded-xl bg-white/5 border border-white/5">
<div className="text-sm font-medium text-slate-200">Blur Background</div>
<Switch
checked={showBlur}
onCheckedChange={onBlurChange}
className="data-[state=checked]:bg-[#34B27B]"
/>
</div>
</div>
<div className="mb-6">
<div className="mb-8">
<Button
onClick={() => setShowCropDropdown(!showCropDropdown)}
variant="outline"
className="w-full gap-2 bg-[#23232a] text-slate-200 border-none hover:border-[#34B27B]"
className="w-full gap-2 bg-white/5 text-slate-200 border-white/10 hover:bg-white/10 hover:border-white/20 hover:text-white h-11 transition-all"
>
<Crop className="w-4 h-4" />
Crop Video
</Button>
<p className="text-[10px] text-slate-400/60 text-center mt-2">
If the preview looks weirdly positioned at any time, try force reloading
<p className="text-[10px] text-slate-500 text-center mt-3 px-4 leading-relaxed">
If the preview looks weirdly positioned at any time, try force reloading (you will lose all your progress)
</p>
</div>
{showCropDropdown && cropRegion && onCropChange && (
<>
<div
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-40 animate-in fade-in duration-200"
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 animate-in fade-in duration-200"
onClick={() => setShowCropDropdown(false)}
/>
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 bg-[#23232a] rounded-2xl shadow-2xl border border-[#34B27B] p-8 w-[90vw] max-w-5xl animate-in zoom-in-95 duration-200">
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[60] bg-[#09090b] rounded-2xl shadow-2xl border border-white/10 p-8 w-[90vw] max-w-5xl animate-in zoom-in-95 duration-200">
<div className="flex items-center justify-between mb-6">
<div>
<span className="text-xl font-bold text-slate-200">Crop Video</span>
@@ -180,7 +194,7 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
variant="ghost"
size="icon"
onClick={() => setShowCropDropdown(false)}
className="hover:bg-[#34B27B]/20 text-slate-200"
className="hover:bg-white/10 text-slate-400 hover:text-white"
>
<X className="w-5 h-5" />
</Button>
@@ -194,6 +208,7 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
<Button
onClick={() => setShowCropDropdown(false)}
size="lg"
className="bg-[#34B27B] hover:bg-[#34B27B]/90 text-white"
>
Done
</Button>
@@ -201,100 +216,107 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
</div>
</>
)}
<Tabs defaultValue="image" className="mb-6 text-slate-200">
<TabsList className="mb-4 bg-[#23232a] border-none text-slate-200">
<TabsTrigger value="image" className="text-slate-200">Image</TabsTrigger>
<TabsTrigger value="color" className="text-slate-200">Color</TabsTrigger>
<TabsTrigger value="gradient" className="text-slate-200">Gradient</TabsTrigger>
<Tabs defaultValue="image" className="flex-1 flex flex-col min-h-0">
<TabsList className="mb-4 bg-white/5 border border-white/5 p-1 w-full grid grid-cols-3 h-auto rounded-xl">
<TabsTrigger value="image" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all">Image</TabsTrigger>
<TabsTrigger value="color" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all">Color</TabsTrigger>
<TabsTrigger value="gradient" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all">Gradient</TabsTrigger>
</TabsList>
<TabsContent value="image">
<div className="grid grid-cols-6 gap-3">
{(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path, idx) => {
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;
})();
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar pr-2">
<TabsContent value="image" className="mt-0">
<div className="grid grid-cols-6 gap-2">
{(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path, idx) => {
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 (
return (
<div
key={path}
className={cn(
"aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all duration-200",
isSelected
? "border-[#34B27B] ring-2 ring-[#34B27B]/30 scale-105 shadow-lg shadow-[#34B27B]/10"
: "border-white/5 hover:border-white/20 hover:scale-105 opacity-70 hover:opacity-100"
)}
style={{ backgroundImage: `url(${path})`, backgroundSize: "cover", backgroundPosition: "center" }}
aria-label={`Wallpaper ${idx + 1}`}
onClick={() => onWallpaperChange(path)}
role="button"
/>
)
})}
</div>
</TabsContent>
<TabsContent value="color" className="mt-0">
<div className="p-1">
<Colorful
color={hsva}
disableAlpha={true}
onChange={(color) => {
setHsva(color.hsva);
onWallpaperChange(hsvaToHex(color.hsva));
}}
style={{ width: '100%', borderRadius: '12px' }}
/>
</div>
</TabsContent>
<TabsContent value="gradient" className="mt-0">
<div className="grid grid-cols-6 gap-2">
{GRADIENTS.map((g, idx) => (
<div
key={path}
key={g}
className={cn(
"aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all w-16 h-16",
isSelected
? "border-[#34B27B] ring-1 ring-[#34B27B] scale-105"
: "border-[#23232a] hover:border-[#34B27B] hover:scale-105"
"aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all duration-200",
gradient === g
? "border-[#34B27B] ring-2 ring-[#34B27B]/30 scale-105 shadow-lg shadow-[#34B27B]/10"
: "border-white/5 hover:border-white/20 hover:scale-105 opacity-70 hover:opacity-100"
)}
style={{ backgroundImage: `url(${path})`, backgroundSize: "cover", backgroundPosition: "center" }}
aria-label={`Wallpaper ${idx + 1}`}
onClick={() => onWallpaperChange(path)}
style={{ background: g }}
aria-label={`Gradient ${idx + 1}`}
onClick={() => { setGradient(g); onWallpaperChange(g); }}
role="button"
/>
)
})}
</div>
</TabsContent>
<TabsContent value="color">
<div className="p-2">
<Colorful
color={hsva}
disableAlpha={true}
onChange={(color) => {
setHsva(color.hsva);
onWallpaperChange(hsvaToHex(color.hsva));
}}
/>
</div>
</TabsContent>
<TabsContent value="gradient">
<div className="grid grid-cols-6 gap-3">
{GRADIENTS.map((g, idx) => (
<div
key={g}
className={cn(
"aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all w-16 h-16",
gradient === g ? "border-[#34B27B] ring-1 ring-[#34B27B] scale-105" : "border-[#23232a] hover:border-[#34B27B] hover:scale-105"
)}
style={{ background: g }}
aria-label={`Gradient ${idx + 1}`}
onClick={() => { setGradient(g); onWallpaperChange(g); }}
role="button"
/>
))}
</div>
</TabsContent>
</Tabs>
<div className="mt-auto pt-4">
<Button
type="button"
size="lg"
onClick={onExport}
className="w-full py-5 text-lg flex items-center justify-center gap-3 bg-[#34B27B] text-white rounded-xl shadow-lg hover:bg-[#34B27B]/80 transition-all"
>
<Download className="w-6 h-6" />
<span className="text-lg">Export Video</span>
</Button>
<button
type="button"
onClick={() => {
window.electronAPI?.openExternalUrl('https://github.com/siddharthvaddem/openscreen/issues/new');
}}
className="w-full mt-3 flex items-center justify-center gap-1 text-[10px] text-slate-400/80 hover:text-slate-200 transition-colors py-1"
>
<Bug className="w-3 h-3 text-white" />
<span>Report Bug</span>
</button>
))}
</div>
</TabsContent>
</div>
</Tabs>
<div className="mt-6 pt-6 border-t border-white/5">
<Button
type="button"
size="lg"
onClick={onExport}
className="w-full py-6 text-lg font-semibold flex items-center justify-center gap-3 bg-[#34B27B] text-white rounded-xl shadow-lg shadow-[#34B27B]/20 hover:bg-[#34B27B]/90 hover:scale-[1.02] active:scale-[0.98] transition-all duration-200"
>
<Download className="w-5 h-5" />
<span>Export Video</span>
</Button>
<button
type="button"
onClick={() => {
window.electronAPI?.openExternalUrl('https://github.com/siddharthvaddem/openscreen/issues/new');
}}
className="w-full mt-4 flex items-center justify-center gap-2 text-xs text-slate-500 hover:text-slate-300 transition-colors py-2 group"
>
<Bug className="w-3 h-3 group-hover:text-[#34B27B] transition-colors" />
<span>Report a Bug</span>
</button>
</div>
</div>
);
}
+70 -60
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
@@ -22,7 +22,7 @@ import {
} from "./types";
import { VideoExporter, type ExportProgress } from "@/lib/exporter";
const WALLPAPER_COUNT = 12;
const WALLPAPER_COUNT = 23;
const WALLPAPER_PATHS = Array.from({ length: WALLPAPER_COUNT }, (_, i) => `/wallpapers/wallpaper${i + 1}.jpg`);
export default function VideoEditor() {
@@ -153,10 +153,7 @@ export default function VideoEditor() {
}
}, [selectedZoomId]);
const selectedZoom = useMemo(() => {
if (!selectedZoomId) return null;
return zoomRegions.find((region) => region.id === selectedZoomId) ?? null;
}, [selectedZoomId, zoomRegions]);
useEffect(() => {
if (selectedZoomId && !zoomRegions.some((region) => region.id === selectedZoomId)) {
@@ -271,35 +268,26 @@ export default function VideoEditor() {
const isMac = navigator.userAgent.includes('Mac');
return (
<div className="flex flex-col h-screen bg-background bg-black">
<div className="flex flex-col h-screen bg-[#09090b] text-slate-200 overflow-hidden selection:bg-[#34B27B]/30">
{/* Drag region for window - more padding on macOS for traffic lights */}
<div
className={`h-8 flex-shrink-0 bg-black/50 backdrop-blur-sm flex items-center justify-between ${isMac ? 'pl-20 pr-4' : 'px-4'}`}
className={`h-10 flex-shrink-0 bg-[#09090b]/80 backdrop-blur-md border-b border-white/5 flex items-center justify-between ${isMac ? 'pl-20 pr-4' : 'px-4'} z-50`}
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
<div className="flex-1" />
<WindowControls />
</div>
<div className="flex flex-1 p-4 gap-4 overflow-hidden">
<div style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
<Toaster position="top-center" />
</div>
<ExportDialog
isOpen={showExportDialog}
onClose={() => setShowExportDialog(false)}
progress={exportProgress}
isExporting={isExporting}
error={exportError}
onCancel={handleCancelExport}
/>
<div className="flex flex-col flex-[7] min-w-0 gap-4">
<div className="flex flex-col gap-2 flex-1">
{videoPath && (
<>
<div className="flex justify-center w-full">
<div className="flex-1 p-4 gap-4 flex min-h-0 relative">
{/* Left Column - Video & Timeline */}
<div className="flex-[7] flex flex-col gap-4 min-w-0 h-full">
{/* Video Preview Area */}
<div className="flex-1 min-h-0 bg-black/40 rounded-2xl border border-white/5 shadow-2xl overflow-hidden relative group">
<div className="absolute inset-0 flex flex-col">
<div className="flex-1 relative min-h-0 flex items-center justify-center">
<VideoPlayback
ref={videoPlaybackRef}
videoPath={videoPath}
videoPath={videoPath || ''}
onDurationChange={setDuration}
onTimeUpdate={setCurrentTime}
onPlayStateChange={setIsPlaying}
@@ -315,45 +303,67 @@ export default function VideoEditor() {
cropRegion={cropRegion}
/>
</div>
<PlaybackControls
isPlaying={isPlaying}
currentTime={currentTime}
duration={duration}
onTogglePlayPause={togglePlayPause}
onSeek={handleSeek}
/>
</>
)}
{/* Floating Playback Controls */}
<div className="px-6 pb-6 pt-2 pointer-events-none">
<div className="pointer-events-auto">
<PlaybackControls
isPlaying={isPlaying}
currentTime={currentTime}
duration={duration}
onTogglePlayPause={togglePlayPause}
onSeek={handleSeek}
/>
</div>
</div>
</div>
</div>
{/* Timeline Area */}
<div className="h-[220px] flex-shrink-0 bg-[#09090b] rounded-2xl border border-white/5 shadow-lg overflow-hidden flex flex-col">
<TimelineEditor
videoDuration={duration}
currentTime={currentTime}
onSeek={handleSeek}
zoomRegions={zoomRegions}
onZoomAdded={handleZoomAdded}
onZoomSpanChange={handleZoomSpanChange}
onZoomDelete={handleZoomDelete}
selectedZoomId={selectedZoomId}
onSelectZoom={handleSelectZoom}
/>
</div>
</div>
<TimelineEditor
videoDuration={duration}
currentTime={currentTime}
onSeek={handleSeek}
zoomRegions={zoomRegions}
onZoomAdded={handleZoomAdded}
onZoomSpanChange={handleZoomSpanChange}
onZoomDelete={handleZoomDelete}
{/* Right Column - Settings */}
<SettingsPanel
selected={wallpaper}
onWallpaperChange={setWallpaper}
selectedZoomDepth={selectedZoomId ? zoomRegions.find(z => z.id === selectedZoomId)?.depth : null}
onZoomDepthChange={(depth) => selectedZoomId && handleZoomDepthChange(depth)}
selectedZoomId={selectedZoomId}
onSelectZoom={handleSelectZoom}
onZoomDelete={handleZoomDelete}
showShadow={showShadow}
onShadowChange={setShowShadow}
showBlur={showBlur}
onBlurChange={setShowBlur}
cropRegion={cropRegion}
onCropChange={setCropRegion}
videoElement={videoPlaybackRef.current?.video || null}
onExport={handleExport}
/>
</div>
<SettingsPanel
selected={wallpaper}
onWallpaperChange={setWallpaper}
selectedZoomDepth={selectedZoom?.depth}
onZoomDepthChange={handleZoomDepthChange}
selectedZoomId={selectedZoomId}
onZoomDelete={handleZoomDelete}
showShadow={showShadow}
onShadowChange={setShowShadow}
showBlur={showBlur}
onBlurChange={setShowBlur}
cropRegion={cropRegion}
onCropChange={setCropRegion}
videoElement={videoPlaybackRef.current?.video || null}
onExport={handleExport}
<Toaster theme="dark" className="pointer-events-auto" />
<ExportDialog
isOpen={showExportDialog}
onClose={() => setShowExportDialog(false)}
progress={exportProgress}
isExporting={isExporting}
error={exportError}
onCancel={handleCancelExport}
/>
</div>
</div>
);
}
@@ -741,7 +741,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
>
<div
ref={focusIndicatorRef}
className="absolute rounded-md border border-sky-400/80 bg-sky-400/20 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]"
className="absolute rounded-md border border-[#34B27B]/80 bg-[#34B27B]/20 shadow-[0_0_0_1px_rgba(52,178,123,0.35)]"
style={{ display: 'none', pointerEvents: 'none' }}
/>
</div>
@@ -3,6 +3,7 @@ import { useTimelineContext } from "dnd-timeline";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import TimelineWrapper from "./TimelineWrapper";
import Row from "./Row";
import Item from "./Item";
@@ -151,41 +152,21 @@ function PlaybackCursor({
<div
className="absolute top-0 bottom-0 pointer-events-none z-50"
style={{
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth - 8}px`,
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth - 1}px`,
}}
>
<div
className="absolute top-3 bottom-3 w-[2px] bg-red-500/90 shadow-[0_0_8px_rgba(239,68,68,0.5)]"
className="absolute top-0 bottom-0 w-[2px] bg-[#34B27B] shadow-[0_0_10px_rgba(52,178,123,0.5)]"
style={{
[sideProperty]: `${offset}px`,
}}
>
<div
className="absolute -top-2 left-1/2 -translate-x-1/2 flex flex-col items-center"
style={{ width: '32px' }}
className="absolute -top-1 left-1/2 -translate-x-1/2"
style={{ width: '12px', height: '12px' }}
>
<div
style={{
width: '8px',
height: '8px',
background: '#ef4444',
borderRadius: '12px 12px 12px 12px/14px 14px 8px 8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.10)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '11px',
fontWeight: 600,
color: '#ef4444',
letterSpacing: '-0.5px',
position: 'relative',
}}
>
</div>
<div className="w-full h-full bg-[#34B27B] rotate-45 rounded-sm shadow-lg border border-white/20" />
</div>
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-red-500/30 rounded-full blur-sm" />
</div>
</div>
);
@@ -241,7 +222,7 @@ function TimelineAxis({
return (
<div
className="h-10 bg-black border-b border-[#18181b] relative overflow-hidden"
className="h-8 bg-[#09090b] border-b border-white/5 relative overflow-hidden select-none"
style={{
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth}px`,
}}
@@ -260,29 +241,13 @@ function TimelineAxis({
return (
<div key={marker.time} style={markerStyle}>
<div style={{ display: 'flex', alignItems: 'center', height: '100%' }}>
<div
style={{
width: '5px',
height: '5px',
borderRadius: '50%',
backgroundColor: marker.time === currentTimeMs ? '#34B27B' : '#94a3b8',
boxShadow: marker.time === currentTimeMs ? '0 0 4px #34B27B55' : 'none',
marginRight: '5px',
marginTop: '2px',
transition: 'background 0.2s, box-shadow 0.2s',
}}
/>
<div className="flex flex-col items-center pb-1">
<div className="h-1.5 w-[1px] bg-white/20 mb-1" />
<span
style={{
fontWeight: marker.time === currentTimeMs ? 700 : 500,
color: marker.time === currentTimeMs ? '#34B27B' : '#94a3b8',
fontSize: '11px',
letterSpacing: '-0.5px',
textShadow: marker.time === currentTimeMs ? '0 1px 6px #34B27B33' : 'none',
marginTop: '2px',
}}
className="select-none"
className={cn(
"text-[10px] font-medium tabular-nums tracking-tight",
marker.time === currentTimeMs ? "text-[#34B27B]" : "text-slate-500"
)}
>
{marker.label}
</span>
@@ -298,7 +263,7 @@ function Timeline({
items,
videoDurationMs,
intervalMs,
currentTimeMs,
currentTimeMs,
onSeek,
onSelectZoom,
selectedZoomId,
@@ -333,10 +298,11 @@ function Timeline({
<div
ref={setTimelineRef}
style={style}
className="select-none bg-black min-h-[120px] relative cursor-pointer"
className="select-none bg-[#09090b] min-h-[140px] relative cursor-pointer group"
onClick={handleTimelineClick}
>
<TimelineAxis intervalMs={intervalMs} videoDurationMs={videoDurationMs} currentTimeMs={currentTimeMs} />
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff03_1px,transparent_1px)] bg-[length:20px_100%] pointer-events-none" />
<TimelineAxis intervalMs={intervalMs} videoDurationMs={videoDurationMs} currentTimeMs={currentTimeMs} />
<PlaybackCursor currentTimeMs={currentTimeMs} videoDurationMs={videoDurationMs} />
<Row id={ROW_ID}>
{items.map((item) => (
@@ -468,33 +434,37 @@ export default function TimelineEditor({
if (!videoDuration || videoDuration === 0) {
return (
<div className="flex-1 flex items-center justify-center rounded-lg">
<span className="text-slate-400 text-sm">Load a video to see timeline</span>
<div className="flex-1 flex items-center justify-center rounded-lg bg-[#09090b]">
<span className="text-slate-500 text-sm">Load a video to see timeline</span>
</div>
);
}
return (
<div className="flex-1 flex flex-col bg-black border border-none rounded-xl shadow-lg overflow-hidden">
<div className="flex items-center gap-3 px-4 py-2.5">
<Button onClick={handleAddZoom} variant="outline" size="sm" className="gap-2 h-8 px-3 text-xs bg-[#23232a] border-none text-slate-200 hover:bg-white hover:text-black">
<Plus className="w-3.5 h-3.5 text-slate-400" />
<div className="flex-1 flex flex-col bg-[#09090b] overflow-hidden">
<div className="flex items-center gap-3 px-4 py-2 border-b border-white/5 bg-[#09090b]">
<Button
onClick={handleAddZoom}
variant="outline"
size="sm"
className="gap-2 h-7 px-3 text-xs bg-white/5 border-white/10 text-slate-200 hover:bg-[#34B27B] hover:text-white hover:border-[#34B27B] transition-all"
>
<Plus className="w-3.5 h-3.5" />
Add Zoom
</Button>
<div className="flex-1" />
<div className="flex items-center gap-3 text-[10px] text-slate-400 font-medium">
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-[#23232a] rounded text-slate-300">Command + Shift + Scroll</kbd>
<div className="flex items-center gap-4 text-[10px] text-slate-500 font-medium">
<span className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 bg-white/5 border border-white/10 rounded text-slate-400 font-sans"> + + Scroll</kbd>
<span>Pan</span>
</span>
<span className="text-slate-600"></span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-[#23232a] rounded text-slate-300">Command + Scroll</kbd>
<span className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 bg-white/5 border border-white/10 rounded text-slate-400 font-sans"> + Scroll</kbd>
<span>Zoom</span>
</span>
</div>
</div>
<div className="mt-4 flex-1 overflow-x-auto overflow-y-hidden bg-[#000]">
<div className="flex-1 overflow-hidden bg-[#09090b] relative">
<TimelineWrapper
range={clampedRange}
videoDuration={videoDuration}