From bf3add7d3f2fd0026258afef767fb98b1f7ef66a Mon Sep 17 00:00:00 2001 From: kk8bit Date: Mon, 14 Apr 2025 21:22:46 +0800 Subject: [PATCH] v0.70.0 --- CHANGELOG.md | 14 + README.md | 11 + locales/zh/main.json | 3 +- locales/zh/settings.json | 8 +- pyproject.toml | 2 +- web/cleanvram.js | 117 +++--- web/getset.js | 5 +- web/gululu.js | 868 +++++++++++++++++++++++++++++++++++++++ web/notification.js | 582 +++++++++++++++----------- web/resourcemonitor.js | 23 +- web/resources/GuLuLu.gif | Bin 0 -> 114614 bytes web/settings.js | 159 ++++--- web/startome.js | 48 +-- web/workflowpng.js | 57 +-- 14 files changed, 1448 insertions(+), 449 deletions(-) create mode 100644 web/gululu.js create mode 100644 web/resources/GuLuLu.gif 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 0000000000000000000000000000000000000000..3b26a2fd00e2ca8de67e02c453f49cc103e460a7 GIT binary patch literal 114614 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 { + 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 }); } }