diff --git a/CHANGELOG.md b/CHANGELOG.md index 34606a0..04e64db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.70.0] - 2025-04-14 +- 设置菜单对UI完全自定义 +- 整合notification到gululu统一控制 +- 新增GuLuLu +- 新增GuLuLu右键菜单 +- 新增GuLuLu设置项 +- 新增GuLuLu继承KayTool通知为流式输出 +- 修复Settings中设置项的bug +- 修复Monitor的初始位置问题 +- 增强Clean VRAM兼容性 +- 修复setget颜色设置问题 +- 新增KayToolActions适配GuLuLu右键菜单 +- UI初始配色调整 + ## [0.65.0] - 2025-04-9 - 大幅度提升Monitor性能 - 增加Clean VRAM diff --git a/README.md b/README.md index 877c257..342f004 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ This is a custom node utility package developed for ComfyUI. Initially created f **📌 所有 KayTool 节点包用到的资源都在 `ComfyUI/custom_nodes/kaytool` 文件夹内。| All KayTool node packages use resources in the `ComfyUI/custom_nodes/kaytool` folder.** +**⚙️ 在ComfyUI左下角的设置菜单里,所有 KayTool 的相关功能都支持自定义设置。| All KayTool features support custom settings in the ComfyUI settings menu.** + ## 资源监视器 Resource Monitor @@ -43,6 +45,15 @@ https://github.com/user-attachments/assets/2cd4179c-57fa-42dc-ae50-2feae9708e42 https://github.com/user-attachments/assets/b8d1d3f0-04d1-46c5-968a-e433778b73e6 +## 咕噜噜 GuLuLu + +- 主打陪伴的咕噜噜,支持调整KayTool的通知位置 + GuLuLu, featuring companionship, supports adjusting KayTool notification positions. +- 继承KayTool的通知为流式输出 + Inherits KayTool notifications as streaming output. +- 提供强大的右键菜单项目,快速调用KayTool的进阶功能 + Provides powerful right-click menu items, quickly calling KayTool's advanced features. + ## workflow PNG功能 Workflow Export to PNG - 支持在右键菜单中`KayTool-workflow PNG`将当前工作流节点地图保存为PNG格式并内嵌工作流信息。 diff --git a/locales/zh/main.json b/locales/zh/main.json index 80193ff..99bc3bf 100644 --- a/locales/zh/main.json +++ b/locales/zh/main.json @@ -6,7 +6,8 @@ "Custom Web Logo (BETA)": "自定义网页徽标", "Workflow PNG": "Workflow PNG", "NodeAlignment": "节点对齐工具栏", - "Resource Monitor": "资源监视器" + "Resource Monitor": "资源监视器", + "GuLuLu": "咕噜噜" }, "nodeCategories": { "Image": "图像", diff --git a/locales/zh/settings.json b/locales/zh/settings.json index 1c1d96c..a40edd6 100644 --- a/locales/zh/settings.json +++ b/locales/zh/settings.json @@ -40,7 +40,13 @@ "name": "文件必须 < 1MB", "tooltip": "这是一个测试功能,文件最好 <1MB ,否则可能无法显示" }, - + "KayTool_GuLuLuSize": { + "name": "你要一个多大的咕噜噜~啊?" + }, + "KayTool_EnableGuLuLu": { + "name": "让咕噜噜陪着你吧~!", + "tooltip": "咕噜噜会继承KayTool的通知项为流式输出,并支持通知框位置调整,以及强大的右键菜单项" + }, "KayTool_EnableAlignmentShortcuts": { "name": "启用快捷键'Shift+W/A/S/D'", "tooltip": "W/A/S/D 对应上、左、下、右对齐" diff --git a/pyproject.toml b/pyproject.toml index 5621b50..e55e43c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "kaytool" description ="KayTool boosts ComfyUI workflow efficiency with powerful tools like: image processing (resize, color, blur, expand, merge, crop), background/mask tasks via BiRefNet/RemBG, wireless data transfer (Get & Out), AI translation (Tencent/Baidu), Professional high-precision resource monitoring, dynamic math operations, flexible text handling, precision sliders, metadata/color profile-supported image saving, batch processing via folder loading, canvas node map export as PNG with metadata, quick Run options via right-click, node alignment toolbar with custom styles, and custom ComfyUI logo integration." -version = "0.65.1" +version = "0.70.0" license = {file = "LICENSE"} dependencies = ["rembg, timm, opencv-python, py-cpuinfo, pynvml"] diff --git a/web/cleanvram.js b/web/cleanvram.js index 8ab83d7..a875476 100644 --- a/web/cleanvram.js +++ b/web/cleanvram.js @@ -1,5 +1,66 @@ import { app } from "../../../scripts/app.js"; -import { showNotification, hideNotification } from "./notification.js"; +import { showNotification, hideNotification } from "./gululu.js"; + +// 全局动作注册表 +const KayToolActions = window.KayToolActions || {}; +window.KayToolActions = KayToolActions; + +async function cleanVRAM() { + const notifyElement = showNotification({ + message: "🧹 GuLuLu: Working...", + size: "medium", + bgColor: "#fff3cd" + }); + + try { + const response = await fetch("/kaytool/clean_vram", { method: "POST" }); + const message = await response.text(); + + hideNotification(notifyElement); + await new Promise(resolve => setTimeout(resolve, 350)); + + if (!response.ok) { + showNotification({ + message: message || "🧹 GuLuLu: Failed due to server error", + bgColor: "#f8d7da", + size: "medium", + timeout: 7000 + }); + return; + } + + const match = message.match(/Freed ([\d.]+) GB \(Allocated: ([\d.]+) GB -> ([\d.]+) GB, Total: ([\d.]+) GB -> ([\d.]+) GB\)/); + if (match) { + showNotification({ + message: `🧹 GuLuLu: Done! +**${match[2]} GB** ➔ **${match[3]} GB** +(Total: ${match[4]} GB ➔ ${match[5]} GB)`, + bgColor: "#d4edda", + size: "medium", + timeout: 8000 + }); + } else { + showNotification({ + message: `🧹 GuLuLu: ${message}`, + bgColor: "#d4edda", + size: "medium", + timeout: 8000 + }); + } + } catch (err) { + hideNotification(notifyElement); + await new Promise(resolve => setTimeout(resolve, 350)); + showNotification({ + message: `🧹 GuLuLu: ${err.message}`, + bgColor: "#f8d7da", + size: "medium", + timeout: 5000 + }); + } +} + +// 注册 Clean VRAM 动作 +KayToolActions.cleanVRAM = cleanVRAM; app.registerExtension({ name: "KayTool.CleanVRAM", @@ -8,7 +69,7 @@ app.registerExtension({ LGraphCanvas.prototype.getCanvasMenuOptions = function (...args) { const options = originalGetCanvasMenuOptions.apply(this, args) || []; const newOptions = [...options]; - const showCleanVRAM = app.ui.settings.getSettingValue("KayTool.ShowCleanVRAM"); + const showCleanVRAM = app.ui.settings.getSettingValue("KayTool.ShowCleanVRAM") ?? true; if (showCleanVRAM) { let kaytoolMenu = newOptions.find(opt => opt?.content === "KayTool") || { @@ -23,57 +84,7 @@ app.registerExtension({ if (!kaytoolMenu.submenu.options.some(opt => opt?.content === "🧹 Clean VRAM")) { kaytoolMenu.submenu.options.push({ content: "🧹 Clean VRAM", - async callback() { - const notifyElement = showNotification({ - message: "🧹 **Clean:** Working...", - size: "medium", - bgColor: "#fff3cd" - }); - - try { - const response = await fetch("/kaytool/clean_vram", { method: "POST" }); - const message = await response.text(); - - hideNotification(notifyElement); - - if (!response.ok) { - showNotification({ - message: message || "🧹 **Clean:** Failed due to server error", - bgColor: "#f8d7da", - size: "medium", - timeout: 7000 - }); - return; - } - - const match = message.match(/Freed ([\d.]+) GB \(Allocated: ([\d.]+) GB -> ([\d.]+) GB, Total: ([\d.]+) GB -> ([\d.]+) GB\)/); - if (match) { - showNotification({ - message: `🧹 **Clean:** Done! -**${match[2]} GB** ➔ **${match[3]} GB** -(Total: ${match[4]} GB ➔ ${match[5]} GB)`, - bgColor: "#d4edda", - size: "medium", - timeout: 8000 - }); - } else { - showNotification({ - message: `🧹 **Clean:** ${message}`, - bgColor: "#d4edda", - size: "medium", - timeout: 8000 - }); - } - } catch (err) { - hideNotification(notifyElement); - showNotification({ - message: `🧹 **Clean:** ${err.message}`, - bgColor: "#f8d7da", - size: "medium", - timeout: 5000 - }); - } - } + callback: cleanVRAM }); } } diff --git a/web/getset.js b/web/getset.js index 7a62bbe..dfefb07 100644 --- a/web/getset.js +++ b/web/getset.js @@ -18,7 +18,6 @@ function showAlert(message) { app.registerExtension({ name: "KayTool.SetGet", registerCustomNodes() { - class KaySetNode extends LGraphNode { defaultVisibility = true; serialize_widgets = true; @@ -27,7 +26,6 @@ app.registerExtension({ constructor(title) { super(title); - // 从 settings 获取当前前景色和背景色 const fgColor = app.ui.settings.getSettingValue("KayTool.SetGetForegroundColor"); const bgColor = app.ui.settings.getSettingValue("KayTool.SetGetBackgroundColor"); this.color = "#" + fgColor; @@ -182,7 +180,6 @@ app.registerExtension({ constructor(title) { super(title); - // 从 settings 获取当前颜色,默认为 "#000000" const fgColor = app.ui.settings.getSettingValue("KayTool.SetGetForegroundColor"); const bgColor = app.ui.settings.getSettingValue("KayTool.SetGetBackgroundColor"); this.color = "#" + fgColor; @@ -289,5 +286,5 @@ app.registerExtension({ title: "𝙆 🛜Get" })); KayGetNode.category = "KayTool"; - }, + } }); \ No newline at end of file diff --git a/web/gululu.js b/web/gululu.js new file mode 100644 index 0000000..585d300 --- /dev/null +++ b/web/gululu.js @@ -0,0 +1,868 @@ +import { app } from "../../../scripts/app.js"; +import { marked } from "./lib/marked.esm.js"; + +// 全局动作注册表 +const KayToolActions = window.KayToolActions || {}; +window.KayToolActions = KayToolActions; + +// GuLuLu 管理器 +const KayGuLuLuManager = { + container: null, + dragState: { isDragging: false, offsetX: 0, offsetY: 0 }, + state: { left: 100, top: 100, transform: "scaleX(1)" }, + pixelCanvas: null, + pixelCtx: null, + pixelDataCache: null, + imgSrc: "/extensions/kaytool/resources/GuLuLu.gif", + effectiveBounds: null, + isAnimating: false, + animationTimer: null, + contextMenu: null, + isEnabled: true, + size: 39, // 默认尺寸 + // GuLuLu 右键菜单配置 + menuConfig: [ + { label: "Say Hello", action: "playAnimation" }, + { label: "🧹 Clean VRAM", action: "cleanVRAM" }, + { label: "📦 Workflow PNG", action: "exportWorkflowPNG" }, + { label: "⭐ Star to me", action: "starToMe" } + ], + + init() { + this.isEnabled = app.ui.settings.getSettingValue("KayTool.EnableGuLuLu") ?? true; + this.size = app.ui.settings.getSettingValue("KayTool.GuLuLuSize") ?? 39; + this.loadState(); + this.injectStyles(); + this.setupGuLuLu(); + this.updatePosition(); + window.KayGuLuLuManager = this; + window.addEventListener("resize", () => this.updatePosition()); + if (this.isEnabled) { + this.startRandomAnimation(); + } + }, + + injectStyles() { + document.head.insertAdjacentHTML( + "beforeend", + `` + ); + }, + + setupGuLuLu() { + if (!this.container) { + this.container = document.createElement("div"); + this.container.id = "kay-gululu-container"; + const img = document.createElement("img"); + img.src = this.imgSrc; + img.alt = "GuLuLu"; + img.onload = () => { + this.updatePixelDataCache(); + this.updatePosition(); + }; + this.container.appendChild(img); + document.body.appendChild(this.container); + + this.pixelCanvas = document.createElement("canvas"); + this.pixelCanvas.style.display = "none"; + document.body.appendChild(this.pixelCanvas); + this.pixelCtx = this.pixelCanvas.getContext("2d", { willReadFrequently: true }); + } + + this.container.classList.toggle("enabled", this.isEnabled); + this.bindEvents(); + }, + + bindEvents() { + if (this.container) { + this.container.removeEventListener("mousemove", this.handleMouseMove); + this.container.removeEventListener("mousedown", this.onDragStart); + this.container.removeEventListener("contextmenu", this.onContextMenu); + } + document.removeEventListener("mousemove", this.onDragging); + document.removeEventListener("mouseup", this.onDragEnd); + document.removeEventListener("click", this.hideContextMenu); + + if (this.container && this.isEnabled) { + this.container.addEventListener("mousemove", this.handleMouseMove.bind(this)); + this.container.addEventListener("mousedown", this.onDragStart.bind(this)); + this.container.addEventListener("contextmenu", this.onContextMenu.bind(this)); + } + document.addEventListener("mousemove", this.onDragging.bind(this)); + document.addEventListener("mouseup", this.onDragEnd.bind(this)); + document.addEventListener("click", this.hideContextMenu.bind(this)); + }, + + updateSize(newSize) { + this.size = newSize; + const img = this.container?.querySelector("img"); + if (img) { + img.style.width = `${this.size}px`; + this.updatePixelDataCache(); + this.updatePosition(); + } + }, + + playAnimation() { + if (!this.isEnabled || this.isAnimating || !this.container) return; + + this.isAnimating = true; + const img = this.container.querySelector("img"); + if (img) { + const src = img.src; + img.src = ""; + img.src = src; + setTimeout(() => { + this.isAnimating = false; + }, 2000); + } + }, + + startRandomAnimation() { + const getRandomInterval = () => { + const min = 300 * 1000; // 10 秒 + const max = 1200 * 1000; // 30 秒 + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + + const scheduleNextAnimation = () => { + clearTimeout(this.animationTimer); + if (!this.isEnabled) return; + const interval = getRandomInterval(); + this.animationTimer = setTimeout(() => { + this.playAnimation(); + scheduleNextAnimation(); + }, interval); + }; + + scheduleNextAnimation(); + }, + + updateEnabledState(enabled) { + this.isEnabled = enabled; + if (!this.container) { + this.setupGuLuLu(); + } else { + this.container.classList.toggle("enabled", enabled); + this.bindEvents(); + } + if (enabled) { + this.updatePosition(); + this.startRandomAnimation(); + } else { + clearTimeout(this.animationTimer); + } + }, + + loadState() { + const savedState = localStorage.getItem("kay-gululu-state"); + if (savedState) { + this.state = JSON.parse(savedState); + } + }, + + saveState() { + localStorage.setItem("kay-gululu-state", JSON.stringify(this.state)); + }, + + updatePosition() { + if (!this.container || !this.isEnabled) return; + const { windowRect, containerRect } = this.getRect(); + + if (!this.pixelDataCache && this.container.querySelector("img").complete) { + this.updatePixelDataCache(); + } + + const bounds = this.effectiveBounds || { left: 0, top: 0, width: containerRect.width, height: containerRect.height }; + this.state.left = Math.max(-bounds.left, Math.min(this.state.left, windowRect.width - (bounds.left + bounds.width))); + this.state.top = Math.max(-bounds.top, Math.min(this.state.top, windowRect.height - (bounds.top + bounds.height))); + + this.container.style.left = `${this.state.left}px`; + this.container.style.top = `${this.state.top}px`; + + const midPoint = windowRect.width / 2; + this.state.transform = containerRect.left + containerRect.width / 2 > midPoint ? "scaleX(-1)" : "scaleX(1)"; + this.container.style.transform = this.state.transform; + }, + + getRect() { + return { + windowRect: { width: window.innerWidth, height: window.innerHeight }, + containerRect: this.container?.getBoundingClientRect() || { left: 0, top: 0, width: 0, height: 0 }, + }; + }, + + updatePixelDataCache() { + const { containerRect } = this.getRect(); + if (containerRect.width === 0 || containerRect.height === 0) { + return; + } + this.pixelCanvas.width = containerRect.width; + this.pixelCanvas.height = containerRect.height; + this.pixelCtx.clearRect(0, 0, this.pixelCanvas.width, this.pixelCanvas.height); + this.pixelCtx.drawImage(this.container.querySelector("img"), 0, 0, containerRect.width, containerRect.height); + this.pixelDataCache = this.pixelCtx.getImageData(0, 0, this.pixelCanvas.width, this.pixelCanvas.height); + + this.calculateEffectiveBounds(); + }, + + calculateEffectiveBounds() { + if (!this.pixelDataCache) return; + + const data = this.pixelDataCache.data; + const width = this.pixelCanvas.width; + const height = this.pixelCanvas.height; + let minX = width, + minY = height, + maxX = 0, + maxY = 0; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + if (data[index + 3] !== 0) { + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + } + + this.effectiveBounds = { + left: minX, + top: minY, + width: maxX - minX + 1, + height: maxY - minY + 1, + }; + }, + + isPixelTransparent(x, y) { + if (!this.pixelDataCache) return true; + const index = (y * this.pixelDataCache.width + x) * 4; + return this.pixelDataCache.data[index + 3] === 0; + }, + + handleMouseMove(e) { + const { containerRect } = this.getRect(); + const offsetX = e.clientX - containerRect.left; + const offsetY = e.clientY - containerRect.top; + + if (offsetX >= 0 && offsetX < containerRect.width && offsetY >= 0 && offsetY < containerRect.height) { + this.container.style.cursor = this.isPixelTransparent(offsetX, offsetY) ? "default" : "move"; + } + }, + + onDragStart(e) { + e.preventDefault(); + const { containerRect } = this.getRect(); + const offsetX = e.clientX - containerRect.left; + const offsetY = e.clientY - containerRect.top; + + if (this.isPixelTransparent(offsetX, offsetY)) return; + + this.dragState.isDragging = true; + this.dragState.offsetX = offsetX; + this.dragState.offsetY = offsetY; + }, + + onDragging(e) { + if (!this.dragState.isDragging) return; + + const { windowRect, containerRect } = this.getRect(); + this.state.left = e.clientX - this.dragState.offsetX; + this.state.top = e.clientY - this.dragState.offsetY; + + const bounds = this.effectiveBounds || { left: 0, top: 0, width: containerRect.width, height: containerRect.height }; + this.state.left = Math.max(-bounds.left, Math.min(this.state.left, windowRect.width - (bounds.left + bounds.width))); + this.state.top = Math.max(-bounds.top, Math.min(this.state.top, windowRect.height - (bounds.top + bounds.height))); + + this.updatePosition(); + }, + + onDragEnd() { + if (this.dragState.isDragging) { + this.dragState.isDragging = false; + this.saveState(); + this.updatePosition(); + } + }, + + onContextMenu(e) { + e.preventDefault(); + const { containerRect } = this.getRect(); + const offsetX = e.clientX - containerRect.left; + const offsetY = e.clientY - containerRect.top; + + if (this.isPixelTransparent(offsetX, offsetY)) return; + + this.hideContextMenu(); + this.showContextMenu(e.clientX, e.clientY); + }, + + showContextMenu(x, y) { + const menuItems = []; + + // 根据 menuConfig 动态生成菜单项 + this.menuConfig.forEach((config) => { + let action; + if (config.action === "playAnimation") { + action = () => this.playAnimation(); + } else if (KayToolActions[config.action]) { + action = () => KayToolActions[config.action](); + } + if (action) { + menuItems.push({ + label: config.label, + action, + }); + } + }); + + this.contextMenu = document.createElement("div"); + this.contextMenu.className = "gululu-context-menu"; + document.body.appendChild(this.contextMenu); + + // 先创建菜单项以计算宽度 + const tempDiv = document.createElement("div"); + tempDiv.style.visibility = "hidden"; + tempDiv.style.position = "absolute"; + tempDiv.style.fontFamily = "'Courier New', monospace"; + tempDiv.style.fontSize = "14px"; + tempDiv.style.fontWeight = "bold"; + tempDiv.style.padding = "4px 12px"; + tempDiv.style.whiteSpace = "nowrap"; + document.body.appendChild(tempDiv); + + let maxWidth = 0; + menuItems.forEach((item) => { + tempDiv.textContent = item.label; + const itemWidth = tempDiv.getBoundingClientRect().width; + maxWidth = Math.max(maxWidth, itemWidth); + }); + document.body.removeChild(tempDiv); + + // 设置菜单固定宽度(包括 padding) + this.contextMenu.style.width = `${maxWidth + 6}px`; // 12px 左右 padding + + menuItems.forEach((item) => { + const menuItem = document.createElement("div"); + menuItem.className = "gululu-context-menu-item"; + menuItem.textContent = item.label; + menuItem.addEventListener("click", (e) => { + e.stopPropagation(); + item.action(); + this.hideContextMenu(); + }); + this.contextMenu.appendChild(menuItem); + }); + + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + const { containerRect } = this.getRect(); + const gululuCenterY = containerRect.top + containerRect.height / 2; + const isAboveHalf = gululuCenterY < windowHeight / 2; + + let menuLeft = x; + let menuTop = y; + + // 计算菜单高度并检查边界 + let { height: menuHeight } = this.contextMenu.getBoundingClientRect(); + + // 垂直方向调整,避免遮挡 GuLuLu,并处理滚动 + if (isAboveHalf) { + menuTop = y; + if (menuTop + menuHeight > windowHeight - 10) { + // 接近底部边界,限制高度,启用滚动 + this.contextMenu.style.maxHeight = `${windowHeight - 20}px`; // 上下各留10px + menuTop = 10; // 确保顶部不贴边 + } + } else { + menuTop = y - menuHeight; + if (menuTop < 10) { + // 接近顶部边界,限制高度,启用滚动 + this.contextMenu.style.maxHeight = `${windowHeight - 20}px`; // 上下各留10px + menuTop = 10; // 确保顶部不贴边 + } + } + + // 水平方向:优先贴近点击点,允许超出右边界 + const { width: menuWidth } = this.contextMenu.getBoundingClientRect(); + if (menuLeft + menuWidth > windowWidth) { + menuLeft = windowWidth - menuWidth; + } + + this.contextMenu.style.left = `${menuLeft - 10}px`; + this.contextMenu.style.top = `${menuTop}px`; + }, + + hideContextMenu() { + if (this.contextMenu) { + this.contextMenu.remove(); + this.contextMenu = null; + } + }, +}; + +// 通知框函数(保持不变) +export function showNotification({ + message, + bgColor = "#fff3cd", + timeout = 0, + onYes = null, + onNo = null, + size = "small" +}) { + const sizes = { + small: { maxWidth: "250px", maxHeight: "150px", padding: "12px 16px" }, + medium: { maxWidth: "400px", maxHeight: "300px", padding: "12px 16px" }, + large: { maxWidth: "600px", maxHeight: "450px", padding: "12px 16px" } + }; + const selectedSize = sizes[size] || sizes.small; + + const div = document.createElement("div"); + Object.assign(div.style, { + position: "fixed", + width: selectedSize.maxWidth, + padding: selectedSize.padding, + backgroundColor: bgColor, + color: "#333", + fontFamily: "'Courier New', monospace", + fontSize: "14px", + wordWrap: "break-word", + zIndex: "10000", + border: "2px solid #000", + borderRadius: "12px", + boxShadow: "4px 4px 0 #999", + opacity: "0", + transition: "opacity 0.3s ease-in-out", + display: "flex", + flexDirection: "column", + gap: "8px", + boxSizing: "border-box" + }); + document.body.appendChild(div); + + const isGuLuLuEnabled = window.KayGuLuLuManager && window.KayGuLuLuManager.isEnabled; + if (isGuLuLuEnabled) { + window.KayGuLuLuManager.playAnimation(); + } + + let tail, tailBorder; + if (isGuLuLuEnabled) { + tail = document.createElement("div"); + Object.assign(tail.style, { + position: "absolute", + width: "0", + height: "0", + borderLeft: "6px solid transparent", + borderRight: "6px solid transparent", + zIndex: "-1" + }); + div.appendChild(tail); + + tailBorder = document.createElement("div"); + Object.assign(tailBorder.style, { + position: "absolute", + width: "0", + height: "0", + borderLeft: "8px solid transparent", + borderRight: "8px solid transparent", + zIndex: "-2" + }); + div.appendChild(tailBorder); + } + + const closeButton = document.createElement("div"); + closeButton.textContent = "X"; + Object.assign(closeButton.style, { + position: "absolute", + top: "12px", + left: "12px", + width: "12px", + height: "12px", + backgroundColor: "#dc3545", + border: "2px solid #000", + borderRadius: "50%", + fontSize: "10px", + lineHeight: "10px", + textAlign: "center", + cursor: "pointer", + boxShadow: "2px 2px 0 #999" + }); + closeButton.addEventListener("click", () => hideNotification(div)); + closeButton.addEventListener("mousedown", () => { + closeButton.style.transform = "translate(1px, 1px)"; + closeButton.style.boxShadow = "0 0 0 #999"; + }); + document.addEventListener("mouseup", () => { + closeButton.style.transform = "translate(0, 0)"; + closeButton.style.boxShadow = "2px 2px 0 #999"; + }); + div.appendChild(closeButton); + + const contentDiv = document.createElement("div"); + Object.assign(contentDiv.style, { + marginTop: "20px", + overflowY: "auto", + overflowX: "hidden", + padding: "0 4px 0 0", + boxSizing: "border-box", + wordBreak: "break-word", + fontFamily: "'Courier New', monospace", + whiteSpace: "pre-wrap" + }); + contentDiv.style.cssText += ` + ::-webkit-scrollbar { width: 8px; } + ::-webkit-scrollbar-track { background: #333; border-left: 2px solid #000; } + ::-webkit-scrollbar-thumb { background: #999; border: 2px solid #000; } + `; + div.appendChild(contentDiv); + + let buttonContainer = null; + if (onYes || onNo) { + buttonContainer = document.createElement("div"); + Object.assign(buttonContainer.style, { + display: "flex", + gap: "8px", + justifyContent: "flex-end", + marginTop: "4px", + paddingBottom: "4px" + }); + div.appendChild(buttonContainer); + + if (onYes) { + const yesButton = document.createElement("button"); + yesButton.textContent = "Yes"; + Object.assign(yesButton.style, { + padding: "4px 8px", + backgroundColor: "#28a745", + border: "2px solid #000", + borderRadius: "8px", + fontFamily: "'Courier New', monospace", + fontSize: "12px", + fontWeight: "bold", + color: "#fff", + cursor: "pointer", + boxShadow: "2px 2px 0 #999" + }); + yesButton.addEventListener("mousedown", () => { + yesButton.style.transform = "translate(1px, 1px)"; + yesButton.style.boxShadow = "0 0 0 #999"; + }); + document.addEventListener("mouseup", () => { + yesButton.style.transform = "translate(0, 0)"; + yesButton.style.boxShadow = "2px 2px 0 #999"; + }); + yesButton.addEventListener("click", () => { + hideNotification(div); + onYes(); + }); + buttonContainer.appendChild(yesButton); + } + + if (onNo) { + const noButton = document.createElement("button"); + noButton.textContent = "No"; + Object.assign(noButton.style, { + padding: "4px 8px", + backgroundColor: "#dc3545", + border: "2px solid #000", + borderRadius: "8px", + fontFamily: "'Courier New', monospace", + fontSize: "12px", + fontWeight: "bold", + color: "#fff", + cursor: "pointer", + boxShadow: "2px 2px 0 #999" + }); + noButton.addEventListener("mousedown", () => { + noButton.style.transform = "translate(1px, 1px)"; + noButton.style.boxShadow = "0 0 0 #999"; + }); + document.addEventListener("mouseup", () => { + noButton.style.transform = "translate(0, 0)"; + noButton.style.boxShadow = "2px 2px 0 #999"; + }); + noButton.addEventListener("click", () => { + hideNotification(div); + onNo(); + }); + buttonContainer.appendChild(noButton); + } + } + + const updatePosition = () => { + if (isGuLuLuEnabled && window.KayGuLuLuManager && window.KayGuLuLuManager.container) { + const gululu = window.KayGuLuLuManager; + const containerRect = gululu.container.getBoundingClientRect(); + const windowRect = { width: window.innerWidth, height: window.innerHeight }; + const notificationRect = div.getBoundingClientRect(); + const bounds = gululu.effectiveBounds || { left: 0, top: 0, width: containerRect.width, height: containerRect.height }; + + const gululuCenterX = containerRect.left + containerRect.width / 2; + const isLeftSide = gululuCenterX < windowRect.width / 2; + const horizontalOffset = 10; + + const gululuCenterY = containerRect.top + containerRect.height / 2; + const isTopHalf = gululuCenterY < windowRect.height / 2; + const verticalOffset = -26; + + let left, top; + + if (isLeftSide) { + left = containerRect.right + horizontalOffset; + if (tail) { + tail.style.left = "-8px"; + tail.style.right = "auto"; + tail.style.borderRight = `8px solid ${bgColor}`; + tail.style.borderLeft = "none"; + tail.style.borderTop = "6px solid transparent"; + tail.style.borderBottom = "6px solid transparent"; + } + if (tailBorder) { + tailBorder.style.left = "-10px"; + tailBorder.style.right = "auto"; + tailBorder.style.borderRight = "10px solid #000"; + tailBorder.style.borderLeft = "none"; + tailBorder.style.borderTop = "8px solid transparent"; + tailBorder.style.borderBottom = "8px solid transparent"; + } + } else { + left = containerRect.left - notificationRect.width - horizontalOffset; + if (tail) { + tail.style.right = "-8px"; + tail.style.left = "auto"; + tail.style.borderLeft = `8px solid ${bgColor}`; + tail.style.borderRight = "none"; + tail.style.borderTop = "6px solid transparent"; + tail.style.borderBottom = "6px solid transparent"; + } + if (tailBorder) { + tailBorder.style.right = "-10px"; + tailBorder.style.left = "auto"; + tailBorder.style.borderLeft = "10px solid #000"; + tailBorder.style.borderRight = "none"; + tailBorder.style.borderTop = "8px solid transparent"; + tailBorder.style.borderBottom = "8px solid transparent"; + } + } + + if (isTopHalf) { + top = containerRect.top + bounds.top + bounds.height + verticalOffset; + if (tail) { + tail.style.top = "12px"; + tail.style.bottom = "auto"; + tail.style.transform = "none"; + } + if (tailBorder) { + tailBorder.style.top = "10px"; + tailBorder.style.bottom = "auto"; + tailBorder.style.transform = "none"; + } + } else { + top = containerRect.top + bounds.top - notificationRect.height - verticalOffset; + if (tail) { + tail.style.top = "auto"; + tail.style.bottom = "12px"; + tail.style.transform = "none"; + } + if (tailBorder) { + tailBorder.style.top = "auto"; + tailBorder.style.bottom = "10px"; + tailBorder.style.transform = "none"; + } + } + + left = Math.max(0, Math.min(left, windowRect.width - notificationRect.width)); + top = Math.max(0, Math.min(top, windowRect.height - notificationRect.height)); + + div.style.left = `${left}px`; + div.style.top = `${top}px`; + } else { + div.style.left = "20px"; + div.style.bottom = "20px"; + div.style.top = "auto"; + } + }; + + const updateHeights = () => { + const buttonHeight = buttonContainer ? buttonContainer.offsetHeight : 0; + const totalExtraHeight = buttonHeight + 36; + div.style.maxHeight = `calc(${selectedSize.maxHeight} + ${totalExtraHeight}px)`; + contentDiv.style.maxHeight = `calc(${selectedSize.maxHeight} - ${totalExtraHeight}px)`; + updatePosition(); + }; + updateHeights(); + + const throttle = (func, limit) => { + let lastCall = 0; + return (...args) => { + const now = Date.now(); + if (now - lastCall >= limit) { + func(...args); + lastCall = now; + } + }; + }; + const onGuLuLuMove = throttle(updatePosition, 16); + if (isGuLuLuEnabled) { + document.addEventListener("mousemove", onGuLuLuMove); + document.addEventListener("mouseup", onGuLuLuMove); + } + window.addEventListener("resize", updatePosition); + + const cleanup = () => { + document.removeEventListener("mousemove", onGuLuLuMove); + document.removeEventListener("mouseup", onGuLuLuMove); + window.removeEventListener("resize", updatePosition); + }; + + try { + marked.setOptions({ breaks: true, gfm: true }); + + if (!message || typeof message !== "string" || message.trim() === "") { + contentDiv.textContent = "无消息显示"; + updateHeights(); + return div; + } + + if (isGuLuLuEnabled) { + const chars = message.split(""); + let charIndex = 0; + let currentMarkdown = ""; + let renderedHtml = ""; + + function typeWriter() { + if (charIndex >= chars.length) { + if (currentMarkdown.trim()) { + renderedHtml += marked.parse(currentMarkdown); + } + contentDiv.innerHTML = renderedHtml; + updateHeights(); + return; + } + + currentMarkdown += chars[charIndex]; + const isNewline = chars[charIndex] === "\n"; + const isDoubleNewline = isNewline && charIndex > 0 && chars[charIndex - 1] === "\n"; + + if (isDoubleNewline) { + if (currentMarkdown.trim()) { + renderedHtml += marked.parse(currentMarkdown); + currentMarkdown = ""; + } + contentDiv.innerHTML = renderedHtml; + } else { + contentDiv.innerHTML = renderedHtml + `
${currentMarkdown}
`; + } + + charIndex++; + setTimeout(typeWriter, 50); + } + + setTimeout(typeWriter, 300); + } else { + contentDiv.innerHTML = marked.parse(message); + updateHeights(); + } + } catch (e) { + contentDiv.textContent = message || "显示消息出错"; + updateHeights(); + } + + setTimeout(() => (div.style.opacity = "1"), 10); + if (timeout > 0) { + setTimeout(() => { + cleanup(); + hideNotification(div); + }, timeout); + } + + const originalHide = hideNotification.bind(null, div); + div.hide = () => { + cleanup(); + originalHide(); + }; + + return div; +} + +export function hideNotification(element) { + if (element?.parentNode) { + element.style.opacity = "0"; + setTimeout(() => element.parentNode?.removeChild(element), 300); + } +} + +// 注册扩展 +app.registerExtension({ + name: "KayTool.GuLuLu", + init() { + KayGuLuLuManager.init(); + }, + cleanup() { + document.removeEventListener("mousemove", KayGuLuLuManager.onDragging); + document.removeEventListener("mouseup", KayGuLuLuManager.onDragEnd); + document.removeEventListener("click", KayGuLuLuManager.hideContextMenu); + KayGuLuLuManager.container?.remove(); + KayGuLuLuManager.pixelCanvas?.remove(); + KayGuLuLuManager.contextMenu?.remove(); + clearTimeout(KayGuLuLuManager.animationTimer); + delete window.KayGuLuLuManager; + }, +}); \ No newline at end of file diff --git a/web/notification.js b/web/notification.js index 9fa4dcf..f9059fb 100644 --- a/web/notification.js +++ b/web/notification.js @@ -1,267 +1,357 @@ -import { marked } from "./lib/marked.esm.js"; +// import { marked } from "./lib/marked.esm.js"; -export function showNotification({ - message, - bgColor = "#fff3cd", - timeout = 0, - onYes = null, - onNo = null, - size = "small" -}) { - const sizes = { - small: { maxWidth: "250px", maxHeight: "150px", padding: "12px 16px" }, - medium: { maxWidth: "400px", maxHeight: "300px", padding: "12px 16px" }, - large: { maxWidth: "600px", maxHeight: "450px", padding: "12px 16px" } - }; - const selectedSize = sizes[size] || sizes.small; +// export function showNotification({ +// message, +// bgColor = "#fff3cd", +// timeout = 0, +// onYes = null, +// onNo = null, +// size = "small" +// }) { +// const sizes = { +// small: { maxWidth: "250px", maxHeight: "150px", padding: "12px 16px" }, +// medium: { maxWidth: "400px", maxHeight: "300px", padding: "12px 16px" }, +// large: { maxWidth: "600px", maxHeight: "450px", padding: "12px 16px" } +// }; +// const selectedSize = sizes[size] || sizes.small; - const div = document.createElement("div"); - Object.assign(div.style, { - position: "fixed", - bottom: "20px", - left: "50px", - width: selectedSize.maxWidth, - padding: selectedSize.padding, - backgroundColor: bgColor, - color: "#333", - fontFamily: "'Courier New', monospace", - fontSize: "14px", - // lineHeight: "1.4", - wordWrap: "break-word", - zIndex: "10000", - border: "2px solid #000", - borderRadius: "12px", - boxShadow: "4px 4px 0 #999", - opacity: "0", - transition: "opacity 0.3s ease-in-out", - display: "flex", - flexDirection: "column", - gap: "8px", - boxSizing: "border-box" - }); - document.body.appendChild(div); +// const div = document.createElement("div"); +// Object.assign(div.style, { +// position: "fixed", +// // Position will be set dynamically below +// width: selectedSize.maxWidth, +// padding: selectedSize.padding, +// backgroundColor: bgColor, +// color: "#333", +// fontFamily: "'Courier New', monospace", +// fontSize: "14px", +// wordWrap: "break-word", +// zIndex: "10000", +// border: "2px solid #000", +// borderRadius: "12px", +// boxShadow: "4px 4px 0 #999", +// opacity: "0", +// transition: "opacity 0.3s ease-in-out", +// display: "flex", +// flexDirection: "column", +// gap: "8px", +// boxSizing: "border-box" +// }); +// document.body.appendChild(div); - const tail = document.createElement("div"); - Object.assign(tail.style, { - position: "absolute", - bottom: "-8px", - left: "10px", - width: "0", - height: "0", - borderLeft: "6px solid transparent", - borderRight: "6px solid transparent", - borderTop: `8px solid ${bgColor}`, - zIndex: "-1" - }); - div.appendChild(tail); +// // Tail for visual connection to GuLuLu +// const tail = document.createElement("div"); +// Object.assign(tail.style, { +// position: "absolute", +// width: "0", +// height: "0", +// borderLeft: "6px solid transparent", +// borderRight: "6px solid transparent", +// zIndex: "-1" +// }); +// div.appendChild(tail); - const tailBorder = document.createElement("div"); - Object.assign(tailBorder.style, { - position: "absolute", - bottom: "-10px", - left: "8px", - width: "0", - height: "0", - borderLeft: "8px solid transparent", - borderRight: "8px solid transparent", - borderTop: "10px solid #000", - zIndex: "-2" - }); - div.appendChild(tailBorder); +// const tailBorder = document.createElement("div"); +// Object.assign(tailBorder.style, { +// position: "absolute", +// width: "0", +// height: "0", +// borderLeft: "8px solid transparent", +// borderRight: "8px solid transparent", +// zIndex: "-2" +// }); +// div.appendChild(tailBorder); - const closeButton = document.createElement("div"); - closeButton.textContent = "X"; - Object.assign(closeButton.style, { - position: "absolute", - top: "12px", - left: "12px", - width: "12px", - height: "12px", - backgroundColor: "#dc3545", - border: "2px solid #000", - borderRadius: "50%", - fontSize: "10px", - lineHeight: "10px", - textAlign: "center", - cursor: "pointer", - boxShadow: "2px 2px 0 #999" - }); - closeButton.addEventListener("click", () => hideNotification(div)); - closeButton.addEventListener("mousedown", () => { - closeButton.style.transform = "translate(1px, 1px)"; - closeButton.style.boxShadow = "0 0 0 #999"; - }); - document.addEventListener("mouseup", () => { - closeButton.style.transform = "translate(0, 0)"; - closeButton.style.boxShadow = "2px 2px 0 #999"; - }); - div.appendChild(closeButton); +// const closeButton = document.createElement("div"); +// closeButton.textContent = "X"; +// Object.assign(closeButton.style, { +// position: "absolute", +// top: "12px", +// left: "12px", +// width: "12px", +// height: "12px", +// backgroundColor: "#dc3545", +// border: "2px solid #000", +// borderRadius: "50%", +// fontSize: "10px", +// lineHeight: "10px", +// textAlign: "center", +// cursor: "pointer", +// boxShadow: "2px 2px 0 #999" +// }); +// closeButton.addEventListener("click", () => hideNotification(div)); +// closeButton.addEventListener("mousedown", () => { +// closeButton.style.transform = "translate(1px, 1px)"; +// closeButton.style.boxShadow = "0 0 0 #999"; +// }); +// document.addEventListener("mouseup", () => { +// closeButton.style.transform = "translate(0, 0)"; +// closeButton.style.boxShadow = "2px 2px 0 #999"; +// }); +// div.appendChild(closeButton); - const contentDiv = document.createElement("div"); - Object.assign(contentDiv.style, { - marginTop: "20px", - overflowY: "auto", - overflowX: "hidden", - padding: "0 4px 0 0", - boxSizing: "border-box", - wordBreak: "break-word", - fontFamily: "'Courier New', monospace", - whiteSpace: "pre-wrap" // 保留换行和空格 - }); - contentDiv.style.cssText += ` - ::-webkit-scrollbar { width: 8px; } - ::-webkit-scrollbar-track { background: #333; border-left: 2px solid #000; } - ::-webkit-scrollbar-thumb { background: #999; border: 2px solid #000; } - `; - div.appendChild(contentDiv); +// const contentDiv = document.createElement("div"); +// Object.assign(contentDiv.style, { +// marginTop: "20px", +// overflowY: "auto", +// overflowX: "hidden", +// padding: "0 4px 0 0", +// boxSizing: "border-box", +// wordBreak: "break-word", +// fontFamily: "'Courier New', monospace", +// whiteSpace: "pre-wrap" +// }); +// contentDiv.style.cssText += ` +// ::-webkit-scrollbar { width: 8px; } +// ::-webkit-scrollbar-track { background: #333; border-left: 2px solid #000; } +// ::-webkit-scrollbar-thumb { background: #999; border: 2px solid #000; } +// `; +// div.appendChild(contentDiv); - let buttonContainer = null; - if (onYes || onNo) { - buttonContainer = document.createElement("div"); - Object.assign(buttonContainer.style, { - display: "flex", - gap: "8px", - justifyContent: "flex-end", - marginTop: "4px", - paddingBottom: "4px" - }); - div.appendChild(buttonContainer); +// let buttonContainer = null; +// if (onYes || onNo) { +// buttonContainer = document.createElement("div"); +// Object.assign(buttonContainer.style, { +// display: "flex", +// gap: "8px", +// justifyContent: "flex-end", +// marginTop: "4px", +// paddingBottom: "4px" +// }); +// div.appendChild(buttonContainer); - if (onYes) { - const yesButton = document.createElement("button"); - yesButton.textContent = "Yes"; - Object.assign(yesButton.style, { - padding: "4px 8px", - backgroundColor: "#28a745", - border: "2px solid #000", - borderRadius: "8px", - fontFamily: "'Courier New', monospace", - fontSize: "12px", - fontWeight: "bold", - color: "#fff", - cursor: "pointer", - boxShadow: "2px 2px 0 #999" - }); - yesButton.addEventListener("mousedown", () => { - yesButton.style.transform = "translate(1px, 1px)"; - yesButton.style.boxShadow = "0 0 0 #999"; - }); - document.addEventListener("mouseup", () => { - yesButton.style.transform = "translate(0, 0)"; - yesButton.style.boxShadow = "2px 2px 0 #999"; - }); - yesButton.addEventListener("click", () => { - hideNotification(div); - onYes(); - }); - buttonContainer.appendChild(yesButton); - } +// if (onYes) { +// const yesButton = document.createElement("button"); +// yesButton.textContent = "Yes"; +// Object.assign(yesButton.style, { +// padding: "4px 8px", +// backgroundColor: "#28a745", +// border: "2px solid #000", +// borderRadius: "8px", +// fontFamily: "'Courier New', monospace", +// fontSize: "12px", +// fontWeight: "bold", +// color: "#fff", +// cursor: "pointer", +// boxShadow: "2px 2px 0 #999" +// }); +// yesButton.addEventListener("mousedown", () => { +// yesButton.style.transform = "translate(1px, 1px)"; +// yesButton.style.boxShadow = "0 0 0 #999"; +// }); +// document.addEventListener("mouseup", () => { +// yesButton.style.transform = "translate(0, 0)"; +// yesButton.style.boxShadow = "2px 2px 0 #999"; +// }); +// yesButton.addEventListener("click", () => { +// hideNotification(div); +// onYes(); +// }); +// buttonContainer.appendChild(yesButton); +// } - if (onNo) { - const noButton = document.createElement("button"); - noButton.textContent = "No"; - Object.assign(noButton.style, { - padding: "4px 8px", - backgroundColor: "#dc3545", - border: "2px solid #000", - borderRadius: "8px", - fontFamily: "'Courier New', monospace", - fontSize: "12px", - fontWeight: "bold", - color: "#fff", - cursor: "pointer", - boxShadow: "2px 2px 0 #999" - }); - noButton.addEventListener("mousedown", () => { - noButton.style.transform = "translate(1px, 1px)"; - noButton.style.boxShadow = "0 0 0 #999"; - }); - document.addEventListener("mouseup", () => { - noButton.style.transform = "translate(0, 0)"; - noButton.style.boxShadow = "2px 2px 0 #999"; - }); - noButton.addEventListener("click", () => { - hideNotification(div); - onNo(); - }); - buttonContainer.appendChild(noButton); - } - } +// if (onNo) { +// const noButton = document.createElement("button"); +// noButton.textContent = "No"; +// Object.assign(noButton.style, { +// padding: "4px 8px", +// backgroundColor: "#dc3545", +// border: "2px solid #000", +// borderRadius: "8px", +// fontFamily: "'Courier New', monospace", +// fontSize: "12px", +// fontWeight: "bold", +// color: "#fff", +// cursor: "pointer", +// boxShadow: "2px 2px 0 #999" +// }); +// noButton.addEventListener("mousedown", () => { +// noButton.style.transform = "translate(1px, 1px)"; +// noButton.style.boxShadow = "0 0 0 #999"; +// }); +// document.addEventListener("mouseup", () => { +// noButton.style.transform = "translate(0, 0)"; +// noButton.style.boxShadow = "2px 2px 0 #999"; +// }); +// noButton.addEventListener("click", () => { +// hideNotification(div); +// onNo(); +// }); +// buttonContainer.appendChild(noButton); +// } +// } - const updateHeights = () => { - const buttonHeight = buttonContainer ? buttonContainer.offsetHeight : 0; - const totalExtraHeight = buttonHeight + 36; - div.style.maxHeight = `calc(${selectedSize.maxHeight} + ${totalExtraHeight}px)`; - contentDiv.style.maxHeight = `calc(${selectedSize.maxHeight} - ${totalExtraHeight}px)`; - }; - updateHeights(); +// // Function to update notification position based on GuLuLu +// const updatePosition = () => { +// if (!window.KayGuLuLuManager || !window.KayGuLuLuManager.container) { +// console.warn("KayGuLuLuManager not found, using fallback position"); +// div.style.left = "50px"; +// div.style.bottom = "20px"; +// return; +// } - // 打字机效果:基于 Markdown 原始文本,仅双换行触发解析 - try { - marked.setOptions({ breaks: true, gfm: true }); +// const gululu = window.KayGuLuLuManager; +// const containerRect = gululu.container.getBoundingClientRect(); +// const windowRect = { width: window.innerWidth, height: window.innerHeight }; +// const notificationRect = div.getBoundingClientRect(); +// const bounds = gululu.effectiveBounds || { left: 0, top: 0, width: containerRect.width, height: containerRect.height }; - if (!message || typeof message !== "string" || message.trim() === "") { - contentDiv.textContent = "No message to display"; - updateHeights(); - return div; - } +// // Determine side placement +// const gululuCenterX = containerRect.left + containerRect.width / 2; +// const isLeftSide = gululuCenterX < windowRect.width / 2; +// const horizontalOffset = 10; // Distance from GuLuLu - // 分割 Markdown 原始文本为字符数组 - const chars = message.split(''); - let charIndex = 0; - let currentMarkdown = ''; // 当前累积的 Markdown 文本 - let renderedHtml = ''; // 已渲染的 HTML +// // Determine vertical placement +// const gululuCenterY = containerRect.top + containerRect.height / 2; +// const isTopHalf = gululuCenterY < windowRect.height / 2; +// const verticalOffset = 10; // Distance from GuLuLu - function typeWriter() { - if (charIndex >= chars.length) { - // 所有字符打完,解析剩余的 Markdown - if (currentMarkdown.trim()) { - renderedHtml += marked.parse(currentMarkdown); - } - contentDiv.innerHTML = renderedHtml; - updateHeights(); - return; - } +// let left, top; - // 逐字符追加到 currentMarkdown - currentMarkdown += chars[charIndex]; - const isNewline = chars[charIndex] === '\n'; - const isDoubleNewline = isNewline && charIndex > 0 && chars[charIndex - 1] === '\n'; +// // Horizontal position +// if (isLeftSide) { +// // Notification to the right of GuLuLu +// left = containerRect.left + bounds.left + bounds.width + horizontalOffset; +// tail.style.left = "10px"; +// tail.style.right = "auto"; +// tailBorder.style.left = "8px"; +// tailBorder.style.right = "auto"; +// } else { +// // Notification to the left of GuLuLu +// left = containerRect.left + bounds.left - notificationRect.width - horizontalOffset; +// tail.style.right = "10px"; +// tail.style.left = "auto"; +// tailBorder.style.right = "8px"; +// tailBorder.style.left = "auto"; +// } - if (isDoubleNewline) { - // 遇到双换行,解析当前累积的 Markdown - if (currentMarkdown.trim()) { - renderedHtml += marked.parse(currentMarkdown); - currentMarkdown = ''; // 重置当前 Markdown - } - contentDiv.innerHTML = renderedHtml; - } else { - // 显示已渲染的 HTML + 当前未解析的 Markdown - contentDiv.innerHTML = renderedHtml + `
${currentMarkdown}
`; - } +// // Vertical position +// if (isTopHalf) { +// // Notification below GuLuLu +// top = containerRect.top + bounds.top + bounds.height + verticalOffset; +// tail.style.top = "auto"; +// tail.style.bottom = "-8px"; +// tail.style.borderTop = `8px solid ${bgColor}`; +// tail.style.borderBottom = "none"; +// tailBorder.style.top = "auto"; +// tailBorder.style.bottom = "-10px"; +// tailBorder.style.borderTop = "10px solid #000"; +// tailBorder.style.borderBottom = "none"; +// } else { +// // Notification above GuLuLu +// top = containerRect.top + bounds.top - notificationRect.height - verticalOffset; +// tail.style.bottom = "auto"; +// tail.style.top = "-8px"; +// tail.style.borderBottom = `8px solid ${bgColor}`; +// tail.style.borderTop = "none"; +// tailBorder.style.bottom = "auto"; +// tailBorder.style.top = "-10px"; +// tailBorder.style.borderBottom = "10px solid #000"; +// tailBorder.style.borderTop = "none"; +// } - charIndex++; - setTimeout(typeWriter, 50); // 字符间间隔 100ms - } +// // Clamp to screen boundaries +// left = Math.max(0, Math.min(left, windowRect.width - notificationRect.width)); +// top = Math.max(0, Math.min(top, windowRect.height - notificationRect.height)); - setTimeout(typeWriter, 300); - } catch (e) { - console.error("Error in typewriter effect:", e); - contentDiv.textContent = message || "Error displaying message"; - updateHeights(); - } +// div.style.left = `${left}px`; +// div.style.top = `${top}px`; +// }; - setTimeout(() => div.style.opacity = "1", 10); - if (timeout > 0) { - setTimeout(() => hideNotification(div), timeout); - } +// // Update heights and allow vertical adjustments for typewriter effect +// const updateHeights = () => { +// const buttonHeight = buttonContainer ? buttonContainer.offsetHeight : 0; +// const totalExtraHeight = buttonHeight + 36; +// div.style.maxHeight = `calc(${selectedSize.maxHeight} + ${totalExtraHeight}px)`; +// contentDiv.style.maxHeight = `calc(${selectedSize.maxHeight} - ${totalExtraHeight}px)`; +// updatePosition(); // Recompute position after height changes +// }; +// updateHeights(); - return div; -} +// // Update position when GuLuLu moves +// const onGuLuLuMove = () => updatePosition(); +// document.addEventListener('mousemove', onGuLuLuMove); +// document.addEventListener('mouseup', onGuLuLuMove); +// window.addEventListener('resize', updatePosition); -export function hideNotification(element) { - if (element?.parentNode) { - element.style.opacity = "0"; - setTimeout(() => element.parentNode?.removeChild(element), 300); - } -} \ No newline at end of file +// // Clean up event listeners when notification is removed +// const cleanup = () => { +// document.removeEventListener('mousemove', onGuLuLuMove); +// document.removeEventListener('mouseup', onGuLuLuMove); +// window.removeEventListener('resize', updatePosition); +// }; + +// // Typewriter effect +// try { +// marked.setOptions({ breaks: true, gfm: true }); + +// if (!message || typeof message !== "string" || message.trim() === "") { +// contentDiv.textContent = "No message to display"; +// updateHeights(); +// return div; +// } + +// const chars = message.split(''); +// let charIndex = 0; +// let currentMarkdown = ''; +// let renderedHtml = ''; + +// function typeWriter() { +// if (charIndex >= chars.length) { +// if (currentMarkdown.trim()) { +// renderedHtml += marked.parse(currentMarkdown); +// } +// contentDiv.innerHTML = renderedHtml; +// updateHeights(); +// return; +// } + +// currentMarkdown += chars[charIndex]; +// const isNewline = chars[charIndex] === '\n'; +// const isDoubleNewline = isNewline && charIndex > 0 && chars[charIndex - 1] === '\n'; + +// if (isDoubleNewline) { +// if (currentMarkdown.trim()) { +// renderedHtml += marked.parse(currentMarkdown); +// currentMarkdown = ''; +// } +// contentDiv.innerHTML = renderedHtml; +// } else { +// contentDiv.innerHTML = renderedHtml + `
${currentMarkdown}
`; +// } + +// charIndex++; +// setTimeout(typeWriter, 50); +// } + +// setTimeout(typeWriter, 300); +// } catch (e) { +// console.error("Error in typewriter effect:", e); +// contentDiv.textContent = message || "Error displaying message"; +// updateHeights(); +// } + +// setTimeout(() => div.style.opacity = "1", 10); +// if (timeout > 0) { +// setTimeout(() => { +// cleanup(); +// hideNotification(div); +// }, timeout); +// } + +// // Override hideNotification to include cleanup +// const originalHide = hideNotification.bind(null, div); +// div.hide = () => { +// cleanup(); +// originalHide(); +// }; + +// return div; +// } + +// export function hideNotification(element) { +// if (element?.parentNode) { +// element.style.opacity = "0"; +// setTimeout(() => element.parentNode?.removeChild(element), 300); +// } +// } \ No newline at end of file diff --git a/web/resourcemonitor.js b/web/resourcemonitor.js index c014229..18ab55f 100644 --- a/web/resourcemonitor.js +++ b/web/resourcemonitor.js @@ -134,6 +134,11 @@ const KayResourceMonitor = { isEnabled: true, isInitialized: false, edgeFadeEnabled: true, + // 硬编码菜单边界 + menuOffsets: { + left: 0, // 左侧菜单宽度 + top: 0 // 顶部菜单高度 + }, easeOutQuad(t) { return t * (2 - t); }, @@ -142,10 +147,11 @@ const KayResourceMonitor = { this.isInitialized = true; this.currentWorkflow.percentage = 0; this.workflowProgress.currentNodePercentage = 0; + this.isEnabled = app.ui.settings.getSettingValue("KayTool.EnableResourceMonitor"); + this.edgeFadeEnabled = app.ui.settings.getSettingValue("KayTool.ResourceMonitorEdgeFade"); this.loadVisibility(); this.startMonitoring(); this.bindEvents(); - await this.loadSettings(); this.setupUI(); this.setupWorkflowListener(); this.setupWebSocketListener(); @@ -719,8 +725,10 @@ const KayResourceMonitor = { }, loadPosition() { const pos = JSON.parse(localStorage.getItem('KayResourceMonitorPosition')) || {}; - this.position.left = pos.left || 0; - this.position.top = pos.top || 0; + const leftOffset = this.menuOffsets.left + 32; // 左侧菜单 + 20px + const topOffset = window.innerHeight - this.size.height - 20; // 底部 20px + this.position.left = pos.left != null ? pos.left : leftOffset; + this.position.top = pos.top != null ? pos.top : topOffset; this.toolbar.style.left = `${this.position.left}px`; this.toolbar.style.top = `${this.position.top}px`; }, @@ -739,8 +747,8 @@ const KayResourceMonitor = { if (!this.toolbar) return; const winW = window.innerWidth; const winH = window.innerHeight; - this.position.left = Math.max(0, Math.min(this.position.left, winW - this.size.width)); - this.position.top = Math.max(0, Math.min(this.position.top, winH - this.size.height)); + this.position.left = Math.max(this.menuOffsets.left, Math.min(this.position.left, winW - this.size.width)); + this.position.top = Math.max(this.menuOffsets.top, Math.min(this.position.top, winH - this.size.height)); this.toolbar.style.left = `${this.position.left}px`; this.toolbar.style.top = `${this.position.top}px`; this.chartCanvas.width = this.toolbar.clientWidth - 20; @@ -762,8 +770,8 @@ const KayResourceMonitor = { if (!this.dragState.isDragging) return; const winW = window.innerWidth; const winH = window.innerHeight; - this.position.left = Math.max(0, Math.min(e.clientX - this.dragState.offsetX, winW - this.size.width)); - this.position.top = Math.max(0, Math.min(e.clientY - this.dragState.offsetY, winH - this.size.height)); + this.position.left = Math.max(this.menuOffsets.left, Math.min(e.clientX - this.dragState.offsetX, winW - this.size.width)); + this.position.top = Math.max(this.menuOffsets.top, Math.min(e.clientY - this.dragState.offsetY, winH - this.size.height)); this.toolbar.style.left = `${this.position.left}px`; this.toolbar.style.top = `${this.position.top}px`; }, @@ -822,7 +830,6 @@ app.registerExtension({ if (app.menu?.settingsGroup) { app.menu.settingsGroup.append(showMenuButton); } - KayResourceMonitor.updateEnabledState(KayResourceMonitor.isEnabled); window.KayResourceMonitor = KayResourceMonitor; } }); \ No newline at end of file diff --git a/web/resources/GuLuLu.gif b/web/resources/GuLuLu.gif new file mode 100644 index 0000000..3b26a2f Binary files /dev/null and b/web/resources/GuLuLu.gif differ diff --git a/web/settings.js b/web/settings.js index eaced8d..e0744b1 100644 --- a/web/settings.js +++ b/web/settings.js @@ -3,6 +3,19 @@ import { app } from "../../../scripts/app.js"; app.registerExtension({ name: "KayTool.Settings", async setup() { + function updateSetGetNodeColors() { + const fgColor = "#" + app.ui.settings.getSettingValue("KayTool.SetGetForegroundColor"); + const bgColor = "#" + app.ui.settings.getSettingValue("KayTool.SetGetBackgroundColor"); + const allNodes = app.graph._nodes; + allNodes.forEach(node => { + if (node.type === "KaySetNode" || node.type === "KayGetNode") { + node.color = fgColor; + node.bgcolor = bgColor; + } + }); + app.graph.setDirtyCanvas(true); + } + const settings = [ { id: "KayTool.ShowRunOption", @@ -17,7 +30,6 @@ app.registerExtension({ type: "boolean", defaultValue: true, category: ["KayTool", "🛜 Set/Get", "ShowSetGetOptions"], - tooltip: "Toggles visibility of 'Set/Get' options in node context menus." }, { id: "KayTool.ShiftR", @@ -41,7 +53,7 @@ app.registerExtension({ category: ["KayTool", "Menu", "ShowStarToMe"], }, { - id: "KayTool.ShowCleanVRAM", + id: "KayTool.ShowCleanVRAM", name: "Show '🧹 Clean VRAM' option in KayTool menu", type: "boolean", defaultValue: true, @@ -54,19 +66,15 @@ app.registerExtension({ defaultValue: 100, attrs: { min: 0, max: 200, step: 10 }, category: ["KayTool", "Workflow PNG", "MarginSize"], - tooltip: "Default value: 100." }, { id: "KayTool.SetGetForegroundColor", name: "Set/Get Node Foreground Color", type: "color", defaultValue: "000000", - category: ["KayTool", "🛜 Set/Get", "ForegroundColor"], + category: ["KayTool", "🛜 Set/Get", "SetGetForegroundColor"], onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - updateSetGetNodeColors(); - if (app.graph) app.graph.setDirtyCanvas(true, true); - } + updateSetGetNodeColors(); } }, { @@ -74,12 +82,9 @@ app.registerExtension({ name: "Set/Get Node Background Color", type: "color", defaultValue: "000000", - category: ["KayTool", "🛜 Set/Get", "BackgroundColor"], + category: ["KayTool", "🛜 Set/Get", "SetGetBackgroundColor"], onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - updateSetGetNodeColors(); - if (app.graph) app.graph.setDirtyCanvas(true, true); - } + updateSetGetNodeColors(); } }, { @@ -94,14 +99,9 @@ app.registerExtension({ defaultValue: "permanent", category: ["KayTool", "NodeAlignment", "DisplayMode"], onChange: (value) => { - const manager = window.KayNodeAlignmentManager; - const initFn = window.initializeKayNodeAlignment; - if (!manager || !initFn) return; - - if (!manager.isInitialized) { - initFn(); + if (window.KayNodeAlignmentManager) { + window.KayNodeAlignmentManager.updateDisplayMode(value); } - manager.updateDisplayMode(value); } }, { @@ -116,19 +116,15 @@ app.registerExtension({ id: "KayTool.NodeAlignBackgroundOpacity", name: "Node Alignment Toolbar Background Opacity", type: "slider", - defaultValue: 100, + defaultValue: 0, attrs: { min: 0, max: 100, step: 1 }, category: ["KayTool", "NodeAlignment", "BackgroundOpacity"], onChange: (newVal) => { const toolbar = document.getElementById('kay-node-alignment-toolbar'); if (toolbar) { - const bgColor = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundColor"); + const bgColor = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundColor") || "000000"; const opacity = newVal / 100; - if (opacity > 0 && /^[0-9A-Fa-f]{6}$/.test(bgColor)) { - toolbar.style.background = `rgba(${parseInt(bgColor.substr(0, 2), 16)}, ${parseInt(bgColor.substr(2, 2), 16)}, ${parseInt(bgColor.substr(4, 2), 16)}, ${opacity})`; - } else { - toolbar.style.background = ''; - } + toolbar.style.background = `rgba(${parseInt(bgColor.substr(0, 2), 16)}, ${parseInt(bgColor.substr(2, 2), 16)}, ${parseInt(bgColor.substr(4, 2), 16)}, ${opacity})`; } } }, @@ -140,13 +136,9 @@ app.registerExtension({ category: ["KayTool", "NodeAlignment", "BackgroundColor"], onChange: (newVal) => { const toolbar = document.getElementById('kay-node-alignment-toolbar'); - if (toolbar && /^[0-9A-Fa-f]{6}$/.test(newVal)) { + if (toolbar && newVal) { const opacity = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundOpacity") / 100; - if (opacity > 0) { - toolbar.style.background = `rgba(${parseInt(newVal.substr(0, 2), 16)}, ${parseInt(newVal.substr(2, 2), 16)}, ${parseInt(newVal.substr(4, 2), 16)}, ${opacity})`; - } else { - toolbar.style.background = ''; - } + toolbar.style.background = `rgba(${parseInt(newVal.substr(0, 2), 16)}, ${parseInt(newVal.substr(2, 2), 16)}, ${parseInt(newVal.substr(4, 2), 16)}, ${opacity})`; } } }, @@ -154,43 +146,37 @@ app.registerExtension({ id: "KayTool.NodeAlignIconBackgroundColor", name: "Node Alignment Icon Background Color", type: "color", - defaultValue: "2b2b2b", + defaultValue: "363636", category: ["KayTool", "NodeAlignment", "IconBackgroundColor"], tooltip: "Use parameter “363636” to match ComfyUI menu bar color for an icon-free background effect", onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - const buttons = document.querySelectorAll('.kay-align-button'); - buttons.forEach(btn => btn.style.backgroundColor = `#${newVal}`); - } + const buttons = document.querySelectorAll('.kay-align-button'); + buttons.forEach(btn => btn.style.backgroundColor = `#${newVal}`); } }, { id: "KayTool.NodeAlignIconColor", name: "Node Alignment Icon Color", type: "color", - defaultValue: "666666", + defaultValue: "999999", category: ["KayTool", "NodeAlignment", "IconColor"], onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - const buttons = document.querySelectorAll('.kay-align-button'); - buttons.forEach(btn => { - const svg = btn.querySelector('svg'); - if (svg) svg.querySelectorAll('path').forEach(path => path.setAttribute('fill', `#${newVal}`)); - }); - } + const buttons = document.querySelectorAll('.kay-align-button'); + buttons.forEach(btn => { + const svg = btn.querySelector('svg'); + if (svg) svg.querySelectorAll('path').forEach(path => path.setAttribute('fill', `#${newVal}`)); + }); } }, { id: "KayTool.NodeAlignDividerColor", name: "Node Alignment Divider Color", type: "color", - defaultValue: "1e1e1e", + defaultValue: "f0ff00", category: ["KayTool", "NodeAlignment", "DividerColor"], onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - const dividers = document.querySelectorAll('.kay-toolbar-divider'); - dividers.forEach(divider => divider.style.background = `#${newVal}`); - } + const dividers = document.querySelectorAll('.kay-toolbar-divider'); + dividers.forEach(divider => divider.style.background = `#${newVal}`); } }, { @@ -214,9 +200,7 @@ app.registerExtension({ onChange: (newVal) => { if (window.KayResourceMonitor) { window.KayResourceMonitor.edgeFadeEnabled = newVal; - if (window.KayResourceMonitor.isVisible && window.KayResourceMonitor.isEnabled) { - window.KayResourceMonitor.drawCurves(); - } + window.KayResourceMonitor.drawCurves(); } } }, @@ -224,7 +208,7 @@ app.registerExtension({ id: "KayTool.ResourceMonitorBackgroundOpacity", name: "Resource Monitor Background Opacity", type: "slider", - defaultValue: 80, + defaultValue: 50, attrs: { min: 0, max: 100, step: 1 }, category: ["KayTool", "Resource Monitor", "BackgroundOpacity"], onChange: (newVal) => { @@ -233,38 +217,43 @@ app.registerExtension({ window.KayResourceMonitor.toolbar.style.background = `rgba(0, 0, 0, ${opacity})`; } } + }, + { + id: "KayTool.GuLuLuSize", + name: "GuLuLu Size (px)", + type: "slider", + defaultValue: 39, + attrs: { min: 36, max: 120, step: 1 }, + category: ["KayTool", "GuLuLu", "Size"], + onChange: (newVal) => { + if (window.KayGuLuLuManager) { + window.KayGuLuLuManager.updateSize(newVal); + } + } + }, + { + id: "KayTool.EnableGuLuLu", + name: "GuLuLu stay with you!", + tooltip: "GuLuLu will inherit KayTool's notification items for streaming output, support adjustments to the notification box position, and include powerful right-click menu options.", + type: "boolean", + defaultValue: true, + category: ["KayTool", "GuLuLu", "EnableGuLuLu"], + onChange: (newVal) => { + if (window.KayGuLuLuManager) { + window.KayGuLuLuManager.updateEnabledState(newVal); + } + } } ]; - for (const setting of settings) { - app.ui.settings.addSetting(setting); - } - }, - init() { - if (!app.graph) { - const checkGraph = setInterval(() => { - if (app.graph) { - clearInterval(checkGraph); - updateSetGetNodeColors(); - } - }, 100); - } else { - updateSetGetNodeColors(); - } - } -}); - -function updateSetGetNodeColors() { - const fgColor = "#" + app.ui.settings.getSettingValue("KayTool.SetGetForegroundColor"); - const bgColor = "#" + app.ui.settings.getSettingValue("KayTool.SetGetBackgroundColor"); - if (app.graph && app.graph._nodes) { - app.graph._nodes.forEach(node => { - if (node.type === "KaySetNode" || node.type === "KayGetNode" || node.type === "KayGet多元Node") { - node.color = fgColor; - node.bgcolor = bgColor; - node.updateColors(); + try { + for (const setting of settings) { + app.ui.settings.addSetting(setting); } - }); - app.graph.setDirtyCanvas(true, true); + updateSetGetNodeColors(); + } catch (error) { + console.error("Error in KayTool.Settings setup:", error); + throw error; + } } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/web/startome.js b/web/startome.js index bab65a5..946a63c 100644 --- a/web/startome.js +++ b/web/startome.js @@ -1,44 +1,44 @@ import { app } from "../../../scripts/app.js"; +// 全局动作注册表 +const KayToolActions = window.KayToolActions || {}; +window.KayToolActions = KayToolActions; + +function starToMe() { + window.open("https://github.com/kk8bit/kaytool", "_blank"); +} + +// 注册 StarToMe 动作 +KayToolActions.starToMe = starToMe; + app.registerExtension({ name: "KayTool.StarToMe", setup() { const originalGetCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = originalGetCanvasMenuOptions.apply(this, arguments) || []; - const showStarToMe = app.ui.settings.getSettingValue("KayTool.ShowStarToMe"); + LGraphCanvas.prototype.getCanvasMenuOptions = function (...args) { + const options = originalGetCanvasMenuOptions.apply(this, args) || []; + const newOptions = [...options]; + const showStarToMe = app.ui.settings.getSettingValue("KayTool.ShowStarToMe") ?? true; if (showStarToMe) { - const newOptions = [...options]; - let kaytoolMenu = newOptions.find(opt => opt && opt.content === "KayTool"); + let kaytoolMenu = newOptions.find(opt => opt?.content === "KayTool") || { + content: "KayTool", + submenu: { options: [] } + }; - if (!kaytoolMenu) { - kaytoolMenu = { - content: "KayTool", - submenu: { - options: [] - } - }; - newOptions.push(null, kaytoolMenu); + if (!newOptions.includes(kaytoolMenu)) { + newOptions.push(null, kaytoolMenu); } - kaytoolMenu.submenu.options = kaytoolMenu.submenu.options || []; - const starOptionExists = kaytoolMenu.submenu.options.some( - opt => opt && opt.content === "⭐ Star to me" - ); - if (!starOptionExists) { + if (!kaytoolMenu.submenu.options.some(opt => opt?.content === "⭐ Star to me")) { kaytoolMenu.submenu.options.push({ content: "⭐ Star to me", - callback: () => { - window.open("https://github.com/kk8bit/kaytool", "_blank"); - } + callback: starToMe }); } - - return newOptions; } - return options; + return newOptions; }; } }); \ No newline at end of file diff --git a/web/workflowpng.js b/web/workflowpng.js index 72e49a4..b07c30d 100644 --- a/web/workflowpng.js +++ b/web/workflowpng.js @@ -1,12 +1,16 @@ import { app } from "../../../scripts/app.js"; import { ComfyWidgets } from "../../../scripts/widgets.js"; -import { showNotification, hideNotification } from "./notification.js"; +import { showNotification, hideNotification } from "./gululu.js"; + +// 全局动作注册表 +const KayToolActions = window.KayToolActions || {}; +window.KayToolActions = KayToolActions; class KayWorkflowImage { extension = "png"; getBounds() { - const marginSize = app.ui.settings.getSettingValue("KayTool.WorkflowPNG"); + const marginSize = app.ui.settings.getSettingValue("KayTool.WorkflowPNG") || 50; const bounds = app.graph._nodes.reduce( (p, node) => { if (node.pos[0] < p[0]) p[0] = node.pos[0]; @@ -33,7 +37,7 @@ class KayWorkflowImage { width: app.canvas.canvas.width, height: app.canvas.canvas.height, offset: app.canvas.ds.offset, - transform: app.canvas.canvas.getContext('2d').getTransform(), + transform: app.canvas.canvas.getContext("2d").getTransform(), }; } @@ -157,6 +161,19 @@ class KayWorkflowImage { } } +function exportWorkflowPNG() { + showNotification({ + message: `GuLuLu: 你需要把工作流信息嵌入到PNG中吗?啊?Do you need to embed Workflow information into PNG? *GuLuLu~Gulu*`, + bgColor: "#fff3cd", + size: "medium", + onYes: () => new KayWorkflowImage().export(true), + onNo: () => new KayWorkflowImage().export(false) + }); +} + +// 注册 Export Workflow PNG 动作 +KayToolActions.exportWorkflowPNG = exportWorkflowPNG; + app.registerExtension({ name: "KayTool.WorkflowPNG", init() { @@ -236,37 +253,25 @@ app.registerExtension({ }, setup() { const originalGetCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; - LGraphCanvas.prototype.getCanvasMenuOptions = function () { - const options = originalGetCanvasMenuOptions.apply(this, arguments) || []; + LGraphCanvas.prototype.getCanvasMenuOptions = function (...args) { + const options = originalGetCanvasMenuOptions.apply(this, args) || []; const newOptions = [...options]; - const showWorkflowPNG = app.ui.settings.getSettingValue("KayTool.ShowWorkflowPNG"); + const showWorkflowPNG = app.ui.settings.getSettingValue("KayTool.ShowWorkflowPNG") ?? true; if (showWorkflowPNG) { - let kaytoolMenu = newOptions.find(opt => opt && opt.content === "KayTool"); - if (!kaytoolMenu) { - kaytoolMenu = { - content: "KayTool", - submenu: { options: [] } - }; + let kaytoolMenu = newOptions.find(opt => opt?.content === "KayTool") || { + content: "KayTool", + submenu: { options: [] } + }; + + if (!newOptions.includes(kaytoolMenu)) { newOptions.push(null, kaytoolMenu); } - kaytoolMenu.submenu.options = kaytoolMenu.submenu.options || []; - const exportOptionExists = kaytoolMenu.submenu.options.some( - opt => opt && opt.content === "📦 Workflow PNG" - ); - if (!exportOptionExists) { + if (!kaytoolMenu.submenu.options.some(opt => opt?.content === "📦 Workflow PNG")) { kaytoolMenu.submenu.options.push({ content: "📦 Workflow PNG", - callback: () => { - showNotification({ - message: `𝙆:你需要把工作流信息嵌入到PNG中吗?Do you need to embed Workflow information into PNG?`, - bgColor: "#fff3cd", - size: "medium", - onYes: () => new KayWorkflowImage().export(true), - onNo: () => new KayWorkflowImage().export(false) - }); - } + callback: exportWorkflowPNG }); } }