diff --git a/README.md b/README.md index ea8c146..df55dc9 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ 转成base64的节点都是输出节点,websocket消息中会包含base64Images和base64Type属性(具体格式请查看ImageNode.py中的ImageToBase64Advanced类源代码,或者自己搭建简单流程运行在浏览器开发者工具-->网络中查看) ## 安装 -- 通过ComfyUI-Manager安装 -- 在ComfyUI安装目录根目录下打开命令行终端,执行以下命令 +- 方式1:通过ComfyUI-Manager安装 +- 方式2:在ComfyUI安装目录根目录下打开命令行终端,执行以下命令 ```sh cd custom_nodes git clone https://github.com/lldacing/comfyui-easyapi-nodes.git @@ -38,13 +38,16 @@ | IntToList | 整型转列表 | | ListMerge | 列表合并 | | JoinList | 列表根据指定分隔符连接 | -| ShowString | 显示字符串 | -| ShowInt | 显示整型 | -| ShowFloat | 显示浮点型 | -| ShowNumber | 显示数字 | +| ShowString | 显示字符串(可指定消息中key值) | +| ShowInt | 显示整型(可指定消息中key值) | +| ShowFloat | 显示浮点型(可指定消息中key值) | +| ShowNumber | 显示数字(可指定消息中key值) | Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带宽可能也会有瓶颈,条件允许可以把图片上传到OSS服务器得到URL,然后用LoadImageFromUrl加载,由于无相关OSS账号,上传OSS节点需自行编写,暂不支持。 +### [示例](example/example.png) + ![save api extended](docs/example_note.png) + ## 功能 - 扩展Save(Api Format)菜单。 - 复制工作流 @@ -68,7 +71,12 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带 - 是否自动展开当前菜单下的子菜单 配置路径:Settings -> [EasyApi] Auto Open Sub Menu + ![save api extended](docs/menu_autoopen.gif) + - 模糊搜索 + 配置路径:Settings -> [EasyApi] Fuzzy Search + ![save api extended](docs/fuzzy_search.png) + - 菜单扩展 - 重设某个节点的id(Node Context Menu) diff --git a/docs/example_note.png b/docs/example_note.png new file mode 100644 index 0000000..0dbf208 Binary files /dev/null and b/docs/example_note.png differ diff --git a/docs/fuzzy_search.png b/docs/fuzzy_search.png new file mode 100644 index 0000000..3655f5b Binary files /dev/null and b/docs/fuzzy_search.png differ diff --git a/docs/settings.png b/docs/settings.png index 4609f4a..97effdb 100644 Binary files a/docs/settings.png and b/docs/settings.png differ diff --git a/static/js/easyapi.js b/static/js/easyapi.js index 63f1f85..acc0722 100644 --- a/static/js/easyapi.js +++ b/static/js/easyapi.js @@ -41,17 +41,292 @@ app.registerExtension({ LiteGraph.ContextMenu.prototype = ctxMenu.prototype; }; app.ui.settings.addSetting({ - id: "Easyapi.ContextMenu.autoopen", - name: "[EasyApi] Auto Open Sub Menu", - type: "boolean", - defaultValue: false, - onChange(value) { - if (value) { - replace(value); - } else { - LiteGraph.ContextMenu = ctxMenu; + id: "Easyapi.ContextMenu.autoopen", + name: "[EasyApi] Auto Open Sub Menu", + type: "boolean", + defaultValue: false, + onChange(value) { + if (value) { + replace(value); + } else { + LiteGraph.ContextMenu = ctxMenu; + } + }, + }); + app.ui.settings.addSetting({ + id: "Easyapi.Search.fuzzy", + name: "[EasyApi] Fuzzy Search", + type: "boolean", + defaultValue: false, + onChange(value) { + LiteGraph.search_fuzzy_match = value; + }, + }); + } +}) + +app.registerExtension({ + name: "Comfy.EasyApi.Search", + async init(app) { + function fuzzy_match_filter(inputStr, str, matchedPos) { + matchedPos = matchedPos || []; + let splitStr = inputStr.split(""); + let j = splitStr.shift() + for (let i in str) { + if (str[i] == j) { + matchedPos.push(i) + if (splitStr.length == 0) { + return true; + } + j = splitStr.shift() + } + } + return false; + } + + function highlight_keywords(oldText, recordPos) { + let newText = ""; + let pos = recordPos.shift(); + let split = oldText.split("") + for (let j in split) { + let c = oldText[j]; + if (j == pos) { + newText += "" + c + ""; + if (recordPos.length > 0) { + pos = recordPos.shift(); + } else { + newText += oldText.slice(parseInt(j) + 1); + break; + } + } else { + newText += c; + } + } + return newText; + } + + let oldOnSearchBox = app.canvas.onSearchBox + let oldShowSearchBox = LGraphCanvas.prototype.showSearchBox + LGraphCanvas.prototype.showSearchBox = function(event, options) { + let that = this; + // copy from oldShowSearBox + let def_options = { slot_from: null + ,node_from: null + ,node_to: null + ,do_type_filter: LiteGraph.search_filter_enabled + ,type_filter_in: false // these are default: pass to set initially set values + ,type_filter_out: false + ,show_general_if_none_on_typefilter: true + ,show_general_after_typefiltered: true + ,hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave + ,show_all_if_empty: true + ,show_all_on_open: LiteGraph.search_show_all_on_open + ,search_fuzzy_match: LiteGraph.search_fuzzy_match || false // easyapi custom setting + }; + options = Object.assign(def_options, options || {}); + if (!options.search_fuzzy_match) { + app.canvas.onSearchBox = oldOnSearchBox; + return oldShowSearchBox?.apply(this, [event, options]); + } + let dialog = oldShowSearchBox?.apply(this, [event, options]) + + app.canvas.onSearchBox = function (helper, inputStr, graphcanvas) { + // let options = {} + var c = 0; + let str = inputStr.toLowerCase(); + var filter = graphcanvas.filter || graphcanvas.graph.filter; + + // filter by type preprocess + if (options.do_type_filter && that.search_box) { + var sIn = that.search_box.querySelector(".slot_in_type_filter"); + var sOut = that.search_box.querySelector(".slot_out_type_filter"); + } else { + var sIn = false; + var sOut = false; + } + + let filtered = []; + + //extras + for (var i in LiteGraph.searchbox_extras) { + var extra = LiteGraph.searchbox_extras[i]; + if ((!options.show_all_if_empty || str) && !fuzzy_match_filter(str, extra.desc)) { + continue; + } + var ctor = LiteGraph.registered_node_types[ extra.type ]; + if( ctor && ctor.filter != filter ) + continue; + if( ! inner_test_filter(extra.type) ) + continue; + // addResult( extra.desc, "searchbox_extra" ); + filtered.push(extra.desc) + if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { + break; + } + } + + let temp_filtered = [] + if (Array.prototype.filter) { //filter supported + var keys = Object.keys( LiteGraph.registered_node_types ); //types + temp_filtered = keys.filter( inner_test_filter ); + } else { + temp_filtered = []; + for (var i in LiteGraph.registered_node_types) { + if( inner_test_filter(i) ) + temp_filtered.push(i); + } + } + + for (var i = 0; i < temp_filtered.length; i++) { + // addResult(filtered[i]); + filtered.push(temp_filtered[i]); + if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { + break; + } + } + + // add general type if filtering + if (options.show_general_after_typefiltered + && (sIn.value || sOut.value) + ){ + let filtered_extra = []; + for (var i in LiteGraph.registered_node_types) { + if( inner_test_filter(i, {inTypeOverride: sIn&&sIn.value?"*":false, outTypeOverride: sOut&&sOut.value?"*":false}) ) + filtered_extra.push(i); + } + for (var i = 0; i < filtered_extra.length; i++) { + // addResult(filtered_extra[i], "generic_type"); + filtered.push(filtered_extra[i]); + if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) { + break; + } } - }, - }); + } + + // check il filtering gave no results + if ((sIn.value || sOut.value) && + ( (helper.childNodes.length == 0 && options.show_general_if_none_on_typefilter) ) + ){ + var filtered_extra = []; + for (var i in LiteGraph.registered_node_types) { + if( inner_test_filter(i, {skipFilter: true}) ) + filtered_extra.push(i); + } + for (var i = 0; i < filtered_extra.length; i++) { + // addResult(filtered_extra[i], "not_in_filter"); + filtered.push(filtered_extra[i]); + if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { + break; + } + } + } + + function inner_test_filter( type, optsIn ) + { + var optsIn = optsIn || {}; + var optsDef = { skipFilter: false + ,inTypeOverride: false + ,outTypeOverride: false + }; + var opts = Object.assign(optsDef,optsIn); + var ctor = LiteGraph.registered_node_types[ type ]; + if(filter && ctor.filter != filter ) + return false; + if ((!options.show_all_if_empty || str) && !fuzzy_match_filter(str, type.toLowerCase()) && (!ctor.title || !fuzzy_match_filter(str, ctor.title.toLowerCase()))) + return false; + + // filter by slot IN, OUT types + if(options.do_type_filter && !opts.skipFilter){ + var sType = type; + + var sV = sIn.value; + if (opts.inTypeOverride!==false) sV = opts.inTypeOverride; + //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 + + if(sIn && sV){ + //console.log("will check filter against "+sV); + if (LiteGraph.registered_slot_in_types[sV] && LiteGraph.registered_slot_in_types[sV].nodes){ // type is stored + //console.debug("check "+sType+" in "+LiteGraph.registered_slot_in_types[sV].nodes); + var doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType); + if (doesInc!==false){ + //console.log(sType+" HAS "+sV); + }else{ + /*console.debug(LiteGraph.registered_slot_in_types[sV]); + console.log(+" DONT includes "+type);*/ + return false; + } + } + } + + var sV = sOut.value; + if (opts.outTypeOverride!==false) sV = opts.outTypeOverride; + //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 + + if(sOut && sV){ + //console.log("search will check filter against "+sV); + if (LiteGraph.registered_slot_out_types[sV] && LiteGraph.registered_slot_out_types[sV].nodes){ // type is stored + //console.debug("check "+sType+" in "+LiteGraph.registered_slot_out_types[sV].nodes); + var doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType); + if (doesInc!==false){ + //console.log(sType+" HAS "+sV); + }else{ + /*console.debug(LiteGraph.registered_slot_out_types[sV]); + console.log(+" DONT includes "+type);*/ + return false; + } + } + } + } + return true; + } + + if (helper.dataset["observer"] != "true") { + helper.dataset["observer"] = "true"; + let observConfig = { attributes: false, childList: true }; + let observer = new MutationObserver(mutations => { + for (let i = 0; i < mutations.length; i++) { + let mutationRecord = mutations[i]; + let searchWord = helper.parentNode.querySelector("input").value; + if (searchWord.trim().length == 0) { + break; + } + for (let item of mutationRecord.addedNodes) { + if (!item.dataset["fuzzy"] && decodeURIComponent(item.dataset["type"]) in LiteGraph.registered_node_types) { + item.dataset["fuzzy"] = "true"; + let type = decodeURIComponent(item.dataset["type"]) + let newDom = document.createElement("span"); + let recordPos = [] + if (fuzzy_match_filter(searchWord, type.toLowerCase(), recordPos)) { + let oldDom = item.firstChild.nextSibling; + let oldText = oldDom.textContent; + let newText = highlight_keywords(oldText, recordPos); + oldDom.innerHTML = newText; + } else { + recordPos = [] + let node = LiteGraph.registered_node_types[type]; + if (node.title && fuzzy_match_filter(searchWord, node.title.toLowerCase(), recordPos)) { + let oldDom = item.firstChild; + let oldText = oldDom.textContent; + let newText = highlight_keywords(oldText, recordPos); + newDom.innerHTML = newText; + item.replaceChild(newDom, oldDom); + } + } + } + } + } + }); + observer.observe(helper, observConfig); + let oldClose = helper.parentNode.close; + helper.parentNode.close = function () { + observer.disconnect(); + oldClose?.apply(this, arguments) + }; + } + + return filtered; + }; + return dialog; + } } }) \ No newline at end of file diff --git a/static/js/workflows.js b/static/js/workflows.js index c8b7cc5..48d167b 100644 --- a/static/js/workflows.js +++ b/static/js/workflows.js @@ -525,7 +525,7 @@ class EasyApiWorkflows { options.splice(options.length - 1, 0, null); } let hasInputLink = node.inputs?.filter(input => input?.link).length > 0; - let hasOutputLink = node.outputs?.filter(output => output?.links).length > 0; + let hasOutputLink = node.outputs?.filter(output => output?.links?.length > 0).length > 0; options.splice(options.length - 1, 0, { content: "Set Node Id (EasyApi)", @@ -788,6 +788,9 @@ class EasyApiWorkflows { app.nodePreviewImages[newNodeId] = app.nodePreviewImages[oldNodeId] delete app.nodePreviewImages[oldNodeId] } + if (newNodeId > app.graph.last_node_id) { + app.graph.last_node_id = newNodeId; + } } }