import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent, type ReactNode } from "react"; import { Switch } from "@/components/ui/base/switch"; import { Palette, Info } from "lucide-react"; import { FloatingDock } from "@/components/ui/aceternity/floating-dock"; import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface"; import { CanvasAppearanceControls } from "@/components/canvas/canvas-appearance-controls"; import { useCanvasOverlayLayer } from "@/components/canvas/canvas-overlay-layer"; import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu"; import { useCanvasCreateCommands } from "@/components/canvas/use-canvas-create-commands"; import { ToolbarSettingsModal } from "@/components/canvas/toolbars/toolbar-settings-modal"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style"; import type { CanvasAppearance } from "@/lib/canvas/canvas-appearance"; import { canvasThemes, type CanvasBackgroundMode, type CanvasTheme } from "@/lib/canvas-theme"; import { defaultToolbarPrefs, readToolbarPrefs, resolveToolbarEntries, type ToolContext, type ToolbarHandlers, type ToolbarPrefs } from "@/lib/canvas/tool-registry"; import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store"; import type { CanvasNodeTypeId, CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas"; export function CanvasToolbar({ selectedCount, workspaceMode, canvasTool, onToolChange, isProjectLinked, canUndo, canRedo, appearance, backgroundMode, showImageInfo, onAddImage, onAddVideo, onAddAudio, onAddText, onChooseStyle, onAddScript, onAddFrame, onAddFolder, onAddDrawing, onAddExtensionNode, onAddWorkflow, onOpenDirector, onUndo, onRedo, onUpload, onDelete, onClear, onDeselect, onAppearanceChange, onSaveAppearanceDefault, onBackgroundModeChange, onShowImageInfoChange, onOpenWorkspace, onOpenMyAssets, onOpenProjectCharacters, }: { selectedCount: number; workspaceMode: CanvasWorkspaceMode; canvasTool: CanvasToolMode; onToolChange: (tool: CanvasToolMode) => void; isProjectLinked: boolean; canUndo: boolean; canRedo: boolean; appearance: CanvasAppearance; backgroundMode: CanvasBackgroundMode; showImageInfo: boolean; onAddImage: () => void; onAddVideo: () => void; onAddAudio: () => void; onAddText: () => void; onChooseStyle: () => void; onAddScript: () => void; onAddFrame: () => void; onAddFolder: () => void; onAddDrawing: () => void; onAddExtensionNode: (type: CanvasNodeTypeId) => void; onAddWorkflow: () => void; onOpenDirector: () => void; onUndo: () => void; onRedo: () => void; onUpload: () => void; onDelete: () => void; onClear: () => void; onDeselect: () => void; onAppearanceChange: (appearance: CanvasAppearance) => void; onSaveAppearanceDefault: (appearance: CanvasAppearance) => void; onBackgroundModeChange: (mode: CanvasBackgroundMode) => void; onShowImageInfoChange: (show: boolean) => void; onOpenWorkspace?: () => void; onOpenMyAssets: () => void; onOpenProjectCharacters: () => void; }) { const rootRef = useRef(null); const { bringToFront, zIndex } = useCanvasOverlayLayer("main-toolbar", "var(--z-toolbar)"); const dockRef = useRef(null); const colorTheme = useActiveTheme(); const theme = canvasThemes[colorTheme]; const [addOpen, setAddOpen] = useState(false); const [appearanceOpen, setAppearanceOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [panelX, setPanelX] = useState(0); const [prefs, setPrefs] = useState(() => readToolbarPrefs("main")); useEffect(() => { if (addOpen || appearanceOpen) bringToFront(); }, [addOpen, appearanceOpen, bringToFront]); // 设置面板关闭后重新读取偏好(用户可能调整了排序/显隐) useEffect(() => { if (!settingsOpen) setPrefs(readToolbarPrefs("main")); }, [settingsOpen]); const placePanel = (event: ReactMouseEvent) => setPanelX(getPanelX(dockRef.current, event.currentTarget)); const runAddAction = (action: () => void) => { action(); setAddOpen(false); }; // 点击外部关闭浮层面板 useEffect(() => { if (!addOpen && !appearanceOpen) return; const closeFloatingPanels = (event: PointerEvent) => { const target = event.target instanceof Node ? event.target : null; if (target && rootRef.current?.contains(target)) return; const element = event.target instanceof Element ? event.target : null; if (element?.closest(".ant-color-picker,.ant-popover")) return; setAddOpen(false); setAppearanceOpen(false); }; document.addEventListener("pointerdown", closeFloatingPanels, true); return () => document.removeEventListener("pointerdown", closeFloatingPanels, true); }, [addOpen, appearanceOpen]); // 构建 handlers(主工具栏只需要部分回调,其余用 no-op 占位满足类型) const handlers: ToolbarHandlers = { onToolChange, onDeselect, onUndo, onRedo, onClear, onAddText, onAddImage, onAddVideo, onAddAudio, onAddScript, onAddFrame, onAddFolder, onAddDrawing, onAddExtensionNode, onAddWorkflow, onChooseStyle, onOpenDirector, onUpload, onOpenWorkspace, onOpenMyAssets, onOpenProjectCharacters, onBackgroundModeChange, onShowImageInfoChange, onToggleAddPanel: (event: ReactMouseEvent) => { placePanel(event); setAppearanceOpen(false); setSettingsOpen(false); setAddOpen((value) => !value); }, onToggleAppearancePanel: (event: ReactMouseEvent) => { placePanel(event); setAddOpen(false); setSettingsOpen(false); setAppearanceOpen((value) => !value); }, onToggleSettingsPanel: () => { setAddOpen(false); setAppearanceOpen(false); setSettingsOpen((value) => !value); }, onDeleteSelected: onDelete, // 以下为多选/节点悬停工具栏回调,主工具栏不使用,用 no-op 占位 onAlign: () => {}, onArrange: () => {}, onCreateStoryboard: () => {}, onCreateReferenceGroup: () => {}, onBatchConnect: () => {}, onMergeVideos: () => {}, onSendSelectionToAgent: () => {}, onNodeInfo: () => {}, onNodeDelete: () => {}, onNodeRetry: () => {}, onNodeEditText: () => {}, onNodeDecreaseFont: () => {}, onNodeIncreaseFont: () => {}, onNodeToggleDialog: () => {}, onNodeAnnotate: () => {}, onNodeGenerateImage: () => {}, onNodeUpload: () => {}, onNodeDownload: () => {}, onNodeSaveAsset: () => {}, onNodeMaskEdit: () => {}, onNodeImageEdit: () => {}, onNodeRemoveBackground: () => {}, onNodeEmotion: () => {}, onNodePortraitTexture: () => {}, onNodeCrop: () => {}, onNodeSplit: () => {}, onNodeUpscale: () => {}, onNodeSuperResolve: () => {}, onNodeAngle: () => {}, onNodeViewImage: () => {}, onNodeExtractVideoFrames: () => {}, onNodeExtractAudioFromVideo: () => {}, onNodeTrimVideoSegments: () => {}, onNodeSubtitles: () => {}, onNodeTimeline: () => {}, onNodeReversePrompt: () => {}, onNodeToggleFreeResize: () => {}, onNodeToggleLocked: () => {}, onNodeCopyPrompt: () => {}, } as ToolbarHandlers; const ctx: ToolContext = { selectedCount, selectedNodeTypes: new Set(), selectedVideoCount: 0, canvasTool, workspaceMode, isProjectLinked, canUndo, canRedo, extractingVideoFrames: false, extractingAudio: false, trimmingVideo: false, mergingVideos: false, addPanelOpen: addOpen, appearancePanelOpen: appearanceOpen, settingsPanelOpen: settingsOpen, handlers, }; const items = resolveToolbarEntries("main", ctx, prefs ?? defaultToolbarPrefs("main")); // 中央空白起点与主工具栏共用同一份命令解析,避免素材类型和插件节点逐渐分叉。 const createCommands = useCanvasCreateCommands(ctx, runAddAction); return (
{addOpen ? ( ) : null} {appearanceOpen ? ( event.stopPropagation()}> } title="画布外观" subtitle="调整整个创作空间" theme={theme} />
媒体信息
) : null}
setSettingsOpen(false)} toolbar="main" />
); } function AddNodeMenu({ x, theme, commands }: { x: number; theme: CanvasTheme; commands: CanvasCreateCommand[]; }) { return ( event.stopPropagation()}> ); } function PanelHeading({ icon, title, subtitle, theme }: { icon: ReactNode; title: string; subtitle: string; theme: CanvasTheme }) { return (
{icon} {title}{subtitle}
); } function getPanelX(dock: HTMLDivElement | null, target: HTMLElement) { if (!dock) return 0; const rootBox = dock.parentElement?.getBoundingClientRect() || dock.getBoundingClientRect(); const box = target.getBoundingClientRect(); return box.left - rootBox.left + box.width / 2; }