diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-08-02 01:41:46 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-08-02 01:41:46 +0200 |
| commit | dd09bf2949b63d17b3e7311a688d7d84c5d6dc52 (patch) | |
| tree | 0db9a5a1577c70629f6fbe06ab3ef344b7a4f359 /apps/web | |
| parent | cd593d1b982eec3e575a921dc6d921bfdea07bfe (diff) | |
| download | opencut-dd09bf2949b63d17b3e7311a688d7d84c5d6dc52.tar.gz | |
feat: add project settings
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/src/components/editor/media-panel/index.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/components/editor/media-panel/store.ts | 8 | ||||
| -rw-r--r-- | apps/web/src/components/editor/media-panel/views/settings.tsx | 246 | ||||
| -rw-r--r-- | apps/web/src/components/editor/properties-panel/property-item.tsx | 10 | ||||
| -rw-r--r-- | apps/web/src/components/icons.tsx | 22 | ||||
| -rw-r--r-- | apps/web/src/components/ui/select.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/ui/tabs.tsx | 4 |
7 files changed, 288 insertions, 8 deletions
diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx index a5cd6686..4d80b642 100644 --- a/apps/web/src/components/editor/media-panel/index.tsx +++ b/apps/web/src/components/editor/media-panel/index.tsx @@ -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 ( diff --git a/apps/web/src/components/editor/media-panel/store.ts b/apps/web/src/components/editor/media-panel/store.ts index c661f51e..577417a9 100644 --- a/apps/web/src/components/editor/media-panel/store.ts +++ b/apps/web/src/components/editor/media-panel/store.ts @@ -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 { diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx new file mode 100644 index 00000000..54387dd7 --- /dev/null +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -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> + ); +} diff --git a/apps/web/src/components/editor/properties-panel/property-item.tsx b/apps/web/src/components/editor/properties-panel/property-item.tsx index 804ffb39..86a8d378 100644 --- a/apps/web/src/components/editor/properties-panel/property-item.tsx +++ b/apps/web/src/components/editor/properties-panel/property-item.tsx @@ -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>; } diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx index 43f953e8..065a123d 100644 --- a/apps/web/src/components/icons.tsx +++ b/apps/web/src/components/icons.tsx @@ -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> + ); +} diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx index ce4f340d..53ca27cb 100644 --- a/apps/web/src/components/ui/select.tsx +++ b/apps/web/src/components/ui/select.tsx @@ -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> )); diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx index e5384daf..ea628959 100644 --- a/apps/web/src/components/ui/tabs.tsx +++ b/apps/web/src/components/ui/tabs.tsx @@ -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} |
