Format all code / Add pre-commit format hook (#81)

* Add format-guard

* Format code
This commit is contained in:
Chenlei Hu
2024-07-02 13:22:37 -04:00
committed by GitHub
parent 4fb7fa9db1
commit acdaa6a594
62 changed files with 28225 additions and 25406 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,255 +3,271 @@
import { api } from "./api";
import { clone } from "./utils";
export class ChangeTracker {
static MAX_HISTORY = 50;
#app;
undo = [];
redo = [];
activeState = null;
isOurLoad = false;
/** @type { import("./workflows").ComfyWorkflow | null } */
workflow;
static MAX_HISTORY = 50;
#app;
undo = [];
redo = [];
activeState = null;
isOurLoad = false;
/** @type { import("./workflows").ComfyWorkflow | null } */
workflow;
ds;
nodeOutputs;
ds;
nodeOutputs;
get app() {
return this.#app ?? this.workflow.manager.app;
}
get app() {
return this.#app ?? this.workflow.manager.app;
}
constructor(workflow) {
this.workflow = workflow;
}
constructor(workflow) {
this.workflow = workflow;
}
#setApp(app) {
this.#app = app;
}
#setApp(app) {
this.#app = app;
}
store() {
this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] };
}
store() {
this.ds = {
scale: this.app.canvas.ds.scale,
offset: [...this.app.canvas.ds.offset],
};
}
restore() {
if (this.ds) {
this.app.canvas.ds.scale = this.ds.scale;
this.app.canvas.ds.offset = this.ds.offset;
}
if (this.nodeOutputs) {
this.app.nodeOutputs = this.nodeOutputs;
}
}
restore() {
if (this.ds) {
this.app.canvas.ds.scale = this.ds.scale;
this.app.canvas.ds.offset = this.ds.offset;
}
if (this.nodeOutputs) {
this.app.nodeOutputs = this.nodeOutputs;
}
}
checkState() {
if (!this.app.graph) return;
checkState() {
if (!this.app.graph) return;
const currentState = this.app.graph.serialize();
if (!this.activeState) {
this.activeState = clone(currentState);
return;
}
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
this.undo.push(this.activeState);
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
this.undo.shift();
}
this.activeState = clone(currentState);
this.redo.length = 0;
this.workflow.unsaved = true;
api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState }));
}
}
const currentState = this.app.graph.serialize();
if (!this.activeState) {
this.activeState = clone(currentState);
return;
}
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
this.undo.push(this.activeState);
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
this.undo.shift();
}
this.activeState = clone(currentState);
this.redo.length = 0;
this.workflow.unsaved = true;
api.dispatchEvent(
new CustomEvent("graphChanged", { detail: this.activeState })
);
}
}
async updateState(source, target) {
const prevState = source.pop();
if (prevState) {
target.push(this.activeState);
this.isOurLoad = true;
await this.app.loadGraphData(prevState, false, false, this.workflow);
this.activeState = prevState;
}
}
async updateState(source, target) {
const prevState = source.pop();
if (prevState) {
target.push(this.activeState);
this.isOurLoad = true;
await this.app.loadGraphData(prevState, false, false, this.workflow);
this.activeState = prevState;
}
}
async undoRedo(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === "y") {
this.updateState(this.redo, this.undo);
return true;
} else if (e.key === "z") {
this.updateState(this.undo, this.redo);
return true;
}
}
}
async undoRedo(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === "y") {
this.updateState(this.redo, this.undo);
return true;
} else if (e.key === "z") {
this.updateState(this.undo, this.redo);
return true;
}
}
}
/** @param { import("./app").ComfyApp } app */
static init(app) {
const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
globalTracker.#setApp(app);
/** @param { import("./app").ComfyApp } app */
static init(app) {
const changeTracker = () =>
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
globalTracker.#setApp(app);
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
const v = await loadGraphData.apply(this, arguments);
const ct = changeTracker();
if (ct.isOurLoad) {
ct.isOurLoad = false;
} else {
ct.checkState();
}
return v;
};
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
const v = await loadGraphData.apply(this, arguments);
const ct = changeTracker();
if (ct.isOurLoad) {
ct.isOurLoad = false;
} else {
ct.checkState();
}
return v;
};
let keyIgnored = false;
window.addEventListener(
"keydown",
(e) => {
requestAnimationFrame(async () => {
let activeEl;
// If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
activeEl = document.activeElement;
if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") {
// Ignore events on inputs, they have their native history
return;
}
}
let keyIgnored = false;
window.addEventListener(
"keydown",
(e) => {
requestAnimationFrame(async () => {
let activeEl;
// If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
activeEl = document.activeElement;
if (
activeEl?.tagName === "INPUT" ||
activeEl?.["type"] === "textarea"
) {
// Ignore events on inputs, they have their native history
return;
}
}
keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta";
if (keyIgnored) return;
keyIgnored =
e.key === "Control" ||
e.key === "Shift" ||
e.key === "Alt" ||
e.key === "Meta";
if (keyIgnored) return;
// Check if this is a ctrl+z ctrl+y
if (await changeTracker().undoRedo(e)) return;
// Check if this is a ctrl+z ctrl+y
if (await changeTracker().undoRedo(e)) return;
// If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(activeEl)) return;
changeTracker().checkState();
});
},
true
);
// If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(activeEl)) return;
changeTracker().checkState();
});
},
true
);
window.addEventListener("keyup", (e) => {
if (keyIgnored) {
keyIgnored = false;
changeTracker().checkState();
}
});
window.addEventListener("keyup", (e) => {
if (keyIgnored) {
keyIgnored = false;
changeTracker().checkState();
}
});
// Handle clicking DOM elements (e.g. widgets)
window.addEventListener("mouseup", () => {
changeTracker().checkState();
});
// Handle clicking DOM elements (e.g. widgets)
window.addEventListener("mouseup", () => {
changeTracker().checkState();
});
// Handle prompt queue event for dynamic widget changes
api.addEventListener("promptQueued", () => {
changeTracker().checkState();
});
// Handle prompt queue event for dynamic widget changes
api.addEventListener("promptQueued", () => {
changeTracker().checkState();
});
// Handle litegraph clicks
// @ts-ignore
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
// @ts-ignore
LGraphCanvas.prototype.processMouseUp = function (e) {
const v = processMouseUp.apply(this, arguments);
changeTracker().checkState();
return v;
};
// @ts-ignore
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
// @ts-ignore
LGraphCanvas.prototype.processMouseDown = function (e) {
const v = processMouseDown.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Handle litegraph clicks
// @ts-ignore
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
// @ts-ignore
LGraphCanvas.prototype.processMouseUp = function (e) {
const v = processMouseUp.apply(this, arguments);
changeTracker().checkState();
return v;
};
// @ts-ignore
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
// @ts-ignore
LGraphCanvas.prototype.processMouseDown = function (e) {
const v = processMouseDown.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Handle litegraph context menu for COMBO widgets
const close = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function (e) {
const v = close.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Handle litegraph context menu for COMBO widgets
const close = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function (e) {
const v = close.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Detects nodes being added via the node search dialog
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
LiteGraph.LGraph.prototype.onNodeAdded = function () {
const v = onNodeAdded?.apply(this, arguments);
const ct = changeTracker();
if (!ct.isOurLoad) {
ct.checkState();
}
return v;
};
// Detects nodes being added via the node search dialog
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
LiteGraph.LGraph.prototype.onNodeAdded = function () {
const v = onNodeAdded?.apply(this, arguments);
const ct = changeTracker();
if (!ct.isOurLoad) {
ct.checkState();
}
return v;
};
// Store node outputs
api.addEventListener("executed", ({ detail }) => {
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
if (!prompt?.workflow) return;
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
const output = nodeOutputs[detail.node];
if (detail.merge && output) {
for (const k in detail.output ?? {}) {
const v = output[k];
if (v instanceof Array) {
output[k] = v.concat(detail.output[k]);
} else {
output[k] = detail.output[k];
}
}
} else {
nodeOutputs[detail.node] = detail.output;
}
});
}
// Store node outputs
api.addEventListener("executed", ({ detail }) => {
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
if (!prompt?.workflow) return;
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
const output = nodeOutputs[detail.node];
if (detail.merge && output) {
for (const k in detail.output ?? {}) {
const v = output[k];
if (v instanceof Array) {
output[k] = v.concat(detail.output[k]);
} else {
output[k] = detail.output[k];
}
}
} else {
nodeOutputs[detail.node] = detail.output;
}
});
}
static bindInput(app, activeEl) {
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") {
for (const evt of ["change", "input", "blur"]) {
if (`on${evt}` in activeEl) {
const listener = () => {
app.workflowManager.activeWorkflow.changeTracker.checkState();
activeEl.removeEventListener(evt, listener);
};
activeEl.addEventListener(evt, listener);
return true;
}
}
}
}
static bindInput(app, activeEl) {
if (
activeEl &&
activeEl.tagName !== "CANVAS" &&
activeEl.tagName !== "BODY"
) {
for (const evt of ["change", "input", "blur"]) {
if (`on${evt}` in activeEl) {
const listener = () => {
app.workflowManager.activeWorkflow.changeTracker.checkState();
activeEl.removeEventListener(evt, listener);
};
activeEl.addEventListener(evt, listener);
return true;
}
}
}
}
static graphEqual(a, b, path = "") {
if (a === b) return true;
static graphEqual(a, b, path = "") {
if (a === b) return true;
if (typeof a == "object" && a && typeof b == "object" && b) {
const keys = Object.getOwnPropertyNames(a);
if (typeof a == "object" && a && typeof b == "object" && b) {
const keys = Object.getOwnPropertyNames(a);
if (keys.length != Object.getOwnPropertyNames(b).length) {
return false;
}
if (keys.length != Object.getOwnPropertyNames(b).length) {
return false;
}
for (const key of keys) {
let av = a[key];
let bv = b[key];
if (!path && key === "nodes") {
// Nodes need to be sorted as the order changes when selecting nodes
av = [...av].sort((a, b) => a.id - b.id);
bv = [...bv].sort((a, b) => a.id - b.id);
} else if (path === "extra.ds") {
// Ignore view changes
continue;
}
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
return false;
}
}
for (const key of keys) {
let av = a[key];
let bv = b[key];
if (!path && key === "nodes") {
// Nodes need to be sorted as the order changes when selecting nodes
av = [...av].sort((a, b) => a.id - b.id);
bv = [...bv].sort((a, b) => a.id - b.id);
} else if (path === "extra.ds") {
// Ignore view changes
continue;
}
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
return false;
}
}
return true;
}
return true;
}
return false;
}
return false;
}
}
const globalTracker = new ChangeTracker({});
const globalTracker = new ChangeTracker({});

View File

@@ -1,121 +1,137 @@
import type { ComfyWorkflow } from "/types/comfyWorkflow";
export const defaultGraph: ComfyWorkflow = {
last_node_id: 9,
last_link_id: 9,
nodes: [
{
id: 7,
type: "CLIPTextEncode",
pos: [413, 389],
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
flags: {},
order: 3,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
properties: {},
widgets_values: ["text, watermark"],
},
{
id: 6,
type: "CLIPTextEncode",
pos: [415, 186],
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
flags: {},
order: 2,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
properties: {},
widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"],
},
{
id: 5,
type: "EmptyLatentImage",
pos: [473, 609],
size: { 0: 315, 1: 106 },
flags: {},
order: 1,
mode: 0,
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
properties: {},
widgets_values: [512, 512, 1],
},
{
id: 3,
type: "KSampler",
pos: [863, 186],
size: { 0: 315, 1: 262 },
flags: {},
order: 4,
mode: 0,
inputs: [
{ name: "model", type: "MODEL", link: 1 },
{ name: "positive", type: "CONDITIONING", link: 4 },
{ name: "negative", type: "CONDITIONING", link: 6 },
{ name: "latent_image", type: "LATENT", link: 2 },
],
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {},
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
},
{
id: 8,
type: "VAEDecode",
pos: [1209, 188],
size: { 0: 210, 1: 46 },
flags: {},
order: 5,
mode: 0,
inputs: [
{ name: "samples", type: "LATENT", link: 7 },
{ name: "vae", type: "VAE", link: 8 },
],
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
properties: {},
},
{
id: 9,
type: "SaveImage",
pos: [1451, 189],
size: { 0: 210, 1: 26 },
flags: {},
order: 6,
mode: 0,
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
properties: {},
},
{
id: 4,
type: "CheckpointLoaderSimple",
pos: [26, 474],
size: { 0: 315, 1: 98 },
flags: {},
order: 0,
mode: 0,
outputs: [
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
],
properties: {},
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
},
],
links: [
[1, 4, 0, 3, 0, "MODEL"],
[2, 5, 0, 3, 3, "LATENT"],
[3, 4, 1, 6, 0, "CLIP"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[5, 4, 1, 7, 0, "CLIP"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[8, 4, 2, 8, 1, "VAE"],
[9, 8, 0, 9, 0, "IMAGE"],
],
groups: [],
config: {},
extra: {},
version: 0.4,
last_node_id: 9,
last_link_id: 9,
nodes: [
{
id: 7,
type: "CLIPTextEncode",
pos: [413, 389],
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
flags: {},
order: 3,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [6],
slot_index: 0,
},
],
properties: {},
widgets_values: ["text, watermark"],
},
{
id: 6,
type: "CLIPTextEncode",
pos: [415, 186],
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
flags: {},
order: 2,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [4],
slot_index: 0,
},
],
properties: {},
widgets_values: [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
],
},
{
id: 5,
type: "EmptyLatentImage",
pos: [473, 609],
size: { 0: 315, 1: 106 },
flags: {},
order: 1,
mode: 0,
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
properties: {},
widgets_values: [512, 512, 1],
},
{
id: 3,
type: "KSampler",
pos: [863, 186],
size: { 0: 315, 1: 262 },
flags: {},
order: 4,
mode: 0,
inputs: [
{ name: "model", type: "MODEL", link: 1 },
{ name: "positive", type: "CONDITIONING", link: 4 },
{ name: "negative", type: "CONDITIONING", link: 6 },
{ name: "latent_image", type: "LATENT", link: 2 },
],
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {},
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
},
{
id: 8,
type: "VAEDecode",
pos: [1209, 188],
size: { 0: 210, 1: 46 },
flags: {},
order: 5,
mode: 0,
inputs: [
{ name: "samples", type: "LATENT", link: 7 },
{ name: "vae", type: "VAE", link: 8 },
],
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
properties: {},
},
{
id: 9,
type: "SaveImage",
pos: [1451, 189],
size: { 0: 210, 1: 26 },
flags: {},
order: 6,
mode: 0,
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
properties: {},
},
{
id: 4,
type: "CheckpointLoaderSimple",
pos: [26, 474],
size: { 0: 315, 1: 98 },
flags: {},
order: 0,
mode: 0,
outputs: [
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
],
properties: {},
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
},
],
links: [
[1, 4, 0, 3, 0, "MODEL"],
[2, 5, 0, 3, 3, "LATENT"],
[3, 4, 1, 6, 0, "CLIP"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[5, 4, 1, 7, 0, "CLIP"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[8, 4, 2, 8, 1, "VAE"],
[9, 8, 0, 9, 0, "IMAGE"],
],
groups: [],
config: {},
extra: {},
version: 0.4,
};

View File

@@ -1,194 +1,216 @@
import { app, ANIM_PREVIEW_WIDGET } from "./app";
import type { LGraphNode, Vector4 } from "/types/litegraph";
const SIZE = Symbol();
interface Rect {
height: number;
width: number;
x: number;
y: number;
height: number;
width: number;
x: number;
y: number;
}
export interface DOMWidget<T = HTMLElement> {
type: string;
name: string;
computedHeight?: number;
element?: T;
options: any;
value?: any;
y?: number;
callback?: (value: any) => void;
draw?: (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) => void;
onRemove?: () => void;
type: string;
name: string;
computedHeight?: number;
element?: T;
options: any;
value?: any;
y?: number;
callback?: (value: any) => void;
draw?: (
ctx: CanvasRenderingContext2D,
node: LGraphNode,
widgetWidth: number,
y: number,
widgetHeight: number
) => void;
onRemove?: () => void;
}
function intersect(a: Rect, b: Rect): Vector4 | null {
const x = Math.max(a.x, b.x);
const num1 = Math.min(a.x + a.width, b.x + b.width);
const y = Math.max(a.y, b.y);
const num2 = Math.min(a.y + a.height, b.y + b.height);
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
else return null;
const x = Math.max(a.x, b.x);
const num1 = Math.min(a.x + a.width, b.x + b.width);
const y = Math.max(a.y, b.y);
const num2 = Math.min(a.y + a.height, b.y + b.height);
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
else return null;
}
function getClipPath(node: LGraphNode, element: HTMLElement): string {
const selectedNode: LGraphNode = Object.values(app.canvas.selected_nodes)[0] as LGraphNode;
if (selectedNode && selectedNode !== node) {
const elRect = element.getBoundingClientRect();
const MARGIN = 7;
const scale = app.canvas.ds.scale;
const selectedNode: LGraphNode = Object.values(
app.canvas.selected_nodes
)[0] as LGraphNode;
if (selectedNode && selectedNode !== node) {
const elRect = element.getBoundingClientRect();
const MARGIN = 7;
const scale = app.canvas.ds.scale;
const bounding = selectedNode.getBounding();
const intersection = intersect(
{ x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale },
{
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN,
width: bounding[2] + MARGIN + MARGIN,
height: bounding[3] + MARGIN + MARGIN,
}
);
const bounding = selectedNode.getBounding();
const intersection = intersect(
{
x: elRect.x / scale,
y: elRect.y / scale,
width: elRect.width / scale,
height: elRect.height / scale,
},
{
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
y:
selectedNode.pos[1] +
app.canvas.ds.offset[1] -
LiteGraph.NODE_TITLE_HEIGHT -
MARGIN,
width: bounding[2] + MARGIN + MARGIN,
height: bounding[3] + MARGIN + MARGIN,
}
);
if (!intersection) {
return "";
}
if (!intersection) {
return "";
}
const widgetRect = element.getBoundingClientRect();
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
const clipWidth = intersection[2] + "px";
const clipHeight = intersection[3] + "px";
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
return path;
}
return "";
const widgetRect = element.getBoundingClientRect();
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
const clipWidth = intersection[2] + "px";
const clipHeight = intersection[3] + "px";
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
return path;
}
return "";
}
function computeSize(size: [number, number]): void {
if (this.widgets?.[0]?.last_y == null) return;
if (this.widgets?.[0]?.last_y == null) return;
let y = this.widgets[0].last_y;
let freeSpace = size[1] - y;
let y = this.widgets[0].last_y;
let freeSpace = size[1] - y;
let widgetHeight = 0;
let dom = [];
for (const w of this.widgets) {
if (w.type === "converted-widget") {
// Ignore
delete w.computedHeight;
} else if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else if (w.element) {
// Extract DOM widget size info
const styles = getComputedStyle(w.element);
let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
let widgetHeight = 0;
let dom = [];
for (const w of this.widgets) {
if (w.type === "converted-widget") {
// Ignore
delete w.computedHeight;
} else if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else if (w.element) {
// Extract DOM widget size info
const styles = getComputedStyle(w.element);
let minHeight =
w.options.getMinHeight?.() ??
parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
let maxHeight =
w.options.getMaxHeight?.() ??
parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height");
if (prefHeight.endsWith?.("%")) {
prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
} else {
prefHeight = parseInt(prefHeight);
if (isNaN(minHeight)) {
minHeight = prefHeight;
}
}
if (isNaN(minHeight)) {
minHeight = 50;
}
if (!isNaN(maxHeight)) {
if (!isNaN(prefHeight)) {
prefHeight = Math.min(prefHeight, maxHeight);
} else {
prefHeight = maxHeight;
}
}
dom.push({
minHeight,
prefHeight,
w,
});
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
let prefHeight =
w.options.getHeight?.() ??
styles.getPropertyValue("--comfy-widget-height");
if (prefHeight.endsWith?.("%")) {
prefHeight =
size[1] *
(parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
} else {
prefHeight = parseInt(prefHeight);
if (isNaN(minHeight)) {
minHeight = prefHeight;
}
}
if (isNaN(minHeight)) {
minHeight = 50;
}
if (!isNaN(maxHeight)) {
if (!isNaN(prefHeight)) {
prefHeight = Math.min(prefHeight, maxHeight);
} else {
prefHeight = maxHeight;
}
}
dom.push({
minHeight,
prefHeight,
w,
});
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
freeSpace -= widgetHeight;
freeSpace -= widgetHeight;
// Calculate sizes with all widgets at their min height
const prefGrow = []; // Nodes that want to grow to their prefd size
const canGrow = []; // Nodes that can grow to auto size
let growBy = 0;
for (const d of dom) {
freeSpace -= d.minHeight;
if (isNaN(d.prefHeight)) {
canGrow.push(d);
d.w.computedHeight = d.minHeight;
} else {
const diff = d.prefHeight - d.minHeight;
if (diff > 0) {
prefGrow.push(d);
growBy += diff;
d.diff = diff;
} else {
d.w.computedHeight = d.minHeight;
}
}
}
// Calculate sizes with all widgets at their min height
const prefGrow = []; // Nodes that want to grow to their prefd size
const canGrow = []; // Nodes that can grow to auto size
let growBy = 0;
for (const d of dom) {
freeSpace -= d.minHeight;
if (isNaN(d.prefHeight)) {
canGrow.push(d);
d.w.computedHeight = d.minHeight;
} else {
const diff = d.prefHeight - d.minHeight;
if (diff > 0) {
prefGrow.push(d);
growBy += diff;
d.diff = diff;
} else {
d.w.computedHeight = d.minHeight;
}
}
}
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
// Allocate space for image
freeSpace -= 220;
}
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
// Allocate space for image
freeSpace -= 220;
}
this.freeWidgetSpace = freeSpace;
this.freeWidgetSpace = freeSpace;
if (freeSpace < 0) {
// Not enough space for all widgets so we need to grow
size[1] -= freeSpace;
this.graph.setDirtyCanvas(true);
} else {
// Share the space between each
const growDiff = freeSpace - growBy;
if (growDiff > 0) {
// All pref sizes can be fulfilled
freeSpace = growDiff;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight;
}
} else {
// We need to grow evenly
const shared = -growDiff / prefGrow.length;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight - shared;
}
freeSpace = 0;
}
if (freeSpace < 0) {
// Not enough space for all widgets so we need to grow
size[1] -= freeSpace;
this.graph.setDirtyCanvas(true);
} else {
// Share the space between each
const growDiff = freeSpace - growBy;
if (growDiff > 0) {
// All pref sizes can be fulfilled
freeSpace = growDiff;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight;
}
} else {
// We need to grow evenly
const shared = -growDiff / prefGrow.length;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight - shared;
}
freeSpace = 0;
}
if (freeSpace > 0 && canGrow.length) {
// Grow any that are auto height
const shared = freeSpace / canGrow.length;
for (const d of canGrow) {
d.w.computedHeight += shared;
}
}
}
if (freeSpace > 0 && canGrow.length) {
// Grow any that are auto height
const shared = freeSpace / canGrow.length;
for (const d of canGrow) {
d.w.computedHeight += shared;
}
}
}
// Position each of the widgets
for (const w of this.widgets) {
w.y = y;
if (w.computedHeight) {
y += w.computedHeight;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
// Position each of the widgets
for (const w of this.widgets) {
w.y = y;
if (w.computedHeight) {
y += w.computedHeight;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
}
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
@@ -197,170 +219,179 @@ const elementWidgets = new Set();
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
//@ts-ignore
LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
const visibleNodes = computeVisibleNodes.apply(this, arguments);
// @ts-ignore
for (const node of app.graph._nodes) {
if (elementWidgets.has(node)) {
const hidden = visibleNodes.indexOf(node) === -1;
for (const w of node.widgets) {
// @ts-ignore
if (w.element) {
// @ts-ignore
w.element.hidden = hidden;
// @ts-ignore
w.element.style.display = hidden ? "none" : undefined;
if (hidden) {
w.options.onHide?.(w);
}
}
}
}
}
const visibleNodes = computeVisibleNodes.apply(this, arguments);
// @ts-ignore
for (const node of app.graph._nodes) {
if (elementWidgets.has(node)) {
const hidden = visibleNodes.indexOf(node) === -1;
for (const w of node.widgets) {
// @ts-ignore
if (w.element) {
// @ts-ignore
w.element.hidden = hidden;
// @ts-ignore
w.element.style.display = hidden ? "none" : undefined;
if (hidden) {
w.options.onHide?.(w);
}
}
}
}
}
return visibleNodes;
return visibleNodes;
};
let enableDomClipping = true;
export function addDomClippingSetting(): void {
app.ui.settings.addSetting({
id: "Comfy.DOMClippingEnabled",
name: "Enable DOM element clipping (enabling may reduce performance)",
type: "boolean",
defaultValue: enableDomClipping,
onChange(value) {
enableDomClipping = !!value;
},
});
app.ui.settings.addSetting({
id: "Comfy.DOMClippingEnabled",
name: "Enable DOM element clipping (enabling may reduce performance)",
type: "boolean",
defaultValue: enableDomClipping,
onChange(value) {
enableDomClipping = !!value;
},
});
}
//@ts-ignore
LGraphNode.prototype.addDOMWidget = function (
name: string,
type: string,
element: HTMLElement,
options: Record<string, any>
name: string,
type: string,
element: HTMLElement,
options: Record<string, any>
): DOMWidget {
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
if (!element.parentElement) {
document.body.append(element);
}
element.hidden = true;
element.style.display = "none";
if (!element.parentElement) {
document.body.append(element);
}
element.hidden = true;
element.style.display = "none";
let mouseDownHandler;
if (element.blur) {
mouseDownHandler = (event) => {
if (!element.contains(event.target)) {
element.blur();
}
};
document.addEventListener("mousedown", mouseDownHandler);
}
let mouseDownHandler;
if (element.blur) {
mouseDownHandler = (event) => {
if (!element.contains(event.target)) {
element.blur();
}
};
document.addEventListener("mousedown", mouseDownHandler);
}
const widget: DOMWidget = {
type,
name,
get value() {
return options.getValue?.() ?? undefined;
},
set value(v) {
options.setValue?.(v);
widget.callback?.(widget.value);
},
draw: function (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) {
if (widget.computedHeight == null) {
computeSize.call(node, node.size);
}
const widget: DOMWidget = {
type,
name,
get value() {
return options.getValue?.() ?? undefined;
},
set value(v) {
options.setValue?.(v);
widget.callback?.(widget.value);
},
draw: function (
ctx: CanvasRenderingContext2D,
node: LGraphNode,
widgetWidth: number,
y: number,
widgetHeight: number
) {
if (widget.computedHeight == null) {
computeSize.call(node, node.size);
}
const hidden =
node.flags?.collapsed ||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
widget.computedHeight <= 0 ||
widget.type === "converted-widget" ||
widget.type === "hidden";
element.hidden = hidden;
element.style.display = hidden ? "none" : null;
if (hidden) {
widget.options.onHide?.(widget);
return;
}
const hidden =
node.flags?.collapsed ||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
widget.computedHeight <= 0 ||
widget.type === "converted-widget" ||
widget.type === "hidden";
element.hidden = hidden;
element.style.display = hidden ? "none" : null;
if (hidden) {
widget.options.onHide?.(widget);
return;
}
const margin = 10;
const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y);
const margin = 10;
const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
Object.assign(element.style, {
transformOrigin: "0 0",
transform: scale,
left: `${transform.a + transform.e + elRect.left}px`,
top: `${transform.d + transform.f + elRect.top}px`,
width: `${widgetWidth - margin * 2}px`,
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
position: "absolute",
// @ts-ignore
zIndex: app.graph._nodes.indexOf(node),
});
Object.assign(element.style, {
transformOrigin: "0 0",
transform: scale,
left: `${transform.a + transform.e + elRect.left}px`,
top: `${transform.d + transform.f + elRect.top}px`,
width: `${widgetWidth - margin * 2}px`,
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
position: "absolute",
// @ts-ignore
zIndex: app.graph._nodes.indexOf(node),
});
if (enableDomClipping) {
element.style.clipPath = getClipPath(node, element);
element.style.willChange = "clip-path";
}
if (enableDomClipping) {
element.style.clipPath = getClipPath(node, element);
element.style.willChange = "clip-path";
}
this.options.onDraw?.(widget);
},
element,
options,
onRemove() {
if (mouseDownHandler) {
document.removeEventListener("mousedown", mouseDownHandler);
}
element.remove();
},
};
this.options.onDraw?.(widget);
},
element,
options,
onRemove() {
if (mouseDownHandler) {
document.removeEventListener("mousedown", mouseDownHandler);
}
element.remove();
},
};
for (const evt of options.selectOn) {
element.addEventListener(evt, () => {
app.canvas.selectNode(this);
app.canvas.bringToFront(this);
});
}
for (const evt of options.selectOn) {
element.addEventListener(evt, () => {
app.canvas.selectNode(this);
app.canvas.bringToFront(this);
});
}
this.addCustomWidget(widget);
elementWidgets.add(this);
this.addCustomWidget(widget);
elementWidgets.add(this);
const collapse = this.collapse;
this.collapse = function () {
collapse.apply(this, arguments);
if (this.flags?.collapsed) {
element.hidden = true;
element.style.display = "none";
}
}
const collapse = this.collapse;
this.collapse = function () {
collapse.apply(this, arguments);
if (this.flags?.collapsed) {
element.hidden = true;
element.style.display = "none";
}
};
const onRemoved = this.onRemoved;
this.onRemoved = function () {
element.remove();
elementWidgets.delete(this);
onRemoved?.apply(this, arguments);
};
const onRemoved = this.onRemoved;
this.onRemoved = function () {
element.remove();
elementWidgets.delete(this);
onRemoved?.apply(this, arguments);
};
if (!this[SIZE]) {
this[SIZE] = true;
const onResize = this.onResize;
this.onResize = function (size) {
options.beforeResize?.call(widget, this);
computeSize.call(this, size);
onResize?.apply(this, arguments);
options.afterResize?.call(widget, this);
};
}
if (!this[SIZE]) {
this[SIZE] = true;
const onResize = this.onResize;
this.onResize = function (size) {
options.beforeResize?.call(widget, this);
computeSize.call(this, size);
onResize?.apply(this, arguments);
options.afterResize?.call(widget, this);
};
}
return widget;
return widget;
};

View File

@@ -41,13 +41,13 @@ function stringify(val, depth, replacer, space, onGetObjID?) {
r
? (o = (onGetObjID && onGetObjID(val)) || null)
: JSON.stringify(val, function (k, v) {
if (a || depth > 0) {
if (replacer) v = replacer(k, v);
if (!k) return (a = Array.isArray(v)), (val = v);
!o && (o = a ? [] : {});
o[k] = _build(v, a ? depth : depth - 1);
}
}),
if (a || depth > 0) {
if (replacer) v = replacer(k, v);
if (!k) return (a = Array.isArray(v)), (val = v);
!o && (o = a ? [] : {});
o[k] = _build(v, a ? depth : depth - 1);
}
}),
o === void 0 ? (a ? [] : {}) : o);
}
return JSON.stringify(_build(val, depth), null, space);
@@ -97,9 +97,12 @@ class ComfyLoggingDialog extends ComfyDialog {
}
export() {
const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], {
type: "application/json",
});
const blob = new Blob(
[stringify([...this.logging.entries], 20, jsonReplacer, "\t")],
{
type: "application/json",
}
);
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
@@ -147,230 +150,234 @@ class ComfyLoggingDialog extends ComfyDialog {
textContent: "Export logs...",
onclick: () => this.export(),
}),
$el("button", {
type: "button",
textContent: "View exported logs...",
onclick: () => this.import(),
}),
...super.createButtons(),
];
}
$el("button", {
type: "button",
textContent: "View exported logs...",
onclick: () => this.import(),
}),
...super.createButtons(),
];
}
getTypeColor(type) {
switch (type) {
case "error":
return "red";
case "warn":
return "orange";
case "debug":
return "dodgerblue";
}
}
getTypeColor(type) {
switch (type) {
case "error":
return "red";
case "warn":
return "orange";
case "debug":
return "dodgerblue";
}
}
show(entries?: any[]) {
if (!entries) entries = this.logging.entries;
this.element.style.width = "100%";
const cols = {
source: "Source",
type: "Type",
timestamp: "Timestamp",
message: "Message",
};
const keys = Object.keys(cols);
const headers = Object.values(cols).map((title) =>
$el("div.comfy-logging-title", {
textContent: title,
})
);
const rows = entries.map((entry, i) => {
return $el(
"div.comfy-logging-log",
{
$: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`),
},
keys.map((key) => {
let v = entry[key];
let color;
if (key === "type") {
color = this.getTypeColor(v);
} else {
v = jsonReplacer(key, v, true);
show(entries?: any[]) {
if (!entries) entries = this.logging.entries;
this.element.style.width = "100%";
const cols = {
source: "Source",
type: "Type",
timestamp: "Timestamp",
message: "Message",
};
const keys = Object.keys(cols);
const headers = Object.values(cols).map((title) =>
$el("div.comfy-logging-title", {
textContent: title,
})
);
const rows = entries.map((entry, i) => {
return $el(
"div.comfy-logging-log",
{
$: (el) =>
el.style.setProperty(
"--row-bg",
`var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`
),
},
keys.map((key) => {
let v = entry[key];
let color;
if (key === "type") {
color = this.getTypeColor(v);
} else {
v = jsonReplacer(key, v, true);
if (typeof v === "object") {
v = stringify(v, 5, jsonReplacer, " ");
}
}
if (typeof v === "object") {
v = stringify(v, 5, jsonReplacer, " ");
}
}
return $el("div", {
style: {
color,
},
textContent: v,
});
})
);
});
return $el("div", {
style: {
color,
},
textContent: v,
});
})
);
});
const grid = $el(
"div.comfy-logging-logs",
{
style: {
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
},
},
[...headers, ...rows]
);
const els = [grid];
if (!this.logging.enabled) {
els.unshift(
$el("h3", {
style: { textAlign: "center" },
textContent: "Logging is disabled",
})
);
}
super.show($el("div", els));
}
const grid = $el(
"div.comfy-logging-logs",
{
style: {
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
},
},
[...headers, ...rows]
);
const els = [grid];
if (!this.logging.enabled) {
els.unshift(
$el("h3", {
style: { textAlign: "center" },
textContent: "Logging is disabled",
})
);
}
super.show($el("div", els));
}
}
export class ComfyLogging {
/**
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
*/
entries = [];
/**
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
*/
entries = [];
#enabled;
#console = {};
#enabled;
#console = {};
app: ComfyApp;
dialog: ComfyLoggingDialog;
app: ComfyApp;
dialog: ComfyLoggingDialog;
get enabled() {
return this.#enabled;
}
get enabled() {
return this.#enabled;
}
set enabled(value) {
if (value === this.#enabled) return;
if (value) {
this.patchConsole();
} else {
this.unpatchConsole();
}
this.#enabled = value;
}
set enabled(value) {
if (value === this.#enabled) return;
if (value) {
this.patchConsole();
} else {
this.unpatchConsole();
}
this.#enabled = value;
}
constructor(app) {
this.app = app;
constructor(app) {
this.app = app;
this.dialog = new ComfyLoggingDialog(this);
this.addSetting();
this.catchUnhandled();
this.addInitData();
}
this.dialog = new ComfyLoggingDialog(this);
this.addSetting();
this.catchUnhandled();
this.addInitData();
}
addSetting() {
const settingId: string = "Comfy.Logging.Enabled";
const htmlSettingId = settingId.replaceAll(".", "-");
const setting = this.app.ui.settings.addSetting({
id: settingId,
name: settingId,
defaultValue: true,
onChange: (value) => {
this.enabled = value;
},
type: (name, setter, value) => {
return $el("tr", [
$el("td", [
$el("label", {
textContent: "Logging",
for: htmlSettingId,
}),
]),
$el("td", [
$el("input", {
id: htmlSettingId,
type: "checkbox",
checked: value,
onchange: (event) => {
setter(event.target.checked);
},
}),
$el("button", {
textContent: "View Logs",
onclick: () => {
this.app.ui.settings.element.close();
this.dialog.show();
},
style: {
fontSize: "14px",
display: "block",
marginTop: "5px",
},
}),
]),
]);
},
});
this.enabled = setting.value;
}
addSetting() {
const settingId: string = "Comfy.Logging.Enabled";
const htmlSettingId = settingId.replaceAll(".", "-");
const setting = this.app.ui.settings.addSetting({
id: settingId,
name: settingId,
defaultValue: true,
onChange: (value) => {
this.enabled = value;
},
type: (name, setter, value) => {
return $el("tr", [
$el("td", [
$el("label", {
textContent: "Logging",
for: htmlSettingId,
}),
]),
$el("td", [
$el("input", {
id: htmlSettingId,
type: "checkbox",
checked: value,
onchange: (event) => {
setter(event.target.checked);
},
}),
$el("button", {
textContent: "View Logs",
onclick: () => {
this.app.ui.settings.element.close();
this.dialog.show();
},
style: {
fontSize: "14px",
display: "block",
marginTop: "5px",
},
}),
]),
]);
},
});
this.enabled = setting.value;
}
patchConsole() {
// Capture common console outputs
const self = this;
for (const type of ["log", "warn", "error", "debug"]) {
const orig = console[type];
this.#console[type] = orig;
console[type] = function () {
orig.apply(console, arguments);
self.addEntry("console", type, ...arguments);
};
}
}
patchConsole() {
// Capture common console outputs
const self = this;
for (const type of ["log", "warn", "error", "debug"]) {
const orig = console[type];
this.#console[type] = orig;
console[type] = function () {
orig.apply(console, arguments);
self.addEntry("console", type, ...arguments);
};
}
}
unpatchConsole() {
// Restore original console functions
for (const type of Object.keys(this.#console)) {
console[type] = this.#console[type];
}
this.#console = {};
}
unpatchConsole() {
// Restore original console functions
for (const type of Object.keys(this.#console)) {
console[type] = this.#console[type];
}
this.#console = {};
}
catchUnhandled() {
// Capture uncaught errors
window.addEventListener("error", (e) => {
this.addEntry("window", "error", e.error ?? "Unknown error");
return false;
});
catchUnhandled() {
// Capture uncaught errors
window.addEventListener("error", (e) => {
this.addEntry("window", "error", e.error ?? "Unknown error");
return false;
});
window.addEventListener("unhandledrejection", (e) => {
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
});
}
window.addEventListener("unhandledrejection", (e) => {
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
});
}
clear() {
this.entries = [];
}
clear() {
this.entries = [];
}
addEntry(source, type, ...args) {
if (this.enabled) {
this.entries.push({
source,
type,
timestamp: new Date(),
message: args,
});
}
}
addEntry(source, type, ...args) {
if (this.enabled) {
this.entries.push({
source,
type,
timestamp: new Date(),
message: args,
});
}
}
log(source, ...args) {
this.addEntry(source, "log", ...args);
}
log(source, ...args) {
this.addEntry(source, "log", ...args);
}
async addInitData() {
if (!this.enabled) return;
const source = "ComfyUI.Logging";
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
const systemStats = await api.getSystemStats();
this.addEntry(source, "debug", systemStats);
}
async addInitData() {
if (!this.enabled) return;
const source = "ComfyUI.Logging";
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
const systemStats = await api.getSystemStats();
this.addEntry(source, "debug", systemStats);
}
}

View File

@@ -23,17 +23,26 @@ export function getPngMetadata(file) {
// Get the length of the chunk
const length = dataView.getUint32(offset);
// Get the chunk type
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
const type = String.fromCharCode(
...pngData.slice(offset + 4, offset + 8)
);
if (type === "tEXt" || type == "comf" || type === "iTXt") {
// Get the keyword
let keyword_end = offset + 8;
while (pngData[keyword_end] !== 0) {
keyword_end++;
}
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
const keyword = String.fromCharCode(
...pngData.slice(offset + 8, keyword_end)
);
// Get the text
const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
const contentJson = new TextDecoder("utf-8").decode(contentArraySegment);
const contentArraySegment = pngData.slice(
keyword_end + 1,
offset + 8 + length
);
const contentJson = new TextDecoder("utf-8").decode(
contentArraySegment
);
txt_chunks[keyword] = contentJson;
}
@@ -53,11 +62,17 @@ function parseExifData(exifData) {
// Function to read 16-bit and 32-bit integers from binary data
function readInt(offset, isLittleEndian, length) {
let arr = exifData.slice(offset, offset + length)
let arr = exifData.slice(offset, offset + length);
if (length === 2) {
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(0, isLittleEndian);
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
0,
isLittleEndian
);
} else if (length === 4) {
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(0, isLittleEndian);
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(
0,
isLittleEndian
);
}
}
@@ -79,7 +94,9 @@ function parseExifData(exifData) {
let value;
if (type === 2) {
// ASCII string
value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1));
value = String.fromCharCode(
...exifData.slice(valueOffset, valueOffset + numValues - 1)
);
}
result[tag] = value;
@@ -94,13 +111,13 @@ function parseExifData(exifData) {
}
function splitValues(input) {
var output = {};
for (var key in input) {
var output = {};
for (var key in input) {
var value = input[key];
var splitValues = value.split(':', 2);
var splitValues = value.split(":", 2);
output[splitValues[0]] = splitValues[1];
}
return output;
}
return output;
}
export function getWebpMetadata(file) {
@@ -111,7 +128,10 @@ export function getWebpMetadata(file) {
const dataView = new DataView(webp.buffer);
// Check that the WEBP signature is present
if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) {
if (
dataView.getUint32(0) !== 0x52494646 ||
dataView.getUint32(8) !== 0x57454250
) {
console.error("Not a valid WEBP file");
r({});
return;
@@ -123,15 +143,22 @@ export function getWebpMetadata(file) {
// Loop through the chunks in the WEBP file
while (offset < webp.length) {
const chunk_length = dataView.getUint32(offset + 4, true);
const chunk_type = String.fromCharCode(...webp.slice(offset, offset + 4));
const chunk_type = String.fromCharCode(
...webp.slice(offset, offset + 4)
);
if (chunk_type === "EXIF") {
if (String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == "Exif\0\0") {
if (
String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) ==
"Exif\0\0"
) {
offset += 6;
}
let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length));
let data = parseExifData(
webp.slice(offset + 8, offset + 8 + chunk_length)
);
for (var key in data) {
var value = data[key] as string;
let index = value.indexOf(':');
let index = value.indexOf(":");
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
}
}
@@ -150,11 +177,17 @@ export function getLatentMetadata(file) {
return new Promise((r) => {
const reader = new FileReader();
reader.onload = (event) => {
const safetensorsData = new Uint8Array(event.target.result as ArrayBuffer);
const safetensorsData = new Uint8Array(
event.target.result as ArrayBuffer
);
const dataView = new DataView(safetensorsData.buffer);
let header_size = dataView.getUint32(0, true);
let offset = 8;
let header = JSON.parse(new TextDecoder().decode(safetensorsData.slice(offset, offset + header_size)));
let header = JSON.parse(
new TextDecoder().decode(
safetensorsData.slice(offset, offset + header_size)
)
);
r(header.__metadata__);
};
@@ -164,7 +197,7 @@ export function getLatentMetadata(file) {
}
function getString(dataView: DataView, offset: number, length: number): string {
let string = '';
let string = "";
for (let i = 0; i < length; i++) {
string += String.fromCharCode(dataView.getUint8(offset + i));
}
@@ -188,7 +221,7 @@ function parseVorbisComment(dataView: DataView): Record<string, string> {
const comment = getString(dataView, offset, commentLength);
offset += commentLength;
const [key, value] = comment.split('=');
const [key, value] = comment.split("=");
comments[key] = value;
}
@@ -200,14 +233,16 @@ function parseVorbisComment(dataView: DataView): Record<string, string> {
export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
return new Promise((r) => {
const reader = new FileReader();
reader.onload = function(event) {
reader.onload = function (event) {
const arrayBuffer = event.target.result as ArrayBuffer;
const dataView = new DataView(arrayBuffer);
// Verify the FLAC signature
const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4));
if (signature !== 'fLaC') {
console.error('Not a valid FLAC file');
const signature = String.fromCharCode(
...new Uint8Array(arrayBuffer, 0, 4)
);
if (signature !== "fLaC") {
console.error("Not a valid FLAC file");
return;
}
@@ -216,12 +251,15 @@ export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
let vorbisComment = null;
while (offset < dataView.byteLength) {
const isLastBlock = dataView.getUint8(offset) & 0x80;
const blockType = dataView.getUint8(offset) & 0x7F;
const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF;
const blockType = dataView.getUint8(offset) & 0x7f;
const blockSize = dataView.getUint32(offset, false) & 0xffffff;
offset += 4;
if (blockType === 4) { // Vorbis Comment block type
vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize));
if (blockType === 4) {
// Vorbis Comment block type
vorbisComment = parseVorbisComment(
new DataView(arrayBuffer, offset, blockSize)
);
}
offset += blockSize;
@@ -241,11 +279,13 @@ export async function importA1111(graph, parameters) {
const opts = parameters
.substr(p)
.split("\n")[1]
.match(new RegExp("\\s*([^:]+:\\s*([^\"\\{].*?|\".*?\"|\\{.*?\\}))\\s*(,|$)", "g"))
.match(
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', "g")
)
.reduce((p, n) => {
const s = n.split(":");
if (s[1].endsWith(',')) {
s[1] = s[1].substr(0, s[1].length -1);
if (s[1].endsWith(",")) {
s[1] = s[1].substr(0, s[1].length - 1);
}
p[s[0].trim().toLowerCase()] = s[1].trim();
return p;
@@ -271,7 +311,7 @@ export async function importA1111(graph, parameters) {
const getWidget = (node, name) => {
return node.widgets.find((w) => w.name === name);
}
};
const setWidgetValue = (node, name, value, isOptionPrefix?) => {
const w = getWidget(node, name);
@@ -286,7 +326,7 @@ export async function importA1111(graph, parameters) {
} else {
w.value = value;
}
}
};
const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
const loras = [];
@@ -320,24 +360,28 @@ export async function importA1111(graph, parameters) {
}
return { text, prevModel, prevClip };
}
};
const replaceEmbeddings = (text) => {
if(!embeddings.length) return text;
if (!embeddings.length) return text;
return text.replaceAll(
new RegExp(
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
"\\b(" +
embeddings
.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join("\\b|\\b") +
")\\b",
"ig"
),
"embedding:$1"
);
}
};
const popOpt = (name) => {
const v = opts[name];
delete opts[name];
return v;
}
};
graph.clear();
graph.add(ckptNode);
@@ -365,7 +409,7 @@ export async function importA1111(graph, parameters) {
model(v) {
setWidgetValue(ckptNode, "ckpt_name", v, true);
},
"vae"(v) {
vae(v) {
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
},
"cfg scale"(v) {
@@ -383,7 +427,9 @@ export async function importA1111(graph, parameters) {
setWidgetValue(samplerNode, "scheduler", "normal");
}
const w = getWidget(samplerNode, "sampler_name");
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
const o = w.options.values.find(
(w) => w === name || w === "sample_" + name
);
if (o) {
setWidgetValue(samplerNode, "sampler_name", o);
}
@@ -431,11 +477,14 @@ export async function importA1111(graph, parameters) {
samplerNode.connect(0, decode, 0);
vaeLoaderNode.connect(0, decode, 1);
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
const upscaleLoaderNode =
LiteGraph.createNode("UpscaleModelLoader");
graph.add(upscaleLoaderNode);
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
const modelUpscaleNode = LiteGraph.createNode(
"ImageUpscaleWithModel"
);
graph.add(modelUpscaleNode);
decode.connect(0, modelUpscaleNode, 1);
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
@@ -444,7 +493,8 @@ export async function importA1111(graph, parameters) {
graph.add(upscaleNode);
modelUpscaleNode.connect(0, upscaleNode, 0);
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
const vaeEncodeNode = (latentNode =
LiteGraph.createNode("VAEEncodeTiled"));
graph.add(vaeEncodeNode);
upscaleNode.connect(0, vaeEncodeNode, 0);
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
@@ -477,14 +527,39 @@ export async function importA1111(graph, parameters) {
}
if (hrSamplerNode) {
setWidgetValue(hrSamplerNode, "steps", hrSteps? +hrSteps : getWidget(samplerNode, "steps").value);
setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value);
setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value);
setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value);
setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1"));
setWidgetValue(
hrSamplerNode,
"steps",
hrSteps ? +hrSteps : getWidget(samplerNode, "steps").value
);
setWidgetValue(
hrSamplerNode,
"cfg",
getWidget(samplerNode, "cfg").value
);
setWidgetValue(
hrSamplerNode,
"scheduler",
getWidget(samplerNode, "scheduler").value
);
setWidgetValue(
hrSamplerNode,
"sampler_name",
getWidget(samplerNode, "sampler_name").value
);
setWidgetValue(
hrSamplerNode,
"denoise",
+(popOpt("denoising strength") || "1")
);
}
let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 });
let n = createLoraNodes(
positiveNode,
positive,
{ node: clipSkipNode, index: 0 },
{ node: ckptNode, index: 0 }
);
positive = n.text;
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
negative = n.text;
@@ -494,7 +569,15 @@ export async function importA1111(graph, parameters) {
graph.arrange();
for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) {
for (const opt of [
"model hash",
"ensd",
"version",
"vae hash",
"ti hashes",
"lora hashes",
"hashes",
]) {
delete opts[opt];
}

View File

@@ -8,67 +8,77 @@ import { TaskItem } from "/types/apiTypes";
export const ComfyDialog = _ComfyDialog;
type Position2D = {
x: number,
y: number
x: number;
y: number;
};
type Props = {
parent?: HTMLElement,
$?: (el: HTMLElement) => void,
dataset?: DOMStringMap,
style?: Partial<CSSStyleDeclaration>,
for?: string,
textContent?: string,
[key: string]: any
parent?: HTMLElement;
$?: (el: HTMLElement) => void;
dataset?: DOMStringMap;
style?: Partial<CSSStyleDeclaration>;
for?: string;
textContent?: string;
[key: string]: any;
};
type Children = Element[] | Element | string | string[];
export function $el(tag: string, propsOrChildren?: Children | Props, children?: Children): HTMLElement {
const split = tag.split(".");
const element = document.createElement(split.shift() as string);
if (split.length > 0) {
element.classList.add(...split);
export function $el(
tag: string,
propsOrChildren?: Children | Props,
children?: Children
): HTMLElement {
const split = tag.split(".");
const element = document.createElement(split.shift() as string);
if (split.length > 0) {
element.classList.add(...split);
}
if (propsOrChildren) {
if (typeof propsOrChildren === "string") {
propsOrChildren = { textContent: propsOrChildren };
} else if (propsOrChildren instanceof Element) {
propsOrChildren = [propsOrChildren];
}
if (Array.isArray(propsOrChildren)) {
element.append(...propsOrChildren);
} else {
const {
parent,
$: cb,
dataset,
style,
...rest
} = propsOrChildren as Props;
if (propsOrChildren) {
if (typeof propsOrChildren === "string") {
propsOrChildren = { textContent: propsOrChildren };
} else if (propsOrChildren instanceof Element) {
propsOrChildren = [propsOrChildren];
}
if (Array.isArray(propsOrChildren)) {
element.append(...propsOrChildren);
} else {
const { parent, $: cb, dataset, style, ...rest } = propsOrChildren as Props;
if (rest.for) {
element.setAttribute("for", rest.for);
}
if (rest.for) {
element.setAttribute("for", rest.for)
}
if (style) {
Object.assign(element.style, style);
}
if (style) {
Object.assign(element.style, style);
}
if (dataset) {
Object.assign(element.dataset, dataset);
}
if (dataset) {
Object.assign(element.dataset, dataset);
}
Object.assign(element, rest);
if (children) {
element.append(...(Array.isArray(children) ? children : [children]));
}
Object.assign(element, rest);
if (children) {
element.append(...(Array.isArray(children) ? children : [children]));
}
if (parent) {
parent.append(element);
}
if (parent) {
parent.append(element);
}
if (cb) {
cb(element);
}
}
if (cb) {
cb(element);
}
}
return element;
}
return element;
}
function dragElement(dragEl, settings) {
@@ -93,12 +103,17 @@ function dragElement(dragEl, settings) {
function ensureInBounds() {
try {
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft));
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop));
newPosX = Math.min(
document.body.clientWidth - dragEl.clientWidth,
Math.max(0, dragEl.offsetLeft)
);
newPosY = Math.min(
document.body.clientHeight - dragEl.clientHeight,
Math.max(0, dragEl.offsetTop)
);
positionElement();
}
catch (exception) {
} catch (exception) {
// robust
}
}
@@ -112,7 +127,8 @@ function dragElement(dragEl, settings) {
// set the element's new position:
if (anchorRight) {
dragEl.style.left = "unset";
dragEl.style.right = document.body.clientWidth - newPosX - dragEl.clientWidth + "px";
dragEl.style.right =
document.body.clientWidth - newPosX - dragEl.clientWidth + "px";
} else {
dragEl.style.left = newPosX + "px";
dragEl.style.right = "unset";
@@ -180,8 +196,14 @@ function dragElement(dragEl, settings) {
posStartX = e.clientX;
posStartY = e.clientY;
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft + posDiffX));
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop + posDiffY));
newPosX = Math.min(
document.body.clientWidth - dragEl.clientWidth,
Math.max(0, dragEl.offsetLeft + posDiffX)
);
newPosY = Math.min(
document.body.clientHeight - dragEl.clientHeight,
Math.max(0, dragEl.offsetTop + posDiffY)
);
positionElement();
}
@@ -226,31 +248,40 @@ class ComfyList {
textContent: section,
}),
$el("div.comfy-list-items", [
...(this.#reverse ? items[section].reverse() : items[section]).map((item: TaskItem) => {
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
const removeAction = "remove" in item ? item.remove : {
name: "Delete",
cb: () => api.deleteItem(this.#type, item.prompt[1]),
};
return $el("div", { textContent: item.prompt[0] + ": " }, [
$el("button", {
textContent: "Load",
onclick: async () => {
await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow, true, false);
if ("outputs" in item) {
app.nodeOutputs = item.outputs;
}
},
}),
$el("button", {
textContent: removeAction.name,
onclick: async () => {
await removeAction.cb();
await this.update();
},
}),
]);
}),
...(this.#reverse ? items[section].reverse() : items[section]).map(
(item: TaskItem) => {
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
const removeAction =
"remove" in item
? item.remove
: {
name: "Delete",
cb: () => api.deleteItem(this.#type, item.prompt[1]),
};
return $el("div", { textContent: item.prompt[0] + ": " }, [
$el("button", {
textContent: "Load",
onclick: async () => {
await app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow,
true,
false
);
if ("outputs" in item) {
app.nodeOutputs = item.outputs;
}
},
}),
$el("button", {
textContent: removeAction.name,
onclick: async () => {
await removeAction.cb();
await this.update();
},
}),
]);
}
),
]),
]),
$el("div.comfy-list-actions", [
@@ -400,8 +431,15 @@ export class ComfyUI {
const autoQueueModeEl = toggleSwitch(
"autoQueueMode",
[
{ text: "instant", tooltip: "A new prompt will be queued as soon as the queue reaches 0" },
{ text: "change", tooltip: "A new prompt will be queued when the queue is at 0 and the graph is/has changed" },
{
text: "instant",
tooltip: "A new prompt will be queued as soon as the queue reaches 0",
},
{
text: "change",
tooltip:
"A new prompt will be queued when the queue is at 0 and the graph is/has changed",
},
],
{
onChange: (value) => {
@@ -435,30 +473,34 @@ export class ComfyUI {
) as HTMLDivElement;
this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
$el("div.drag-handle.comfy-menu-header", {
style: {
overflow: "hidden",
position: "relative",
width: "100%",
cursor: "default"
}
}, [
$el("span.drag-handle"),
$el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }),
$el("div.comfy-menu-actions", [
$el("button.comfy-settings-btn", {
textContent: "⚙️",
onclick: () => this.settings.show(),
}),
$el("button.comfy-close-menu-btn", {
textContent: "\u00d7",
onclick: () => {
this.menuContainer.style.display = "none";
this.menuHamburger.style.display = "flex";
},
}),
]),
]),
$el(
"div.drag-handle.comfy-menu-header",
{
style: {
overflow: "hidden",
position: "relative",
width: "100%",
cursor: "default",
},
},
[
$el("span.drag-handle"),
$el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }),
$el("div.comfy-menu-actions", [
$el("button.comfy-settings-btn", {
textContent: "⚙️",
onclick: () => this.settings.show(),
}),
$el("button.comfy-close-menu-btn", {
textContent: "\u00d7",
onclick: () => {
this.menuContainer.style.display = "none";
this.menuHamburger.style.display = "flex";
},
}),
]),
]
),
$el("button.comfy-queue-btn", {
id: "queue-button",
textContent: "Queue Prompt",
@@ -469,70 +511,95 @@ export class ComfyUI {
$el("input", {
type: "checkbox",
onchange: (i) => {
document.getElementById("extraOptions").style.display = i.srcElement.checked ? "block" : "none";
this.batchCount = i.srcElement.checked ?
Number.parseInt((document.getElementById("batchCountInputRange") as HTMLInputElement).value) : 1;
(document.getElementById("autoQueueCheckbox") as HTMLInputElement).checked = false;
document.getElementById("extraOptions").style.display = i
.srcElement.checked
? "block"
: "none";
this.batchCount = i.srcElement.checked
? Number.parseInt(
(
document.getElementById(
"batchCountInputRange"
) as HTMLInputElement
).value
)
: 1;
(
document.getElementById("autoQueueCheckbox") as HTMLInputElement
).checked = false;
this.autoQueueEnabled = false;
},
}),
]),
]),
$el("div", { id: "extraOptions", style: { width: "100%", display: "none" } }, [
$el("div", [
$el("label", { innerHTML: "Batch count" }),
$el("input", {
id: "batchCountInputNumber",
type: "number",
value: this.batchCount,
min: "1",
style: { width: "35%", "marginLeft": "0.4em" },
oninput: (i) => {
this.batchCount = i.target.value;
/* Even though an <input> element with a type of range logically represents a number (since
$el(
"div",
{ id: "extraOptions", style: { width: "100%", display: "none" } },
[
$el("div", [
$el("label", { innerHTML: "Batch count" }),
$el("input", {
id: "batchCountInputNumber",
type: "number",
value: this.batchCount,
min: "1",
style: { width: "35%", marginLeft: "0.4em" },
oninput: (i) => {
this.batchCount = i.target.value;
/* Even though an <input> element with a type of range logically represents a number (since
it's used for numeric input), the value it holds is still treated as a string in HTML and
JavaScript. This behavior is consistent across all <input> elements regardless of their type
(like text, number, or range), where the .value property is always a string. */
(document.getElementById("batchCountInputRange") as HTMLInputElement).value = this.batchCount.toString();
},
}),
$el("input", {
id: "batchCountInputRange",
type: "range",
min: "1",
max: "100",
value: this.batchCount,
oninput: (i) => {
this.batchCount = i.srcElement.value;
// Note
(document.getElementById("batchCountInputNumber") as HTMLInputElement).value = i.srcElement.value;
},
}),
]),
$el("div", [
$el("label", {
for: "autoQueueCheckbox",
innerHTML: "Auto Queue"
}),
$el("input", {
id: "autoQueueCheckbox",
type: "checkbox",
checked: false,
title: "Automatically queue prompt when the queue size hits 0",
onchange: (e) => {
this.autoQueueEnabled = e.target.checked;
autoQueueModeEl.style.display = this.autoQueueEnabled ? "" : "none";
}
}),
autoQueueModeEl
])
]),
(
document.getElementById(
"batchCountInputRange"
) as HTMLInputElement
).value = this.batchCount.toString();
},
}),
$el("input", {
id: "batchCountInputRange",
type: "range",
min: "1",
max: "100",
value: this.batchCount,
oninput: (i) => {
this.batchCount = i.srcElement.value;
// Note
(
document.getElementById(
"batchCountInputNumber"
) as HTMLInputElement
).value = i.srcElement.value;
},
}),
]),
$el("div", [
$el("label", {
for: "autoQueueCheckbox",
innerHTML: "Auto Queue",
}),
$el("input", {
id: "autoQueueCheckbox",
type: "checkbox",
checked: false,
title: "Automatically queue prompt when the queue size hits 0",
onchange: (e) => {
this.autoQueueEnabled = e.target.checked;
autoQueueModeEl.style.display = this.autoQueueEnabled
? ""
: "none";
},
}),
autoQueueModeEl,
]),
]
),
$el("div.comfy-menu-btns", [
$el("button", {
id: "queue-front-button",
textContent: "Queue Front",
onclick: () => app.queuePrompt(-1, this.batchCount)
onclick: () => app.queuePrompt(-1, this.batchCount),
}),
$el("button", {
$: (b) => (this.queue.button = b as HTMLButtonElement),
@@ -567,7 +634,7 @@ export class ComfyUI {
filename += ".json";
}
}
app.graphToPrompt().then(p => {
app.graphToPrompt().then((p) => {
const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
@@ -598,7 +665,7 @@ export class ComfyUI {
filename += ".json";
}
}
app.graphToPrompt().then(p => {
app.graphToPrompt().then((p) => {
const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
@@ -616,34 +683,48 @@ export class ComfyUI {
});
},
}),
$el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }),
$el("button", {
id: "comfy-load-button",
textContent: "Load",
onclick: () => fileInput.click(),
}),
$el("button", {
id: "comfy-refresh-button",
textContent: "Refresh",
onclick: () => app.refreshComboInNodes()
onclick: () => app.refreshComboInNodes(),
}),
$el("button", { id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace() }),
$el("button", {
id: "comfy-clear-button", textContent: "Clear", onclick: () => {
id: "comfy-clipspace-button",
textContent: "Clipspace",
onclick: () => app.openClipspace(),
}),
$el("button", {
id: "comfy-clear-button",
textContent: "Clear",
onclick: () => {
if (!confirmClear.value || confirm("Clear workflow?")) {
app.clean();
app.graph.clear();
app.resetView();
}
}
},
}),
$el("button", {
id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => {
id: "comfy-load-default-button",
textContent: "Load Default",
onclick: async () => {
if (!confirmClear.value || confirm("Load default workflow?")) {
app.resetView();
await app.loadGraphData()
await app.loadGraphData();
}
}
},
}),
$el("button", {
id: "comfy-reset-view-button", textContent: "Reset View", onclick: async () => {
id: "comfy-reset-view-button",
textContent: "Reset View",
onclick: async () => {
app.resetView();
}
},
}),
]) as HTMLDivElement;
@@ -652,7 +733,10 @@ export class ComfyUI {
name: "Enable Dev mode Options",
type: "boolean",
defaultValue: false,
onChange: function (value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none" },
onChange: function (value) {
document.getElementById("comfy-dev-save-api-button").style.display =
value ? "flex" : "none";
},
});
// @ts-ignore
@@ -662,7 +746,8 @@ export class ComfyUI {
}
setStatus(status) {
this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
this.queueSize.textContent =
"Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
if (status) {
if (
this.lastQueueSize != 0 &&

View File

@@ -2,63 +2,63 @@ import { ComfyDialog } from "../dialog";
import { $el } from "../../ui";
export class ComfyAsyncDialog extends ComfyDialog {
#resolve;
#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),
});
})
);
}
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();
});
show(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
super.show(html);
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
showModal(html) {
this.element.addEventListener("close", () => {
this.close();
});
showModal(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
this.element.showModal();
super.show(html);
this.element.showModal();
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
close(result = null) {
this.#resolve(result);
this.element.close();
super.close();
}
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;
}
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;
}
}

View File

@@ -19,145 +19,159 @@ import { prop } from "../../utils";
* }} ComfyButtonProps
*/
export class ComfyButton {
#over = 0;
#popupOpen = false;
isOver = false;
iconElement = $el("i.mdi");
contentElement = $el("span");
/**
* @type {import("./popup").ComfyPopup}
*/
popup;
#over = 0;
#popupOpen = false;
isOver = false;
iconElement = $el("i.mdi");
contentElement = $el("span");
/**
* @type {import("./popup").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();
}
}
/**
* @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.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.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);
});
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();
}
}
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);
};
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").ComfyPopup } popup
* @param { "click" | "hover" } mode
*/
withPopup(popup, mode = "click") {
this.popup = popup;
/**
*
* @param { import("./popup").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;
});
}
}
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();
});
popup.addEventListener("change", () => {
this.#popupOpen = popup.open;
this.updateClasses();
});
return this;
}
return this;
}
}

View File

@@ -5,41 +5,41 @@ import { ComfyButton } from "./button";
import { prop } from "../../utils";
export class ComfyButtonGroup {
element = $el("div.comfyui-button-group");
element = $el("div.comfyui-button-group");
/** @param {Array<ComfyButton | HTMLElement>} buttons */
constructor(...buttons) {
this.buttons = prop(this, "buttons", buttons, () => this.update());
}
/** @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
* @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} 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;
}
}
/** @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));
}
update() {
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
}
}

View File

@@ -5,124 +5,133 @@ import { $el } from "../../ui";
import { applyClasses } from "../utils";
export class ComfyPopup extends EventTarget {
element = $el("div.comfyui-popup");
element = $el("div.comfyui-popup");
/**
* @param {{
* target: HTMLElement,
* container?: HTMLElement,
* classList?: import("../utils").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;
/**
* @param {{
* target: HTMLElement,
* container?: HTMLElement,
* classList?: import("../utils").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);
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();
}
});
}
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;
}
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 });
#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"));
}
this.dispatchEvent(new CustomEvent("close"));
this.dispatchEvent(new CustomEvent("change"));
}
#show() {
this.element.classList.add("open");
this.update();
#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 });
}
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"));
}
this.dispatchEvent(new CustomEvent("open"));
this.dispatchEvent(new CustomEvent("change"));
}
#escHandler = (e) => {
if (e.key === "Escape") {
this.open = false;
e.preventDefault();
e.stopImmediatePropagation();
}
};
#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;
}
};
#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");
}
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");
}
};
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");
}
};
}

View File

@@ -6,38 +6,47 @@ import { prop } from "../../utils";
import { ComfyPopup } from "./popup";
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,
});
/**
* @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.arrow.withPopup(this.popup, mode);
this.items = prop(this, "items", items, () => this.update());
}
this.items = prop(this, "items", items, () => this.update());
}
update() {
this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b));
}
update() {
this.popup.element.replaceChildren(
...this.items.map((b) => b.element ?? b)
);
}
}

View File

@@ -1,6 +1,8 @@
import { $el } from "../ui";
export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarget {
export class ComfyDialog<
T extends HTMLElement = HTMLElement,
> extends EventTarget {
element: T;
textElement: HTMLElement;
#buttons: HTMLButtonElement[] | null;
@@ -9,7 +11,10 @@ export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarge
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()]),
$el("div.comfy-modal-content", [
$el("p", { $: (p) => (this.textElement = p) }),
...this.createButtons(),
]),
]) as T;
}
@@ -33,7 +38,9 @@ export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarge
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
this.textElement.replaceChildren(
...(html instanceof Array ? html : [html])
);
}
this.element.style.display = "flex";
}

View File

@@ -27,8 +27,8 @@
import { $el } from "../ui";
$el("style", {
parent: document.head,
textContent: `
parent: document.head,
textContent: `
.draggable-item {
position: relative;
will-change: transform;
@@ -40,7 +40,7 @@ $el("style", {
.draggable-item.is-draggable {
z-index: 10;
}
`
`,
});
export class DraggableList extends EventTarget {
@@ -57,9 +57,9 @@ export class DraggableList extends EventTarget {
offDrag = [];
constructor(element, itemSelector) {
super();
super();
this.listContainer = element;
this.itemSelector = itemSelector;
this.itemSelector = itemSelector;
if (!this.listContainer) return;
@@ -71,7 +71,9 @@ export class DraggableList extends EventTarget {
getAllItems() {
if (!this.items?.length) {
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
this.items = Array.from(
this.listContainer.querySelectorAll(this.itemSelector)
);
this.items.forEach((element) => {
element.classList.add("is-idle");
});
@@ -80,7 +82,9 @@ export class DraggableList extends EventTarget {
}
getIdleItems() {
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
return this.getAllItems().filter((item) =>
item.classList.contains("is-idle")
);
}
isItemAbove(item) {
@@ -106,18 +110,24 @@ export class DraggableList extends EventTarget {
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.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.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) },
detail: {
element: this.draggableItem,
position: this.getAllItems().indexOf(this.draggableItem),
},
})
);
}
@@ -250,7 +260,11 @@ export class DraggableList extends EventTarget {
this.dispatchEvent(
new CustomEvent("dragend", {
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
detail: {
element: this.draggableItem,
oldPosition,
newPosition: reorderedItems.indexOf(this.draggableItem),
},
})
);
}

View File

@@ -57,7 +57,11 @@ export function createImageHost(node) {
}
const nw = node.size[0];
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
({ cellWidth: w, cellHeight: h } = calculateImageGrid(
currentImgs,
nw - 20,
elH
));
w += "px";
h += "px";
@@ -86,10 +90,13 @@ export function createImageHost(node) {
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]);
const over = document.elementFromPoint(
app.canvas.mouse[0],
app.canvas.mouse[1]
);
el.style.pointerEvents = "none";
if(!over) return;
if (!over) return;
// Set the overIndex so Open Image etc work
const idx = currentImgs.indexOf(over);
node.overIndex = idx;

View File

@@ -48,7 +48,11 @@ export class ComfyAppMenu {
content: t,
});
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
this.logo = $el(
"h1.comfyui-logo.nlg-hide",
{ title: "ComfyUI" },
"ComfyUI"
);
this.saveButton = new ComfySplitButton(
{
primary: getSaveButton(),
@@ -71,7 +75,8 @@ export class ComfyAppMenu {
new ComfyButton({
icon: "api",
content: "Export (API Format)",
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
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,
@@ -101,7 +106,10 @@ export class ComfyAppMenu {
content: "Clear",
tooltip: "Clears current workflow",
action: () => {
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
if (
!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) ||
confirm("Clear workflow?")
) {
app.clean();
app.graph.clear();
}
@@ -126,7 +134,9 @@ export class ComfyAppMenu {
this.mobileMenuButton = new ComfyButton({
icon: "menu",
action: (_, btn) => {
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
btn.icon = this.element.classList.toggle("expanded")
? "menu-open"
: "menu";
window.dispatchEvent(new Event("resize"));
},
classList: "comfyui-button comfyui-menu-button",
@@ -239,7 +249,10 @@ export class ComfyAppMenu {
idx--;
}
} else if (innerSize > this.element.clientWidth) {
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(
window.innerWidth,
innerSize
);
// We need to shrink
if (idx < this.#sizeBreaks.length - 1) {
idx++;
@@ -254,19 +267,26 @@ export class ComfyAppMenu {
clearTimeout(this.#cacheTimeout);
if (this.#cachedInnerSize) {
// Extend cache time
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
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
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);
this.#cacheTimeout = setTimeout(
() => (this.#cachedInnerSize = null),
100
);
}
return this.#cachedInnerSize;
}

View File

@@ -14,7 +14,10 @@ export class ComfyQueueButton {
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);
await this.app.queuePrompt(
e?.shiftKey ? -1 : 0,
this.queueOptions.batchCount
);
};
constructor(app) {
@@ -63,7 +66,10 @@ export class ComfyQueueButton {
}
});
this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"]));
this.queueOptions.addEventListener(
"autoQueueMode",
(e) => (this.autoQueueMode = e["detail"])
);
api.addEventListener("graphChanged", () => {
if (this.autoQueueMode === "change") {
@@ -79,10 +85,14 @@ export class ComfyQueueButton {
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.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)) {
if (
this.autoQueueMode === "instant" ||
(this.autoQueueMode === "change" && this.graphHasChanged)
) {
this.graphHasChanged = false;
this.queuePrompt();
}

View File

@@ -69,11 +69,14 @@ export class ComfyViewList {
},
});
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,
]);
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) {
@@ -155,7 +158,9 @@ export class ComfyViewList {
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
await this.app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow
);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}

View File

@@ -31,7 +31,9 @@ export class ComfyViewQueueList extends ComfyViewList {
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
await this.app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow
);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
@@ -51,5 +53,5 @@ export class ComfyViewQueueList extends ComfyViewList {
},
],
};
}
};
}

View File

@@ -37,14 +37,21 @@ export class ComfyWorkflowsMenu {
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]),
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.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", () => {
@@ -85,7 +92,10 @@ export class ComfyWorkflowsMenu {
};
#bindEvents() {
this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive);
this.app.workflowManager.addEventListener(
"changeWorkflow",
this.#updateActive
);
this.app.workflowManager.addEventListener("rename", this.#updateActive);
this.app.workflowManager.addEventListener("delete", this.#updateActive);
@@ -157,10 +167,15 @@ export class ComfyWorkflowsMenu {
name: "Comfy.Workflows",
async beforeRegisterNodeDef(nodeType) {
function getImageWidget(node) {
const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional };
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"));
const imageWidget = node.widgets.find(
(w) => w.name === (inputs[input]?.[1]?.widget ?? "image")
);
if (imageWidget) return imageWidget;
}
}
@@ -213,8 +228,13 @@ export class ComfyWorkflowsMenu {
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);
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
@@ -238,10 +258,13 @@ export class ComfyWorkflowsMenu {
submenu: {
options: [
{
callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow),
callback: () =>
sendToWorkflow(img, app.workflowManager.activeWorkflow),
title: "[Current workflow]",
},
...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)),
...self.#getFavoriteMenuOptions(
sendToWorkflow.bind(null, img)
),
null,
...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
],
@@ -315,7 +338,9 @@ export class ComfyWorkflowsContent {
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.popup.addEventListener("close", () =>
this.element.replaceChildren(this.actions, this.spinner)
);
this.app.workflowManager.addEventListener("favorite", (e) => {
const workflow = e["detail"];
@@ -331,7 +356,9 @@ export class ComfyWorkflowsContent {
app.workflowManager.addEventListener(e, () => this.updateOpen());
}
this.app.workflowManager.addEventListener("rename", () => this.load());
this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive());
this.app.workflowManager.addEventListener("execute", (e) =>
this.#updateActive()
);
}
async load() {
@@ -339,7 +366,12 @@ export class ComfyWorkflowsContent {
this.updateTree();
this.updateFavorites();
this.updateOpen();
this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement);
this.element.replaceChildren(
this.actions,
this.openElement,
this.favoritesElement,
this.treeElement
);
}
updateOpen() {
@@ -368,7 +400,7 @@ export class ComfyWorkflowsContent {
if (w.unsaved) {
wrapper.element.classList.add("unsaved");
}
if(w === this.app.workflowManager.activeWorkflow) {
if (w === this.app.workflowManager.activeWorkflow) {
wrapper.element.classList.add("active");
}
@@ -383,7 +415,9 @@ export class ComfyWorkflowsContent {
updateFavorites() {
const current = this.favoritesElement;
const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)];
const favorites = [
...this.app.workflowManager.workflows.filter((w) => w.isFavorite),
];
this.favoritesElement = $el("div.comfyui-workflows-favorites", [
$el("h3", "Favorites"),
@@ -437,7 +471,10 @@ export class ComfyWorkflowsContent {
hideTreeParents(element) {
// Hide all parents if no children are visible
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
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") {
@@ -450,7 +487,10 @@ export class ComfyWorkflowsContent {
}
showTreeParents(element) {
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
if (
element.parentElement?.classList.contains("comfyui-workflows-tree") ===
false
) {
element.parentElement.style.removeProperty("display");
this.showTreeParents(element.parentElement);
}
@@ -490,7 +530,9 @@ export class ComfyWorkflowsContent {
for (let i = 0; i < workflow.pathParts.length; i++) {
currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i];
const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot);
const parentNode =
nodes[currentPath] ??
this.#createNode(currentPath, workflow, i, currentRoot);
nodes[currentPath] = parentNode;
currentRoot = parentNode;
@@ -559,7 +601,9 @@ export class ComfyWorkflowsContent {
/** @param {ComfyWorkflow} workflow */
#getFavoriteTooltip(workflow) {
return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites";
return workflow.isFavorite
? "Remove this workflow from your favorites"
: "Add this workflow to your favorites";
}
/** @param {ComfyWorkflow} workflow */
@@ -568,7 +612,9 @@ export class ComfyWorkflowsContent {
icon: this.#getFavoriteIcon(workflow),
overIcon: this.#getFavoriteOverIcon(workflow),
iconSize: 18,
classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""),
classList:
"comfyui-button comfyui-workflows-file-action-favorite" +
(primary ? " comfyui-workflows-file-action-primary" : ""),
tooltip: this.#getFavoriteTooltip(workflow),
action: (e) => {
e.stopImmediatePropagation();
@@ -628,7 +674,9 @@ export class ComfyWorkflowsContent {
#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.",
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,
@@ -646,7 +694,11 @@ export class ComfyWorkflowsContent {
#getWorkflowElement(workflow) {
return new WorkflowElement(this, workflow, {
primary: this.#getFavoriteButton(workflow, true),
buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)],
buttons: [
this.#getInsertButton(workflow),
this.#getRenameButton(workflow),
this.#getDeleteButton(workflow),
],
});
}
@@ -660,14 +712,17 @@ export class ComfyWorkflowsContent {
#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();
};
},
});
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
@@ -676,7 +731,10 @@ export class ComfyWorkflowsContent {
if (leaf) {
nodeElement = this.#createLeafNode(workflow).element;
} else {
nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]);
nodeElement = $el("li", [
$el("i.mdi.mdi-18px.mdi-folder"),
$el("span", part),
]);
}
parentNode.append(nodeElement);
return parentNode;
@@ -703,7 +761,11 @@ class WorkflowElement {
},
title: this.workflow.path,
},
[this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)]
[
this.primary?.element,
$el("span", workflow.name),
...buttons.map((b) => b.element),
]
);
}
}
@@ -732,7 +794,11 @@ class WidgetSelectionDialog extends ComfyAsyncDialog {
"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(
"span",
{ dataset: { id: opt.node.id } },
`${opt.node.title ?? opt.node.type} ${opt.widget.name}`
),
$el(
"button.comfyui-button",
{
@@ -760,4 +826,4 @@ class WidgetSelectionDialog extends ComfyAsyncDialog {
])
);
}
}
}

View File

@@ -19,7 +19,14 @@ interface SettingOption {
interface SettingParams {
id: string;
name: string;
type: string | ((name: string, setter: (v: any) => void, value: any, attrs: any) => HTMLElement);
type:
| string
| ((
name: string,
setter: (v: any) => void,
value: any,
attrs: any
) => HTMLElement);
defaultValue: any;
onChange?: (newValue: any, oldValue?: any) => void;
attrs?: any;
@@ -81,7 +88,7 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
new CustomEvent(id + ".change", {
detail: {
value,
oldValue
oldValue,
},
})
);
@@ -115,8 +122,7 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
if (this.app.storageLocation === "browser") {
try {
value = JSON.parse(value);
} catch (error) {
}
} catch (error) {}
}
}
return value ?? defaultValue;
@@ -145,7 +151,16 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
}
addSetting(params: SettingParams) {
const { id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined } = params;
const {
id,
name,
type,
defaultValue,
onChange,
attrs = {},
tooltip = "",
options = undefined,
} = params;
if (!id) {
throw new Error("Settings must have an ID");
}
@@ -272,7 +287,8 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
style: { maxWidth: "4rem" },
oninput: (e) => {
setter(e.target.value);
e.target.previousElementSibling.value = e.target.value;
e.target.previousElementSibling.value =
e.target.value;
},
}),
]
@@ -291,7 +307,10 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
setter(e.target.value);
},
},
(typeof options === "function" ? options(value) : options || []).map((opt) => {
(typeof options === "function"
? options(value)
: options || []
).map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
@@ -309,7 +328,9 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
case "text":
default:
if (type !== "text") {
console.warn(`Unsupported setting type '${type}, defaulting to text`);
console.warn(
`Unsupported setting type '${type}, defaulting to text`
);
}
element = $el("tr", [
@@ -356,7 +377,10 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
},
[$el("th"), $el("th", { style: { width: "33%" } })]
),
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean)
...this.settings
.sort((a, b) => a.name.localeCompare(b.name))
.map((s) => s.render())
.filter(Boolean)
);
this.element.showModal();
}

View File

@@ -1,6 +1,5 @@
import "./spinner.css";
export function createSpinner() {
const div = document.createElement("div");
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;

View File

@@ -20,7 +20,10 @@ export function toggleSwitch(name, items, e?) {
if (selectedIndex != null) {
elements[selectedIndex].classList.remove("comfy-toggle-selected");
}
onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] });
onChange?.({
item: items[index],
prev: selectedIndex == null ? undefined : items[selectedIndex],
});
selectedIndex = index;
elements[selectedIndex].classList.add("comfy-toggle-selected");
}

View File

@@ -3,16 +3,14 @@ import { $el } from "../ui";
import { createSpinner } from "./spinner";
import "./userSelection.css";
interface SelectedUser {
username: string;
userId: string;
created: boolean;
}
export class UserSelectionScreen {
async show(users, user): Promise<SelectedUser>{
async show(users, user): Promise<SelectedUser> {
const userSelection = document.getElementById("comfy-user-selection");
userSelection.style.display = "";
return new Promise((resolve) => {
@@ -22,7 +20,9 @@ export class UserSelectionScreen {
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];
const button = userSelection.getElementsByClassName(
"comfy-user-button-next"
)[0];
let inputActive = null;
input.addEventListener("focus", () => {
@@ -45,7 +45,8 @@ export class UserSelectionScreen {
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (inputActive == null) {
error.textContent = "Please enter a username or select an existing user.";
error.textContent =
"Please enter a username or select an existing user.";
} else if (inputActive) {
const username = input.value.trim();
if (!username) {
@@ -54,41 +55,59 @@ export class UserSelectionScreen {
}
// Create new user
// @ts-ignore
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
input.disabled = select.disabled = input.readonly = select.readonly = true;
input.disabled =
select.disabled =
// @ts-ignore
input.readonly =
// @ts-ignore
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;
let message =
"Error creating user: " + resp.status + " " + resp.statusText;
try {
const res = await resp.json();
if(res.error) {
if (res.error) {
message = res.error;
}
} catch (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.";
// @ts-ignore
error.textContent =
err.message ??
err.statusText ??
err ??
"An unknown error occurred.";
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
input.disabled = select.disabled = input.readonly = select.readonly = false;
input.disabled =
select.disabled =
// @ts-ignore
input.readonly =
// @ts-ignore
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 });
resolve({
username: users[select.value],
userId: select.value,
created: false,
});
}
});

View File

@@ -57,7 +57,9 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
// Find node with matching S&R property name
// @ts-ignore
let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]);
let nodes = app.graph._nodes.filter(
(n) => n.properties?.["Node name for S&R"] === split[0]
);
// If we cant, see if there is a node with that title
if (!nodes.length) {
// @ts-ignore
@@ -76,7 +78,13 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
const widget = node.widgets?.find((w) => w.name === split[1]);
if (!widget) {
console.warn("Unable to find widget", split[1], "on node", split[0], node);
console.warn(
"Unable to find widget",
split[1],
"on node",
split[0],
node
);
return match;
}
@@ -84,13 +92,19 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
});
}
export async function addStylesheet(urlOrFile: string, relativeTo?: string): Promise<void> {
export async function addStylesheet(
urlOrFile: string,
relativeTo?: string
): Promise<void> {
return new Promise((res, rej) => {
let url;
if (urlOrFile.endsWith(".js")) {
url = urlOrFile.substr(0, urlOrFile.length - 2) + "css";
} else {
url = new URL(urlOrFile, relativeTo ?? `${window.location.protocol}//${window.location.host}`).toString();
url = new URL(
urlOrFile,
relativeTo ?? `${window.location.protocol}//${window.location.host}`
).toString();
}
$el("link", {
parent: document.head,
@@ -103,7 +117,6 @@ export async function addStylesheet(urlOrFile: string, relativeTo?: string): Pro
});
}
/**
* @param { string } filename
* @param { Blob } blob
@@ -147,7 +160,10 @@ export function prop(target, name, defaultValue, onChanged) {
export function getStorageValue(id) {
const clientId = api.clientId ?? api.initialClientId;
return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id);
return (
(clientId && sessionStorage.getItem(`${id}:${clientId}`)) ??
localStorage.getItem(id)
);
}
export function setStorageValue(id, value) {
@@ -156,4 +172,4 @@ export function setStorageValue(id, value) {
sessionStorage.setItem(`${id}:${clientId}`, value);
}
localStorage.setItem(id, value);
}
}

View File

@@ -1,20 +1,23 @@
import { api } from "./api"
import { api } from "./api";
import "./domWidget";
import type { ComfyApp } from "./app";
import type { IWidget, LGraphNode } from "/types/litegraph";
import { ComfyNodeDef } from "/types/apiTypes";
export type ComfyWidgetConstructor = (
node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app?: ComfyApp, widgetName?: string) =>
{widget: IWidget, minWidth?: number; minHeight?: number };
node: LGraphNode,
inputName: string,
inputData: ComfyNodeDef,
app?: ComfyApp,
widgetName?: string
) => { widget: IWidget; minWidth?: number; minHeight?: number };
let controlValueRunBefore = false;
export function updateControlWidgetLabel(widget) {
let replacement = "after";
let find = "before";
if (controlValueRunBefore) {
[find, replacement] = [replacement, find]
[find, replacement] = [replacement, find];
}
widget.label = (widget.label ?? widget.name).replace(find, replacement);
}
@@ -22,9 +25,14 @@ export function updateControlWidgetLabel(widget) {
const IS_CONTROL_WIDGET = Symbol();
const HAS_EXECUTED = Symbol();
function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enable_rounding) {
function getNumberDefaults(
inputData: ComfyNodeDef,
defaultStep,
precision,
enable_rounding
) {
let defaultVal = inputData[1]["default"];
let { min, max, step, round} = inputData[1];
let { min, max, step, round } = inputData[1];
if (defaultVal == undefined) defaultVal = 0;
if (min == undefined) min = 0;
@@ -33,30 +41,52 @@ function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enab
// precision is the number of decimal places to show.
// by default, display the the smallest number of decimal places such that changes of size step are visible.
if (precision == undefined) {
precision = Math.max(-Math.floor(Math.log10(step)),0);
precision = Math.max(-Math.floor(Math.log10(step)), 0);
}
if (enable_rounding && (round == undefined || round === true)) {
// by default, round the value to those decimal places shown.
round = Math.round(1000000*Math.pow(0.1,precision))/1000000;
round = Math.round(1000000 * Math.pow(0.1, precision)) / 1000000;
}
return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } };
return {
val: defaultVal,
config: { min, max, step: 10.0 * step, round, precision },
};
}
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values, widgetName, inputData: ComfyNodeDef) {
export function addValueControlWidget(
node,
targetWidget,
defaultValue = "randomize",
values,
widgetName,
inputData: ComfyNodeDef
) {
let name = inputData[1]?.control_after_generate;
if(typeof name !== "string") {
if (typeof name !== "string") {
name = widgetName;
}
const widgets = addValueControlWidgets(node, targetWidget, defaultValue, {
addFilterList: false,
controlAfterGenerateName: name
}, inputData);
const widgets = addValueControlWidgets(
node,
targetWidget,
defaultValue,
{
addFilterList: false,
controlAfterGenerateName: name,
},
inputData
);
return widgets[0];
}
export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData: ComfyNodeDef) {
export function addValueControlWidgets(
node,
targetWidget,
defaultValue = "randomize",
options,
inputData: ComfyNodeDef
) {
if (!defaultValue) defaultValue = "randomize";
if (!options) options = {};
@@ -67,10 +97,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
} else if (typeof inputData?.[1]?.[defaultName] === "string") {
name = inputData?.[1]?.[defaultName];
} else if (inputData?.[1]?.control_prefix) {
name = inputData?.[1]?.control_prefix + " " + name
name = inputData?.[1]?.control_prefix + " " + name;
}
return name;
}
};
const widgets = [];
const valueControl = node.addWidget(
@@ -120,16 +150,23 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
const regex = new RegExp(filter.substring(1, filter.length - 1));
check = (item) => regex.test(item);
} catch (error) {
console.error("Error constructing RegExp filter for node " + node.id, filter, error);
console.error(
"Error constructing RegExp filter for node " + node.id,
filter,
error
);
}
}
if (!check) {
const lower = filter.toLocaleLowerCase();
check = (item) => item.toLocaleLowerCase().includes(lower);
}
values = values.filter(item => check(item));
values = values.filter((item) => check(item));
if (!values.length && targetWidget.options.values.length) {
console.warn("Filter for node " + node.id + " has filtered out all items", filter);
console.warn(
"Filter for node " + node.id + " has filtered out all items",
filter
);
}
}
let current_index = values.indexOf(targetWidget.value);
@@ -141,8 +178,8 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
break;
case "increment-wrap":
current_index += 1;
if ( current_index >= current_length ) {
current_index = 0;
if (current_index >= current_length) {
current_index = 0;
}
break;
case "decrement":
@@ -181,7 +218,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
targetWidget.value -= targetWidget.options.step / 10;
break;
case "randomize":
targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min;
targetWidget.value =
Math.floor(Math.random() * range) *
(targetWidget.options.step / 10) +
min;
break;
default:
break;
@@ -190,8 +230,7 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
* ranges and set them to min or max.*/
if (targetWidget.value < min) targetWidget.value = min;
if (targetWidget.value > max)
targetWidget.value = max;
if (targetWidget.value > max) targetWidget.value = max;
targetWidget.callback(targetWidget.value);
}
};
@@ -213,20 +252,39 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
};
return widgets;
};
}
function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) {
const seed = createIntWidget(node, inputName, inputData, app, true);
const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData);
const seedControl = addValueControlWidget(
node,
seed.widget,
"randomize",
undefined,
widgetName,
inputData
);
seed.widget.linkedWidgets = [seedControl];
return seed;
}
function createIntWidget(node, inputName, inputData: ComfyNodeDef, app, isSeedInput: boolean = false) {
function createIntWidget(
node,
inputName,
inputData: ComfyNodeDef,
app,
isSeedInput: boolean = false
) {
const control = inputData[1]?.control_after_generate;
if (!isSeedInput && control) {
return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined);
return seedWidget(
node,
inputName,
inputData,
app,
typeof control === "string" ? control : undefined
);
}
let widgetType = isSlider(inputData[1]["display"], app);
@@ -275,10 +333,10 @@ function addMultilineWidget(node, name, opts, app) {
function isSlider(display, app) {
if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
return "number"
return "number";
}
return (display==="slider") ? "slider" : "number"
return display === "slider" ? "slider" : "number";
}
export function initWidgets(app) {
@@ -288,7 +346,8 @@ export function initWidgets(app) {
type: "combo",
defaultValue: "after",
options: ["before", "after"],
tooltip: "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
tooltip:
"Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
onChange(value) {
controlValueRunBefore = value === "before";
for (const n of app.graph._nodes) {
@@ -313,21 +372,41 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
"INT:seed": seedWidget,
"INT:noise_seed": seedWidget,
FLOAT(node, inputName, inputData: ComfyNodeDef, app) {
let widgetType: "number" | "slider" = isSlider(inputData[1]["display"], app);
let precision = app.ui.settings.getSettingValue("Comfy.FloatRoundingPrecision");
let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding")
let widgetType: "number" | "slider" = isSlider(
inputData[1]["display"],
app
);
let precision = app.ui.settings.getSettingValue(
"Comfy.FloatRoundingPrecision"
);
let disable_rounding = app.ui.settings.getSettingValue(
"Comfy.DisableFloatRounding"
);
if (precision == 0) precision = undefined;
const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding);
return { widget: node.addWidget(widgetType, inputName, val,
function (v) {
if (config.round) {
this.value = Math.round((v + Number.EPSILON)/config.round)*config.round;
if (this.value > config.max) this.value = config.max;
if (this.value < config.min) this.value = config.min;
} else {
this.value = v;
}
}, config) };
const { val, config } = getNumberDefaults(
inputData,
0.5,
precision,
!disable_rounding
);
return {
widget: node.addWidget(
widgetType,
inputName,
val,
function (v) {
if (config.round) {
this.value =
Math.round((v + Number.EPSILON) / config.round) * config.round;
if (this.value > config.max) this.value = config.max;
if (this.value < config.min) this.value = config.min;
} else {
this.value = v;
}
},
config
),
};
},
INT(node, inputName, inputData: ComfyNodeDef, app) {
return createIntWidget(node, inputName, inputData, app);
@@ -336,12 +415,9 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
let defaultVal = false;
let options = {};
if (inputData[1]) {
if (inputData[1].default)
defaultVal = inputData[1].default;
if (inputData[1].label_on)
options["on"] = inputData[1].label_on;
if (inputData[1].label_off)
options["off"] = inputData[1].label_off;
if (inputData[1].default) defaultVal = inputData[1].default;
if (inputData[1].label_on) options["on"] = inputData[1].label_on;
if (inputData[1].label_off) options["off"] = inputData[1].label_off;
}
return {
widget: node.addWidget(
@@ -349,8 +425,8 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
inputName,
defaultVal,
() => {},
options,
)
options
),
};
},
STRING(node, inputName, inputData: ComfyNodeDef, app) {
@@ -359,12 +435,19 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
let res;
if (multiline) {
res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
res = addMultilineWidget(
node,
inputName,
{ defaultVal, ...inputData[1] },
app
);
} else {
res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
res = {
widget: node.addWidget("text", inputName, defaultVal, () => {}, {}),
};
}
if(inputData[1].dynamicPrompts != undefined)
if (inputData[1].dynamicPrompts != undefined)
res.widget.dynamicPrompts = inputData[1].dynamicPrompts;
return res;
@@ -375,18 +458,35 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
if (inputData[1] && inputData[1].default) {
defaultValue = inputData[1].default;
}
const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
const res = {
widget: node.addWidget("combo", inputName, defaultValue, () => {}, {
values: type,
}),
};
if (inputData[1]?.control_after_generate) {
// TODO make combo handle a widget node type?
// @ts-ignore
res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData);
res.widget.linkedWidgets = addValueControlWidgets(
node,
res.widget,
undefined,
undefined,
inputData
);
}
return res;
},
IMAGEUPLOAD(node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app) {
IMAGEUPLOAD(
node: LGraphNode,
inputName: string,
inputData: ComfyNodeDef,
app
) {
// TODO make image upload handle a custom node type?
// @ts-ignore
const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image"));
const imageWidget = node.widgets.find(
(w) => w.name === (inputData[1]?.widget ?? "image")
);
let uploadWidget;
function showImage(name) {
@@ -402,18 +502,20 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
subfolder = name.substring(0, folder_separator);
name = name.substring(folder_separator + 1);
}
img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`);
img.src = api.apiURL(
`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
);
// @ts-ignore
node.setSizeForImage?.();
}
var default_value = imageWidget.value;
Object.defineProperty(imageWidget, "value", {
set : function(value) {
set: function (value) {
this._real_value = value;
},
get : function() {
get: function () {
if (!this._real_value) {
return default_value;
}
@@ -428,11 +530,11 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
value += real_value.filename;
if(real_value.type && real_value.type !== "input")
if (real_value.type && real_value.type !== "input")
value += ` [${real_value.type}]`;
}
return value;
}
},
});
// Add our own callback to the combo widget to render an image when it changes
@@ -535,15 +637,15 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
};
// @ts-ignore
node.pasteFile = function(file) {
node.pasteFile = function (file) {
if (file.type.startsWith("image/")) {
const is_pasted = (file.name === "image.png") &&
(file.lastModified - Date.now() < 2000);
const is_pasted =
file.name === "image.png" && file.lastModified - Date.now() < 2000;
uploadFile(file, true, is_pasted);
return true;
}
return false;
}
};
return { widget: uploadWidget };
},

View File

@@ -57,7 +57,10 @@ export class ComfyWorkflowManager extends EventTarget {
#bindExecutionEvents() {
// TODO: on reload, set active prompt based on the latest ws message
const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt }));
const emit = () =>
this.dispatchEvent(
new CustomEvent("execute", { detail: this.activePrompt })
);
let executing = null;
api.addEventListener("execution_start", (e) => {
this.#activePromptId = e.detail.prompt_id;
@@ -106,14 +109,21 @@ export class ComfyWorkflowManager extends EventTarget {
favorites = new Set();
}
const workflows = (await api.listUserData("workflows", true, true)).map((w) => {
let workflow = this.workflowLookup[w[0]];
if (!workflow) {
workflow = new ComfyWorkflow(this, w[0], w.slice(1), favorites.has(w[0]));
this.workflowLookup[workflow.path] = workflow;
const workflows = (await api.listUserData("workflows", true, true)).map(
(w) => {
let workflow = this.workflowLookup[w[0]];
if (!workflow) {
workflow = new ComfyWorkflow(
this,
w[0],
w.slice(1),
favorites.has(w[0])
);
this.workflowLookup[workflow.path] = workflow;
}
return workflow;
}
return workflow;
});
);
this.workflows = workflows;
} catch (error) {
@@ -124,7 +134,9 @@ export class ComfyWorkflowManager extends EventTarget {
async saveWorkflowMetadata() {
await api.storeUserData("workflows/.index.json", {
favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)],
favorites: [
...this.workflows.filter((w) => w.isFavorite).map((w) => w.path),
],
});
}
@@ -137,13 +149,20 @@ export class ComfyWorkflowManager extends EventTarget {
const found = this.workflows.find((w) => w.path === workflow);
if (found) {
workflow = found;
workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
workflow.unsaved =
!workflow ||
getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
}
}
if (!(workflow instanceof ComfyWorkflow)) {
// Still not found, either reloading a deleted workflow or blank
workflow = new ComfyWorkflow(this, workflow || "Unsaved Workflow" + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : ""));
workflow = new ComfyWorkflow(
this,
workflow ||
"Unsaved Workflow" +
(this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "")
);
}
const index = this.openWorkflows.indexOf(workflow);
@@ -293,7 +312,9 @@ export class ComfyWorkflow {
async getWorkflowData() {
const resp = await api.getUserData("workflows/" + this.path);
if (resp.status !== 200) {
alert(`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
alert(
`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`
);
return;
}
return await resp.json();
@@ -301,7 +322,12 @@ export class ComfyWorkflow {
load = async () => {
if (this.isOpen) {
await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this);
await this.manager.app.loadGraphData(
this.changeTracker.activeState,
true,
true,
this
);
} else {
const data = await this.getWorkflowData();
if (!data) return;
@@ -327,7 +353,12 @@ export class ComfyWorkflow {
await this.manager.saveWorkflowMetadata();
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
} catch (error) {
alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error));
alert(
"Error favoriting workflow " +
this.path +
"\n" +
(error.message ?? error)
);
}
}
@@ -336,15 +367,29 @@ export class ComfyWorkflow {
*/
async rename(path) {
path = appendJsonExt(path);
let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path);
let resp = await api.moveUserData(
"workflows/" + this.path,
"workflows/" + path
);
if (resp.status === 409) {
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return resp;
resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path, { overwrite: true });
if (
!confirm(
`Workflow '${path}' already exists, do you want to overwrite it?`
)
)
return resp;
resp = await api.moveUserData(
"workflows/" + this.path,
"workflows/" + path,
{ overwrite: true }
);
}
if (resp.status !== 200) {
alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
alert(
`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`
);
return;
}
@@ -367,7 +412,10 @@ export class ComfyWorkflow {
const old = localStorage.getItem("litegrapheditor_clipboard");
const graph = new LGraph(data);
const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true });
const canvas = new LGraphCanvas(null, graph, {
skip_events: true,
skip_render: true,
});
canvas.selectNodes();
canvas.copyToClipboard();
this.manager.app.canvas.pasteFromClipboard();
@@ -406,7 +454,10 @@ export class ComfyWorkflow {
*/
async #save(path, overwrite) {
if (!path) {
path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow");
path = prompt(
"Save workflow as:",
trimJsonExt(this.path) ?? this.name ?? "workflow"
);
if (!path) return;
}
@@ -414,14 +465,27 @@ export class ComfyWorkflow {
const p = await this.manager.app.graphToPrompt();
const json = JSON.stringify(p.workflow, null, 2);
let resp = await api.storeUserData("workflows/" + path, json, { stringify: false, throwOnError: false, overwrite });
let resp = await api.storeUserData("workflows/" + path, json, {
stringify: false,
throwOnError: false,
overwrite,
});
if (resp.status === 409) {
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return;
resp = await api.storeUserData("workflows/" + path, json, { stringify: false });
if (
!confirm(
`Workflow '${path}' already exists, do you want to overwrite it?`
)
)
return;
resp = await api.storeUserData("workflows/" + path, json, {
stringify: false,
});
}
if (resp.status !== 200) {
alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`);
alert(
`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`
);
return;
}