feat: add project settings

This commit is contained in:
Maze Winther 2025-08-02 01:41:46 +02:00
parent cd593d1b98
commit dd09bf2949
7 changed files with 288 additions and 8 deletions

View File

@ -6,6 +6,7 @@ import { useMediaPanelStore, Tab } from "./store";
import { TextView } from "./views/text";
import { AudioView } from "./views/audio";
import { Separator } from "@/components/ui/separator";
import { SettingsView } from "./views/settings";
export function MediaPanel() {
const { activeTab } = useMediaPanelStore();
@ -44,6 +45,7 @@ export function MediaPanel() {
Adjustment view coming soon...
</div>
),
settings: <SettingsView />,
};
return (

View File

@ -9,6 +9,7 @@ import {
SlidersHorizontalIcon,
LucideIcon,
TypeIcon,
SettingsIcon,
} from "lucide-react";
import { create } from "zustand";
@ -21,7 +22,8 @@ export type Tab =
| "transitions"
| "captions"
| "filters"
| "adjustment";
| "adjustment"
| "settings";
export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = {
media: {
@ -60,6 +62,10 @@ export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = {
icon: SlidersHorizontalIcon,
label: "Adjustment",
},
settings: {
icon: SettingsIcon,
label: "Settings",
},
};
interface MediaPanelStore {

View File

@ -0,0 +1,246 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
PropertyItem,
PropertyItemLabel,
PropertyItemValue,
} from "../../properties-panel/property-item";
import { FPS_PRESETS } from "@/constants/timeline-constants";
import { useProjectStore } from "@/stores/project-store";
import { useEditorStore } from "@/stores/editor-store";
import { useAspectRatio } from "@/hooks/use-aspect-ratio";
import Image from "next/image";
import { cn } from "@/lib/utils";
import { colors } from "@/data/colors";
import { PipetteIcon } from "lucide-react";
import { useMemo, memo, useCallback } from "react";
export function SettingsView() {
return <ProjectSettingsTabs />;
}
function ProjectSettingsTabs() {
return (
<div className="h-full flex flex-col">
<Tabs defaultValue="project-info" className="flex flex-col h-full">
<div className="px-3 pt-4 pb-0">
<TabsList>
<TabsTrigger value="project-info">Project info</TabsTrigger>
<TabsTrigger value="background">Background</TabsTrigger>
</TabsList>
</div>
<Separator className="my-4" />
<ScrollArea className="flex-1">
<TabsContent value="project-info" className="p-5 pt-0 mt-0">
<ProjectInfoView />
</TabsContent>
<TabsContent value="background" className="p-4 pt-0">
<BackgroundView />
</TabsContent>
</ScrollArea>
</Tabs>
</div>
);
}
function ProjectInfoView() {
const { activeProject, updateProjectFps } = useProjectStore();
const { canvasSize, canvasPresets, setCanvasSize } = useEditorStore();
const { getDisplayName } = useAspectRatio();
const handleAspectRatioChange = (value: string) => {
const preset = canvasPresets.find((p) => p.name === value);
if (preset) {
setCanvasSize({ width: preset.width, height: preset.height });
}
};
const handleFpsChange = (value: string) => {
const fps = parseFloat(value);
updateProjectFps(fps);
};
return (
<div className="flex flex-col gap-4">
<PropertyItem direction="column">
<PropertyItemLabel>Name</PropertyItemLabel>
<PropertyItemValue>
{activeProject?.name || "Untitled project"}
</PropertyItemValue>
</PropertyItem>
<PropertyItem direction="column">
<PropertyItemLabel>Aspect ratio</PropertyItemLabel>
<PropertyItemValue>
<Select
value={getDisplayName()}
onValueChange={handleAspectRatioChange}
>
<SelectTrigger className="bg-panel-accent">
<SelectValue placeholder="Select an aspect ratio" />
</SelectTrigger>
<SelectContent>
{canvasPresets.map((preset) => (
<SelectItem key={preset.name} value={preset.name}>
{preset.name}
</SelectItem>
))}
</SelectContent>
</Select>
</PropertyItemValue>
</PropertyItem>
<PropertyItem direction="column">
<PropertyItemLabel>Frame rate</PropertyItemLabel>
<PropertyItemValue>
<Select
value={(activeProject?.fps || 30).toString()}
onValueChange={handleFpsChange}
>
<SelectTrigger className="bg-panel-accent">
<SelectValue placeholder="Select a frame rate" />
</SelectTrigger>
<SelectContent>
{FPS_PRESETS.map((preset) => (
<SelectItem key={preset.value} value={preset.value}>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
</PropertyItemValue>
</PropertyItem>
</div>
);
}
const BlurPreview = memo(
({
blur,
isSelected,
onSelect,
}: {
blur: { label: string; value: number };
isSelected: boolean;
onSelect: () => void;
}) => (
<div
className={cn(
"w-full aspect-square rounded-sm cursor-pointer hover:outline-2 hover:outline-primary relative overflow-hidden",
isSelected && "outline-2 outline-primary"
)}
onClick={onSelect}
>
<Image
src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt={`Blur preview ${blur.label}`}
fill
className="object-cover"
style={{ filter: `blur(${blur.value}px)` }}
priority
/>
<div className="absolute bottom-1 left-1 right-1 text-center">
<span className="text-xs text-white bg-black/50 px-1 rounded">
{blur.label}
</span>
</div>
</div>
)
);
BlurPreview.displayName = "BlurPreview";
function BackgroundView() {
const { activeProject, updateBackgroundType } = useProjectStore();
const blurLevels = useMemo(
() => [
{ label: "Light", value: 4 },
{ label: "Medium", value: 8 },
{ label: "Heavy", value: 18 },
],
[]
);
const handleBlurSelect = useCallback(
async (blurIntensity: number) => {
await updateBackgroundType("blur", { blurIntensity });
},
[updateBackgroundType]
);
const handleColorSelect = useCallback(
async (color: string) => {
await updateBackgroundType("color", { backgroundColor: color });
},
[updateBackgroundType]
);
const currentBlurIntensity = activeProject?.blurIntensity || 8;
const isBlurBackground = activeProject?.backgroundType === "blur";
const currentBackgroundColor = activeProject?.backgroundColor || "#000000";
const isColorBackground = activeProject?.backgroundType === "color";
const blurPreviews = useMemo(
() =>
blurLevels.map((blur) => (
<BlurPreview
key={blur.value}
blur={blur}
isSelected={isBlurBackground && currentBlurIntensity === blur.value}
onSelect={() => handleBlurSelect(blur.value)}
/>
)),
[blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect]
);
const colorPreviews = useMemo(
() =>
colors.map((color) => (
<div
key={color}
className={cn(
"w-full aspect-square rounded-sm cursor-pointer hover:border-2 hover:border-primary",
isColorBackground &&
color === currentBackgroundColor &&
"border-2 border-primary"
)}
style={{ backgroundColor: color }}
onClick={() => handleColorSelect(color)}
/>
)),
[isColorBackground, currentBackgroundColor, handleColorSelect]
);
return (
<div className="flex flex-col gap-4">
<PropertyItem direction="column" className="gap-2">
<PropertyItemLabel>Blur</PropertyItemLabel>
<PropertyItemValue>
<div className="grid grid-cols-4 gap-2 w-full">{blurPreviews}</div>
</PropertyItemValue>
</PropertyItem>
<PropertyItem direction="column" className="gap-2">
<PropertyItemLabel>Color</PropertyItemLabel>
<PropertyItemValue>
<div className="grid grid-cols-4 gap-2 w-full">
<div className="w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary flex items-center justify-center">
<PipetteIcon className="size-4" />
</div>
{colorPreviews}
</div>
</PropertyItemValue>
</PropertyItem>
</div>
);
}

View File

@ -17,7 +17,7 @@ export function PropertyItem({
"flex gap-2",
direction === "row"
? "items-center justify-between gap-6"
: "flex-col gap-1",
: "flex-col gap-1.5",
className
)}
>
@ -33,7 +33,11 @@ export function PropertyItemLabel({
children: React.ReactNode;
className?: string;
}) {
return <label className={cn("text-xs", className)}>{children}</label>;
return (
<label className={cn("text-xs text-muted-foreground", className)}>
{children}
</label>
);
}
export function PropertyItemValue({
@ -43,5 +47,5 @@ export function PropertyItemValue({
children: React.ReactNode;
className?: string;
}) {
return <div className={cn("flex-1", className)}>{children}</div>;
return <div className={cn("flex-1 text-sm", className)}>{children}</div>;
}

View File

@ -163,3 +163,25 @@ export function DataBuddyIcon({
</svg>
);
}
export function SquareSlashIcon({
className,
size = 24,
}: {
className?: string;
size?: number;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
className={className}
fill="none"
viewBox="0 0 256 256"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<line x1="9" x2="15" y1="15" y2="9" />
</svg>
);
}

View File

@ -35,14 +35,14 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"flex gap-1 h-7 cursor-pointer w-auto items-center justify-between whitespace-nowrap rounded-md bg-accent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDown className="size-3 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));

View File

@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
"inline-flex h-auto items-center justify-center gap-2 rounded-lg bg-transparent p-0 text-muted-foreground",
className
)}
{...props}
@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
"inline-flex items-center cursor-pointer justify-center whitespace-nowrap rounded-lg px-3 py-1 text-sm font-medium ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground",
className
)}
{...props}