import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent, type ReactNode } from "react"; import { Segmented, Switch } from "antd"; import { CircleDot, Grid2x2, Moon, Palette, Sun, Square, Info } from "lucide-react"; import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler"; import { FloatingDock } from "@/components/ui/aceternity/floating-dock"; import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface"; import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu"; import { ToolbarSettingsModal } from "@/components/canvas/toolbars/toolbar-settings-modal"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style"; import { canvasThemes, type CanvasBackgroundMode, type CanvasColorTheme, type CanvasTheme } from "@/lib/canvas-theme"; import { defaultToolbarPrefs, readToolbarPrefs, resolveAddNodeMenuCommands, resolveToolbarEntries, type AddNodeMenuCommand, type ToolContext, type ToolbarHandlers, type ToolbarPrefs } from "@/lib/canvas/tool-registry"; import { useThemeStore } from "@/stores/use-theme-store"; import type { CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas"; export function CanvasToolbar({ selectedCount, workspaceMode, canvasTool, onToolChange, isProjectLinked, canUndo, canRedo, backgroundMode, showImageInfo, onAddImage, onAddVideo, onAddAudio, onAddText, onChooseStyle, onAddScript, onAddFrame, onAddDrawing, onOpenDirector, onUndo, onRedo, onUpload, onDelete, onClear, onDeselect, onBackgroundModeChange, onShowImageInfoChange, onOpenMyAssets, onOpenProjectCharacters, }: { selectedCount: number; workspaceMode: CanvasWorkspaceMode; canvasTool: CanvasToolMode; onToolChange: (tool: CanvasToolMode) => void; isProjectLinked: boolean; canUndo: boolean; canRedo: boolean; backgroundMode: CanvasBackgroundMode; showImageInfo: boolean; onAddImage: () => void; onAddVideo: () => void; onAddAudio: () => void; onAddText: () => void; onChooseStyle: () => void; onAddScript: () => void; onAddFrame: () => void; onAddDrawing: () => void; onOpenDirector: () => void; onUndo: () => void; onRedo: () => void; onUpload: () => void; onDelete: () => void; onClear: () => void; onDeselect: () => void; onBackgroundModeChange: (mode: CanvasBackgroundMode) => void; onShowImageInfoChange: (show: boolean) => void; onOpenMyAssets: () => void; onOpenProjectCharacters: () => void; }) { const rootRef = useRef(null); const dockRef = useRef(null); const colorTheme = useThemeStore((state) => state.theme); const setTheme = useThemeStore((state) => state.setTheme); 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 (!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; 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, onAddDrawing, onChooseStyle, onOpenDirector, onUpload, 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: () => {}, onMergeVideos: () => {}, onNodeInfo: () => {}, onNodeDelete: () => {}, onNodeRetry: () => {}, onNodeEditText: () => {}, onNodeDecreaseFont: () => {}, onNodeIncreaseFont: () => {}, onNodeToggleDialog: () => {}, onNodeAnnotate: () => {}, onNodeGenerateImage: () => {}, onNodeUpload: () => {}, onNodeDownload: () => {}, onNodeSaveAsset: () => {}, onNodeMaskEdit: () => {}, onNodeEmotion: () => {}, onNodePortraitTexture: () => {}, onNodeCrop: () => {}, onNodeSplit: () => {}, onNodeUpscale: () => {}, onNodeSuperResolve: () => {}, onNodeAngle: () => {}, onNodeViewImage: () => {}, onNodeExtractVideoLastFrame: () => {}, onNodeExtractAudioFromVideo: () => {}, onNodeTrimVideoRegenerate: () => {}, onNodeSubtitles: () => {}, onNodeTimeline: () => {}, onNodeReversePrompt: () => {}, onNodeToggleFreeResize: () => {}, onNodeToggleLocked: () => {}, onNodeCopyPrompt: () => {}, } as ToolbarHandlers; const ctx: ToolContext = { selectedCount, selectedNodeTypes: new Set(), selectedVideoCount: 0, canvasTool, workspaceMode, isProjectLinked, canUndo, canRedo, extractingVideoFrame: false, extractingAudio: false, trimmingVideo: false, mergingVideos: false, addPanelOpen: addOpen, appearancePanelOpen: appearanceOpen, settingsPanelOpen: settingsOpen, handlers, }; const items = resolveToolbarEntries("main", ctx, prefs ?? defaultToolbarPrefs("main")); // 解析添加节点菜单命令——onClick 绑定到 runAddAction 以在执行后关闭面板 const addNodeCommands = resolveAddNodeMenuCommands(ctx); const toCommand = (cmd: AddNodeMenuCommand): CanvasCreateCommand => ({ id: cmd.id, label: cmd.label, icon: cmd.icon, badge: cmd.badge, section: cmd.section, onClick: () => runAddAction(() => cmd.run(ctx)), }); const createCommands = addNodeCommands.map(toCommand); return (
{addOpen ? ( ) : null} {appearanceOpen ? ( event.stopPropagation()}> } title="画布外观" subtitle="调整整个创作空间" theme={theme} />
主题模式
浅色 深色
空间网格
onBackgroundModeChange(value as CanvasBackgroundMode)} options={[ { value: "dots", label: 点 }, { value: "lines", label: 线 }, { value: "blank", label: 空白 }, ]} />
图片信息
) : 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 CanvasThemeButton({ colorTheme, targetTheme, onThemeChange, children }: { colorTheme: CanvasColorTheme; targetTheme: CanvasColorTheme; onThemeChange: (theme: CanvasColorTheme) => void; children: ReactNode }) { const theme = canvasThemes[colorTheme]; const active = colorTheme === targetTheme; return ( {children} ); } 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; }