Spaces:
Running
on
Zero
Running
on
Zero
import { app } from "../../../scripts/app.js"; | |
import { api } from "../../../scripts/api.js"; | |
import { $el } from "../../../scripts/ui.js"; | |
// Adds workflow management | |
// Original implementation by https://github.com/i-h4x | |
// Thanks for permission to reimplement as an extension | |
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); // convert the data to a JSON string | |
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) { | |
// An image is selected so select that | |
img = this.imgs[this.imageIndex]; | |
} else if (this.overIndex != null) { | |
// No image is selected but one is hovered | |
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; | |
}; | |
}, | |
}); | |