764 lines
21 KiB
JavaScript
764 lines
21 KiB
JavaScript
|
// @ts-check
|
||
|
|
||
|
import { ComfyButton } from "../components/button.js";
|
||
|
import { prop, getStorageValue, setStorageValue } from "../../utils.js";
|
||
|
import { $el } from "../../ui.js";
|
||
|
import { api } from "../../api.js";
|
||
|
import { ComfyPopup } from "../components/popup.js";
|
||
|
import { createSpinner } from "../spinner.js";
|
||
|
import { ComfyWorkflow, trimJsonExt } from "../../workflows.js";
|
||
|
import { ComfyAsyncDialog } from "../components/asyncDialog.js";
|
||
|
|
||
|
export class ComfyWorkflowsMenu {
|
||
|
#first = true;
|
||
|
element = $el("div.comfyui-workflows");
|
||
|
|
||
|
get open() {
|
||
|
return this.popup.open;
|
||
|
}
|
||
|
|
||
|
set open(open) {
|
||
|
this.popup.open = open;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {import("../../app.js").ComfyApp} app
|
||
|
*/
|
||
|
constructor(app) {
|
||
|
this.app = app;
|
||
|
this.#bindEvents();
|
||
|
|
||
|
const classList = {
|
||
|
"comfyui-workflows-button": true,
|
||
|
"comfyui-button": true,
|
||
|
unsaved: getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true",
|
||
|
running: false,
|
||
|
};
|
||
|
this.buttonProgress = $el("div.comfyui-workflows-button-progress");
|
||
|
this.workflowLabel = $el("span.comfyui-workflows-label", "");
|
||
|
this.button = new ComfyButton({
|
||
|
content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]),
|
||
|
icon: "chevron-down",
|
||
|
classList,
|
||
|
});
|
||
|
|
||
|
this.element.append(this.button.element);
|
||
|
|
||
|
this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" });
|
||
|
this.content = new ComfyWorkflowsContent(app, this.popup);
|
||
|
this.popup.children = [this.content.element];
|
||
|
this.popup.addEventListener("change", () => {
|
||
|
this.button.icon = "chevron-" + (this.popup.open ? "up" : "down");
|
||
|
});
|
||
|
this.button.withPopup(this.popup);
|
||
|
|
||
|
this.unsaved = prop(this, "unsaved", classList.unsaved, (v) => {
|
||
|
classList.unsaved = v;
|
||
|
this.button.classList = classList;
|
||
|
setStorageValue("Comfy.PreviousWorkflowUnsaved", v);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
#updateProgress = () => {
|
||
|
const prompt = this.app.workflowManager.activePrompt;
|
||
|
let percent = 0;
|
||
|
if (this.app.workflowManager.activeWorkflow === prompt?.workflow) {
|
||
|
const total = Object.values(prompt.nodes);
|
||
|
const done = total.filter(Boolean);
|
||
|
percent = (done.length / total.length) * 100;
|
||
|
}
|
||
|
this.buttonProgress.style.width = percent + "%";
|
||
|
};
|
||
|
|
||
|
#updateActive = () => {
|
||
|
const active = this.app.workflowManager.activeWorkflow;
|
||
|
this.button.tooltip = active.path;
|
||
|
this.workflowLabel.textContent = active.name;
|
||
|
this.unsaved = active.unsaved;
|
||
|
|
||
|
if (this.#first) {
|
||
|
this.#first = false;
|
||
|
this.content.load();
|
||
|
}
|
||
|
|
||
|
this.#updateProgress();
|
||
|
};
|
||
|
|
||
|
#bindEvents() {
|
||
|
this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive);
|
||
|
this.app.workflowManager.addEventListener("rename", this.#updateActive);
|
||
|
this.app.workflowManager.addEventListener("delete", this.#updateActive);
|
||
|
|
||
|
this.app.workflowManager.addEventListener("save", () => {
|
||
|
this.unsaved = this.app.workflowManager.activeWorkflow.unsaved;
|
||
|
});
|
||
|
|
||
|
this.app.workflowManager.addEventListener("execute", (e) => {
|
||
|
this.#updateProgress();
|
||
|
});
|
||
|
|
||
|
api.addEventListener("graphChanged", () => {
|
||
|
this.unsaved = true;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
#getMenuOptions(callback) {
|
||
|
const menu = [];
|
||
|
const directories = new Map();
|
||
|
for (const workflow of this.app.workflowManager.workflows || []) {
|
||
|
const path = workflow.pathParts;
|
||
|
if (!path) continue;
|
||
|
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: trimJsonExt(path[path.length - 1]),
|
||
|
callback: () => callback(workflow),
|
||
|
});
|
||
|
}
|
||
|
return menu;
|
||
|
}
|
||
|
|
||
|
#getFavoriteMenuOptions(callback) {
|
||
|
const menu = [];
|
||
|
for (const workflow of this.app.workflowManager.workflows || []) {
|
||
|
if (workflow.isFavorite) {
|
||
|
menu.push({
|
||
|
title: "⭐ " + workflow.name,
|
||
|
callback: () => callback(workflow),
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
return menu;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {import("../../app.js").ComfyApp} app
|
||
|
*/
|
||
|
registerExtension(app) {
|
||
|
const self = this;
|
||
|
app.registerExtension({
|
||
|
name: "Comfy.Workflows",
|
||
|
async beforeRegisterNodeDef(nodeType) {
|
||
|
function getImageWidget(node) {
|
||
|
const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional };
|
||
|
for (const input in inputs) {
|
||
|
if (inputs[input][0] === "IMAGEUPLOAD") {
|
||
|
const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image"));
|
||
|
if (imageWidget) return imageWidget;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function setWidgetImage(node, widget, img) {
|
||
|
const url = new URL(img.src);
|
||
|
const filename = url.searchParams.get("filename");
|
||
|
const subfolder = url.searchParams.get("subfolder");
|
||
|
const type = url.searchParams.get("type");
|
||
|
const imageId = `${subfolder ? subfolder + "/" : ""}${filename} [${type}]`;
|
||
|
widget.value = imageId;
|
||
|
node.imgs = [img];
|
||
|
app.graph.setDirtyCanvas(true, true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {HTMLImageElement} img
|
||
|
* @param {ComfyWorkflow} workflow
|
||
|
*/
|
||
|
async function sendToWorkflow(img, workflow) {
|
||
|
await workflow.load();
|
||
|
let options = [];
|
||
|
const nodes = app.graph.computeExecutionOrder(false);
|
||
|
for (const node of nodes) {
|
||
|
const widget = getImageWidget(node);
|
||
|
if (widget == null) continue;
|
||
|
|
||
|
if (node.title?.toLowerCase().includes("input")) {
|
||
|
options = [{ widget, node }];
|
||
|
break;
|
||
|
} else {
|
||
|
options.push({ widget, node });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!options.length) {
|
||
|
alert("No image nodes have been found in this workflow!");
|
||
|
return;
|
||
|
} else if (options.length > 1) {
|
||
|
const dialog = new WidgetSelectionDialog(options);
|
||
|
const res = await dialog.show(app);
|
||
|
if (!res) return;
|
||
|
options = [res];
|
||
|
}
|
||
|
|
||
|
setWidgetImage(options[0].node, options[0].widget, img);
|
||
|
}
|
||
|
|
||
|
const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"];
|
||
|
nodeType.prototype["getExtraMenuOptions"] = function (_, options) {
|
||
|
const r = getExtraMenuOptions?.apply?.(this, arguments);
|
||
|
|
||
|
if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) {
|
||
|
const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this);
|
||
|
let img;
|
||
|
if (t.imageIndex != null) {
|
||
|
// An image is selected so select that
|
||
|
img = t.imgs?.[t.imageIndex];
|
||
|
} else if (t.overIndex != null) {
|
||
|
// No image is selected but one is hovered
|
||
|
img = t.img?.s[t.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, app.workflowManager.activeWorkflow),
|
||
|
title: "[Current workflow]",
|
||
|
},
|
||
|
...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)),
|
||
|
null,
|
||
|
...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
|
||
|
],
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return r;
|
||
|
};
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class ComfyWorkflowsContent {
|
||
|
element = $el("div.comfyui-workflows-panel");
|
||
|
treeState = {};
|
||
|
treeFiles = {};
|
||
|
/** @type { Map<ComfyWorkflow, WorkflowElement> } */
|
||
|
openFiles = new Map();
|
||
|
/** @type {WorkflowElement} */
|
||
|
activeElement = null;
|
||
|
|
||
|
/**
|
||
|
* @param {import("../../app.js").ComfyApp} app
|
||
|
* @param {ComfyPopup} popup
|
||
|
*/
|
||
|
constructor(app, popup) {
|
||
|
this.app = app;
|
||
|
this.popup = popup;
|
||
|
this.actions = $el("div.comfyui-workflows-actions", [
|
||
|
new ComfyButton({
|
||
|
content: "Default",
|
||
|
icon: "file-code",
|
||
|
iconSize: 18,
|
||
|
classList: "comfyui-button primary",
|
||
|
tooltip: "Load default workflow",
|
||
|
action: () => {
|
||
|
popup.open = false;
|
||
|
app.loadGraphData();
|
||
|
app.resetView();
|
||
|
},
|
||
|
}).element,
|
||
|
new ComfyButton({
|
||
|
content: "Browse",
|
||
|
icon: "folder",
|
||
|
iconSize: 18,
|
||
|
tooltip: "Browse for an image or exported workflow",
|
||
|
action: () => {
|
||
|
popup.open = false;
|
||
|
app.ui.loadFile();
|
||
|
},
|
||
|
}).element,
|
||
|
new ComfyButton({
|
||
|
content: "Blank",
|
||
|
icon: "plus-thick",
|
||
|
iconSize: 18,
|
||
|
tooltip: "Create a new blank workflow",
|
||
|
action: () => {
|
||
|
app.workflowManager.setWorkflow(null);
|
||
|
app.clean();
|
||
|
app.graph.clear();
|
||
|
app.workflowManager.activeWorkflow.track();
|
||
|
popup.open = false;
|
||
|
},
|
||
|
}).element,
|
||
|
]);
|
||
|
|
||
|
this.spinner = createSpinner();
|
||
|
this.element.replaceChildren(this.actions, this.spinner);
|
||
|
|
||
|
this.popup.addEventListener("open", () => this.load());
|
||
|
this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner));
|
||
|
|
||
|
this.app.workflowManager.addEventListener("favorite", (e) => {
|
||
|
const workflow = e["detail"];
|
||
|
const button = this.treeFiles[workflow.path]?.primary;
|
||
|
if (!button) return; // Can happen when a workflow is renamed
|
||
|
button.icon = this.#getFavoriteIcon(workflow);
|
||
|
button.overIcon = this.#getFavoriteOverIcon(workflow);
|
||
|
this.updateFavorites();
|
||
|
});
|
||
|
|
||
|
for (const e of ["save", "open", "close", "changeWorkflow"]) {
|
||
|
// TODO: dont be lazy and just update the specific element
|
||
|
app.workflowManager.addEventListener(e, () => this.updateOpen());
|
||
|
}
|
||
|
this.app.workflowManager.addEventListener("rename", () => this.load());
|
||
|
this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive());
|
||
|
}
|
||
|
|
||
|
async load() {
|
||
|
await this.app.workflowManager.loadWorkflows();
|
||
|
this.updateTree();
|
||
|
this.updateFavorites();
|
||
|
this.updateOpen();
|
||
|
this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement);
|
||
|
}
|
||
|
|
||
|
updateOpen() {
|
||
|
const current = this.openElement;
|
||
|
this.openFiles.clear();
|
||
|
|
||
|
this.openElement = $el("div.comfyui-workflows-open", [
|
||
|
$el("h3", "Open"),
|
||
|
...this.app.workflowManager.openWorkflows.map((w) => {
|
||
|
const wrapper = new WorkflowElement(this, w, {
|
||
|
primary: { element: $el("i.mdi.mdi-18px.mdi-progress-pencil") },
|
||
|
buttons: [
|
||
|
this.#getRenameButton(w),
|
||
|
new ComfyButton({
|
||
|
icon: "close",
|
||
|
iconSize: 18,
|
||
|
classList: "comfyui-button comfyui-workflows-file-action",
|
||
|
tooltip: "Close workflow",
|
||
|
action: (e) => {
|
||
|
e.stopImmediatePropagation();
|
||
|
this.app.workflowManager.closeWorkflow(w);
|
||
|
},
|
||
|
}),
|
||
|
],
|
||
|
});
|
||
|
if (w.unsaved) {
|
||
|
wrapper.element.classList.add("unsaved");
|
||
|
}
|
||
|
if(w === this.app.workflowManager.activeWorkflow) {
|
||
|
wrapper.element.classList.add("active");
|
||
|
}
|
||
|
|
||
|
this.openFiles.set(w, wrapper);
|
||
|
return wrapper.element;
|
||
|
}),
|
||
|
]);
|
||
|
|
||
|
this.#updateActive();
|
||
|
current?.replaceWith(this.openElement);
|
||
|
}
|
||
|
|
||
|
updateFavorites() {
|
||
|
const current = this.favoritesElement;
|
||
|
const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)];
|
||
|
|
||
|
this.favoritesElement = $el("div.comfyui-workflows-favorites", [
|
||
|
$el("h3", "Favorites"),
|
||
|
...favorites
|
||
|
.map((w) => {
|
||
|
return this.#getWorkflowElement(w).element;
|
||
|
})
|
||
|
.filter(Boolean),
|
||
|
]);
|
||
|
|
||
|
current?.replaceWith(this.favoritesElement);
|
||
|
}
|
||
|
|
||
|
filterTree() {
|
||
|
if (!this.filterText) {
|
||
|
this.treeRoot.classList.remove("filtered");
|
||
|
// Unfilter whole tree
|
||
|
for (const item of Object.values(this.treeFiles)) {
|
||
|
item.element.parentElement.style.removeProperty("display");
|
||
|
this.showTreeParents(item.element.parentElement);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
this.treeRoot.classList.add("filtered");
|
||
|
const searchTerms = this.filterText.toLocaleLowerCase().split(" ");
|
||
|
for (const item of Object.values(this.treeFiles)) {
|
||
|
const parts = item.workflow.pathParts;
|
||
|
let termIndex = 0;
|
||
|
let valid = false;
|
||
|
for (const part of parts) {
|
||
|
let currentIndex = 0;
|
||
|
do {
|
||
|
currentIndex = part.indexOf(searchTerms[termIndex], currentIndex);
|
||
|
if (currentIndex > -1) currentIndex += searchTerms[termIndex].length;
|
||
|
} while (currentIndex !== -1 && ++termIndex < searchTerms.length);
|
||
|
|
||
|
if (termIndex >= searchTerms.length) {
|
||
|
valid = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (valid) {
|
||
|
item.element.parentElement.style.removeProperty("display");
|
||
|
this.showTreeParents(item.element.parentElement);
|
||
|
} else {
|
||
|
item.element.parentElement.style.display = "none";
|
||
|
this.hideTreeParents(item.element.parentElement);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
hideTreeParents(element) {
|
||
|
// Hide all parents if no children are visible
|
||
|
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
|
||
|
for (let i = 1; i < element.parentElement.children.length; i++) {
|
||
|
const c = element.parentElement.children[i];
|
||
|
if (c.style.display !== "none") {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
element.parentElement.style.display = "none";
|
||
|
this.hideTreeParents(element.parentElement);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
showTreeParents(element) {
|
||
|
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
|
||
|
element.parentElement.style.removeProperty("display");
|
||
|
this.showTreeParents(element.parentElement);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
updateTree() {
|
||
|
const current = this.treeElement;
|
||
|
const nodes = {};
|
||
|
let typingTimeout;
|
||
|
|
||
|
this.treeFiles = {};
|
||
|
this.treeRoot = $el("ul.comfyui-workflows-tree");
|
||
|
this.treeElement = $el("section", [
|
||
|
$el("header", [
|
||
|
$el("h3", "Browse"),
|
||
|
$el("div.comfy-ui-workflows-search", [
|
||
|
$el("i.mdi.mdi-18px.mdi-magnify"),
|
||
|
$el("input", {
|
||
|
placeholder: "Search",
|
||
|
value: this.filterText ?? "",
|
||
|
oninput: (e) => {
|
||
|
this.filterText = e.target["value"]?.trim();
|
||
|
clearTimeout(typingTimeout);
|
||
|
typingTimeout = setTimeout(() => this.filterTree(), 250);
|
||
|
},
|
||
|
}),
|
||
|
]),
|
||
|
]),
|
||
|
this.treeRoot,
|
||
|
]);
|
||
|
|
||
|
for (const workflow of this.app.workflowManager.workflows) {
|
||
|
if (!workflow.pathParts) continue;
|
||
|
|
||
|
let currentPath = "";
|
||
|
let currentRoot = this.treeRoot;
|
||
|
|
||
|
for (let i = 0; i < workflow.pathParts.length; i++) {
|
||
|
currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i];
|
||
|
const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot);
|
||
|
|
||
|
nodes[currentPath] = parentNode;
|
||
|
currentRoot = parentNode;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
current?.replaceWith(this.treeElement);
|
||
|
this.filterTree();
|
||
|
}
|
||
|
|
||
|
#expandNode(el, workflow, thisPath, i) {
|
||
|
const expanded = !el.classList.toggle("closed");
|
||
|
if (expanded) {
|
||
|
let c = "";
|
||
|
for (let j = 0; j <= i; j++) {
|
||
|
c += (c ? "\\" : "") + workflow.pathParts[j];
|
||
|
this.treeState[c] = true;
|
||
|
}
|
||
|
} else {
|
||
|
let c = thisPath;
|
||
|
for (let j = i + 1; j < workflow.pathParts.length; j++) {
|
||
|
c += (c ? "\\" : "") + workflow.pathParts[j];
|
||
|
delete this.treeState[c];
|
||
|
}
|
||
|
delete this.treeState[thisPath];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#updateActive() {
|
||
|
this.#removeActive();
|
||
|
|
||
|
const active = this.app.workflowManager.activePrompt;
|
||
|
if (!active?.workflow) return;
|
||
|
|
||
|
const open = this.openFiles.get(active.workflow);
|
||
|
if (!open) return;
|
||
|
|
||
|
this.activeElement = open;
|
||
|
|
||
|
const total = Object.values(active.nodes);
|
||
|
const done = total.filter(Boolean);
|
||
|
const percent = done.length / total.length;
|
||
|
open.element.classList.add("running");
|
||
|
open.element.style.setProperty("--progress", percent * 100 + "%");
|
||
|
open.primary.element.classList.remove("mdi-progress-pencil");
|
||
|
open.primary.element.classList.add("mdi-play");
|
||
|
}
|
||
|
|
||
|
#removeActive() {
|
||
|
if (!this.activeElement) return;
|
||
|
this.activeElement.element.classList.remove("running");
|
||
|
this.activeElement.element.style.removeProperty("--progress");
|
||
|
this.activeElement.primary.element.classList.add("mdi-progress-pencil");
|
||
|
this.activeElement.primary.element.classList.remove("mdi-play");
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getFavoriteIcon(workflow) {
|
||
|
return workflow.isFavorite ? "star" : "file-outline";
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getFavoriteOverIcon(workflow) {
|
||
|
return workflow.isFavorite ? "star-off" : "star-outline";
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getFavoriteTooltip(workflow) {
|
||
|
return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites";
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getFavoriteButton(workflow, primary) {
|
||
|
return new ComfyButton({
|
||
|
icon: this.#getFavoriteIcon(workflow),
|
||
|
overIcon: this.#getFavoriteOverIcon(workflow),
|
||
|
iconSize: 18,
|
||
|
classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""),
|
||
|
tooltip: this.#getFavoriteTooltip(workflow),
|
||
|
action: (e) => {
|
||
|
e.stopImmediatePropagation();
|
||
|
workflow.favorite(!workflow.isFavorite);
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getDeleteButton(workflow) {
|
||
|
const deleteButton = new ComfyButton({
|
||
|
icon: "delete",
|
||
|
tooltip: "Delete this workflow",
|
||
|
classList: "comfyui-button comfyui-workflows-file-action",
|
||
|
iconSize: 18,
|
||
|
action: async (e, btn) => {
|
||
|
e.stopImmediatePropagation();
|
||
|
|
||
|
if (btn.icon === "delete-empty") {
|
||
|
btn.enabled = false;
|
||
|
await workflow.delete();
|
||
|
await this.load();
|
||
|
} else {
|
||
|
btn.icon = "delete-empty";
|
||
|
btn.element.style.background = "red";
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
deleteButton.element.addEventListener("mouseleave", () => {
|
||
|
deleteButton.icon = "delete";
|
||
|
deleteButton.element.style.removeProperty("background");
|
||
|
});
|
||
|
return deleteButton;
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getInsertButton(workflow) {
|
||
|
return new ComfyButton({
|
||
|
icon: "file-move-outline",
|
||
|
iconSize: 18,
|
||
|
tooltip: "Insert this workflow into the current workflow",
|
||
|
classList: "comfyui-button comfyui-workflows-file-action",
|
||
|
action: (e) => {
|
||
|
if (!this.app.shiftDown) {
|
||
|
this.popup.open = false;
|
||
|
}
|
||
|
e.stopImmediatePropagation();
|
||
|
if (!this.app.shiftDown) {
|
||
|
this.popup.open = false;
|
||
|
}
|
||
|
workflow.insert();
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getRenameButton(workflow) {
|
||
|
return new ComfyButton({
|
||
|
icon: "pencil",
|
||
|
tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.",
|
||
|
classList: "comfyui-button comfyui-workflows-file-action",
|
||
|
iconSize: 18,
|
||
|
enabled: !!workflow.path,
|
||
|
action: async (e) => {
|
||
|
e.stopImmediatePropagation();
|
||
|
const newName = prompt("Enter new name", workflow.path);
|
||
|
if (newName) {
|
||
|
await workflow.rename(newName);
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#getWorkflowElement(workflow) {
|
||
|
return new WorkflowElement(this, workflow, {
|
||
|
primary: this.#getFavoriteButton(workflow, true),
|
||
|
buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)],
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/** @param {ComfyWorkflow} workflow */
|
||
|
#createLeafNode(workflow) {
|
||
|
const fileNode = this.#getWorkflowElement(workflow);
|
||
|
this.treeFiles[workflow.path] = fileNode;
|
||
|
return fileNode;
|
||
|
}
|
||
|
|
||
|
#createNode(currentPath, workflow, i, currentRoot) {
|
||
|
const part = workflow.pathParts[i];
|
||
|
|
||
|
const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), {
|
||
|
$: (el) => {
|
||
|
el.onclick = (e) => {
|
||
|
this.#expandNode(el, workflow, currentPath, i);
|
||
|
e.stopImmediatePropagation();
|
||
|
};
|
||
|
},
|
||
|
});
|
||
|
currentRoot.append(parentNode);
|
||
|
|
||
|
// Create a node for the current part and an inner UL for its children if it isnt a leaf node
|
||
|
const leaf = i === workflow.pathParts.length - 1;
|
||
|
let nodeElement;
|
||
|
if (leaf) {
|
||
|
nodeElement = this.#createLeafNode(workflow).element;
|
||
|
} else {
|
||
|
nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]);
|
||
|
}
|
||
|
parentNode.append(nodeElement);
|
||
|
return parentNode;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class WorkflowElement {
|
||
|
/**
|
||
|
* @param { ComfyWorkflowsContent } parent
|
||
|
* @param { ComfyWorkflow } workflow
|
||
|
*/
|
||
|
constructor(parent, workflow, { tagName = "li", primary, buttons }) {
|
||
|
this.parent = parent;
|
||
|
this.workflow = workflow;
|
||
|
this.primary = primary;
|
||
|
this.buttons = buttons;
|
||
|
|
||
|
this.element = $el(
|
||
|
tagName + ".comfyui-workflows-tree-file",
|
||
|
{
|
||
|
onclick: () => {
|
||
|
workflow.load();
|
||
|
this.parent.popup.open = false;
|
||
|
},
|
||
|
title: this.workflow.path,
|
||
|
},
|
||
|
[this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)]
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class WidgetSelectionDialog extends ComfyAsyncDialog {
|
||
|
#options;
|
||
|
|
||
|
/**
|
||
|
* @param {Array<{widget: {name: string}, node: {pos: [number, number], title: string, id: string, type: string}}>} options
|
||
|
*/
|
||
|
constructor(options) {
|
||
|
super();
|
||
|
this.#options = options;
|
||
|
}
|
||
|
|
||
|
show(app) {
|
||
|
this.element.classList.add("comfy-widget-selection-dialog");
|
||
|
return super.show(
|
||
|
$el("div", [
|
||
|
$el("h2", "Select image target"),
|
||
|
$el(
|
||
|
"p",
|
||
|
"This workflow has multiple image loader nodes, you can rename a node to include 'input' in the title for it to be automatically selected, or select one below."
|
||
|
),
|
||
|
$el(
|
||
|
"section",
|
||
|
this.#options.map((opt) => {
|
||
|
return $el("div.comfy-widget-selection-item", [
|
||
|
$el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`),
|
||
|
$el(
|
||
|
"button.comfyui-button",
|
||
|
{
|
||
|
onclick: () => {
|
||
|
app.canvas.ds.offset[0] = -opt.node.pos[0] + 50;
|
||
|
app.canvas.ds.offset[1] = -opt.node.pos[1] + 50;
|
||
|
app.canvas.selectNode(opt.node);
|
||
|
app.graph.setDirtyCanvas(true, true);
|
||
|
},
|
||
|
},
|
||
|
"Show"
|
||
|
),
|
||
|
$el(
|
||
|
"button.comfyui-button.primary",
|
||
|
{
|
||
|
onclick: () => {
|
||
|
this.close(opt);
|
||
|
},
|
||
|
},
|
||
|
"Select"
|
||
|
),
|
||
|
]);
|
||
|
})
|
||
|
),
|
||
|
])
|
||
|
);
|
||
|
}
|
||
|
}
|