Use new TS frontend uncompressed (#4379)

* Swap frontend uncompressed

* Add uncompressed files
This commit is contained in:
Chenlei Hu
2024-08-15 16:50:25 -04:00
committed by GitHub
parent 3f5939add6
commit bfc214f434
84 changed files with 99382 additions and 34288 deletions

View File

@@ -1,64 +1,2 @@
import { ComfyDialog } from "../dialog.js";
import { $el } from "../../ui.js";
export class ComfyAsyncDialog extends ComfyDialog {
#resolve;
constructor(actions) {
super(
"dialog.comfy-dialog.comfyui-dialog",
actions?.map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
return $el("button.comfyui-button", {
type: "button",
textContent: opt.text,
onclick: () => this.close(opt.value ?? opt.text),
});
})
);
}
show(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
showModal(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
this.element.showModal();
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
close(result = null) {
this.#resolve(result);
this.element.close();
super.close();
}
static async prompt({ title = null, message, actions }) {
const dialog = new ComfyAsyncDialog(actions);
const content = [$el("span", message)];
if (title) {
content.unshift($el("h3", title));
}
const res = await dialog.showModal(content);
dialog.element.remove();
return res;
}
}
// Shim for scripts\ui\components\asyncDialog.ts
export const ComfyAsyncDialog = window.comfyAPI.asyncDialog.ComfyAsyncDialog;

View File

@@ -1,163 +1,2 @@
// @ts-check
import { $el } from "../../ui.js";
import { applyClasses, toggleElement } from "../utils.js";
import { prop } from "../../utils.js";
/**
* @typedef {{
* icon?: string;
* overIcon?: string;
* iconSize?: number;
* content?: string | HTMLElement;
* tooltip?: string;
* enabled?: boolean;
* action?: (e: Event, btn: ComfyButton) => void,
* classList?: import("../utils.js").ClassList,
* visibilitySetting?: { id: string, showValue: any },
* app?: import("../../app.js").ComfyApp
* }} ComfyButtonProps
*/
export class ComfyButton {
#over = 0;
#popupOpen = false;
isOver = false;
iconElement = $el("i.mdi");
contentElement = $el("span");
/**
* @type {import("./popup.js").ComfyPopup}
*/
popup;
/**
* @param {ComfyButtonProps} opts
*/
constructor({
icon,
overIcon,
iconSize,
content,
tooltip,
action,
classList = "comfyui-button",
visibilitySetting,
app,
enabled = true,
}) {
this.element = $el("button", {
onmouseenter: () => {
this.isOver = true;
if(this.overIcon) {
this.updateIcon();
}
},
onmouseleave: () => {
this.isOver = false;
if(this.overIcon) {
this.updateIcon();
}
}
}, [this.iconElement, this.contentElement]);
this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon }));
this.overIcon = prop(this, "overIcon", overIcon, () => {
if(this.isOver) {
this.updateIcon();
}
});
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
this.content = prop(
this,
"content",
content,
toggleElement(this.contentElement, {
onShow: (el, v) => {
if (typeof v === "string") {
el.textContent = v;
} else {
el.replaceChildren(v);
}
},
})
);
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
if (v) {
this.element.title = v;
} else {
this.element.removeAttribute("title");
}
});
this.classList = prop(this, "classList", classList, this.updateClasses);
this.hidden = prop(this, "hidden", false, this.updateClasses);
this.enabled = prop(this, "enabled", enabled, () => {
this.updateClasses();
this.element.disabled = !this.enabled;
});
this.action = prop(this, "action", action);
this.element.addEventListener("click", (e) => {
if (this.popup) {
// we are either a touch device or triggered by click not hover
if (!this.#over) {
this.popup.toggle();
}
}
this.action?.(e, this);
});
if (visibilitySetting?.id) {
const settingUpdated = () => {
this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue;
};
app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated);
settingUpdated();
}
}
updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
updateClasses = () => {
const internalClasses = [];
if (this.hidden) {
internalClasses.push("hidden");
}
if (!this.enabled) {
internalClasses.push("disabled");
}
if (this.popup) {
if (this.#popupOpen) {
internalClasses.push("popup-open");
} else {
internalClasses.push("popup-closed");
}
}
applyClasses(this.element, this.classList, ...internalClasses);
};
/**
*
* @param { import("./popup.js").ComfyPopup } popup
* @param { "click" | "hover" } mode
*/
withPopup(popup, mode = "click") {
this.popup = popup;
if (mode === "hover") {
for (const el of [this.element, this.popup.element]) {
el.addEventListener("mouseenter", () => {
this.popup.open = !!++this.#over;
});
el.addEventListener("mouseleave", () => {
this.popup.open = !!--this.#over;
});
}
}
popup.addEventListener("change", () => {
this.#popupOpen = popup.open;
this.updateClasses();
});
return this;
}
}
// Shim for scripts\ui\components\button.ts
export const ComfyButton = window.comfyAPI.button.ComfyButton;

View File

@@ -1,45 +1,2 @@
// @ts-check
import { $el } from "../../ui.js";
import { ComfyButton } from "./button.js";
import { prop } from "../../utils.js";
export class ComfyButtonGroup {
element = $el("div.comfyui-button-group");
/** @param {Array<ComfyButton | HTMLElement>} buttons */
constructor(...buttons) {
this.buttons = prop(this, "buttons", buttons, () => this.update());
}
/**
* @param {ComfyButton} button
* @param {number} index
*/
insert(button, index) {
this.buttons.splice(index, 0, button);
this.update();
}
/** @param {ComfyButton} button */
append(button) {
this.buttons.push(button);
this.update();
}
/** @param {ComfyButton|number} indexOrButton */
remove(indexOrButton) {
if (typeof indexOrButton !== "number") {
indexOrButton = this.buttons.indexOf(indexOrButton);
}
if (indexOrButton > -1) {
const r = this.buttons.splice(indexOrButton, 1);
this.update();
return r;
}
}
update() {
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
}
}
// Shim for scripts\ui\components\buttonGroup.ts
export const ComfyButtonGroup = window.comfyAPI.buttonGroup.ComfyButtonGroup;

View File

@@ -1,128 +1,2 @@
// @ts-check
import { prop } from "../../utils.js";
import { $el } from "../../ui.js";
import { applyClasses } from "../utils.js";
export class ComfyPopup extends EventTarget {
element = $el("div.comfyui-popup");
/**
* @param {{
* target: HTMLElement,
* container?: HTMLElement,
* classList?: import("../utils.js").ClassList,
* ignoreTarget?: boolean,
* closeOnEscape?: boolean,
* position?: "absolute" | "relative",
* horizontal?: "left" | "right"
* }} param0
* @param {...HTMLElement} children
*/
constructor(
{
target,
container = document.body,
classList = "",
ignoreTarget = true,
closeOnEscape = true,
position = "absolute",
horizontal = "left",
},
...children
) {
super();
this.target = target;
this.ignoreTarget = ignoreTarget;
this.container = container;
this.position = position;
this.closeOnEscape = closeOnEscape;
this.horizontal = horizontal;
container.append(this.element);
this.children = prop(this, "children", children, () => {
this.element.replaceChildren(...this.children);
this.update();
});
this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal));
this.open = prop(this, "open", false, (v, o) => {
if (v === o) return;
if (v) {
this.#show();
} else {
this.#hide();
}
});
}
toggle() {
this.open = !this.open;
}
#hide() {
this.element.classList.remove("open");
window.removeEventListener("resize", this.update);
window.removeEventListener("click", this.#clickHandler, { capture: true });
window.removeEventListener("keydown", this.#escHandler, { capture: true });
this.dispatchEvent(new CustomEvent("close"));
this.dispatchEvent(new CustomEvent("change"));
}
#show() {
this.element.classList.add("open");
this.update();
window.addEventListener("resize", this.update);
window.addEventListener("click", this.#clickHandler, { capture: true });
if (this.closeOnEscape) {
window.addEventListener("keydown", this.#escHandler, { capture: true });
}
this.dispatchEvent(new CustomEvent("open"));
this.dispatchEvent(new CustomEvent("change"));
}
#escHandler = (e) => {
if (e.key === "Escape") {
this.open = false;
e.preventDefault();
e.stopImmediatePropagation();
}
};
#clickHandler = (e) => {
/** @type {any} */
const target = e.target;
if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) {
this.open = false;
}
};
update = () => {
const rect = this.target.getBoundingClientRect();
this.element.style.setProperty("--bottom", "unset");
if (this.position === "absolute") {
if (this.horizontal === "left") {
this.element.style.setProperty("--left", rect.left + "px");
} else {
this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px");
}
this.element.style.setProperty("--top", rect.bottom + "px");
this.element.style.setProperty("--limit", rect.bottom + "px");
} else {
this.element.style.setProperty("--left", 0 + "px");
this.element.style.setProperty("--top", rect.height + "px");
this.element.style.setProperty("--limit", rect.height + "px");
}
const thisRect = this.element.getBoundingClientRect();
if (thisRect.height < 30) {
// Move up instead
this.element.style.setProperty("--top", "unset");
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
this.element.style.setProperty("--limit", rect.height + 5 + "px");
}
};
}
// Shim for scripts\ui\components\popup.ts
export const ComfyPopup = window.comfyAPI.popup.ComfyPopup;

View File

@@ -1,43 +1,2 @@
// @ts-check
import { $el } from "../../ui.js";
import { ComfyButton } from "./button.js";
import { prop } from "../../utils.js";
import { ComfyPopup } from "./popup.js";
export class ComfySplitButton {
/**
* @param {{
* primary: ComfyButton,
* mode?: "hover" | "click",
* horizontal?: "left" | "right",
* position?: "relative" | "absolute"
* }} param0
* @param {Array<ComfyButton> | Array<HTMLElement>} items
*/
constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) {
this.arrow = new ComfyButton({
icon: "chevron-down",
});
this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [
$el("div.comfyui-split-primary", primary.element),
$el("div.comfyui-split-arrow", this.arrow.element),
]);
this.popup = new ComfyPopup({
target: this.element,
container: position === "relative" ? this.element : document.body,
classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
closeOnEscape: mode === "click",
position,
horizontal,
});
this.arrow.withPopup(this.popup, mode);
this.items = prop(this, "items", items, () => this.update());
}
update() {
this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b));
}
}
// Shim for scripts\ui\components\splitButton.ts
export const ComfySplitButton = window.comfyAPI.splitButton.ComfySplitButton;

View File

@@ -1,38 +1,2 @@
import { $el } from "../ui.js";
export class ComfyDialog extends EventTarget {
#buttons;
constructor(type = "div", buttons = null) {
super();
this.#buttons = buttons;
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
]);
}
createButtons() {
return (
this.#buttons ?? [
$el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
}),
]
);
}
close() {
this.element.style.display = "none";
}
show(html) {
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
}
this.element.style.display = "flex";
}
}
// Shim for scripts\ui\dialog.ts
export const ComfyDialog = window.comfyAPI.dialog.ComfyDialog;

View File

@@ -1,287 +1,2 @@
// @ts-check
/*
Original implementation:
https://github.com/TahaSh/drag-to-reorder
MIT License
Copyright (c) 2023 Taha Shashtari
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { $el } from "../ui.js";
$el("style", {
parent: document.head,
textContent: `
.draggable-item {
position: relative;
will-change: transform;
user-select: none;
}
.draggable-item.is-idle {
transition: 0.25s ease transform;
}
.draggable-item.is-draggable {
z-index: 10;
}
`
});
export class DraggableList extends EventTarget {
listContainer;
draggableItem;
pointerStartX;
pointerStartY;
scrollYMax;
itemsGap = 0;
items = [];
itemSelector;
handleClass = "drag-handle";
off = [];
offDrag = [];
constructor(element, itemSelector) {
super();
this.listContainer = element;
this.itemSelector = itemSelector;
if (!this.listContainer) return;
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
this.off.push(this.on(document, "mouseup", this.dragEnd));
this.off.push(this.on(document, "touchend", this.dragEnd));
}
getAllItems() {
if (!this.items?.length) {
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
this.items.forEach((element) => {
element.classList.add("is-idle");
});
}
return this.items;
}
getIdleItems() {
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
}
isItemAbove(item) {
return item.hasAttribute("data-is-above");
}
isItemToggled(item) {
return item.hasAttribute("data-is-toggled");
}
on(source, event, listener, options) {
listener = listener.bind(this);
source.addEventListener(event, listener, options);
return () => source.removeEventListener(event, listener);
}
dragStart(e) {
if (e.target.classList.contains(this.handleClass)) {
this.draggableItem = e.target.closest(this.itemSelector);
}
if (!this.draggableItem) return;
this.pointerStartX = e.clientX || e.touches[0].clientX;
this.pointerStartY = e.clientY || e.touches[0].clientY;
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
this.setItemsGap();
this.initDraggableItem();
this.initItemsState();
this.offDrag.push(this.on(document, "mousemove", this.drag));
this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false }));
this.dispatchEvent(
new CustomEvent("dragstart", {
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
})
);
}
setItemsGap() {
if (this.getIdleItems().length <= 1) {
this.itemsGap = 0;
return;
}
const item1 = this.getIdleItems()[0];
const item2 = this.getIdleItems()[1];
const item1Rect = item1.getBoundingClientRect();
const item2Rect = item2.getBoundingClientRect();
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
}
initItemsState() {
this.getIdleItems().forEach((item, i) => {
if (this.getAllItems().indexOf(this.draggableItem) > i) {
item.dataset.isAbove = "";
}
});
}
initDraggableItem() {
this.draggableItem.classList.remove("is-idle");
this.draggableItem.classList.add("is-draggable");
}
drag(e) {
if (!this.draggableItem) return;
e.preventDefault();
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const listRect = this.listContainer.getBoundingClientRect();
if (clientY > listRect.bottom) {
if (this.listContainer.scrollTop < this.scrollYMax) {
this.listContainer.scrollBy(0, 10);
this.pointerStartY -= 10;
}
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
this.pointerStartY += 10;
this.listContainer.scrollBy(0, -10);
}
const pointerOffsetX = clientX - this.pointerStartX;
const pointerOffsetY = clientY - this.pointerStartY;
this.updateIdleItemsStateAndPosition();
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
}
updateIdleItemsStateAndPosition() {
const draggableItemRect = this.draggableItem.getBoundingClientRect();
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
// Update state
this.getIdleItems().forEach((item) => {
const itemRect = item.getBoundingClientRect();
const itemY = itemRect.top + itemRect.height / 2;
if (this.isItemAbove(item)) {
if (draggableItemY <= itemY) {
item.dataset.isToggled = "";
} else {
delete item.dataset.isToggled;
}
} else {
if (draggableItemY >= itemY) {
item.dataset.isToggled = "";
} else {
delete item.dataset.isToggled;
}
}
});
// Update position
this.getIdleItems().forEach((item) => {
if (this.isItemToggled(item)) {
const direction = this.isItemAbove(item) ? 1 : -1;
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
} else {
item.style.transform = "";
}
});
}
dragEnd() {
if (!this.draggableItem) return;
this.applyNewItemsOrder();
this.cleanup();
}
applyNewItemsOrder() {
const reorderedItems = [];
let oldPosition = -1;
this.getAllItems().forEach((item, index) => {
if (item === this.draggableItem) {
oldPosition = index;
return;
}
if (!this.isItemToggled(item)) {
reorderedItems[index] = item;
return;
}
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
reorderedItems[newIndex] = item;
});
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index];
if (typeof item === "undefined") {
reorderedItems[index] = this.draggableItem;
}
}
reorderedItems.forEach((item) => {
this.listContainer.appendChild(item);
});
this.items = reorderedItems;
this.dispatchEvent(
new CustomEvent("dragend", {
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
})
);
}
cleanup() {
this.itemsGap = 0;
this.items = [];
this.unsetDraggableItem();
this.unsetItemState();
this.offDrag.forEach((f) => f());
this.offDrag = [];
}
unsetDraggableItem() {
this.draggableItem.style = null;
this.draggableItem.classList.remove("is-draggable");
this.draggableItem.classList.add("is-idle");
this.draggableItem = null;
}
unsetItemState() {
this.getIdleItems().forEach((item, i) => {
delete item.dataset.isAbove;
delete item.dataset.isToggled;
item.style.transform = "";
});
}
dispose() {
this.off.forEach((f) => f());
}
}
// Shim for scripts\ui\draggableList.ts
export const DraggableList = window.comfyAPI.draggableList.DraggableList;

View File

@@ -1,97 +1,3 @@
import { $el } from "../ui.js";
export function calculateImageGrid(imgs, dw, dh) {
let best = 0;
let w = imgs[0].naturalWidth;
let h = imgs[0].naturalHeight;
const numImages = imgs.length;
let cellWidth, cellHeight, cols, rows, shiftX;
// compact style
for (let c = 1; c <= numImages; c++) {
const r = Math.ceil(numImages / c);
const cW = dw / c;
const cH = dh / r;
const scaleX = cW / w;
const scaleY = cH / h;
const scale = Math.min(scaleX, scaleY, 1);
const imageW = w * scale;
const imageH = h * scale;
const area = imageW * imageH * numImages;
if (area > best) {
best = area;
cellWidth = imageW;
cellHeight = imageH;
cols = c;
rows = r;
shiftX = c * ((cW - imageW) / 2);
}
}
return { cellWidth, cellHeight, cols, rows, shiftX };
}
export function createImageHost(node) {
const el = $el("div.comfy-img-preview");
let currentImgs;
let first = true;
function updateSize() {
let w = null;
let h = null;
if (currentImgs) {
let elH = el.clientHeight;
if (first) {
first = false;
// On first run, if we are small then grow a bit
if (elH < 190) {
elH = 190;
}
el.style.setProperty("--comfy-widget-min-height", elH);
} else {
el.style.setProperty("--comfy-widget-min-height", null);
}
const nw = node.size[0];
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
w += "px";
h += "px";
el.style.setProperty("--comfy-img-preview-width", w);
el.style.setProperty("--comfy-img-preview-height", h);
}
}
return {
el,
updateImages(imgs) {
if (imgs !== currentImgs) {
if (currentImgs == null) {
requestAnimationFrame(() => {
updateSize();
});
}
el.replaceChildren(...imgs);
currentImgs = imgs;
node.onResize(node.size);
node.graph.setDirtyCanvas(true, true);
}
},
getHeight() {
updateSize();
},
onDraw() {
// Element from point uses a hittest find elements so we need to toggle pointer events
el.style.pointerEvents = "all";
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
el.style.pointerEvents = "none";
if(!over) return;
// Set the overIndex so Open Image etc work
const idx = currentImgs.indexOf(over);
node.overIndex = idx;
},
};
}
// Shim for scripts\ui\imagePreview.ts
export const calculateImageGrid = window.comfyAPI.imagePreview.calculateImageGrid;
export const createImageHost = window.comfyAPI.imagePreview.createImageHost;

View File

@@ -1,302 +1,2 @@
// @ts-check
import { $el } from "../../ui.js";
import { downloadBlob } from "../../utils.js";
import { ComfyButton } from "../components/button.js";
import { ComfyButtonGroup } from "../components/buttonGroup.js";
import { ComfySplitButton } from "../components/splitButton.js";
import { ComfyViewHistoryButton } from "./viewHistory.js";
import { ComfyQueueButton } from "./queueButton.js";
import { ComfyWorkflowsMenu } from "./workflows.js";
import { ComfyViewQueueButton } from "./viewQueue.js";
import { getInteruptButton } from "./interruptButton.js";
const collapseOnMobile = (t) => {
(t.element ?? t).classList.add("comfyui-menu-mobile-collapse");
return t;
};
const showOnMobile = (t) => {
(t.element ?? t).classList.add("lt-lg-show");
return t;
};
export class ComfyAppMenu {
#sizeBreak = "lg";
#lastSizeBreaks = {
lg: null,
md: null,
sm: null,
xs: null,
};
#sizeBreaks = Object.keys(this.#lastSizeBreaks);
#cachedInnerSize = null;
#cacheTimeout = null;
/**
* @param { import("../../app.js").ComfyApp } app
*/
constructor(app) {
this.app = app;
this.workflows = new ComfyWorkflowsMenu(app);
const getSaveButton = (t) =>
new ComfyButton({
icon: "content-save",
tooltip: "Save the current workflow",
action: () => app.workflowManager.activeWorkflow.save(),
content: t,
});
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
this.saveButton = new ComfySplitButton(
{
primary: getSaveButton(),
mode: "hover",
position: "absolute",
},
getSaveButton("Save"),
new ComfyButton({
icon: "content-save-edit",
content: "Save As",
tooltip: "Save the current graph as a new workflow",
action: () => app.workflowManager.activeWorkflow.save(true),
}),
new ComfyButton({
icon: "download",
content: "Export",
tooltip: "Export the current workflow as JSON",
action: () => this.exportWorkflow("workflow", "workflow"),
}),
new ComfyButton({
icon: "api",
content: "Export (API Format)",
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
action: () => this.exportWorkflow("workflow_api", "output"),
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
app,
})
);
this.actionsGroup = new ComfyButtonGroup(
new ComfyButton({
icon: "refresh",
content: "Refresh",
tooltip: "Refresh widgets in nodes to find new models or files",
action: () => app.refreshComboInNodes(),
}),
new ComfyButton({
icon: "clipboard-edit-outline",
content: "Clipspace",
tooltip: "Open Clipspace window",
action: () => app["openClipspace"](),
}),
new ComfyButton({
icon: "fit-to-page-outline",
content: "Reset View",
tooltip: "Reset the canvas view",
action: () => app.resetView(),
}),
new ComfyButton({
icon: "cancel",
content: "Clear",
tooltip: "Clears current workflow",
action: () => {
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
app.clean();
app.graph.clear();
}
},
})
);
this.settingsGroup = new ComfyButtonGroup(
new ComfyButton({
icon: "cog",
content: "Settings",
tooltip: "Open settings",
action: () => {
app.ui.settings.show();
},
})
);
this.viewGroup = new ComfyButtonGroup(
new ComfyViewHistoryButton(app).element,
new ComfyViewQueueButton(app).element,
getInteruptButton("nlg-hide").element
);
this.mobileMenuButton = new ComfyButton({
icon: "menu",
action: (_, btn) => {
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
window.dispatchEvent(new Event("resize"));
},
classList: "comfyui-button comfyui-menu-button",
});
this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [
this.logo,
this.workflows.element,
this.saveButton.element,
collapseOnMobile(this.actionsGroup).element,
$el("section.comfyui-menu-push"),
collapseOnMobile(this.settingsGroup).element,
collapseOnMobile(this.viewGroup).element,
getInteruptButton("lt-lg-show").element,
new ComfyQueueButton(app).element,
showOnMobile(this.mobileMenuButton).element,
]);
let resizeHandler;
this.menuPositionSetting = app.ui.settings.addSetting({
id: "Comfy.UseNewMenu",
defaultValue: "Disabled",
name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.",
type: "combo",
options: ["Disabled", "Top", "Bottom"],
onChange: async (v) => {
if (v && v !== "Disabled") {
if (!resizeHandler) {
resizeHandler = () => {
this.calculateSizeBreak();
};
window.addEventListener("resize", resizeHandler);
}
this.updatePosition(v);
} else {
if (resizeHandler) {
window.removeEventListener("resize", resizeHandler);
resizeHandler = null;
}
document.body.style.removeProperty("display");
app.ui.menuContainer.style.removeProperty("display");
this.element.style.display = "none";
app.ui.restoreMenuPosition();
}
window.dispatchEvent(new Event("resize"));
},
});
}
updatePosition(v) {
document.body.style.display = "grid";
this.app.ui.menuContainer.style.display = "none";
this.element.style.removeProperty("display");
this.position = v;
if (v === "Bottom") {
this.app.bodyBottom.append(this.element);
} else {
this.app.bodyTop.prepend(this.element);
}
this.calculateSizeBreak();
}
updateSizeBreak(idx, prevIdx, direction) {
const newSize = this.#sizeBreaks[idx];
if (newSize === this.#sizeBreak) return;
this.#cachedInnerSize = null;
clearTimeout(this.#cacheTimeout);
this.#sizeBreak = this.#sizeBreaks[idx];
for (let i = 0; i < this.#sizeBreaks.length; i++) {
const sz = this.#sizeBreaks[i];
if (sz === this.#sizeBreak) {
this.element.classList.add(sz);
} else {
this.element.classList.remove(sz);
}
if (i < idx) {
this.element.classList.add("lt-" + sz);
} else {
this.element.classList.remove("lt-" + sz);
}
}
if (idx) {
// We're on a small screen, force the menu at the top
if (this.position !== "Top") {
this.updatePosition("Top");
}
} else if (this.position != this.menuPositionSetting.value) {
// Restore user position
this.updatePosition(this.menuPositionSetting.value);
}
// Allow multiple updates, but prevent bouncing
if (!direction) {
direction = prevIdx - idx;
} else if (direction != prevIdx - idx) {
return;
}
this.calculateSizeBreak(direction);
}
calculateSizeBreak(direction = 0) {
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak);
const currIdx = idx;
const innerSize = this.calculateInnerSize(idx);
if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) {
if (idx > 0) {
idx--;
}
} else if (innerSize > this.element.clientWidth) {
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
// We need to shrink
if (idx < this.#sizeBreaks.length - 1) {
idx++;
}
}
this.updateSizeBreak(idx, currIdx, direction);
}
calculateInnerSize(idx) {
// Cache the inner size to prevent too much calculation when resizing the window
clearTimeout(this.#cacheTimeout);
if (this.#cachedInnerSize) {
// Extend cache time
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
} else {
let innerSize = 0;
let count = 1;
for (const c of this.element.children) {
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items
innerSize += c.clientWidth;
count++;
}
innerSize += 8 * count;
this.#cachedInnerSize = innerSize;
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
}
return this.#cachedInnerSize;
}
/**
* @param {string} defaultName
*/
getFilename(defaultName) {
if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) {
defaultName = prompt("Save workflow as:", defaultName);
if (!defaultName) return;
if (!defaultName.toLowerCase().endsWith(".json")) {
defaultName += ".json";
}
}
return defaultName;
}
/**
* @param {string} [filename]
* @param { "workflow" | "output" } [promptProperty]
*/
async exportWorkflow(filename, promptProperty) {
if (this.app.workflowManager.activeWorkflow?.path) {
filename = this.app.workflowManager.activeWorkflow.name;
}
const p = await this.app.graphToPrompt();
const json = JSON.stringify(p[promptProperty], null, 2);
const blob = new Blob([json], { type: "application/json" });
const file = this.getFilename(filename);
if (!file) return;
downloadBlob(file, blob);
}
}
// Shim for scripts\ui\menu\index.ts
export const ComfyAppMenu = window.comfyAPI.index.ComfyAppMenu;

View File

@@ -1,23 +1,2 @@
// @ts-check
import { api } from "../../api.js";
import { ComfyButton } from "../components/button.js";
export function getInteruptButton(visibility) {
const btn = new ComfyButton({
icon: "close",
tooltip: "Cancel current generation",
enabled: false,
action: () => {
api.interrupt();
},
classList: ["comfyui-button", "comfyui-interrupt-button", visibility],
});
api.addEventListener("status", ({ detail }) => {
const sz = detail?.exec_info?.queue_remaining;
btn.enabled = sz > 0;
});
return btn;
}
// Shim for scripts\ui\menu\interruptButton.ts
export const getInterruptButton = window.comfyAPI.interruptButton.getInterruptButton;

View File

@@ -1,710 +0,0 @@
.relative {
position: relative;
}
.hidden {
display: none !important;
}
.mdi.rotate270::before {
transform: rotate(270deg);
}
/* Generic */
.comfyui-button {
display: flex;
align-items: center;
gap: 0.5em;
cursor: pointer;
border: none;
border-radius: 4px;
padding: 4px 8px;
box-sizing: border-box;
margin: 0;
transition: box-shadow 0.1s;
}
.comfyui-button:active {
box-shadow: inset 1px 1px 10px rgba(0, 0, 0, 0.5);
}
.comfyui-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.primary .comfyui-button,
.primary.comfyui-button {
background-color: var(--primary-bg) !important;
color: var(--primary-fg) !important;
}
.primary .comfyui-button:not(:disabled):hover,
.primary.comfyui-button:not(:disabled):hover {
background-color: var(--primary-hover-bg) !important;
color: var(--primary-hover-fg) !important;
}
/* Popup */
.comfyui-popup {
position: absolute;
left: var(--left);
right: var(--right);
top: var(--top);
bottom: var(--bottom);
z-index: 2000;
max-height: calc(100vh - var(--limit) - 10px);
box-shadow: 3px 3px 5px 0px rgba(0, 0, 0, 0.3);
}
.comfyui-popup:not(.open) {
display: none;
}
.comfyui-popup.right.open {
border-top-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
overflow: hidden;
}
/* Split button */
.comfyui-split-button {
position: relative;
display: flex;
}
.comfyui-split-primary {
flex: auto;
}
.comfyui-split-primary .comfyui-button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 1px solid var(--comfy-menu-bg);
width: 100%;
}
.comfyui-split-arrow .comfyui-button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding-left: 2px;
padding-right: 2px;
}
.comfyui-split-button-popup {
white-space: nowrap;
background-color: var(--content-bg);
color: var(--content-fg);
display: flex;
flex-direction: column;
overflow: auto;
}
.comfyui-split-button-popup.hover {
z-index: 2001;
}
.comfyui-split-button-popup > .comfyui-button {
border: none;
background-color: transparent;
color: var(--fg-color);
padding: 8px 12px 8px 8px;
}
.comfyui-split-button-popup > .comfyui-button:not(:disabled):hover {
background-color: var(--comfy-input-bg);
}
/* Button group */
.comfyui-button-group {
display: flex;
border-radius: 4px;
overflow: hidden;
}
.comfyui-button-group > .comfyui-button,
.comfyui-button-group > .comfyui-button-wrapper > .comfyui-button {
padding: 4px 10px;
border-radius: 0;
}
/* Menu */
.comfyui-menu {
width: 100vw;
background: var(--comfy-menu-bg);
color: var(--fg-color);
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
display: flex;
padding: 4px 8px;
align-items: center;
gap: 8px;
box-sizing: border-box;
z-index: 1000;
order: 0;
grid-column: 1/-1;
overflow: auto;
max-height: 90vh;
}
.comfyui-menu>* {
flex-shrink: 0;
}
.comfyui-menu .mdi::before {
font-size: 18px;
}
.comfyui-menu .comfyui-button {
background: var(--comfy-input-bg);
color: var(--fg-color);
white-space: nowrap;
}
.comfyui-menu .comfyui-button:not(:disabled):hover {
background: var(--border-color);
color: var(--content-fg);
}
.comfyui-menu .comfyui-split-button-popup > .comfyui-button {
border-radius: 0;
background-color: transparent;
}
.comfyui-menu .comfyui-split-button-popup > .comfyui-button:not(:disabled):hover {
background-color: var(--comfy-input-bg);
}
.comfyui-menu .comfyui-split-button-popup.left {
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.comfyui-menu .comfyui-button.popup-open {
background-color: var(--content-bg);
color: var(--content-fg);
}
.comfyui-menu-push {
margin-left: -0.8em;
flex: auto;
}
.comfyui-logo {
font-size: 1.2em;
margin: 0;
user-select: none;
cursor: default;
}
/* Workflows */
.comfyui-workflows-button {
flex-direction: row-reverse;
max-width: 200px;
position: relative;
z-index: 0;
}
.comfyui-workflows-button.popup-open {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.comfyui-workflows-button.unsaved {
font-style: italic;
}
.comfyui-workflows-button-progress {
position: absolute;
top: 0;
left: 0;
background-color: green;
height: 100%;
border-radius: 4px;
z-index: -1;
}
.comfyui-workflows-button > span {
flex: auto;
text-align: left;
overflow: hidden;
}
.comfyui-workflows-button-inner {
display: flex;
align-items: center;
gap: 7px;
width: 150px;
}
.comfyui-workflows-label {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
flex: auto;
position: relative;
}
.comfyui-workflows-button.unsaved .comfyui-workflows-label {
padding-left: 8px;
}
.comfyui-workflows-button.unsaved .comfyui-workflows-label:after {
content: "*";
position: absolute;
top: 0;
left: 0;
}
.comfyui-workflows-button-inner .mdi-graph::before {
transform: rotate(-90deg);
}
.comfyui-workflows-popup {
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
padding: 10px;
overflow: auto;
background-color: var(--content-bg);
color: var(--content-fg);
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
z-index: 400;
}
.comfyui-workflows-panel {
min-height: 150px;
}
.comfyui-workflows-panel .lds-ring {
transform: translate(-50%);
position: absolute;
left: 50%;
top: 75px;
}
.comfyui-workflows-panel h3 {
margin: 10px 0 10px 0;
font-size: 11px;
opacity: 0.8;
}
.comfyui-workflows-panel section header {
display: flex;
justify-content: space-between;
align-items: center;
}
.comfy-ui-workflows-search .mdi {
position: relative;
top: 2px;
pointer-events: none;
}
.comfy-ui-workflows-search input {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: none;
border-radius: 4px;
padding: 4px 10px;
margin-left: -24px;
text-indent: 18px;
}
.comfy-ui-workflows-search input:placeholder-shown {
width: 10px;
}
.comfy-ui-workflows-search input:placeholder-shown:focus {
width: auto;
}
.comfyui-workflows-actions {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.comfyui-workflows-actions .comfyui-button {
background: var(--comfy-input-bg);
color: var(--input-text);
}
.comfyui-workflows-actions .comfyui-button:not(:disabled):hover {
background: var(--primary-bg);
color: var(--primary-fg);
}
.comfyui-workflows-favorites,
.comfyui-workflows-open {
border-bottom: 1px solid var(--comfy-input-bg);
padding-bottom: 5px;
margin-bottom: 5px;
}
.comfyui-workflows-open .active {
font-weight: bold;
color: var(--primary-fg);
}
.comfyui-workflows-favorites:empty {
display: none;
}
.comfyui-workflows-tree {
padding: 0;
margin: 0;
}
.comfyui-workflows-tree:empty::after {
content: "No saved workflows";
display: block;
text-align: center;
}
.comfyui-workflows-tree > ul {
padding: 0;
}
.comfyui-workflows-tree > ul ul {
margin: 0;
padding: 0 0 0 25px;
}
.comfyui-workflows-tree:not(.filtered) .closed > ul {
display: none;
}
.comfyui-workflows-tree li,
.comfyui-workflows-tree-file {
--item-height: 32px;
list-style-type: none;
height: var(--item-height);
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
user-select: none;
}
.comfyui-workflows-tree-file.active::before,
.comfyui-workflows-tree li:hover::before,
.comfyui-workflows-tree-file:hover::before {
content: "";
position: absolute;
width: 100%;
left: 0;
height: var(--item-height);
background-color: var(--content-hover-bg);
color: var(--content-hover-fg);
z-index: -1;
}
.comfyui-workflows-tree-file.active::before {
background-color: var(--primary-bg);
color: var(--primary-fg);
}
.comfyui-workflows-tree-file.running:not(:hover)::before {
content: "";
position: absolute;
width: var(--progress, 0);
left: 0;
height: var(--item-height);
background-color: green;
z-index: -1;
}
.comfyui-workflows-tree-file.unsaved span {
font-style: italic;
}
.comfyui-workflows-tree-file span {
flex: auto;
}
.comfyui-workflows-tree-file span + .comfyui-workflows-file-action {
margin-left: 10px;
}
.comfyui-workflows-tree-file .comfyui-workflows-file-action {
background-color: transparent;
color: var(--fg-color);
padding: 2px 4px;
}
.comfyui-workflows-tree-file.active .comfyui-workflows-file-action {
color: var(--primary-fg);
}
.lg ~ .comfyui-workflows-popup .comfyui-workflows-tree-file:not(:hover) .comfyui-workflows-file-action {
opacity: 0;
}
.comfyui-workflows-tree-file .comfyui-workflows-file-action:hover {
background-color: var(--primary-bg);
color: var(--primary-fg);
}
.comfyui-workflows-tree-file .comfyui-workflows-file-action-primary {
background-color: transparent;
color: var(--fg-color);
padding: 2px 4px;
margin: 0 -4px;
}
.comfyui-workflows-file-action-favorite .mdi-star {
color: orange;
}
/* View List */
.comfyui-view-list-popup {
padding: 10px;
background-color: var(--content-bg);
color: var(--content-fg);
min-width: 170px;
min-height: 435px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
.comfyui-view-list-popup h3 {
margin: 0 0 5px 0;
}
.comfyui-view-list-items {
width: 100%;
background: var(--comfy-menu-bg);
border-radius: 5px;
display: flex;
justify-content: center;
flex: auto;
align-items: center;
flex-direction: column;
}
.comfyui-view-list-items section {
max-height: 400px;
overflow: auto;
width: 100%;
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
justify-content: center;
gap: 5px;
padding: 5px 0;
}
.comfyui-view-list-items section + section {
border-top: 1px solid var(--border-color);
margin-top: 10px;
padding-top: 5px;
}
.comfyui-view-list-items section h5 {
grid-column: 1 / 4;
text-align: center;
margin: 5px;
}
.comfyui-view-list-items span {
text-align: center;
padding: 0 2px;
}
.comfyui-view-list-popup header {
margin-bottom: 10px;
display: flex;
gap: 5px;
}
.comfyui-view-list-popup header .comfyui-button {
border: 1px solid transparent;
}
.comfyui-view-list-popup header .comfyui-button:not(:disabled):hover {
border: 1px solid var(--comfy-menu-bg);
}
/* Queue button */
.comfyui-queue-button .comfyui-split-primary .comfyui-button {
padding-right: 12px;
}
.comfyui-queue-count {
margin-left: 5px;
border-radius: 10px;
background-color: rgb(8, 80, 153);
padding: 2px 4px;
font-size: 10px;
min-width: 1em;
display: inline-block;
}
/* Queue options*/
.comfyui-queue-options {
padding: 10px;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
display: flex;
gap: 10px;
}
.comfyui-queue-batch {
display: flex;
flex-direction: column;
border-right: 1px solid var(--comfy-menu-bg);
padding-right: 10px;
gap: 5px;
}
.comfyui-queue-batch input {
width: 145px;
}
.comfyui-queue-batch .comfyui-queue-batch-value {
width: 70px;
}
.comfyui-queue-mode {
display: flex;
flex-direction: column;
}
.comfyui-queue-mode span {
font-weight: bold;
margin-bottom: 2px;
}
.comfyui-queue-mode label {
display: flex;
flex-direction: row-reverse;
justify-content: start;
gap: 5px;
padding: 2px 0;
}
.comfyui-queue-mode label input {
padding: 0;
margin: 0;
}
/** Send to workflow widget selection dialog */
.comfy-widget-selection-dialog {
border: none;
}
.comfy-widget-selection-dialog div {
color: var(--fg-color);
font-family: Arial, Helvetica, sans-serif;
}
.comfy-widget-selection-dialog h2 {
margin-top: 0;
}
.comfy-widget-selection-dialog section {
width: fit-content;
display: flex;
flex-direction: column;
}
.comfy-widget-selection-item {
display: flex;
gap: 10px;
align-items: center;
}
.comfy-widget-selection-item span {
margin-right: auto;
}
.comfy-widget-selection-item span::before {
content: '#' attr(data-id);
opacity: 0.5;
margin-right: 5px;
}
.comfy-modal .comfy-widget-selection-item button {
font-size: 1em;
}
/***** Responsive *****/
.lg.comfyui-menu .lt-lg-show {
display: none !important;
}
.comfyui-menu:not(.lg) .nlg-hide {
display: none !important;
}
/** Large screen */
.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-button span,
.lg.comfyui-menu>.comfyui-menu-mobile-collapse.comfyui-button span {
display: none;
}
.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-popup .comfyui-button span {
display: unset;
}
/** Non large screen */
.lt-lg.comfyui-menu {
flex-wrap: wrap;
}
.lt-lg.comfyui-menu > *:not(.comfyui-menu-mobile-collapse) {
order: 1;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse {
order: 9999;
width: 100%;
}
.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse {
order: -1;
}
.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-button {
top: unset;
bottom: 4px;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button-group {
flex-wrap: wrap;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button,
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button {
padding: 10px;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button,
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button-wrapper {
width: 100%;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-popup {
position: static;
background-color: var(--comfy-input-bg);
max-width: unset;
max-height: 50vh;
overflow: auto;
}
.lt-lg.comfyui-menu:not(.expanded) > .comfyui-menu-mobile-collapse {
display: none;
}
.lt-lg .comfyui-queue-button {
margin-right: 44px;
}
.lt-lg .comfyui-menu-button {
position: absolute;
top: 4px;
right: 8px;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-view-list-popup {
border-radius: 0;
}
.lt-lg.comfyui-menu .comfyui-workflows-popup {
width: 100vw;
}
/** Small */
.lt-md .comfyui-workflows-button-inner {
width: unset !important;
}
.lt-md .comfyui-workflows-label {
display: none;
}
/** Extra small */
.lt-sm .comfyui-queue-button {
margin-right: 0;
width: 100%;
}
.lt-sm .comfyui-queue-button .comfyui-button {
justify-content: center;
}
.lt-sm .comfyui-interrupt-button {
margin-right: 45px;
}
.comfyui-body-bottom .lt-sm.comfyui-menu > .comfyui-menu-button{
bottom: 41px;
}

View File

@@ -1,93 +1,2 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { $el } from "../../ui.js";
import { api } from "../../api.js";
import { ComfySplitButton } from "../components/splitButton.js";
import { ComfyQueueOptions } from "./queueOptions.js";
import { prop } from "../../utils.js";
export class ComfyQueueButton {
element = $el("div.comfyui-queue-button");
#internalQueueSize = 0;
queuePrompt = async (e) => {
this.#internalQueueSize += this.queueOptions.batchCount;
// Hold shift to queue front, event is undefined when auto-queue is enabled
await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount);
};
constructor(app) {
this.app = app;
this.queueSizeElement = $el("span.comfyui-queue-count", {
textContent: "?",
});
const queue = new ComfyButton({
content: $el("div", [
$el("span", {
textContent: "Queue",
}),
this.queueSizeElement,
]),
icon: "play",
classList: "comfyui-button",
action: this.queuePrompt,
});
this.queueOptions = new ComfyQueueOptions(app);
const btn = new ComfySplitButton(
{
primary: queue,
mode: "click",
position: "absolute",
horizontal: "right",
},
this.queueOptions.element
);
btn.element.classList.add("primary");
this.element.append(btn.element);
this.autoQueueMode = prop(this, "autoQueueMode", "", () => {
switch (this.autoQueueMode) {
case "instant":
queue.icon = "infinity";
break;
case "change":
queue.icon = "auto-mode";
break;
default:
queue.icon = "play";
break;
}
});
this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"]));
api.addEventListener("graphChanged", () => {
if (this.autoQueueMode === "change") {
if (this.#internalQueueSize) {
this.graphHasChanged = true;
} else {
this.graphHasChanged = false;
this.queuePrompt();
}
}
});
api.addEventListener("status", ({ detail }) => {
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
if (this.#internalQueueSize != null) {
this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
if (!this.#internalQueueSize && !app.lastExecutionError) {
if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) {
this.graphHasChanged = false;
this.queuePrompt();
}
}
}
});
}
}
// Shim for scripts\ui\menu\queueButton.ts
export const ComfyQueueButton = window.comfyAPI.queueButton.ComfyQueueButton;

View File

@@ -1,77 +1,2 @@
// @ts-check
import { $el } from "../../ui.js";
import { prop } from "../../utils.js";
export class ComfyQueueOptions extends EventTarget {
element = $el("div.comfyui-queue-options");
constructor(app) {
super();
this.app = app;
this.batchCountInput = $el("input", {
className: "comfyui-queue-batch-value",
type: "number",
min: "1",
value: "1",
oninput: () => (this.batchCount = +this.batchCountInput.value),
});
this.batchCountRange = $el("input", {
type: "range",
min: "1",
max: "100",
value: "1",
oninput: () => (this.batchCount = +this.batchCountRange.value),
});
this.element.append(
$el("div.comfyui-queue-batch", [
$el(
"label",
{
textContent: "Batch count: ",
},
this.batchCountInput
),
this.batchCountRange,
])
);
const createOption = (text, value, checked = false) =>
$el(
"label",
{ textContent: text },
$el("input", {
type: "radio",
name: "AutoQueueMode",
checked,
value,
oninput: (e) => (this.autoQueueMode = e.target["value"]),
})
);
this.autoQueueEl = $el("div.comfyui-queue-mode", [
$el("span", "Auto Queue:"),
createOption("Disabled", "", true),
createOption("Instant", "instant"),
createOption("On Change", "change"),
]);
this.element.append(this.autoQueueEl);
this.batchCount = prop(this, "batchCount", 1, () => {
this.batchCountInput.value = this.batchCount + "";
this.batchCountRange.value = this.batchCount + "";
});
this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => {
this.dispatchEvent(
new CustomEvent("autoQueueMode", {
detail: this.autoQueueMode,
})
);
});
}
}
// Shim for scripts\ui\menu\queueOptions.ts
export const ComfyQueueOptions = window.comfyAPI.queueOptions.ComfyQueueOptions;

View File

@@ -1,27 +0,0 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { ComfyViewList, ComfyViewListButton } from "./viewList.js";
export class ComfyViewHistoryButton extends ComfyViewListButton {
constructor(app) {
super(app, {
button: new ComfyButton({
content: "View History",
icon: "history",
tooltip: "View history",
classList: "comfyui-button comfyui-history-button",
}),
list: ComfyViewHistoryList,
mode: "History",
});
}
}
export class ComfyViewHistoryList extends ComfyViewList {
async loadItems() {
const items = await super.loadItems();
items["History"].reverse();
return items;
}
}

View File

@@ -1,203 +0,0 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { $el } from "../../ui.js";
import { api } from "../../api.js";
import { ComfyPopup } from "../components/popup.js";
export class ComfyViewListButton {
get open() {
return this.popup.open;
}
set open(open) {
this.popup.open = open;
}
constructor(app, { button, list, mode }) {
this.app = app;
this.button = button;
this.element = $el("div.comfyui-button-wrapper", this.button.element);
this.popup = new ComfyPopup({
target: this.element,
container: this.element,
horizontal: "right",
});
this.list = new (list ?? ComfyViewList)(app, mode, this.popup);
this.popup.children = [this.list.element];
this.popup.addEventListener("open", () => {
this.list.update();
});
this.popup.addEventListener("close", () => {
this.list.close();
});
this.button.withPopup(this.popup);
api.addEventListener("status", () => {
if (this.popup.open) {
this.popup.update();
}
});
}
}
export class ComfyViewList {
popup;
constructor(app, mode, popup) {
this.app = app;
this.mode = mode;
this.popup = popup;
this.type = mode.toLowerCase();
this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`);
this.clear = new ComfyButton({
icon: "cancel",
content: "Clear",
action: async () => {
this.showSpinner(false);
await api.clearItems(this.type);
await this.update();
},
});
this.refresh = new ComfyButton({
icon: "refresh",
content: "Refresh",
action: async () => {
await this.update(false);
},
});
this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [
$el("h3", mode),
$el("header", [this.clear.element, this.refresh.element]),
this.items,
]);
api.addEventListener("status", () => {
if (this.popup.open) {
this.update();
}
});
}
async close() {
this.items.replaceChildren();
}
async update(resize = true) {
this.showSpinner(resize);
const res = await this.loadItems();
let any = false;
const names = Object.keys(res);
const sections = names
.map((section) => {
const items = res[section];
if (items?.length) {
any = true;
} else {
return;
}
const rows = [];
if (names.length > 1) {
rows.push($el("h5", section));
}
rows.push(...items.flatMap((item) => this.createRow(item, section)));
return $el("section", rows);
})
.filter(Boolean);
if (any) {
this.items.replaceChildren(...sections);
} else {
this.items.replaceChildren($el("h5", "None"));
}
this.popup.update();
this.clear.enabled = this.refresh.enabled = true;
this.element.style.removeProperty("height");
}
showSpinner(resize = true) {
// if (!this.spinner) {
// this.spinner = createSpinner();
// }
// if (!resize) {
// this.element.style.height = this.element.clientHeight + "px";
// }
// this.clear.enabled = this.refresh.enabled = false;
// this.items.replaceChildren(
// $el(
// "div",
// {
// style: {
// fontSize: "18px",
// },
// },
// this.spinner
// )
// );
// this.popup.update();
}
async loadItems() {
return await api.getItems(this.type);
}
getRow(item, section) {
return {
text: item.prompt[0] + "",
actions: [
{
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
} catch (error) {
alert("Error loading workflow: " + error.message);
console.error(error);
}
},
},
{
text: "Delete",
action: async () => {
try {
await api.deleteItem(this.type, item.prompt[1]);
this.update();
} catch (error) {}
},
},
],
};
}
createRow = (item, section) => {
const row = this.getRow(item, section);
return [
$el("span", row.text),
...row.actions.map(
(a) =>
new ComfyButton({
content: a.text,
action: async (e, btn) => {
btn.enabled = false;
try {
await a.action();
} catch (error) {
throw error;
} finally {
btn.enabled = true;
}
},
}).element
),
];
};
}

View File

@@ -1,55 +0,0 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { ComfyViewList, ComfyViewListButton } from "./viewList.js";
import { api } from "../../api.js";
export class ComfyViewQueueButton extends ComfyViewListButton {
constructor(app) {
super(app, {
button: new ComfyButton({
content: "View Queue",
icon: "format-list-numbered",
tooltip: "View queue",
classList: "comfyui-button comfyui-queue-button",
}),
list: ComfyViewQueueList,
mode: "Queue",
});
}
}
export class ComfyViewQueueList extends ComfyViewList {
getRow = (item, section) => {
if (section !== "Running") {
return super.getRow(item, section);
}
return {
text: item.prompt[0] + "",
actions: [
{
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
} catch (error) {
alert("Error loading workflow: " + error.message);
console.error(error);
}
},
},
{
text: "Cancel",
action: async () => {
try {
await api.interrupt();
} catch (error) {}
},
},
],
};
}
}

View File

@@ -1,770 +1,3 @@
// @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) {
const openWorkflow = app.workflowManager.openWorkflows.find((w) => w.path === workflow.path);
if (openWorkflow) {
workflow = openWorkflow;
}
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);
const setting = app.ui.settings.getSettingValue("Comfy.UseNewMenu", false);
if (setting && setting != "Disabled") {
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"
),
]);
})
),
])
);
}
}
// Shim for scripts\ui\menu\workflows.ts
export const ComfyWorkflowsMenu = window.comfyAPI.workflows.ComfyWorkflowsMenu;
export const ComfyWorkflowsContent = window.comfyAPI.workflows.ComfyWorkflowsContent;

View File

@@ -1,333 +1,2 @@
import { $el } from "../ui.js";
import { api } from "../api.js";
import { ComfyDialog } from "./dialog.js";
export class ComfySettingsDialog extends ComfyDialog {
constructor(app) {
super();
this.app = app;
this.settingsValues = {};
this.settingsLookup = {};
this.element = $el(
"dialog",
{
id: "comfy-settings-dialog",
parent: document.body,
},
[
$el("table.comfy-modal-content.comfy-table", [
$el(
"caption",
{ textContent: "Settings" },
$el("button.comfy-btn", {
type: "button",
textContent: "\u00d7",
onclick: () => {
this.element.close();
},
})
),
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
$el("button", {
type: "button",
textContent: "Close",
style: {
cursor: "pointer",
},
onclick: () => {
this.element.close();
},
}),
]),
]
);
}
get settings() {
return Object.values(this.settingsLookup);
}
#dispatchChange(id, value, oldValue) {
this.dispatchEvent(
new CustomEvent(id + ".change", {
detail: {
value,
oldValue
},
})
);
}
async load() {
if (this.app.storageLocation === "browser") {
this.settingsValues = localStorage;
} else {
this.settingsValues = await api.getSettings();
}
// Trigger onChange for any settings added before load
for (const id in this.settingsLookup) {
const value = this.settingsValues[this.getId(id)];
this.settingsLookup[id].onChange?.(value);
this.#dispatchChange(id, value);
}
}
getId(id) {
if (this.app.storageLocation === "browser") {
id = "Comfy.Settings." + id;
}
return id;
}
getSettingValue(id, defaultValue) {
let value = this.settingsValues[this.getId(id)];
if(value != null) {
if(this.app.storageLocation === "browser") {
try {
value = JSON.parse(value);
} catch (error) {
}
}
}
return value ?? defaultValue;
}
async setSettingValueAsync(id, value) {
const json = JSON.stringify(value);
localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage
let oldValue = this.getSettingValue(id, undefined);
this.settingsValues[this.getId(id)] = value;
if (id in this.settingsLookup) {
this.settingsLookup[id].onChange?.(value, oldValue);
}
this.#dispatchChange(id, value, oldValue);
await api.storeSetting(id, value);
}
setSettingValue(id, value) {
this.setSettingValueAsync(id, value).catch((err) => {
alert(`Error saving setting '${id}'`);
console.error(err);
});
}
addSetting({ id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined }) {
if (!id) {
throw new Error("Settings must have an ID");
}
if (id in this.settingsLookup) {
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`);
}
let skipOnChange = false;
let value = this.getSettingValue(id);
if (value == null) {
if (this.app.isNewUserSession) {
// Check if we have a localStorage value but not a setting value and we are a new user
const localValue = localStorage["Comfy.Settings." + id];
if (localValue) {
value = JSON.parse(localValue);
this.setSettingValue(id, value); // Store on the server
}
}
if (value == null) {
value = defaultValue;
}
}
// Trigger initial setting of value
if (!skipOnChange) {
onChange?.(value, undefined);
}
this.settingsLookup[id] = {
id,
onChange,
name,
render: () => {
if (type === "hidden") return;
const setter = (v) => {
if (onChange) {
onChange(v, value);
}
this.setSettingValue(id, v);
value = v;
};
value = this.getSettingValue(id, defaultValue);
let element;
const htmlID = id.replaceAll(".", "-");
const labelCell = $el("td", [
$el("label", {
for: htmlID,
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
textContent: name,
}),
]);
if (typeof type === "function") {
element = type(name, setter, value, attrs);
} else {
switch (type) {
case "boolean":
element = $el("tr", [
labelCell,
$el("td", [
$el("input", {
id: htmlID,
type: "checkbox",
checked: value,
onchange: (event) => {
const isChecked = event.target.checked;
if (onChange !== undefined) {
onChange(isChecked);
}
this.setSettingValue(id, isChecked);
},
}),
]),
]);
break;
case "number":
element = $el("tr", [
labelCell,
$el("td", [
$el("input", {
type,
value,
id: htmlID,
oninput: (e) => {
setter(e.target.value);
},
...attrs,
}),
]),
]);
break;
case "slider":
element = $el("tr", [
labelCell,
$el("td", [
$el(
"div",
{
style: {
display: "grid",
gridAutoFlow: "column",
},
},
[
$el("input", {
...attrs,
value,
type: "range",
oninput: (e) => {
setter(e.target.value);
e.target.nextElementSibling.value = e.target.value;
},
}),
$el("input", {
...attrs,
value,
id: htmlID,
type: "number",
style: { maxWidth: "4rem" },
oninput: (e) => {
setter(e.target.value);
e.target.previousElementSibling.value = e.target.value;
},
}),
]
),
]),
]);
break;
case "combo":
element = $el("tr", [
labelCell,
$el("td", [
$el(
"select",
{
oninput: (e) => {
setter(e.target.value);
},
},
(typeof options === "function" ? options(value) : options || []).map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
const v = opt.value ?? opt.text;
return $el("option", {
value: v,
textContent: opt.text,
selected: value + "" === v + "",
});
})
),
]),
]);
break;
case "text":
default:
if (type !== "text") {
console.warn(`Unsupported setting type '${type}, defaulting to text`);
}
element = $el("tr", [
labelCell,
$el("td", [
$el("input", {
value,
id: htmlID,
oninput: (e) => {
setter(e.target.value);
},
...attrs,
}),
]),
]);
break;
}
}
if (tooltip) {
element.title = tooltip;
}
return element;
},
};
const self = this;
return {
get value() {
return self.getSettingValue(id, defaultValue);
},
set value(v) {
self.setSettingValue(id, v);
},
};
}
show() {
this.textElement.replaceChildren(
$el(
"tr",
{
style: { display: "none" },
},
[$el("th"), $el("th", { style: { width: "33%" } })]
),
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean)
);
this.element.showModal();
}
}
// Shim for scripts\ui\settings.ts
export const ComfySettingsDialog = window.comfyAPI.settings.ComfySettingsDialog;

View File

@@ -1,34 +0,0 @@
.lds-ring {
display: inline-block;
position: relative;
width: 1em;
height: 1em;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 100%;
height: 100%;
border: 0.15em solid #fff;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -1,9 +1,2 @@
import { addStylesheet } from "../utils.js";
addStylesheet(import.meta.url);
export function createSpinner() {
const div = document.createElement("div");
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
return div.firstElementChild;
}
// Shim for scripts\ui\spinner.ts
export const createSpinner = window.comfyAPI.spinner.createSpinner;

View File

@@ -1,60 +1,2 @@
import { $el } from "../ui.js";
/**
* @typedef { { text: string, value?: string, tooltip?: string } } ToggleSwitchItem
*/
/**
* Creates a toggle switch element
* @param { string } name
* @param { Array<string | ToggleSwitchItem } items
* @param { Object } [opts]
* @param { (e: { item: ToggleSwitchItem, prev?: ToggleSwitchItem }) => void } [opts.onChange]
*/
export function toggleSwitch(name, items, { onChange } = {}) {
let selectedIndex;
let elements;
function updateSelected(index) {
if (selectedIndex != null) {
elements[selectedIndex].classList.remove("comfy-toggle-selected");
}
onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] });
selectedIndex = index;
elements[selectedIndex].classList.add("comfy-toggle-selected");
}
elements = items.map((item, i) => {
if (typeof item === "string") item = { text: item };
if (!item.value) item.value = item.text;
const toggle = $el(
"label",
{
textContent: item.text,
title: item.tooltip ?? "",
},
$el("input", {
name,
type: "radio",
value: item.value ?? item.text,
checked: item.selected,
onchange: () => {
updateSelected(i);
},
})
);
if (item.selected) {
updateSelected(i);
}
return toggle;
});
const container = $el("div.comfy-toggle-switch", elements);
if (selectedIndex == null) {
elements[0].children[0].checked = true;
updateSelected(0);
}
return container;
}
// Shim for scripts\ui\toggleSwitch.ts
export const toggleSwitch = window.comfyAPI.toggleSwitch.toggleSwitch;

View File

@@ -1,135 +0,0 @@
.comfy-user-selection {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
font-family: sans-serif;
background: linear-gradient(var(--tr-even-bg-color), var(--tr-odd-bg-color));
}
.comfy-user-selection-inner {
background: var(--comfy-menu-bg);
margin-top: -30vh;
padding: 20px 40px;
border-radius: 10px;
min-width: 365px;
position: relative;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.comfy-user-selection-inner form {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.comfy-user-selection-inner h1 {
margin: 10px 0 30px 0;
font-weight: normal;
}
.comfy-user-selection-inner label {
display: flex;
flex-direction: column;
width: 100%;
}
.comfy-user-selection input,
.comfy-user-selection select {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 0;
border-radius: 5px;
padding: 5px;
margin-top: 10px;
}
.comfy-user-selection input::placeholder {
color: var(--descrip-text);
opacity: 1;
}
.comfy-user-existing {
width: 100%;
}
.no-users .comfy-user-existing {
display: none;
}
.comfy-user-selection-inner .or-separator {
margin: 10px 0;
padding: 10px;
display: block;
text-align: center;
width: 100%;
color: var(--descrip-text);
}
.comfy-user-selection-inner .or-separator {
overflow: hidden;
text-align: center;
margin-left: -10px;
}
.comfy-user-selection-inner .or-separator::before,
.comfy-user-selection-inner .or-separator::after {
content: "";
background-color: var(--border-color);
position: relative;
height: 1px;
vertical-align: middle;
display: inline-block;
width: calc(50% - 20px);
top: -1px;
}
.comfy-user-selection-inner .or-separator::before {
right: 10px;
margin-left: -50%;
}
.comfy-user-selection-inner .or-separator::after {
left: 10px;
margin-right: -50%;
}
.comfy-user-selection-inner section {
width: 100%;
padding: 10px;
margin: -10px;
transition: background-color 0.2s;
}
.comfy-user-selection-inner section.selected {
background: var(--border-color);
border-radius: 5px;
}
.comfy-user-selection-inner footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.comfy-user-selection-inner .comfy-user-error {
color: var(--error-text);
margin-bottom: 10px;
}
.comfy-user-button-next {
font-size: 16px;
padding: 6px 10px;
width: 100px;
display: flex;
gap: 5px;
align-items: center;
justify-content: center;
}

View File

@@ -1,114 +1,2 @@
import { api } from "../api.js";
import { $el } from "../ui.js";
import { addStylesheet } from "../utils.js";
import { createSpinner } from "./spinner.js";
export class UserSelectionScreen {
async show(users, user) {
// This will rarely be hit so move the loading to on demand
await addStylesheet(import.meta.url);
const userSelection = document.getElementById("comfy-user-selection");
userSelection.style.display = "";
return new Promise((resolve) => {
const input = userSelection.getElementsByTagName("input")[0];
const select = userSelection.getElementsByTagName("select")[0];
const inputSection = input.closest("section");
const selectSection = select.closest("section");
const form = userSelection.getElementsByTagName("form")[0];
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
const button = userSelection.getElementsByClassName("comfy-user-button-next")[0];
let inputActive = null;
input.addEventListener("focus", () => {
inputSection.classList.add("selected");
selectSection.classList.remove("selected");
inputActive = true;
});
select.addEventListener("focus", () => {
inputSection.classList.remove("selected");
selectSection.classList.add("selected");
inputActive = false;
select.style.color = "";
});
select.addEventListener("blur", () => {
if (!select.value) {
select.style.color = "var(--descrip-text)";
}
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (inputActive == null) {
error.textContent = "Please enter a username or select an existing user.";
} else if (inputActive) {
const username = input.value.trim();
if (!username) {
error.textContent = "Please enter a username.";
return;
}
// Create new user
input.disabled = select.disabled = input.readonly = select.readonly = true;
const spinner = createSpinner();
button.prepend(spinner);
try {
const resp = await api.createUser(username);
if (resp.status >= 300) {
let message = "Error creating user: " + resp.status + " " + resp.statusText;
try {
const res = await resp.json();
if(res.error) {
message = res.error;
}
} catch (error) {
}
throw new Error(message);
}
resolve({ username, userId: await resp.json(), created: true });
} catch (err) {
spinner.remove();
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred.";
input.disabled = select.disabled = input.readonly = select.readonly = false;
return;
}
} else if (!select.value) {
error.textContent = "Please select an existing user.";
return;
} else {
resolve({ username: users[select.value], userId: select.value, created: false });
}
});
if (user) {
const name = localStorage["Comfy.userName"];
if (name) {
input.value = name;
}
}
if (input.value) {
// Focus the input, do this separately as sometimes browsers like to fill in the value
input.focus();
}
const userIds = Object.keys(users ?? {});
if (userIds.length) {
for (const u of userIds) {
$el("option", { textContent: users[u], value: u, parent: select });
}
select.style.color = "var(--descrip-text)";
if (select.value) {
// Focus the select, do this separately as sometimes browsers like to fill in the value
select.focus();
}
} else {
userSelection.classList.add("no-users");
input.focus();
}
}).then((r) => {
userSelection.remove();
return r;
});
}
}
// Shim for scripts\ui\userSelection.ts
export const UserSelectionScreen = window.comfyAPI.userSelection.UserSelectionScreen;

View File

@@ -1,56 +1,3 @@
/**
* @typedef { string | string[] | Record<string, boolean> } ClassList
*/
/**
* @param { HTMLElement } element
* @param { ClassList } classList
* @param { string[] } requiredClasses
*/
export function applyClasses(element, classList, ...requiredClasses) {
classList ??= "";
let str;
if (typeof classList === "string") {
str = classList;
} else if (classList instanceof Array) {
str = classList.join(" ");
} else {
str = Object.entries(classList).reduce((p, c) => {
if (c[1]) {
p += (p.length ? " " : "") + c[0];
}
return p;
}, "");
}
element.className = str;
if (requiredClasses) {
element.classList.add(...requiredClasses);
}
}
/**
* @param { HTMLElement } element
* @param { { onHide?: (el: HTMLElement) => void, onShow?: (el: HTMLElement, value) => void } } [param1]
* @returns
*/
export function toggleElement(element, { onHide, onShow } = {}) {
let placeholder;
let hidden;
return (value) => {
if (value) {
if (hidden) {
hidden = false;
placeholder.replaceWith(element);
}
onShow?.(element, value);
} else {
if (!placeholder) {
placeholder = document.createComment("");
}
hidden = true;
element.replaceWith(placeholder);
onHide?.(element);
}
};
}
// Shim for scripts\ui\utils.ts
export const applyClasses = window.comfyAPI.utils.applyClasses;
export const toggleElement = window.comfyAPI.utils.toggleElement;