import { useEffect, useMemo, useState, type ReactNode } from "react"; import { App, Modal, Segmented, Tooltip } from "antd"; import { Download, Ellipsis, FolderPlus, Image as ImageIcon, Info, MessageSquare, Minus, Music2, Pencil, Plus, RefreshCw, Settings2, Trash2, Upload, Video } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { formatBytes, getDataUrlByteSize } from "@/lib/image-utils"; import { useCopyText } from "@/hooks/use-copy-text"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasNodeType, type CanvasNodeData, type ViewportTransform } from "@/types/canvas"; import type { CanvasNodeToolbarItem } from "@/types/canvas-plugin"; import { ImageToolSettingsModal, type ImageToolbarSettingsTool } from "./canvas-image-toolbar-settings-modal"; import { IMAGE_QUICK_TOOLS_STORAGE_KEY, buildImageToolbarTools, defaultImageQuickToolIds, readImageQuickToolsConfig, type ImageQuickToolId } from "./canvas-image-toolbar-tools"; type CanvasNodeHoverToolbarProps = { node: CanvasNodeData | null; viewport: ViewportTransform; onKeep: (nodeId: string) => void; onLeave: () => void; onInfo: (node: CanvasNodeData) => void; onEditText: (node: CanvasNodeData) => void; onDecreaseFont: (node: CanvasNodeData) => void; onIncreaseFont: (node: CanvasNodeData) => void; onToggleDialog: (node: CanvasNodeData) => void; onGenerateImage: (node: CanvasNodeData) => void; onUpload: (node: CanvasNodeData) => void; onDownload: (node: CanvasNodeData) => void; onSaveAsset: (node: CanvasNodeData) => void; onMaskEdit: (node: CanvasNodeData) => void; onCrop: (node: CanvasNodeData) => void; onSplit: (node: CanvasNodeData) => void; onUpscale: (node: CanvasNodeData) => void; onSuperResolve: (node: CanvasNodeData) => void; onAngle: (node: CanvasNodeData) => void; onViewImage: (node: CanvasNodeData) => void; onReversePrompt: (node: CanvasNodeData) => void; onRetry: (node: CanvasNodeData) => void; onToggleFreeResize: (node: CanvasNodeData) => void; onDelete: (node: CanvasNodeData) => void; extraTools?: CanvasNodeToolbarItem[]; }; type ToolbarTool = { id: string; title: string; label: string; icon: ReactNode; onClick: () => void; active?: boolean; danger?: boolean; }; export function CanvasNodeHoverToolbar({ node, viewport, onKeep, onLeave, onInfo, onEditText, onDecreaseFont, onIncreaseFont, onToggleDialog, onGenerateImage, onUpload, onDownload, onSaveAsset, onMaskEdit, onCrop, onSplit, onUpscale, onSuperResolve, onAngle, onViewImage, onReversePrompt, onRetry, onToggleFreeResize, onDelete, extraTools = [], }: CanvasNodeHoverToolbarProps) { const [quickImageToolIds, setQuickImageToolIds] = useState(defaultImageQuickToolIds); const [showImageToolLabels, setShowImageToolLabels] = useState(true); const [draftImageToolIds, setDraftImageToolIds] = useState(defaultImageQuickToolIds); const [draftShowImageToolLabels, setDraftShowImageToolLabels] = useState(true); const [imageToolSettingsOpen, setImageToolSettingsOpen] = useState(false); const { message } = App.useApp(); const copyText = useCopyText(); useEffect(() => { try { const stored = window.localStorage.getItem(IMAGE_QUICK_TOOLS_STORAGE_KEY); if (!stored) return; const parsed = JSON.parse(stored) as unknown; const config = readImageQuickToolsConfig(parsed); setQuickImageToolIds(config.ids); setShowImageToolLabels(config.showLabels); } catch { window.localStorage.removeItem(IMAGE_QUICK_TOOLS_STORAGE_KEY); } }, []); useEffect(() => { setImageToolSettingsOpen(false); }, [node?.id]); if (!node) return null; const activeNode = node; const left = viewport.x + (node.position.x + node.width / 2) * viewport.k; const top = viewport.y + node.position.y * viewport.k - 14; const isImage = node.type === CanvasNodeType.Image; const isVideo = node.type === CanvasNodeType.Video; const isAudio = node.type === CanvasNodeType.Audio; const hasImage = isImage && Boolean(node.metadata?.content); const hasVideo = isVideo && Boolean(node.metadata?.content); const hasAudio = isAudio && Boolean(node.metadata?.content); const isText = node.type === CanvasNodeType.Text; const isConfig = node.type === CanvasNodeType.Config; const canOpenDialog = isText || hasImage || isVideo; const canRetry = node.metadata?.status === "error"; const quickImageToolIdSet = new Set(quickImageToolIds); const copyImagePrompt = (target: CanvasNodeData) => { const prompt = target.metadata?.prompt?.trim(); if (!prompt) { message.warning("暂无可复制的提示词"); return; } copyText(prompt, "提示词已复制"); }; const imageTools = buildImageToolbarTools(node, { onUpload, onToggleFreeResize, onMaskEdit, onCrop, onSplit, onUpscale, onSuperResolve, onAngle, onViewImage, onCopyPrompt: copyImagePrompt, onReversePrompt }); function openImageToolSettings() { onKeep(activeNode.id); setDraftImageToolIds(quickImageToolIds); setDraftShowImageToolLabels(showImageToolLabels); setImageToolSettingsOpen(true); } const baseToolbarTools: ToolbarTool[] = [ { id: "info", title: "查看节点信息", label: "信息", icon: , onClick: () => onInfo(node) }, { id: "delete", title: "移除节点", label: "删除", icon: , onClick: () => onDelete(node), danger: true }, ]; const nodeToolbarTools: ToolbarTool[] = [ ...(canRetry ? [{ id: "retry", title: "重新生成", label: "重试", icon: , onClick: () => onRetry(node) }] : []), ...(hasImage || hasVideo || isText ? [{ id: "saveAsset", title: "加入我的资产", label: "存资产", icon: , onClick: () => onSaveAsset(node) }] : []), ...(hasImage || hasVideo || hasAudio ? [{ id: "download", title: hasAudio ? "下载音频" : hasVideo ? "下载视频" : "下载图片", label: "下载", icon: , onClick: () => onDownload(node) }] : []), ...(canOpenDialog ? [{ id: "edit", title: "编辑", label: "编辑", icon: , onClick: () => onToggleDialog(node) }] : []), ...(isText ? [{ id: "editText", title: "编辑文本", label: "编辑文字", icon: , onClick: () => onEditText(node) }] : []), ...(isText ? [{ id: "generateImage", title: "用文本生图", label: "生图", icon: , onClick: () => onGenerateImage(node) }] : []), ...(isConfig ? [{ id: "config", title: "生成配置", label: "生成配置", icon: , onClick: () => onToggleDialog(node) }] : []), ...(isText ? [{ id: "decreaseFont", title: "减小字号", label: "缩小", icon: , onClick: () => onDecreaseFont(node) }] : []), ...(isText ? [{ id: "increaseFont", title: "增大字号", label: "放大", icon: , onClick: () => onIncreaseFont(node) }] : []), ...(isImage && !hasImage ? [{ id: "uploadImage", title: "上传图片", label: "上传图片", icon: , onClick: () => onUpload(node) }] : []), ...(isVideo ? [{ id: "uploadVideo", title: hasVideo ? "替换视频" : "上传视频", label: hasVideo ? "替换视频" : "上传视频", icon: