mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 15:10:06 +00:00
Format all code / Add pre-commit format hook (#81)
* Add format-guard * Format code
This commit is contained in:
File diff suppressed because it is too large
Load Diff
5471
src/scripts/app.ts
5471
src/scripts/app.ts
File diff suppressed because it is too large
Load Diff
@@ -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({});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user