ui improvements & more wallpapers
|
After Width: | Height: | Size: 1008 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 890 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 496 KiB |
@@ -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}%`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||