mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-06 01:14:36 +08:00
feat(toolbar): enhance image node hover toolbar with new tools and settings modal
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Camera, Copy, Lock, LockOpen, Maximize2, Scissors, Sparkles, Upload, ZoomIn } from "lucide-react";
|
||||
|
||||
import type { CanvasNodeData } from "../types";
|
||||
|
||||
export type ImageNodeActionToolId = "copyPrompt" | "replace" | "resize" | "crop" | "upscale" | "superResolve" | "angle" | "view";
|
||||
export type ImageQuickToolId = "info" | "delete" | "saveAsset" | "download" | "edit" | ImageNodeActionToolId;
|
||||
|
||||
export type ImageToolHandlers = {
|
||||
onUpload: (node: CanvasNodeData) => void;
|
||||
onToggleFreeResize: (node: CanvasNodeData) => void;
|
||||
onCrop: (node: CanvasNodeData) => void;
|
||||
onUpscale: (node: CanvasNodeData) => void;
|
||||
onSuperResolve: (node: CanvasNodeData) => void;
|
||||
onAngle: (node: CanvasNodeData) => void;
|
||||
onViewImage: (node: CanvasNodeData) => void;
|
||||
onCopyPrompt: (node: CanvasNodeData) => void;
|
||||
};
|
||||
|
||||
export type ImageToolDefinition = {
|
||||
id: ImageNodeActionToolId;
|
||||
defaultVisible: boolean;
|
||||
panelLabel: string;
|
||||
label: string | ((node: CanvasNodeData) => string);
|
||||
title: string | ((node: CanvasNodeData) => string);
|
||||
icon: (node: CanvasNodeData) => ReactNode;
|
||||
active?: (node: CanvasNodeData) => boolean;
|
||||
run: (node: CanvasNodeData, handlers: ImageToolHandlers) => void;
|
||||
};
|
||||
|
||||
export type ImageQuickToolsConfig = {
|
||||
ids: ImageQuickToolId[];
|
||||
showLabels: boolean;
|
||||
};
|
||||
|
||||
export const IMAGE_QUICK_TOOLS_STORAGE_KEY = "canvas-image-quick-tools-v3";
|
||||
|
||||
const defaultBaseToolIds: ImageQuickToolId[] = ["info", "delete", "saveAsset", "download", "edit"];
|
||||
|
||||
export const imageToolDefinitions: ImageToolDefinition[] = [
|
||||
{
|
||||
id: "copyPrompt",
|
||||
defaultVisible: true,
|
||||
panelLabel: "复制提示词",
|
||||
label: "复制提示词",
|
||||
title: "复制生成该图片的提示词",
|
||||
icon: () => <Copy className="size-4" />,
|
||||
run: (node, handlers) => handlers.onCopyPrompt(node),
|
||||
},
|
||||
{
|
||||
id: "replace",
|
||||
defaultVisible: true,
|
||||
panelLabel: "替换图片",
|
||||
label: "替换图片",
|
||||
title: "替换图片",
|
||||
icon: () => <Upload className="size-4" />,
|
||||
run: (node, handlers) => handlers.onUpload(node),
|
||||
},
|
||||
{
|
||||
id: "resize",
|
||||
defaultVisible: true,
|
||||
panelLabel: "锁比例",
|
||||
label: (node) => (node.metadata?.freeResize ? "自由比例" : "锁比例"),
|
||||
title: (node) => (node.metadata?.freeResize ? "切换为等比缩放" : "切换为自由比例"),
|
||||
icon: (node) => (node.metadata?.freeResize ? <LockOpen className="size-4" /> : <Lock className="size-4" />),
|
||||
active: (node) => Boolean(node.metadata?.freeResize),
|
||||
run: (node, handlers) => handlers.onToggleFreeResize(node),
|
||||
},
|
||||
{
|
||||
id: "crop",
|
||||
defaultVisible: true,
|
||||
panelLabel: "裁剪",
|
||||
label: "裁剪",
|
||||
title: "裁剪并生成新节点",
|
||||
icon: () => <Scissors className="size-4" />,
|
||||
run: (node, handlers) => handlers.onCrop(node),
|
||||
},
|
||||
{
|
||||
id: "upscale",
|
||||
defaultVisible: true,
|
||||
panelLabel: "放大",
|
||||
label: "放大",
|
||||
title: "放大图片分辨率",
|
||||
icon: () => <ZoomIn className="size-4" />,
|
||||
run: (node, handlers) => handlers.onUpscale(node),
|
||||
},
|
||||
{
|
||||
id: "superResolve",
|
||||
defaultVisible: true,
|
||||
panelLabel: "超分",
|
||||
label: "超分",
|
||||
title: "AI 超分",
|
||||
icon: () => <Sparkles className="size-4" />,
|
||||
run: (node, handlers) => handlers.onSuperResolve(node),
|
||||
},
|
||||
{
|
||||
id: "angle",
|
||||
defaultVisible: true,
|
||||
panelLabel: "多角度",
|
||||
label: "多角度",
|
||||
title: "生成角度",
|
||||
icon: () => <Camera className="size-4" />,
|
||||
run: (node, handlers) => handlers.onAngle(node),
|
||||
},
|
||||
{
|
||||
id: "view",
|
||||
defaultVisible: true,
|
||||
panelLabel: "查看大图",
|
||||
label: "查看大图",
|
||||
title: "查看图片详情",
|
||||
icon: () => <Maximize2 className="size-4" />,
|
||||
run: (node, handlers) => handlers.onViewImage(node),
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultImageQuickToolIds: ImageQuickToolId[] = [...defaultBaseToolIds, ...imageToolDefinitions.filter((tool) => tool.defaultVisible).map((tool) => tool.id)];
|
||||
|
||||
export function buildImageToolbarTools(node: CanvasNodeData, handlers: ImageToolHandlers) {
|
||||
return imageToolDefinitions.map((tool) => ({
|
||||
id: tool.id,
|
||||
label: resolveToolText(tool.label, node),
|
||||
title: resolveToolText(tool.title, node),
|
||||
icon: tool.icon(node),
|
||||
active: tool.active?.(node),
|
||||
onClick: () => tool.run(node, handlers),
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeImageQuickToolIds(value: unknown[]) {
|
||||
const allIds: ImageQuickToolId[] = [...defaultBaseToolIds, ...imageToolDefinitions.map((tool) => tool.id)];
|
||||
const ids = new Set(allIds);
|
||||
return allIds.filter((id) => value.includes(id) && ids.has(id));
|
||||
}
|
||||
|
||||
export function readImageQuickToolsConfig(value: unknown): ImageQuickToolsConfig {
|
||||
if (Array.isArray(value)) return { ids: normalizeImageQuickToolIds(value), showLabels: true };
|
||||
if (!value || typeof value !== "object") return { ids: defaultImageQuickToolIds, showLabels: true };
|
||||
const data = value as Partial<ImageQuickToolsConfig>;
|
||||
return {
|
||||
ids: Array.isArray(data.ids) ? normalizeImageQuickToolIds(data.ids) : defaultImageQuickToolIds,
|
||||
showLabels: data.showLabels !== false,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveToolText(value: string | ((node: CanvasNodeData) => string), node: CanvasNodeData) {
|
||||
return typeof value === "function" ? value(node) : value;
|
||||
}
|
||||
Reference in New Issue
Block a user