| | import { app } from "../../../scripts/app.js"; |
| | import { ComfyWidgets } from "../../../scripts/widgets.js"; |
| | import { $el } from "../../../scripts/ui.js"; |
| | import { api } from "../../../scripts/api.js"; |
| |
|
| | const CHECKPOINT_LOADER = "CheckpointLoader|pysssss"; |
| | const LORA_LOADER = "LoraLoader|pysssss"; |
| |
|
| | function getType(node) { |
| | if (node.comfyClass === CHECKPOINT_LOADER) { |
| | return "checkpoints"; |
| | } |
| | return "loras"; |
| | } |
| |
|
| | app.registerExtension({ |
| | name: "pysssss.Combo++", |
| | init() { |
| | $el("style", { |
| | textContent: ` |
| | .litemenu-entry:hover .pysssss-combo-image { |
| | display: block; |
| | } |
| | .pysssss-combo-image { |
| | display: none; |
| | position: absolute; |
| | left: 0; |
| | top: 0; |
| | transform: translate(-100%, 0); |
| | width: 256px; |
| | height: 256px; |
| | background-size: cover; |
| | background-position: center; |
| | filter: brightness(65%); |
| | } |
| | `, |
| | parent: document.body, |
| | }); |
| |
|
| | const submenuSetting = app.ui.settings.addSetting({ |
| | id: "pysssss.Combo++.Submenu", |
| | name: "🐍 Enable submenu in custom nodes", |
| | defaultValue: true, |
| | type: "boolean", |
| | }); |
| |
|
| | |
| | const getOrSet = (target, name, create) => { |
| | if (name in target) return target[name]; |
| | return (target[name] = create()); |
| | }; |
| | 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 isCustomItem = (value) => value && typeof value === "object" && "image" in value && value.content; |
| | |
| | const splitBy = (navigator.platform || navigator.userAgent).includes("Win") ? /\/|\\/ : /\//; |
| |
|
| | contextMenuHook["ctor"].push(function (values, options) { |
| | |
| | |
| | if (options.parentMenu?.options?.className === "dark") { |
| | options.className = "dark"; |
| | } |
| | }); |
| |
|
| | |
| | contextMenuHook["addItem"].push(function (el, menu, [name, value, options]) { |
| | if (el && isCustomItem(value) && value?.image && !value.submenu) { |
| | el.textContent += " *"; |
| | $el("div.pysssss-combo-image", { |
| | parent: el, |
| | style: { |
| | backgroundImage: `url(/pysssss/view/${encodeURIComponent(value.image)})`, |
| | }, |
| | }); |
| | } |
| | }); |
| |
|
| | function buildMenu(widget, values) { |
| | const lookup = { |
| | "": { options: [] }, |
| | }; |
| |
|
| | |
| | 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) { |
| | |
| | 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, |
| | }, |
| | }; |
| |
|
| | |
| | lookup[path] = sub.submenu; |
| | lookup[prevPath].options.push(sub); |
| | } |
| | } |
| | } |
| | } |
| |
|
| | return lookup[""].options; |
| | } |
| |
|
| | |
| | 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; |
| |
|
| | |
| | Object.defineProperty(res.widget.options, "values", { |
| | get() { |
| | if (submenuSetting.value) { |
| | if (!menu) { |
| | |
| | menu = buildMenu(res.widget, values); |
| | } |
| | return menu; |
| | } |
| | return values; |
| | }, |
| | set(v) { |
| | |
| | values = v; |
| | menu = null; |
| | }, |
| | }); |
| |
|
| | Object.defineProperty(res.widget, "value", { |
| | get() { |
| | |
| | |
| | |
| | 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) { |
| | |
| | return; |
| | } |
| | value = v; |
| | }, |
| | }); |
| | } |
| |
|
| | return res; |
| | }; |
| | }, |
| | async beforeRegisterNodeDef(nodeType, nodeData, app) { |
| | const isCkpt = nodeType.comfyClass === CHECKPOINT_LOADER; |
| | const isLora = nodeType.comfyClass === 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); |
| |
|
| | let exampleWidget; |
| |
|
| | const get = async (route, suffix) => { |
| | const url = encodeURIComponent(`${getType(nodeType)}${suffix || ""}`); |
| | return await api.fetchApi(`/pysssss/${route}/${url}`); |
| | }; |
| |
|
| | const getExample = async () => { |
| | if (exampleList.value === "[none]") { |
| | if (exampleWidget) { |
| | exampleWidget.inputEl.remove(); |
| | exampleWidget = null; |
| | this.widgets.length -= 1; |
| | } |
| | return; |
| | } |
| |
|
| | const v = this.widgets[0].value.content; |
| | const pos = v.lastIndexOf("."); |
| | const name = v.substr(0, pos); |
| |
|
| | const example = await (await get("view", `/${name}/${exampleList.value}`)).text(); |
| | if (!exampleWidget) { |
| | exampleWidget = ComfyWidgets["STRING"](this, "prompt", ["STRING", { multiline: true }], app).widget; |
| | exampleWidget.inputEl.readOnly = true; |
| | exampleWidget.inputEl.style.opacity = 0.6; |
| | } |
| | exampleWidget.value = example; |
| | }; |
| |
|
| | const exampleCb = exampleList.callback; |
| | exampleList.callback = function () { |
| | getExample(); |
| | return exampleCb?.apply(this, arguments) ?? exampleList.value; |
| | }; |
| |
|
| | const listExamples = async () => { |
| | exampleList.disabled = true; |
| | exampleList.options.values = ["[none]"]; |
| | exampleList.value = "[none]"; |
| | let examples = []; |
| | if (this.widgets[0].value?.content) { |
| | try { |
| | examples = await (await get("examples", `/${this.widgets[0].value.content}`)).json(); |
| | } catch (error) {} |
| | } |
| | exampleList.options.values = ["[none]", ...examples]; |
| | exampleList.callback(); |
| | exampleList.disabled = !examples.length; |
| | app.graph.setDirtyCanvas(true, true); |
| | }; |
| |
|
| | const modelWidget = this.widgets[0]; |
| | const modelCb = modelWidget.callback; |
| | let prev = undefined; |
| | 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; |
| | } |
| | return ret; |
| | }; |
| | setTimeout(() => { |
| | modelWidget.callback(); |
| | }, 30); |
| | }; |
| | } |
| |
|
| | const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; |
| | nodeType.prototype.getExtraMenuOptions = function (_, options) { |
| | if (this.imgs) { |
| | |
| | let img; |
| | if (this.imageIndex != null) { |
| | |
| | img = this.imgs[this.imageIndex]; |
| | } else if (this.overIndex != null) { |
| | |
| | img = this.imgs[this.overIndex]; |
| | } |
| | if (img) { |
| | const nodes = app.graph._nodes.filter( |
| | (n) => n.comfyClass === LORA_LOADER || n.comfyClass === CHECKPOINT_LOADER |
| | ); |
| | if (nodes.length) { |
| | options.unshift({ |
| | content: "Save as Preview", |
| | submenu: { |
| | options: nodes.map((n) => ({ |
| | content: n.widgets[0].value.content, |
| | 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(); |
| | }, |
| | })), |
| | }, |
| | }); |
| | } |
| | } |
| | } |
| | return getExtraMenuOptions?.apply(this, arguments); |
| | }; |
| | }, |
| | }); |
| |
|