From e693a569b375395dd4073e486448b964540917ca Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Sat, 8 Mar 2025 20:20:51 +0000 Subject: [PATCH] Rework betterCombos --- py/better_combos.py | 69 ++--- web/js/assets/no-image.png | Bin 0 -> 2705 bytes web/js/betterCombos.js | 564 ++++++++++++++++++++++--------------- 3 files changed, 356 insertions(+), 277 deletions(-) create mode 100644 web/js/assets/no-image.png diff --git a/py/better_combos.py b/py/better_combos.py index 8c32acb..d31043b 100644 --- a/py/better_combos.py +++ b/py/better_combos.py @@ -64,19 +64,20 @@ async def get_examples(request): type, name) if not file_path: return web.Response(status=404) - + file_path_no_ext = os.path.splitext(file_path)[0] examples = [] if os.path.isdir(file_path_no_ext): examples += sorted(map(lambda t: os.path.relpath(t, file_path_no_ext), - glob.glob(file_path_no_ext + "/*.txt"))) - + glob.glob(file_path_no_ext + "/*.txt"))) + if os.path.isfile(file_path_no_ext + ".txt"): examples += ["notes"] - + return web.json_response(examples) + @PromptServer.instance.routes.post("/pysssss/examples/{name}") async def save_example(request): name = request.match_info["name"] @@ -91,7 +92,7 @@ async def save_example(request): type, name) if not file_path: return web.Response(status=404) - + if not example_name.endswith(".txt"): example_name += ".txt" @@ -105,83 +106,57 @@ async def save_example(request): return web.Response(status=201) -def populate_items(names, type): - for idx, item_name in enumerate(names): +@PromptServer.instance.routes.get("/pysssss/images/{type}") +async def get_images(request): + type = request.match_info["type"] + names = folder_paths.get_filename_list(type) + images = {} + for item_name in names: file_name = os.path.splitext(item_name)[0] file_path = folder_paths.get_full_path(type, item_name) if file_path is None: - print(f"(pysssss:better_combos) Unable to get path for {type} {item_name}") continue file_path_no_ext = os.path.splitext(file_path)[0] for ext in ["png", "jpg", "jpeg", "preview.png", "preview.jpeg"]: - has_image = os.path.isfile(file_path_no_ext + "." + ext) - if has_image: - item_image = f"{file_name}.{ext}" + if os.path.isfile(file_path_no_ext + "." + ext): + images[item_name] = f"{type}/{file_name}.{ext}" break - names[idx] = { - "content": item_name, - "image": f"{type}/{item_image}" if has_image else None, - } - names.sort(key=lambda i: i["content"].lower()) + return web.json_response(images) class LoraLoaderWithImages(LoraLoader): RETURN_TYPES = (*LoraLoader.RETURN_TYPES, "STRING",) + RETURN_NAMES = (*getattr(LoraLoader, "RETURN_NAMES", + LoraLoader.RETURN_TYPES), "example") @classmethod def INPUT_TYPES(s): types = super().INPUT_TYPES() - names = types["required"]["lora_name"][0] - populate_items(names, "loras") - types["optional"] = { "prompt": ("HIDDEN",) } + types["optional"] = {"prompt": ("STRING", {"hidden": True})} return types - @classmethod - def VALIDATE_INPUTS(s, lora_name): - types = super().INPUT_TYPES() - names = types["required"]["lora_name"][0] - - name = lora_name["content"] - if name in names: - return True - else: - return f"Lora not found: {name}" - def load_lora(self, **kwargs): - kwargs["lora_name"] = kwargs["lora_name"]["content"] prompt = kwargs.pop("prompt", "") return (*super().load_lora(**kwargs), prompt) class CheckpointLoaderSimpleWithImages(CheckpointLoaderSimple): RETURN_TYPES = (*CheckpointLoaderSimple.RETURN_TYPES, "STRING",) - + RETURN_NAMES = (*getattr(CheckpointLoaderSimple, "RETURN_NAMES", + CheckpointLoaderSimple.RETURN_TYPES), "example") + @classmethod def INPUT_TYPES(s): types = super().INPUT_TYPES() - names = types["required"]["ckpt_name"][0] - populate_items(names, "checkpoints") - types["optional"] = { "prompt": ("HIDDEN",) } + types["optional"] = {"prompt": ("STRING", {"hidden": True})} return types - @classmethod - def VALIDATE_INPUTS(s, ckpt_name): - types = super().INPUT_TYPES() - names = types["required"]["ckpt_name"][0] - - name = ckpt_name["content"] - if name in names: - return True - else: - return f"Checkpoint not found: {name}" - def load_checkpoint(self, **kwargs): - kwargs["ckpt_name"] = kwargs["ckpt_name"]["content"] prompt = kwargs.pop("prompt", "") return (*super().load_checkpoint(**kwargs), prompt) diff --git a/web/js/assets/no-image.png b/web/js/assets/no-image.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b58b2c9d3ad82a2a949933648f8691ff36ba84 GIT binary patch literal 2705 zcmeH|`9IY81IOQUh>;=drW_rsVkKvej5CdMoT+t;yO2r9nHd`$n@C%(FiEH|#HOkq+1w7T>B~Ie6z**paO`a;Ev?4mTS zhS>919Rsi1R}v^0mwv~eBGfeI#QEHG)Vx?dt`vOw?&mLGpQP5xI<#suJ;>|>VAox& zrI~B&$nxk+g5;bcZT&{=LeO985oaEoS}3ceH#bWkDX|ikj46|RS{R@Cs+r@_(ls_V z!_Cf@QL7!cBLy&LhhJh=gS5EKl)3SOt}A-9oBt1%mE0%kmIFzVwe`zNt+(26th|zC zvMY`8v?(kYG$N)en^eSavuEJigt`~JhjSofPW9YudI@B%fbb9+)e?#}{T;D6qTG)0c9K>OyDx%rTrhxs1=p6Y|wO zS6Mx8$>p~9j+1_Hius1Ac z6Y9ABXt5sMMVftdJV+2aU9~KPrb@^*_R0G9z(^;@h4DjN)3{iK?g_=GO%t;q>F4y@ zG(Pg~U#R=z@&@@jB8Lrq34j1=+)yRCfr;q!+AR2#O$8c{T04({H=!BqFWM3#qyevn zU26w7eW-jO|0`mZy?1YEr|9(t0bI>q;X#%hG#}#}F9rtGyiBYz+deh`QG`ENUSPJN zR9d58BP-JHtURutt5AmkroM9s|3(AMSNJAcsYFqH5h#f5JUvmnC=@vJ@rhbmA z6z}KGwtH7K?jGrIt%|lhM!K=5J<&tz*b@%kJBqWXToNtj8Zp75g$k{yV z5{3rgB&JN5FvAJ&R0ZN+Z_{KWhi>A)#+jQSON-# z;8V!E+)3CFmr5LOR3&whXgMIg1h!;f!IQ2O_)boK*X|Doc*6B{8xX9fd`wk?#6r>u zaCU_N8;qSCOx4u+#C~WhSW$m~6vddC|0E7!2D2{>WbJ?2@R;YNWaebcT5~j=$?bSg z0ezK1_%tP&(T%EJ#B&ptz%NdT;Q|n`LWp6-=**ca0J_m}7ZBN;w#%gAPIn|uYJ2ko z{Ti#ejapDwEVgA7Mwd$1AM8ZO*Ack}wzJ94s!y!+Z@>B+;v(^)A$&ezaA_A0cGTso z%x;scd|tY z*R;sk{cTH1{G=A9*xoaX#nrj-Os+S2WP1F zMw{oZ0vo=Ztoq7+|d&EGg#rKmGx$G document.querySelector(`#${CSS.escape(imageId)}`); +function getWidgetName(type) { + return type === "checkpoints" ? "ckpt_name" : "lora_name"; +} + +function encodeRFC3986URIComponent(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); +} const calculateImagePosition = (el, bodyRect) => { let { top, left, right } = el.getBoundingClientRect(); @@ -39,256 +45,368 @@ const calculateImagePosition = (el, bodyRect) => { return { left: Math.round(left), top: Math.round(top), isLeft: !isSpaceRight }; }; -function showImage(el, imageId) { - const img = getImage(imageId); - if (img) { - const bodyRect = document.body.getBoundingClientRect(); - if (!bodyRect) return; +function showImage(relativeToEl, imageEl) { + const bodyRect = document.body.getBoundingClientRect(); + if (!bodyRect) return; - const { left, top, isLeft } = calculateImagePosition(el, bodyRect); + const { left, top, isLeft } = calculateImagePosition(relativeToEl, bodyRect); - img.style.display = "block"; - img.style.left = `${left}px`; - img.style.top = `${top}px`; + imageEl.style.left = `${left}px`; + imageEl.style.top = `${top}px`; - if (isLeft) { - img.classList.add("left"); - } else { - img.classList.remove("left"); - } + if (isLeft) { + imageEl.classList.add("left"); + } else { + imageEl.classList.remove("left"); } + + document.body.appendChild(imageEl); } -function closeImage(imageId) { - const img = getImage(imageId); - if (img) { - img.style.display = "none"; - } -} +let imagesByType = {}; +const loadImageList = async (type) => { + imagesByType[type] = await (await api.fetchApi(`/pysssss/images/${type}`)).json(); +}; app.registerExtension({ name: "pysssss.Combo++", init() { + const displayOptions = { "List (normal)": 0, "Tree (subfolders)": 1, "Thumbnails (grid)": 2 }; + const displaySetting = app.ui.settings.addSetting({ + id: "pysssss.Combo++.Submenu", + name: "🐍 Lora/Checkpoint loader display mode", + defaultValue: 1, + type: "combo", + options: (value) => { + value = +value; + + return Object.entries(displayOptions).map(([k, v]) => ({ + value: v, + text: k, + selected: k === value, + })); + }, + }); + $el("style", { textContent: ` - .litemenu-entry:hover .pysssss-combo-image { - display: block; - } .pysssss-combo-image { - display: none; position: absolute; left: 0; top: 0; width: ${IMAGE_WIDTH}px; height: ${IMAGE_HEIGHT}px; - background-size: contain; - background-repeat: no-repeat; + object-fit: contain; + object-position: top left; z-index: 9999; } .pysssss-combo-image.left { - background-position: top right; + object-position: top right; } + .pysssss-combo-folder { opacity: 0.7 } + .pysssss-combo-folder-arrow { display: inline-block; width: 15px; } + .pysssss-combo-folder:hover { background-color: rgba(255, 255, 255, 0.1); } + .pysssss-combo-prefix { display: none } + + /* Special handling for when the filter input is populated to revert to normal */ + .litecontextmenu:has(input:not(:placeholder-shown)) .pysssss-combo-folder-contents { + display: block !important; + } + .litecontextmenu:has(input:not(:placeholder-shown)) .pysssss-combo-folder { + display: none; + } + .litecontextmenu:has(input:not(:placeholder-shown)) .pysssss-combo-prefix { + display: inline; + } + .litecontextmenu:has(input:not(:placeholder-shown)) .litemenu-entry { + padding-left: 2px !important; + } + + /* Grid mode */ + .pysssss-combo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 10px; + overflow-x: hidden; + max-width: 60vw; + } + .pysssss-combo-grid .comfy-context-menu-filter { + grid-column: 1 / -1; + position: sticky; + top: 0; + } + .pysssss-combo-grid .litemenu-entry { + word-break: break-word; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + } + .pysssss-combo-grid .litemenu-entry:before { + content: ""; + display: block; + width: 100%; + height: 250px; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + /* No-image image attribution: Picture icons created by Pixel perfect - Flaticon */ + background-image: var(--background-image, url(extensions/ComfyUI-Custom-Scripts/js/assets/no-image.png)); + } + `, parent: document.body, }); + const p1 = loadImageList("checkpoints"); + const p2 = loadImageList("loras"); - const submenuSetting = app.ui.settings.addSetting({ - id: "pysssss.Combo++.Submenu", - name: "🐍 Enable submenu in custom nodes", - defaultValue: true, - type: "boolean", - }); - - // Ensure hook callbacks are available - const getOrSet = (target, name, create) => { - if (name in target) return target[name]; - return (target[name] = create()); + const refreshComboInNodes = app.refreshComboInNodes; + app.refreshComboInNodes = async function () { + const r = await Promise.all([ + refreshComboInNodes.apply(this, arguments), + loadImageList("checkpoints").catch(() => {}), + loadImageList("loras").catch(() => {}), + ]); + return r[0]; }; - const symbol = getOrSet(window, "__pysssss__", () => Symbol("__pysssss__")); - const store = getOrSet(window, symbol, () => ({})); - const contextMenuHook = getOrSet(store, "contextMenuHook", () => ({})); - for (const e of ["ctor", "preAddItem", "addItem"]) { - if (!contextMenuHook[e]) { - contextMenuHook[e] = []; + + const imageHost = $el("img.pysssss-combo-image"); + + const positionMenu = (menu, fillWidth) => { + // compute best position + let left = app.canvas.last_mouse[0] - 10; + let top = app.canvas.last_mouse[1] - 10; + + const body_rect = document.body.getBoundingClientRect(); + const root_rect = menu.getBoundingClientRect(); + + if (body_rect.width && left > body_rect.width - root_rect.width - 10) left = body_rect.width - root_rect.width - 10; + if (body_rect.height && top > body_rect.height - root_rect.height - 10) top = body_rect.height - root_rect.height - 10; + + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + if (fillWidth) { + menu.style.right = "10px"; } - } - // // Checks if this is a custom combo item - const isCustomItem = (value) => value && typeof value === "object" && "image" in value && value.content; - // Simple check for what separator to split by - const splitBy = (navigator.platform || navigator.userAgent).includes("Win") ? /\/|\\/ : /\//; + }; - contextMenuHook["ctor"].push(function (values, options) { - // Copy the class from the parent so if we are dark we are also dark - // this enables the filter box - if (options.parentMenu?.options?.className === "dark") { - options.className = "dark"; + const updateMenu = async (menu, type) => { + try { + await p1; + await p2; + } catch (error) { + console.error(error); + console.error("Error loading pysssss.betterCombos data") } - }); - function encodeRFC3986URIComponent(str) { - return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); - } + // Clamp max height so it doesn't overflow the screen + const position = menu.getBoundingClientRect(); + const maxHeight = window.innerHeight - position.top - 20; + menu.style.maxHeight = `${maxHeight}px`; - // After an element is created for an item, add an image if it has one - contextMenuHook["addItem"].push(function (el, menu, [name, value, options]) { - if (el && isCustomItem(value) && value?.image && !value.submenu) { - const key = `pysssss-image-combo-${name}`; - el.textContent += " *"; - $el("div.pysssss-combo-image", { - id: key, - parent: document.body, - style: { - backgroundImage: `url(/pysssss/view/${encodeRFC3986URIComponent(value.image)})`, - }, - }); - const showHandler = () => showImage(el, key); - const closeHandler = () => closeImage(key); + const images = imagesByType[type]; + const items = menu.querySelectorAll(".litemenu-entry"); - el.addEventListener("mouseenter", showHandler, { passive: true }); - el.addEventListener("mouseleave", closeHandler, { passive: true }); - el.addEventListener("click", closeHandler, { passive: true }); - } - }); + // Add image handler to items + const addImageHandler = (item) => { + const text = item.getAttribute("data-value").trim(); + if (images[text]) { + const textNode = document.createTextNode("*"); + item.appendChild(textNode); - function buildMenu(widget, values) { - const lookup = { - "": { options: [] }, + item.addEventListener( + "mouseover", + () => { + imageHost.src = `/pysssss/view/${encodeRFC3986URIComponent(images[text])}?${+new Date()}`; + document.body.appendChild(imageHost); + showImage(item, imageHost); + }, + { passive: true } + ); + item.addEventListener( + "mouseout", + () => { + imageHost.remove(); + }, + { passive: true } + ); + item.addEventListener( + "click", + () => { + imageHost.remove(); + }, + { passive: true } + ); + } }; - // Split paths into menu structure - for (const value of values) { - const split = value.content.split(splitBy); - let path = ""; - for (let i = 0; i < split.length; i++) { - const s = split[i]; - const last = i === split.length - 1; - if (last) { - // Leaf node, manually add handler that sets the lora - lookup[path].options.push({ - ...value, - title: s, - callback: () => { - widget.value = value; - widget.callback(value); - app.graph.setDirtyCanvas(true); - }, - }); - } else { - const prevPath = path; - path += s + splitBy; - if (!lookup[path]) { - const sub = { - title: s, - submenu: { - options: [], - title: s, - }, - }; + const createTree = () => { + // Create a map to store folder structures + const folderMap = new Map(); + const rootItems = []; + const splitBy = (navigator.platform || navigator.userAgent).includes("Win") ? /\/|\\/ : /\//; + const itemsSymbol = Symbol("items"); - // Add to tree - lookup[path] = sub.submenu; - lookup[prevPath].options.push(sub); + // First pass - organize items into folder structure + for (const item of items) { + const path = item.getAttribute("data-value").split(splitBy); + + // Remove path from visible text + item.textContent = path[path.length - 1]; + if (path.length > 1) { + // Add the prefix path back in so it can be filtered on + const prefix = $el("span.pysssss-combo-prefix", { + textContent: path.slice(0, -1).join("/") + "/", + }); + item.prepend(prefix); + } + + addImageHandler(item); + + if (path.length === 1) { + rootItems.push(item); + continue; + } + + // Temporarily remove the item from current position + item.remove(); + + // Create folder hierarchy + let currentLevel = folderMap; + for (let i = 0; i < path.length - 1; i++) { + const folder = path[i]; + if (!currentLevel.has(folder)) { + currentLevel.set(folder, new Map()); } + currentLevel = currentLevel.get(folder); + } + + // Store the actual item in the deepest folder + if (!currentLevel.has(itemsSymbol)) { + currentLevel.set(itemsSymbol, []); + } + currentLevel.get(itemsSymbol).push(item); + } + + const createFolderElement = (name) => { + const folder = $el("div.litemenu-entry.pysssss-combo-folder", { + innerHTML: `▶ ${name}`, + style: { paddingLeft: "5px" }, + }); + return folder; + }; + + const insertFolderStructure = (parentElement, map, level = 0) => { + for (const [folderName, content] of map.entries()) { + if (folderName === itemsSymbol) continue; + + const folderElement = createFolderElement(folderName); + folderElement.style.paddingLeft = `${level * 10 + 5}px`; + parentElement.appendChild(folderElement); + + const childContainer = $el("div.pysssss-combo-folder-contents", { + style: { display: "none" }, + }); + + // Add items in this folder + const items = content.get(itemsSymbol) || []; + for (const item of items) { + item.style.paddingLeft = `${(level + 1) * 10 + 14}px`; + childContainer.appendChild(item); + } + + // Recursively add subfolders + insertFolderStructure(childContainer, content, level + 1); + parentElement.appendChild(childContainer); + + // Add click handler for folder + folderElement.addEventListener("click", (e) => { + e.stopPropagation(); + const arrow = folderElement.querySelector(".pysssss-combo-folder-arrow"); + const contents = folderElement.nextElementSibling; + if (contents.style.display === "none") { + contents.style.display = "block"; + arrow.textContent = "▼"; + } else { + contents.style.display = "none"; + arrow.textContent = "▶"; + } + }); + } + }; + + insertFolderStructure(items[0].parentElement, folderMap); + positionMenu(menu); + }; + + const addImageData = (item) => { + const text = item.getAttribute("data-value").trim(); + if (images[text]) { + item.style.setProperty("--background-image", `url(/pysssss/view/${encodeRFC3986URIComponent(images[text])})`); + } + }; + + if (displaySetting.value === 1 || displaySetting.value === true) { + createTree(); + } else if (displaySetting.value === 2) { + menu.classList.add("pysssss-combo-grid"); + + for (const item of items) { + addImageData(item); + } + positionMenu(menu, true); + } else { + for (const item of items) { + addImageHandler(item); + } + } + }; + + const mutationObserver = new MutationObserver((mutations) => { + const node = app.canvas.current_node; + + if (!node || (node.comfyClass !== LORA_LOADER && node.comfyClass !== CHECKPOINT_LOADER)) { + return; + } + + for (const mutation of mutations) { + for (const removed of mutation.removedNodes) { + if (removed.classList?.contains("litecontextmenu")) { + imageHost.remove(); + } + } + + for (const added of mutation.addedNodes) { + if (added.classList?.contains("litecontextmenu")) { + const overWidget = app.canvas.getWidgetAtCursor(); + const type = getType(node); + if (overWidget?.name === getWidgetName(type)) { + requestAnimationFrame(() => { + // Bad hack to prevent showing on right click menu by checking for the filter input + if (!added.querySelector(".comfy-context-menu-filter")) return; + updateMenu(added, type); + }); + } + return; } } } - - return lookup[""].options; - } - - // Override COMBO widgets to patch their values - const combo = ComfyWidgets["COMBO"]; - ComfyWidgets["COMBO"] = function (node, inputName, inputData) { - const type = inputData[0]; - const res = combo.apply(this, arguments); - if (isCustomItem(type[0])) { - let value = res.widget.value; - let values = res.widget.options.values; - let menu = null; - - // Override the option values to check if we should render a menu structure - Object.defineProperty(res.widget.options, "values", { - get() { - let v = values; - - if (submenuSetting.value) { - if (!menu) { - // Only build the menu once - menu = buildMenu(res.widget, values); - } - v = menu; - } - - const valuesIncludes = v.includes; - v.includes = function (searchElement) { - const includesFromMenuItems = function (items) { - for (const item of items) { - if (includesFromMenuItem(item)) { - return true; - } - } - return false; - }; - const includesFromMenuItem = function (item) { - if (item.submenu) { - return includesFromMenuItems(item.submenu.options); - } else { - return item.content === searchElement.content; - } - }; - - const includes = valuesIncludes.apply(this, arguments) || includesFromMenuItems(this); - return includes; - }; - - return v; - }, - set(v) { - // Options are changing (refresh) so reset the menu so it can be rebuilt if required - values = v; - menu = null; - }, - }); - - Object.defineProperty(res.widget, "value", { - get() { - // HACK: litegraph supports rendering items with "content" in the menu, but not on the widget - // This detects when its being called by the widget drawing and just returns the text - // Also uses the content for the same image replacement value - if (res.widget) { - const stack = new Error().stack; - if (stack.includes("drawNodeWidgets") || stack.includes("saveImageExtraOutput")) { - return (value || type[0]).content; - } - } - return value; - }, - set(v) { - if (v?.submenu) { - // Dont allow selection of submenus - return; - } - value = v; - }, - }); - } - - return res; - }; + }); + mutationObserver.observe(document.body, { childList: true, subtree: false }); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { - const isCkpt = nodeType.comfyClass === CHECKPOINT_LOADER; - const isLora = nodeType.comfyClass === LORA_LOADER; + const isCkpt = nodeData.name === CHECKPOINT_LOADER; + const isLora = nodeData.name === LORA_LOADER; if (isCkpt || isLora) { const onAdded = nodeType.prototype.onAdded; nodeType.prototype.onAdded = function () { onAdded?.apply(this, arguments); const { widget: exampleList } = ComfyWidgets["COMBO"](this, "example", [[""], {}], app); - + this.widgets.find((w) => w.name === "prompt").computeSize = () => [0, -4]; let exampleWidget; const get = async (route, suffix) => { - const url = encodeURIComponent(`${getType(nodeType)}${suffix || ""}`); + const url = encodeRFC3986URIComponent(`${getType(nodeType)}${suffix || ""}`); return await api.fetchApi(`/pysssss/${route}/${url}`); }; @@ -302,7 +420,7 @@ app.registerExtension({ return; } - const v = this.widgets[0].value.content; + const v = this.widgets[0].value; const pos = v.lastIndexOf("."); const name = v.substr(0, pos); let exampleName = exampleList.value; @@ -332,9 +450,9 @@ app.registerExtension({ exampleList.options.values = ["[none]"]; exampleList.value = "[none]"; let examples = []; - if (this.widgets[0].value?.content) { + if (this.widgets[0].value) { try { - examples = await (await get("examples", `/${this.widgets[0].value.content}`)).json(); + examples = await (await get("examples", `/${this.widgets[0].value}`)).json(); } catch (error) {} } exampleList.options.values = ["[none]", ...examples]; @@ -353,9 +471,6 @@ app.registerExtension({ modelWidget.callback = function () { const ret = modelCb?.apply(this, arguments) ?? modelWidget.value; let v = ret; - if (ret?.content) { - v = ret.content; - } if (prev !== v) { listExamples(); prev = v; @@ -366,13 +481,6 @@ app.registerExtension({ modelWidget.callback(); }, 30); }; - - // Prevent adding HIDDEN inputs - const addInput = nodeType.prototype.addInput ?? LGraphNode.prototype.addInput; - nodeType.prototype.addInput = function (_, type) { - if (type === "HIDDEN") return; - return addInput.apply(this, arguments); - }; } const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; @@ -394,25 +502,21 @@ app.registerExtension({ content: "Save as Preview", submenu: { options: nodes.map((n) => ({ - content: n.widgets[0].value.content, + content: n.widgets[0].value, callback: async () => { const url = new URL(img.src); - const { image } = await api.fetchApi( - "/pysssss/save/" + encodeURIComponent(`${getType(n)}/${n.widgets[0].value.content}`), - { - method: "POST", - body: JSON.stringify({ - filename: url.searchParams.get("filename"), - subfolder: url.searchParams.get("subfolder"), - type: url.searchParams.get("type"), - }), - headers: { - "content-type": "application/json", - }, - } - ); - n.widgets[0].value.image = image; - app.refreshComboInNodes(); + await api.fetchApi("/pysssss/save/" + encodeRFC3986URIComponent(`${getType(n)}/${n.widgets[0].value}`), { + method: "POST", + body: JSON.stringify({ + filename: url.searchParams.get("filename"), + subfolder: url.searchParams.get("subfolder"), + type: url.searchParams.get("type"), + }), + headers: { + "content-type": "application/json", + }, + }); + loadImageList(getType(n)); }, })), },