feat: add project settings
This commit is contained in:
parent
cd593d1b98
commit
dd09bf2949
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue