"use client"; import { useState } from "react"; import { PanelView } from "@/components/editor/panels/assets/views/base-panel"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { FPS_PRESETS } from "@/fps/presets"; import { floatToFrameRate, frameRateToFloat } from "@/fps/utils"; import { useEditor } from "@/editor/use-editor"; import { Section, SectionContent, SectionHeader, SectionTitle, } from "@/components/section"; import { BackgroundContent } from "./background"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { NumberField } from "@/components/ui/number-field"; import { useEditorStore } from "@/editor/editor-store"; import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft"; import { HugeiconsIcon } from "@hugeicons/react"; import { Tick02Icon } from "@hugeicons/core-free-icons"; import { cn } from "@/utils/ui"; import { dimensionToAspectRatio } from "@/utils/geometry"; import { formatNumberForDisplay } from "@/utils/math"; import { OcSquarePlusIcon } from "@/components/icons"; import type { TCanvasSize } from "@/project/types"; type SettingsView = "project-info" | "background"; function isSettingsView(value: string): value is SettingsView { return value === "project-info" || value === "background"; } const PRESET_LABELS: Record = { "1:1": "1:1", "16:9": "16:9", "9:16": "9:16", "4:3": "4:3", }; function areCanvasSizesEqual({ left, right, }: { left: TCanvasSize; right: TCanvasSize; }) { return left.width === right.width && left.height === right.height; } function formatCanvasDimension({ value }: { value: number }) { return formatNumberForDisplay({ value, maxFractionDigits: 0 }); } function parseCanvasDimension({ input }: { input: string }): number | null { const trimmed = input.trim(); if (!trimmed) return null; const parsed = Number(trimmed); if (!Number.isFinite(parsed)) return null; const rounded = Math.round(parsed); return rounded > 0 ? rounded : null; } function useCanvasDimensionDraft({ value, onCommit, }: { value: number; onCommit: (value: number) => void; }) { const [pendingValue, setPendingValue] = useState(value); return usePropertyDraft({ displayValue: formatCanvasDimension({ value }), parse: (input) => parseCanvasDimension({ input }), onStartEditing: () => { setPendingValue(value); }, onPreview: (nextValue) => { setPendingValue(nextValue); }, onCommit: () => { if (pendingValue !== value) { onCommit(pendingValue); } }, }); } export function SettingsView() { const [view, setView] = useState("project-info"); const editor = useEditor(); const activeProject = useEditor((e) => e.project.getActive()); const { canvasPresets } = useEditorStore(); const currentCanvasSize = activeProject.settings.canvasSize; const canvasSizeMode = activeProject.settings.canvasSizeMode ?? "preset"; const lastCustomCanvasSize = activeProject.settings.lastCustomCanvasSize ?? null; const presetItems = canvasPresets.map((preset, index) => { const ratio = dimensionToAspectRatio(preset); return { id: index.toString(), label: PRESET_LABELS[ratio] ?? ratio, ratio, canvasSize: preset, }; }); const selectedPresetId = canvasSizeMode === "preset" ? (presetItems.find((preset) => areCanvasSizesEqual({ left: preset.canvasSize, right: currentCanvasSize, }), )?.id ?? null) : null; const updateCustomCanvasSize = ({ canvasSize, }: { canvasSize: TCanvasSize; }) => { const shouldUpdateCanvasSize = !areCanvasSizesEqual({ left: canvasSize, right: currentCanvasSize, }); const shouldUpdateLastCustomCanvasSize = lastCustomCanvasSize === null || !areCanvasSizesEqual({ left: canvasSize, right: lastCustomCanvasSize, }); const shouldUpdateCanvasSizeMode = canvasSizeMode !== "custom"; if ( !shouldUpdateCanvasSize && !shouldUpdateLastCustomCanvasSize && !shouldUpdateCanvasSizeMode ) { return; } editor.project.updateSettings({ settings: { ...(shouldUpdateCanvasSize ? { canvasSize } : {}), ...(shouldUpdateCanvasSizeMode ? { canvasSizeMode: "custom" as const } : {}), lastCustomCanvasSize: canvasSize, }, }); }; const selectPresetCanvasSize = ({ canvasSize, }: { canvasSize: TCanvasSize; }) => { const shouldUpdateCanvasSize = !areCanvasSizesEqual({ left: canvasSize, right: currentCanvasSize, }); const shouldUpdateCanvasSizeMode = canvasSizeMode !== "preset"; if (!shouldUpdateCanvasSize && !shouldUpdateCanvasSizeMode) return; editor.project.updateSettings({ settings: { ...(shouldUpdateCanvasSize ? { canvasSize } : {}), ...(shouldUpdateCanvasSizeMode ? { canvasSizeMode: "preset" as const } : {}), }, }); }; const selectCustomCanvasSize = () => { updateCustomCanvasSize({ canvasSize: lastCustomCanvasSize ?? currentCanvasSize, }); }; const widthDraft = useCanvasDimensionDraft({ value: currentCanvasSize.width, onCommit: (width) => updateCustomCanvasSize({ canvasSize: { width, height: currentCanvasSize.height }, }), }); const heightDraft = useCanvasDimensionDraft({ value: currentCanvasSize.height, onCommit: (height) => updateCustomCanvasSize({ canvasSize: { width: currentCanvasSize.width, height }, }), }); const isCustomSelected = canvasSizeMode === "custom"; return ( { if (isSettingsView(value)) { setView(value); } }} > Project info Background } > {view === "project-info" && (
Name {activeProject.metadata.name}
Frame rate
Aspect ratio {presetItems.map((preset) => ( } isSelected={selectedPresetId === preset.id} onClick={() => { selectPresetCanvasSize({ canvasSize: preset.canvasSize, }); }} /> ))}
} isSelected={isCustomSelected} onClick={selectCustomCanvasSize} uiOptions={
} />
)} {view === "background" && }
); } function AspectRatioItem({ label, previewIcon, isSelected, onClick, uiOptions, }: { label: string; previewIcon: React.ReactNode; isSelected: boolean; onClick: () => void; uiOptions?: React.ReactNode; }) { return ( ); } function AspectRatioPreview({ ratio }: { ratio?: string }) { if (!ratio) return null; const [w, h] = ratio.split(":").map(Number); const maxSize = 16; const width = w >= h ? maxSize : (w / h) * maxSize; const height = h >= w ? maxSize : (h / w) * maxSize; return (
); }