| import { app } from "../../../scripts/app.js"; |
| import { api } from "../../../scripts/api.js"; |
| import { $el } from "../../../scripts/ui.js"; |
|
|
| |
| |
| |
|
|
| const style = ` |
| #comfy-save-button, #comfy-load-button { |
| position: relative; |
| overflow: hidden; |
| } |
| .pysssss-workflow-arrow { |
| position: absolute; |
| top: 0; |
| bottom: 0; |
| right: 0; |
| font-size: 12px; |
| display: flex; |
| align-items: center; |
| width: 24px; |
| justify-content: center; |
| background: rgba(255,255,255,0.1); |
| } |
| .pysssss-workflow-arrow:after { |
| content: "▼"; |
| } |
| .pysssss-workflow-arrow:hover { |
| filter: brightness(1.6); |
| background-color: var(--comfy-menu-bg); |
| } |
| .pysssss-workflow-load .litemenu-entry:not(.has_submenu):before, |
| .pysssss-workflow-load ~ .litecontextmenu .litemenu-entry:not(.has_submenu):before { |
| content: "🎛️"; |
| padding-right: 5px; |
| } |
| .pysssss-workflow-load .litemenu-entry.has_submenu:before, |
| .pysssss-workflow-load ~ .litecontextmenu .litemenu-entry.has_submenu:before { |
| content: "📂"; |
| padding-right: 5px; |
| position: relative; |
| top: -1px; |
| } |
| .pysssss-workflow-popup ~ .litecontextmenu { |
| transform: scale(1.3); |
| } |
| `; |
|
|
| async function getWorkflows() { |
| const response = await api.fetchApi("/pysssss/workflows", { cache: "no-store" }); |
| return await response.json(); |
| } |
|
|
| async function getWorkflow(name) { |
| const response = await api.fetchApi(`/pysssss/workflows/${encodeURIComponent(name)}`, { cache: "no-store" }); |
| return await response.json(); |
| } |
|
|
| async function saveWorkflow(name, workflow, overwrite) { |
| try { |
| const response = await api.fetchApi("/pysssss/workflows", { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify({ name, workflow, overwrite }), |
| }); |
| if (response.status === 201) { |
| return true; |
| } |
| if (response.status === 409) { |
| return false; |
| } |
| throw new Error(response.statusText); |
| } catch (error) { |
| console.error(error); |
| } |
| } |
|
|
| class PysssssWorkflows { |
| async load() { |
| this.workflows = await getWorkflows(); |
| if(this.workflows.length) { |
| this.workflows.sort(); |
| } |
| this.loadMenu.style.display = this.workflows.length ? "flex" : "none"; |
| } |
|
|
| getMenuOptions(callback) { |
| const menu = []; |
| const directories = new Map(); |
| for (const workflow of this.workflows || []) { |
| const path = workflow.split("/"); |
| let parent = menu; |
| let currentPath = ""; |
| for (let i = 0; i < path.length - 1; i++) { |
| currentPath += "/" + path[i]; |
| let newParent = directories.get(currentPath); |
| if (!newParent) { |
| newParent = { |
| title: path[i], |
| has_submenu: true, |
| submenu: { |
| options: [], |
| }, |
| }; |
| parent.push(newParent); |
| newParent = newParent.submenu.options; |
| directories.set(currentPath, newParent); |
| } |
| parent = newParent; |
| } |
| parent.push({ |
| title: path[path.length - 1], |
| callback: () => callback(workflow), |
| }); |
| } |
| return menu; |
| } |
|
|
| constructor() { |
| function addWorkflowMenu(type, getOptions) { |
| return $el("div.pysssss-workflow-arrow", { |
| parent: document.getElementById(`comfy-${type}-button`), |
| onclick: (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
|
|
| LiteGraph.closeAllContextMenus(); |
| const menu = new LiteGraph.ContextMenu( |
| getOptions(), |
| { |
| event: e, |
| scale: 1.3, |
| }, |
| window |
| ); |
| menu.root.classList.add("pysssss-workflow-popup"); |
| menu.root.classList.add(`pysssss-workflow-${type}`); |
| }, |
| }); |
| } |
|
|
| this.loadMenu = addWorkflowMenu("load", () => |
| this.getMenuOptions(async (workflow) => { |
| const json = await getWorkflow(workflow); |
| app.loadGraphData(json); |
| }) |
| ); |
| addWorkflowMenu("save", () => { |
| return [ |
| { |
| title: "Save as", |
| callback: () => { |
| let filename = prompt("Enter filename", this.workflowName || "workflow"); |
| if (filename) { |
| if (!filename.toLowerCase().endsWith(".json")) { |
| filename += ".json"; |
| } |
|
|
| this.workflowName = filename; |
|
|
| const json = JSON.stringify(app.graph.serialize(), null, 2); |
| const blob = new Blob([json], { type: "application/json" }); |
| const url = URL.createObjectURL(blob); |
| const a = $el("a", { |
| href: url, |
| download: filename, |
| style: { display: "none" }, |
| parent: document.body, |
| }); |
| a.click(); |
| setTimeout(function () { |
| a.remove(); |
| window.URL.revokeObjectURL(url); |
| }, 0); |
| } |
| }, |
| }, |
| { |
| title: "Save to workflows", |
| callback: async () => { |
| const name = prompt("Enter filename", this.workflowName || "workflow"); |
| if (name) { |
| this.workflowName = name; |
|
|
| const data = app.graph.serialize(); |
| if (!(await saveWorkflow(name, data))) { |
| if (confirm("A workspace with this name already exists, do you want to overwrite it?")) { |
| await saveWorkflow(name, app.graph.serialize(), true); |
| } else { |
| return; |
| } |
| } |
| await this.load(); |
| } |
| }, |
| }, |
| ]; |
| }); |
| this.load(); |
|
|
| const handleFile = app.handleFile; |
| const self = this; |
| app.handleFile = function (file) { |
| if (file?.name?.endsWith(".json")) { |
| self.workflowName = file.name; |
| } else { |
| self.workflowName = null; |
| } |
| return handleFile.apply(this, arguments); |
| }; |
| } |
| } |
|
|
| const refreshComboInNodes = app.refreshComboInNodes; |
| let workflows; |
|
|
| async function sendToWorkflow(img, workflow) { |
| const graph = !workflow ? app.graph.serialize() : await getWorkflow(workflow); |
| const nodes = graph.nodes.filter((n) => n.type === "LoadImage"); |
| let targetNode; |
| if (nodes.length === 0) { |
| alert("To send the image to another workflow, that workflow must have a LoadImage node."); |
| return; |
| } else if (nodes.length > 1) { |
| targetNode = nodes.find((n) => n.title?.toLowerCase().includes("input")); |
| if (!targetNode) { |
| targetNode = nodes[0]; |
| alert( |
| "The target workflow has multiple LoadImage nodes, include 'input' in the name of the one you want to use. The first one will be used here." |
| ); |
| } |
| } else { |
| targetNode = nodes[0]; |
| } |
|
|
| const blob = await (await fetch(img.src)).blob(); |
| const name = |
| (workflow || "sendtoworkflow").replace(/\//g, "_") + |
| "-" + |
| +new Date() + |
| new URLSearchParams(img.src.split("?")[1]).get("filename"); |
| const body = new FormData(); |
| body.append("image", new File([blob], name)); |
|
|
| const resp = await api.fetchApi("/upload/image", { |
| method: "POST", |
| body, |
| }); |
|
|
| if (resp.status === 200) { |
| await refreshComboInNodes.call(app); |
| targetNode.widgets_values[0] = name; |
| app.loadGraphData(graph); |
| app.graph.getNodeById(targetNode.id); |
| } else { |
| alert(resp.status + " - " + resp.statusText); |
| } |
| } |
|
|
| app.registerExtension({ |
| name: "pysssss.Workflows", |
| init() { |
| $el("style", { |
| textContent: style, |
| parent: document.head, |
| }); |
| }, |
| async setup() { |
| workflows = new PysssssWorkflows(); |
| app.refreshComboInNodes = function () { |
| workflows.load(); |
| refreshComboInNodes.apply(this, arguments); |
| }; |
|
|
| const comfyDefault = "[ComfyUI Default]"; |
| const defaultWorkflow = app.ui.settings.addSetting({ |
| id: "pysssss.Workflows.Default", |
| name: "🐍 Default Workflow", |
| defaultValue: comfyDefault, |
| type: "combo", |
| options: (value) => |
| [comfyDefault, ...workflows.workflows].map((m) => ({ |
| value: m, |
| text: m, |
| selected: m === value, |
| })), |
| }); |
|
|
| document.getElementById("comfy-load-default-button").onclick = async function () { |
| if ( |
| localStorage["Comfy.Settings.Comfy.ConfirmClear"] === "false" || |
| confirm(`Load default workflow (${defaultWorkflow.value})?`) |
| ) { |
| if (defaultWorkflow.value === comfyDefault) { |
| app.loadGraphData(); |
| } else { |
| const json = await getWorkflow(defaultWorkflow.value); |
| app.loadGraphData(json); |
| } |
| } |
| }; |
| }, |
| async beforeRegisterNodeDef(nodeType, nodeData, app) { |
| const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; |
| nodeType.prototype.getExtraMenuOptions = function (_, options) { |
| const r = getExtraMenuOptions?.apply?.(this, arguments); |
| let img; |
| if (this.imageIndex != null) { |
| |
| img = this.imgs[this.imageIndex]; |
| } else if (this.overIndex != null) { |
| |
| img = this.imgs[this.overIndex]; |
| } |
|
|
| if (img) { |
| let pos = options.findIndex((o) => o.content === "Save Image"); |
| if (pos === -1) { |
| pos = 0; |
| } else { |
| pos++; |
| } |
|
|
| options.splice(pos, 0, { |
| content: "Send to workflow", |
| has_submenu: true, |
| submenu: { |
| options: [ |
| { callback: () => sendToWorkflow(img), title: "[Current workflow]" }, |
| ...workflows.getMenuOptions(sendToWorkflow.bind(null, img)), |
| ], |
| }, |
| }); |
| } |
|
|
| return r; |
| }; |
| }, |
| }); |
|
|