diff --git a/README.md b/README.md index 058f2b9..12012d9 100644 --- a/README.md +++ b/README.md @@ -375,6 +375,19 @@ AudioCropProcess_UTK - **操作系统**:Windows, macOS, Linux - **GPU支持**:NVIDIA CUDA (推荐) +## 界面设置与 Nodes 2.0 说明 + +### 启用上下文菜单自动嵌套子目录 + +在 ComfyUI 设置中可开启「启用上下文菜单自动嵌套子目录」。开启后,在模型/文件选择等带路径的 combo 下拉中,选项会按子目录折叠为层级菜单,便于在子目录较多的场景下选择。 + +**关于 Nodes 2.0**:该功能通过修补 LiteGraph 的 `ContextMenu` 实现。若使用 **Nodes 2.0**(官方 Vue 前端),combo 下拉可能不再经过 `ContextMenu`,导致本功能不生效。此时可: + +- 在 ComfyUI 菜单中切换回 **LiteGraph Canvas**(关闭 Nodes 2.0)以使用本功能,或 +- 关注 ComfyUI 后续是否提供 combo 选项转换的扩展 API。 + +验证方式:开启本设置后,点击任意模型/文件类节点的下拉,若浏览器控制台出现 `UniversalToolkit contextMenu nest patch applied` 日志,说明补丁已生效;若无该日志,则当前前端未使用 ContextMenu,本功能在该界面下不可用。 + ## 📁 项目结构 ``` diff --git a/nodes/tools/lora_info_db.json b/nodes/tools/lora_info_db.json index 23bd567..95d7f3d 100644 --- a/nodes/tools/lora_info_db.json +++ b/nodes/tools/lora_info_db.json @@ -66,5 +66,12 @@ "examplePrompt": null, "baseModel": "Flux.1 Kontext", "metaInfo": "{\n \"name\": \"HDR360\",\n \"sshs_legacy_hash\": \"6bb22ed6\",\n \"format\": \"pt\",\n \"training_info\": \"{\\\"step\\\": 5500, \\\"epoch\\\": 56}\",\n \"sshs_model_hash\": \"d904a130eb54877361ec4cd9ebd82c3f937c1128a56ea68115c5db0ba1291638\",\n \"version\": \"1.0\",\n \"ss_base_model_version\": \"sd_1.5\",\n \"ss_output_name\": \"HDR360\",\n \"software\": \"{\\\"name\\\": \\\"ai-toolkit\\\", \\\"repo\\\": \\\"https://github.com/ostris/ai-toolkit\\\", \\\"version\\\": \\\"0.3.3\\\"}\"\n}" + }, + "zimage\\ysy zbase alpha\\ysy zbase alpha_000003000.safetensors": { + "output": "", + "trainedWords": "", + "examplePrompt": null, + "baseModel": "", + "metaInfo": "{\n \"name\": \"ysy zbase alpha\",\n \"training_info\": \"{\\\"step\\\": 3000, \\\"epoch\\\": 40}\",\n \"version\": \"1.0\",\n \"ss_tag_frequency\": \"{\\\"1_Tagaysy\\\": {\\\"Tagaysy\\\": 1}}\",\n \"format\": \"pt\",\n \"sshs_legacy_hash\": \"7d24986a\",\n \"ss_base_model_version\": \"zimage\",\n \"ss_output_name\": \"ysy zbase alpha\",\n \"sshs_model_hash\": \"17b842123ec61768345a3e0d7a8d84625ec9f9a7f0a4d97336c4f72f84cd0228\",\n \"software\": \"{\\\"name\\\": \\\"ai-toolkit\\\", \\\"repo\\\": \\\"https://github.com/ostris/ai-toolkit\\\", \\\"version\\\": \\\"0.7.22\\\"}\"\n}" } } \ No newline at end of file diff --git a/web/context_menu_nest.js b/web/context_menu_nest.js new file mode 100644 index 0000000..2460065 --- /dev/null +++ b/web/context_menu_nest.js @@ -0,0 +1,144 @@ +/** + * 启用上下文菜单自动嵌套子目录 + * 参考 ComfyUI-Easy-Use 的 easyContextMenu.js 实现,仅移植路径嵌套逻辑,不含缩略图。 + */ + +import { app } from "../../scripts/app.js"; + +const SETTING_ID = "UniversalToolkit.ContextMenuNestSub"; +const THRESHOLD = 10; + +app.registerExtension({ + name: "UniversalToolkit.contextMenuNest", + async setup(app) { + app.ui.settings.addSetting({ + id: SETTING_ID, + name: "启用上下文菜单自动嵌套子目录(不适用于 Nodes 2.0)", + type: "boolean", + defaultValue: true, + tooltip: "仅在使用 LiteGraph Canvas 时生效;Nodes 2.0 下 combo 下拉不经过 ContextMenu,本功能不可用。", + }); + + const getEnabled = () => !!app.ui.settings.getSettingValue(SETTING_ID, true); + const existingContextMenu = LiteGraph.ContextMenu; + + LiteGraph.ContextMenu = function (values, options) { + // 方案一:验证 Nodes 2.0 下是否仍会调用此处(点击 combo 下拉时若出现 log 则补丁生效) + console.log("UniversalToolkit contextMenu nest patch applied", values?.length); + const enabled = getEnabled(); + if ( + !enabled || + (values?.length || 0) <= THRESHOLD || + !(options?.callback) || + values.some((i) => typeof i !== "string") + ) { + return existingContextMenu.apply(this, [...arguments]); + } + + const compatValues = values; + const originalValues = [...compatValues]; + const folders = {}; + const specialOps = []; + const folderless = []; + + for (const value of compatValues) { + const splitBy = value.indexOf("/") > -1 ? "/" : "\\"; + const valueSplit = value.split(splitBy); + if (valueSplit.length > 1) { + const key = valueSplit.shift(); + folders[key] = folders[key] || []; + folders[key].push(valueSplit.join(splitBy)); + } else if (value === "CHOOSE" || value.startsWith("DISABLE ")) { + specialOps.push(value); + } else { + folderless.push(value); + } + } + + const foldersCount = Object.keys(folders).length; + if (foldersCount === 0) { + return existingContextMenu.apply(this, [...arguments]); + } + + const oldCallback = options.callback; + options.callback = null; + const newCallback = (item, opts) => { + if (["None", "无", "無", "なし"].includes(item.content)) { + oldCallback("None", opts); + } else { + const full = originalValues.find((i) => i.endsWith(item.content)); + oldCallback(full != null ? full : item.content, opts); + } + }; + + const addContent = (content) => ({ + content, + callback: newCallback, + }); + + const add_sub_folder = (folder, folderName) => { + const subs = []; + const less = []; + const b = folder.map((name) => { + const _folders = {}; + const splitBy = name.indexOf("/") > -1 ? "/" : "\\"; + const valueSplit = name.split(splitBy); + if (valueSplit.length > 1) { + const key = valueSplit.shift(); + _folders[key] = _folders[key] || []; + _folders[key].push(valueSplit.join(splitBy)); + } + const subKeys = Object.keys(_folders); + if (subKeys.length > 0) { + const key = subKeys[0]; + const val = _folders[key][0]; + if (key && val) subs.push({ key, value: val }); + else less.push(addContent(name)); + } else { + less.push(addContent(name)); + } + return addContent(name); + }); + + if (subs.length > 0) { + const subsObj = {}; + subs.forEach((item) => { + subsObj[item.key] = subsObj[item.key] || []; + subsObj[item.key].push(item.value); + }); + return [ + ...Object.entries(subsObj).map((f) => ({ + content: f[0], + has_submenu: true, + callback: () => {}, + submenu: { + options: add_sub_folder(f[1], f[0]), + }, + })), + ...less, + ]; + } + return b; + }; + + const newValues = []; + for (const [folderName, folder] of Object.entries(folders)) { + newValues.push({ + content: folderName, + has_submenu: true, + callback: () => {}, + submenu: { + options: add_sub_folder(folder, folderName), + }, + }); + } + newValues.push(...folderless.map((f) => addContent(f))); + if (specialOps.length > 0) { + newValues.push(...specialOps.map((f) => addContent(f))); + } + + return existingContextMenu.call(this, newValues, options); + }; + LiteGraph.ContextMenu.prototype = existingContextMenu.prototype; + }, +}); diff --git a/web/universal_toolkit.js b/web/universal_toolkit.js index 0f5179e..5fc4ed0 100644 --- a/web/universal_toolkit.js +++ b/web/universal_toolkit.js @@ -1,4 +1,5 @@ import { app } from "../../scripts/app.js"; +import "./context_menu_nest.js"; app.registerExtension({ name: "ComfyUI.UniversalToolkit",