Files
openscreen/src/components/video-editor/SettingsPanel.tsx
T
2025-11-08 21:33:03 -07:00

182 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cn } from "@/lib/utils";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { useMemo, useState } from "react";
import Colorful from '@uiw/react-color-colorful';
import { hsvaToHex } from '@uiw/color-convert';
import { Trash2 } from "lucide-react";
import type { ZoomDepth } from "./types";
import { ZOOM_DEPTH_SCALES } from "./types";
const WALLPAPER_COUNT = 12;
const WALLPAPER_PATHS = 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)",
];
interface SettingsPanelProps {
selected: string;
onWallpaperChange: (path: string) => void;
selectedZoomDepth?: ZoomDepth | null;
onZoomDepthChange?: (depth: ZoomDepth) => void;
selectedZoomId?: string | null;
onZoomDelete?: (id: string) => void;
}
const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string; description: string }> = [
{ depth: 1, label: "Subtle", description: "Gentle focus" },
{ depth: 2, label: "Medium", description: "Balanced zoom" },
{ depth: 3, label: "Deep", description: "Bold spotlight" },
];
export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete }: SettingsPanelProps) {
const [hsva, setHsva] = useState({ h: 0, s: 0, v: 68, a: 1 });
const [gradient, setGradient] = useState<string>(GRADIENTS[0]);
const zoomEnabled = Boolean(selectedZoomDepth);
const handleDeleteClick = () => {
if (selectedZoomId && onZoomDelete) {
onZoomDelete(selectedZoomId);
}
};
const scaleLabels = useMemo(() => {
return ZOOM_DEPTH_OPTIONS.reduce<Record<ZoomDepth, string>>((acc, option) => {
const scale = ZOOM_DEPTH_SCALES[option.depth];
acc[option.depth] = `${scale.toFixed(2)}×`;
return acc;
}, { 1: "", 2: "", 3: "" });
}, []);
return (
<div className="flex-[3] min-w-0 bg-card border border-border rounded-xl p-8 flex flex-col shadow-sm">
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-slate-600">Zoom Region</span>
{zoomEnabled && selectedZoomDepth && (
<span className="text-xs uppercase tracking-wide text-slate-400">
Active · {scaleLabels[selectedZoomDepth]}
</span>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{ZOOM_DEPTH_OPTIONS.map((option) => {
const isActive = selectedZoomDepth === option.depth;
return (
<Button
key={option.depth}
type="button"
variant="outline"
disabled={!zoomEnabled}
onClick={() => onZoomDepthChange?.(option.depth)}
className={cn(
"h-auto w-full rounded-xl border bg-muted/30 px-4 py-4 text-left shadow-sm transition-all",
"flex flex-col gap-2",
zoomEnabled ? "opacity-100" : "opacity-60",
isActive
? "border-primary/70 bg-primary/10 text-primary shadow-primary/20"
: "border-border/60 hover:border-primary/40 hover:bg-muted/60"
)}
>
<span className="text-sm font-semibold tracking-tight">{option.label}</span>
<span className="text-xs font-medium text-slate-500">
{scaleLabels[option.depth]}
</span>
<span className="text-xs text-slate-400 leading-snug">{option.description}</span>
</Button>
);
})}
</div>
{!zoomEnabled && (
<p className="text-xs text-slate-400 mt-2">Select a zoom region in the timeline to adjust its depth.</p>
)}
{zoomEnabled && (
<Button
onClick={handleDeleteClick}
variant="destructive"
size="sm"
className="mt-3 w-full gap-2"
>
<Trash2 className="w-4 h-4" />
Delete Zoom
</Button>
)}
</div>
<div className="mb-6">
<div className="flex items-center gap-2 mb-4">
<Switch/>
<div className="text-sm">Shadow</div>
</div>
</div>
<Tabs defaultValue="image" className="mb-6">
<TabsList className="mb-4">
<TabsTrigger value="image">Image</TabsTrigger>
<TabsTrigger value="color">Color</TabsTrigger>
<TabsTrigger value="gradient">Gradient</TabsTrigger>
</TabsList>
<TabsContent value="image">
<div className="grid grid-cols-6 gap-3">
{WALLPAPER_PATHS.map((path, idx) => (
<div
key={path}
className={cn(
"aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all w-16 h-16",
selected === path
? "border-primary/40 ring-1 ring-primary/40 scale-105"
: "border-border hover:border-primary/60 hover:scale-105"
)}
style={{ backgroundImage: `url(${path})`, backgroundSize: "cover", backgroundPosition: "center" }}
aria-label={`Wallpaper ${idx + 1}`}
onClick={() => onWallpaperChange(path)}
role="button"
/>
))}
</div>
</TabsContent>
<TabsContent value="color">
<Colorful
color={hsva}
disableAlpha={true}
onChange={(color) => {
setHsva(color.hsva);
onWallpaperChange(hsvaToHex(color.hsva));
}}
/>
</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-primary ring-1 ring-primary/40 scale-105" : "border-border hover:border-primary/60 hover:scale-105"
)}
style={{ background: g }}
aria-label={`Gradient ${idx + 1}`}
onClick={() => { setGradient(g); onWallpaperChange(g); }}
role="button"
/>
))}
</div>
</TabsContent>
</Tabs>
</div>
);
}