mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
Replace \t with spaces (#80)
This commit is contained in:
@@ -9,423 +9,423 @@ import type { LGraphNode, LGraphNodeConstructor } from "/types/litegraph";
|
||||
const ORDER: symbol = Symbol();
|
||||
|
||||
function merge(target, source) {
|
||||
if (typeof target === "object" && typeof source === "object") {
|
||||
for (const key in source) {
|
||||
const sv = source[key];
|
||||
if (typeof sv === "object") {
|
||||
let tv = target[key];
|
||||
if (!tv) tv = target[key] = {};
|
||||
merge(tv, source[key]);
|
||||
} else {
|
||||
target[key] = sv;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof target === "object" && typeof source === "object") {
|
||||
for (const key in source) {
|
||||
const sv = source[key];
|
||||
if (typeof sv === "object") {
|
||||
let tv = target[key];
|
||||
if (!tv) tv = target[key] = {};
|
||||
merge(tv, source[key]);
|
||||
} else {
|
||||
target[key] = sv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
return target;
|
||||
}
|
||||
|
||||
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
tabs: Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}>;
|
||||
selectedNodeIndex: number | null | undefined;
|
||||
selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs";
|
||||
selectedGroup: string | undefined;
|
||||
modifications: Record<string, Record<string, Record<string, { name?: string | undefined, visible?: boolean | undefined }>>> = {};
|
||||
nodeItems: any[];
|
||||
app: ComfyApp;
|
||||
groupNodeType: LGraphNodeConstructor<LGraphNode>;
|
||||
groupNodeDef: any;
|
||||
groupData: any;
|
||||
tabs: Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}>;
|
||||
selectedNodeIndex: number | null | undefined;
|
||||
selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs";
|
||||
selectedGroup: string | undefined;
|
||||
modifications: Record<string, Record<string, Record<string, { name?: string | undefined, visible?: boolean | undefined }>>> = {};
|
||||
nodeItems: any[];
|
||||
app: ComfyApp;
|
||||
groupNodeType: LGraphNodeConstructor<LGraphNode>;
|
||||
groupNodeDef: any;
|
||||
groupData: any;
|
||||
|
||||
innerNodesList: HTMLUListElement;
|
||||
widgetsPage: HTMLElement;
|
||||
inputsPage: HTMLElement;
|
||||
outputsPage: HTMLElement;
|
||||
draggable: any;
|
||||
innerNodesList: HTMLUListElement;
|
||||
widgetsPage: HTMLElement;
|
||||
inputsPage: HTMLElement;
|
||||
outputsPage: HTMLElement;
|
||||
draggable: any;
|
||||
|
||||
get selectedNodeInnerIndex() {
|
||||
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex;
|
||||
}
|
||||
get selectedNodeInnerIndex() {
|
||||
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex;
|
||||
}
|
||||
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.element = $el("dialog.comfy-group-manage", {
|
||||
parent: document.body,
|
||||
}) as HTMLDialogElement;
|
||||
}
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.element = $el("dialog.comfy-group-manage", {
|
||||
parent: document.body,
|
||||
}) as HTMLDialogElement;
|
||||
}
|
||||
|
||||
changeTab(tab) {
|
||||
this.tabs[this.selectedTab].tab.classList.remove("active");
|
||||
this.tabs[this.selectedTab].page.classList.remove("active");
|
||||
this.tabs[tab].tab.classList.add("active");
|
||||
this.tabs[tab].page.classList.add("active");
|
||||
this.selectedTab = tab;
|
||||
}
|
||||
changeTab(tab) {
|
||||
this.tabs[this.selectedTab].tab.classList.remove("active");
|
||||
this.tabs[this.selectedTab].page.classList.remove("active");
|
||||
this.tabs[tab].tab.classList.add("active");
|
||||
this.tabs[tab].page.classList.add("active");
|
||||
this.selectedTab = tab;
|
||||
}
|
||||
|
||||
changeNode(index, force?) {
|
||||
if (!force && this.selectedNodeIndex === index) return;
|
||||
changeNode(index, force?) {
|
||||
if (!force && this.selectedNodeIndex === index) return;
|
||||
|
||||
if (this.selectedNodeIndex != null) {
|
||||
this.nodeItems[this.selectedNodeIndex].classList.remove("selected");
|
||||
}
|
||||
this.nodeItems[index].classList.add("selected");
|
||||
this.selectedNodeIndex = index;
|
||||
if (this.selectedNodeIndex != null) {
|
||||
this.nodeItems[this.selectedNodeIndex].classList.remove("selected");
|
||||
}
|
||||
this.nodeItems[index].classList.add("selected");
|
||||
this.selectedNodeIndex = index;
|
||||
|
||||
if (!this.buildInputsPage() && this.selectedTab === "Inputs") {
|
||||
this.changeTab("Widgets");
|
||||
}
|
||||
if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") {
|
||||
this.changeTab("Outputs");
|
||||
}
|
||||
if (!this.buildOutputsPage() && this.selectedTab === "Outputs") {
|
||||
this.changeTab("Inputs");
|
||||
}
|
||||
if (!this.buildInputsPage() && this.selectedTab === "Inputs") {
|
||||
this.changeTab("Widgets");
|
||||
}
|
||||
if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") {
|
||||
this.changeTab("Outputs");
|
||||
}
|
||||
if (!this.buildOutputsPage() && this.selectedTab === "Outputs") {
|
||||
this.changeTab("Inputs");
|
||||
}
|
||||
|
||||
this.changeTab(this.selectedTab);
|
||||
}
|
||||
this.changeTab(this.selectedTab);
|
||||
}
|
||||
|
||||
getGroupData() {
|
||||
this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
|
||||
this.groupNodeDef = this.groupNodeType.nodeData;
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
|
||||
}
|
||||
getGroupData() {
|
||||
this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
|
||||
this.groupNodeDef = this.groupNodeType.nodeData;
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
|
||||
}
|
||||
|
||||
changeGroup(group, reset = true) {
|
||||
this.selectedGroup = group;
|
||||
this.getGroupData();
|
||||
changeGroup(group, reset = true) {
|
||||
this.selectedGroup = group;
|
||||
this.getGroupData();
|
||||
|
||||
const nodes = this.groupData.nodeData.nodes;
|
||||
this.nodeItems = nodes.map((n, i) =>
|
||||
$el(
|
||||
"li.draggable-item",
|
||||
{
|
||||
dataset: {
|
||||
nodeindex: n.index + "",
|
||||
},
|
||||
onclick: () => {
|
||||
this.changeNode(i);
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("span.drag-handle"),
|
||||
$el(
|
||||
"div",
|
||||
{
|
||||
textContent: n.title ?? n.type,
|
||||
},
|
||||
n.title
|
||||
? $el("span", {
|
||||
textContent: n.type,
|
||||
})
|
||||
: []
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
const nodes = this.groupData.nodeData.nodes;
|
||||
this.nodeItems = nodes.map((n, i) =>
|
||||
$el(
|
||||
"li.draggable-item",
|
||||
{
|
||||
dataset: {
|
||||
nodeindex: n.index + "",
|
||||
},
|
||||
onclick: () => {
|
||||
this.changeNode(i);
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("span.drag-handle"),
|
||||
$el(
|
||||
"div",
|
||||
{
|
||||
textContent: n.title ?? n.type,
|
||||
},
|
||||
n.title
|
||||
? $el("span", {
|
||||
textContent: n.type,
|
||||
})
|
||||
: []
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
this.innerNodesList.replaceChildren(...this.nodeItems);
|
||||
this.innerNodesList.replaceChildren(...this.nodeItems);
|
||||
|
||||
if (reset) {
|
||||
this.selectedNodeIndex = null;
|
||||
this.changeNode(0);
|
||||
} else {
|
||||
const items = this.draggable.getAllItems();
|
||||
let index = items.findIndex(item => item.classList.contains("selected"));
|
||||
if(index === -1) index = this.selectedNodeIndex;
|
||||
this.changeNode(index, true);
|
||||
}
|
||||
if (reset) {
|
||||
this.selectedNodeIndex = null;
|
||||
this.changeNode(0);
|
||||
} else {
|
||||
const items = this.draggable.getAllItems();
|
||||
let index = items.findIndex(item => item.classList.contains("selected"));
|
||||
if(index === -1) index = this.selectedNodeIndex;
|
||||
this.changeNode(index, true);
|
||||
}
|
||||
|
||||
const ordered = [...nodes];
|
||||
this.draggable?.dispose();
|
||||
this.draggable = new DraggableList(this.innerNodesList, "li");
|
||||
this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => {
|
||||
if (oldPosition === newPosition) return;
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i });
|
||||
}
|
||||
});
|
||||
}
|
||||
const ordered = [...nodes];
|
||||
this.draggable?.dispose();
|
||||
this.draggable = new DraggableList(this.innerNodesList, "li");
|
||||
this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => {
|
||||
if (oldPosition === newPosition) return;
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
storeModification(props: { nodeIndex?: number; section: symbol; prop: string; value: any }) {
|
||||
const { nodeIndex, section, prop, value } = props;
|
||||
const groupMod = (this.modifications[this.selectedGroup] ??= {});
|
||||
const nodesMod = (groupMod.nodes ??= {});
|
||||
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {});
|
||||
const typeMod = (nodeMod[section] ??= {});
|
||||
if (typeof value === "object") {
|
||||
const objMod = (typeMod[prop] ??= {});
|
||||
Object.assign(objMod, value);
|
||||
} else {
|
||||
typeMod[prop] = value;
|
||||
}
|
||||
}
|
||||
storeModification(props: { nodeIndex?: number; section: symbol; prop: string; value: any }) {
|
||||
const { nodeIndex, section, prop, value } = props;
|
||||
const groupMod = (this.modifications[this.selectedGroup] ??= {});
|
||||
const nodesMod = (groupMod.nodes ??= {});
|
||||
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {});
|
||||
const typeMod = (nodeMod[section] ??= {});
|
||||
if (typeof value === "object") {
|
||||
const objMod = (typeMod[prop] ??= {});
|
||||
Object.assign(objMod, value);
|
||||
} else {
|
||||
typeMod[prop] = value;
|
||||
}
|
||||
}
|
||||
|
||||
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
||||
if (value === placeholder) value = "";
|
||||
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
||||
if (value === placeholder) value = "";
|
||||
|
||||
const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop];
|
||||
if (mods) {
|
||||
if (mods.name != null) {
|
||||
value = mods.name;
|
||||
}
|
||||
if (mods.visible != null) {
|
||||
checked = mods.visible;
|
||||
}
|
||||
}
|
||||
const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop];
|
||||
if (mods) {
|
||||
if (mods.name != null) {
|
||||
value = mods.name;
|
||||
}
|
||||
if (mods.visible != null) {
|
||||
checked = mods.visible;
|
||||
}
|
||||
}
|
||||
|
||||
return $el("div", [
|
||||
$el("input", {
|
||||
value,
|
||||
placeholder,
|
||||
type: "text",
|
||||
onchange: (e) => {
|
||||
this.storeModification({ section, prop, value: { name: e.target.value } });
|
||||
},
|
||||
}),
|
||||
$el("label", { textContent: "Visible" }, [
|
||||
$el("input", {
|
||||
type: "checkbox",
|
||||
checked,
|
||||
disabled: !checkable,
|
||||
onchange: (e) => {
|
||||
this.storeModification({ section, prop, value: { visible: !!e.target.checked } });
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
return $el("div", [
|
||||
$el("input", {
|
||||
value,
|
||||
placeholder,
|
||||
type: "text",
|
||||
onchange: (e) => {
|
||||
this.storeModification({ section, prop, value: { name: e.target.value } });
|
||||
},
|
||||
}),
|
||||
$el("label", { textContent: "Visible" }, [
|
||||
$el("input", {
|
||||
type: "checkbox",
|
||||
checked,
|
||||
disabled: !checkable,
|
||||
onchange: (e) => {
|
||||
this.storeModification({ section, prop, value: { visible: !!e.target.checked } });
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
buildWidgetsPage() {
|
||||
const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
|
||||
const items = Object.keys(widgets ?? {});
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||
this.widgetsPage.replaceChildren(
|
||||
...items.map((oldName) => {
|
||||
return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false);
|
||||
})
|
||||
);
|
||||
return !!items.length;
|
||||
}
|
||||
buildWidgetsPage() {
|
||||
const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
|
||||
const items = Object.keys(widgets ?? {});
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||
this.widgetsPage.replaceChildren(
|
||||
...items.map((oldName) => {
|
||||
return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false);
|
||||
})
|
||||
);
|
||||
return !!items.length;
|
||||
}
|
||||
|
||||
buildInputsPage() {
|
||||
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex];
|
||||
const items = Object.keys(inputs ?? {});
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||
this.inputsPage.replaceChildren(
|
||||
...items
|
||||
.map((oldName) => {
|
||||
let value = inputs[oldName];
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
buildInputsPage() {
|
||||
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex];
|
||||
const items = Object.keys(inputs ?? {});
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||
this.inputsPage.replaceChildren(
|
||||
...items
|
||||
.map((oldName) => {
|
||||
let value = inputs[oldName];
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false);
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
return !!items.length;
|
||||
}
|
||||
return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false);
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
return !!items.length;
|
||||
}
|
||||
|
||||
buildOutputsPage() {
|
||||
const nodes = this.groupData.nodeData.nodes;
|
||||
const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]);
|
||||
const outputs = innerNodeDef?.output ?? [];
|
||||
const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
|
||||
buildOutputsPage() {
|
||||
const nodes = this.groupData.nodeData.nodes;
|
||||
const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]);
|
||||
const outputs = innerNodeDef?.output ?? [];
|
||||
const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
|
||||
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
|
||||
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex];
|
||||
const checkable = node.type !== "PrimitiveNode";
|
||||
this.outputsPage.replaceChildren(
|
||||
...outputs
|
||||
.map((type, slot) => {
|
||||
const groupOutputIndex = groupOutputs?.[slot];
|
||||
const oldName = innerNodeDef.output_name?.[slot] ?? type;
|
||||
let value = config?.[slot]?.name;
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null;
|
||||
if (!value || value === oldName) {
|
||||
value = "";
|
||||
}
|
||||
return this.getEditElement("output", slot, value, oldName, visible, checkable);
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
return !!outputs.length;
|
||||
}
|
||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
|
||||
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex];
|
||||
const checkable = node.type !== "PrimitiveNode";
|
||||
this.outputsPage.replaceChildren(
|
||||
...outputs
|
||||
.map((type, slot) => {
|
||||
const groupOutputIndex = groupOutputs?.[slot];
|
||||
const oldName = innerNodeDef.output_name?.[slot] ?? type;
|
||||
let value = config?.[slot]?.name;
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null;
|
||||
if (!value || value === oldName) {
|
||||
value = "";
|
||||
}
|
||||
return this.getEditElement("output", slot, value, oldName, visible, checkable);
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
return !!outputs.length;
|
||||
}
|
||||
|
||||
show(type?) {
|
||||
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b));
|
||||
show(type?) {
|
||||
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.innerNodesList = $el("ul.comfy-group-manage-list-items") as HTMLUListElement;
|
||||
this.widgetsPage = $el("section.comfy-group-manage-node-page");
|
||||
this.inputsPage = $el("section.comfy-group-manage-node-page");
|
||||
this.outputsPage = $el("section.comfy-group-manage-node-page");
|
||||
const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]);
|
||||
this.innerNodesList = $el("ul.comfy-group-manage-list-items") as HTMLUListElement;
|
||||
this.widgetsPage = $el("section.comfy-group-manage-node-page");
|
||||
this.inputsPage = $el("section.comfy-group-manage-node-page");
|
||||
this.outputsPage = $el("section.comfy-group-manage-node-page");
|
||||
const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]);
|
||||
|
||||
this.tabs = [
|
||||
["Inputs", this.inputsPage],
|
||||
["Widgets", this.widgetsPage],
|
||||
["Outputs", this.outputsPage],
|
||||
].reduce((p, [name, page]: [string, HTMLElement]) => {
|
||||
p[name] = {
|
||||
tab: $el("a", {
|
||||
onclick: () => {
|
||||
this.changeTab(name);
|
||||
},
|
||||
textContent: name,
|
||||
}),
|
||||
page,
|
||||
};
|
||||
return p;
|
||||
}, {}) as any;
|
||||
this.tabs = [
|
||||
["Inputs", this.inputsPage],
|
||||
["Widgets", this.widgetsPage],
|
||||
["Outputs", this.outputsPage],
|
||||
].reduce((p, [name, page]: [string, HTMLElement]) => {
|
||||
p[name] = {
|
||||
tab: $el("a", {
|
||||
onclick: () => {
|
||||
this.changeTab(name);
|
||||
},
|
||||
textContent: name,
|
||||
}),
|
||||
page,
|
||||
};
|
||||
return p;
|
||||
}, {}) as any;
|
||||
|
||||
const outer = $el("div.comfy-group-manage-outer", [
|
||||
$el("header", [
|
||||
$el("h2", "Group Nodes"),
|
||||
$el(
|
||||
"select",
|
||||
{
|
||||
onchange: (e) => {
|
||||
this.changeGroup(e.target.value);
|
||||
},
|
||||
},
|
||||
groupNodes.map((g) =>
|
||||
$el("option", {
|
||||
textContent: g,
|
||||
selected: "workflow/" + g === type,
|
||||
value: g,
|
||||
})
|
||||
)
|
||||
),
|
||||
]),
|
||||
$el("main", [
|
||||
$el("section.comfy-group-manage-list", this.innerNodesList),
|
||||
$el("section.comfy-group-manage-node", [
|
||||
$el(
|
||||
"header",
|
||||
Object.values(this.tabs).map((t) => t.tab)
|
||||
),
|
||||
pages,
|
||||
]),
|
||||
]),
|
||||
$el("footer", [
|
||||
$el(
|
||||
"button.comfy-btn",
|
||||
{
|
||||
onclick: (e) => {
|
||||
// @ts-ignore
|
||||
const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup);
|
||||
if (node) {
|
||||
alert("This group node is in use in the current workflow, please first remove these.");
|
||||
return;
|
||||
}
|
||||
if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) {
|
||||
delete app.graph.extra.groupNodes[this.selectedGroup];
|
||||
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
|
||||
}
|
||||
this.show();
|
||||
},
|
||||
},
|
||||
"Delete Group Node"
|
||||
),
|
||||
$el(
|
||||
"button.comfy-btn",
|
||||
{
|
||||
onclick: async () => {
|
||||
let nodesByType;
|
||||
let recreateNodes = [];
|
||||
const types = {};
|
||||
for (const g in this.modifications) {
|
||||
const type = app.graph.extra.groupNodes[g];
|
||||
let config = (type.config ??= {});
|
||||
const outer = $el("div.comfy-group-manage-outer", [
|
||||
$el("header", [
|
||||
$el("h2", "Group Nodes"),
|
||||
$el(
|
||||
"select",
|
||||
{
|
||||
onchange: (e) => {
|
||||
this.changeGroup(e.target.value);
|
||||
},
|
||||
},
|
||||
groupNodes.map((g) =>
|
||||
$el("option", {
|
||||
textContent: g,
|
||||
selected: "workflow/" + g === type,
|
||||
value: g,
|
||||
})
|
||||
)
|
||||
),
|
||||
]),
|
||||
$el("main", [
|
||||
$el("section.comfy-group-manage-list", this.innerNodesList),
|
||||
$el("section.comfy-group-manage-node", [
|
||||
$el(
|
||||
"header",
|
||||
Object.values(this.tabs).map((t) => t.tab)
|
||||
),
|
||||
pages,
|
||||
]),
|
||||
]),
|
||||
$el("footer", [
|
||||
$el(
|
||||
"button.comfy-btn",
|
||||
{
|
||||
onclick: (e) => {
|
||||
// @ts-ignore
|
||||
const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup);
|
||||
if (node) {
|
||||
alert("This group node is in use in the current workflow, please first remove these.");
|
||||
return;
|
||||
}
|
||||
if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) {
|
||||
delete app.graph.extra.groupNodes[this.selectedGroup];
|
||||
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
|
||||
}
|
||||
this.show();
|
||||
},
|
||||
},
|
||||
"Delete Group Node"
|
||||
),
|
||||
$el(
|
||||
"button.comfy-btn",
|
||||
{
|
||||
onclick: async () => {
|
||||
let nodesByType;
|
||||
let recreateNodes = [];
|
||||
const types = {};
|
||||
for (const g in this.modifications) {
|
||||
const type = app.graph.extra.groupNodes[g];
|
||||
let config = (type.config ??= {});
|
||||
|
||||
let nodeMods = this.modifications[g]?.nodes;
|
||||
if (nodeMods) {
|
||||
const keys = Object.keys(nodeMods);
|
||||
if (nodeMods[keys[0]][ORDER]) {
|
||||
// If any node is reordered, they will all need sequencing
|
||||
const orderedNodes = [];
|
||||
const orderedMods = {};
|
||||
const orderedConfig = {};
|
||||
let nodeMods = this.modifications[g]?.nodes;
|
||||
if (nodeMods) {
|
||||
const keys = Object.keys(nodeMods);
|
||||
if (nodeMods[keys[0]][ORDER]) {
|
||||
// If any node is reordered, they will all need sequencing
|
||||
const orderedNodes = [];
|
||||
const orderedMods = {};
|
||||
const orderedConfig = {};
|
||||
|
||||
for (const n of keys) {
|
||||
const order = nodeMods[n][ORDER].order;
|
||||
orderedNodes[order] = type.nodes[+n];
|
||||
orderedMods[order] = nodeMods[n];
|
||||
orderedNodes[order].index = order;
|
||||
}
|
||||
for (const n of keys) {
|
||||
const order = nodeMods[n][ORDER].order;
|
||||
orderedNodes[order] = type.nodes[+n];
|
||||
orderedMods[order] = nodeMods[n];
|
||||
orderedNodes[order].index = order;
|
||||
}
|
||||
|
||||
// Rewrite links
|
||||
for (const l of type.links) {
|
||||
if (l[0] != null) l[0] = type.nodes[l[0]].index;
|
||||
if (l[2] != null) l[2] = type.nodes[l[2]].index;
|
||||
}
|
||||
// Rewrite links
|
||||
for (const l of type.links) {
|
||||
if (l[0] != null) l[0] = type.nodes[l[0]].index;
|
||||
if (l[2] != null) l[2] = type.nodes[l[2]].index;
|
||||
}
|
||||
|
||||
// Rewrite externals
|
||||
if (type.external) {
|
||||
for (const ext of type.external) {
|
||||
ext[0] = type.nodes[ext[0]];
|
||||
}
|
||||
}
|
||||
// Rewrite externals
|
||||
if (type.external) {
|
||||
for (const ext of type.external) {
|
||||
ext[0] = type.nodes[ext[0]];
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite modifications
|
||||
for (const id of keys) {
|
||||
if (config[id]) {
|
||||
orderedConfig[type.nodes[id].index] = config[id];
|
||||
}
|
||||
delete config[id];
|
||||
}
|
||||
// Rewrite modifications
|
||||
for (const id of keys) {
|
||||
if (config[id]) {
|
||||
orderedConfig[type.nodes[id].index] = config[id];
|
||||
}
|
||||
delete config[id];
|
||||
}
|
||||
|
||||
type.nodes = orderedNodes;
|
||||
nodeMods = orderedMods;
|
||||
type.config = config = orderedConfig;
|
||||
}
|
||||
type.nodes = orderedNodes;
|
||||
nodeMods = orderedMods;
|
||||
type.config = config = orderedConfig;
|
||||
}
|
||||
|
||||
merge(config, nodeMods);
|
||||
}
|
||||
merge(config, nodeMods);
|
||||
}
|
||||
|
||||
types[g] = type;
|
||||
types[g] = type;
|
||||
|
||||
if (!nodesByType) {
|
||||
// @ts-ignore
|
||||
nodesByType = app.graph._nodes.reduce((p, n) => {
|
||||
p[n.type] ??= [];
|
||||
p[n.type].push(n);
|
||||
return p;
|
||||
}, {});
|
||||
}
|
||||
if (!nodesByType) {
|
||||
// @ts-ignore
|
||||
nodesByType = app.graph._nodes.reduce((p, n) => {
|
||||
p[n.type] ??= [];
|
||||
p[n.type].push(n);
|
||||
return p;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const nodes = nodesByType["workflow/" + g];
|
||||
if (nodes) recreateNodes.push(...nodes);
|
||||
}
|
||||
const nodes = nodesByType["workflow/" + g];
|
||||
if (nodes) recreateNodes.push(...nodes);
|
||||
}
|
||||
|
||||
await GroupNodeConfig.registerFromWorkflow(types, {});
|
||||
await GroupNodeConfig.registerFromWorkflow(types, {});
|
||||
|
||||
for (const node of recreateNodes) {
|
||||
node.recreate();
|
||||
}
|
||||
for (const node of recreateNodes) {
|
||||
node.recreate();
|
||||
}
|
||||
|
||||
this.modifications = {};
|
||||
this.app.graph.setDirtyCanvas(true, true);
|
||||
this.changeGroup(this.selectedGroup, false);
|
||||
},
|
||||
},
|
||||
"Save"
|
||||
),
|
||||
$el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"),
|
||||
]),
|
||||
]);
|
||||
this.modifications = {};
|
||||
this.app.graph.setDirtyCanvas(true, true);
|
||||
this.changeGroup(this.selectedGroup, false);
|
||||
},
|
||||
},
|
||||
"Save"
|
||||
),
|
||||
$el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"),
|
||||
]),
|
||||
]);
|
||||
|
||||
this.element.replaceChildren(outer);
|
||||
this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]);
|
||||
this.element.showModal();
|
||||
this.element.replaceChildren(outer);
|
||||
this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]);
|
||||
this.element.showModal();
|
||||
|
||||
this.element.addEventListener("close", () => {
|
||||
this.draggable?.dispose();
|
||||
});
|
||||
}
|
||||
this.element.addEventListener("close", () => {
|
||||
this.draggable?.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { api } from "./api";
|
||||
import type { ComfyApp } from "./app";
|
||||
|
||||
$el("style", {
|
||||
textContent: `
|
||||
textContent: `
|
||||
.comfy-logging-logs {
|
||||
display: grid;
|
||||
color: var(--fg-color);
|
||||
@@ -23,131 +23,131 @@ $el("style", {
|
||||
padding: 5px;
|
||||
}
|
||||
`,
|
||||
parent: document.body,
|
||||
parent: document.body,
|
||||
});
|
||||
|
||||
// Stringify function supporting max depth and removal of circular references
|
||||
// https://stackoverflow.com/a/57193345
|
||||
function stringify(val, depth, replacer, space, onGetObjID?) {
|
||||
depth = isNaN(+depth) ? 1 : depth;
|
||||
var recursMap = new WeakMap();
|
||||
function _build(val, depth, o?, a?, r?) {
|
||||
// (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
|
||||
return !val || typeof val != "object"
|
||||
? val
|
||||
: ((r = recursMap.has(val)),
|
||||
recursMap.set(val, true),
|
||||
(a = Array.isArray(val)),
|
||||
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);
|
||||
}
|
||||
}),
|
||||
o === void 0 ? (a ? [] : {}) : o);
|
||||
}
|
||||
return JSON.stringify(_build(val, depth), null, space);
|
||||
depth = isNaN(+depth) ? 1 : depth;
|
||||
var recursMap = new WeakMap();
|
||||
function _build(val, depth, o?, a?, r?) {
|
||||
// (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
|
||||
return !val || typeof val != "object"
|
||||
? val
|
||||
: ((r = recursMap.has(val)),
|
||||
recursMap.set(val, true),
|
||||
(a = Array.isArray(val)),
|
||||
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);
|
||||
}
|
||||
}),
|
||||
o === void 0 ? (a ? [] : {}) : o);
|
||||
}
|
||||
return JSON.stringify(_build(val, depth), null, space);
|
||||
}
|
||||
|
||||
const jsonReplacer = (k, v, ui) => {
|
||||
if (v instanceof Array && v.length === 1) {
|
||||
v = v[0];
|
||||
}
|
||||
if (v instanceof Date) {
|
||||
v = v.toISOString();
|
||||
if (ui) {
|
||||
v = v.split("T")[1];
|
||||
}
|
||||
}
|
||||
if (v instanceof Error) {
|
||||
let err = "";
|
||||
if (v.name) err += v.name + "\n";
|
||||
if (v.message) err += v.message + "\n";
|
||||
if (v.stack) err += v.stack + "\n";
|
||||
if (!err) {
|
||||
err = v.toString();
|
||||
}
|
||||
v = err;
|
||||
}
|
||||
return v;
|
||||
if (v instanceof Array && v.length === 1) {
|
||||
v = v[0];
|
||||
}
|
||||
if (v instanceof Date) {
|
||||
v = v.toISOString();
|
||||
if (ui) {
|
||||
v = v.split("T")[1];
|
||||
}
|
||||
}
|
||||
if (v instanceof Error) {
|
||||
let err = "";
|
||||
if (v.name) err += v.name + "\n";
|
||||
if (v.message) err += v.message + "\n";
|
||||
if (v.stack) err += v.stack + "\n";
|
||||
if (!err) {
|
||||
err = v.toString();
|
||||
}
|
||||
v = err;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
const fileInput: HTMLInputElement = $el("input", {
|
||||
type: "file",
|
||||
accept: ".json",
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
type: "file",
|
||||
accept: ".json",
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
}) as HTMLInputElement;
|
||||
|
||||
class ComfyLoggingDialog extends ComfyDialog {
|
||||
logging: any;
|
||||
logging: any;
|
||||
|
||||
constructor(logging) {
|
||||
super();
|
||||
this.logging = logging;
|
||||
}
|
||||
constructor(logging) {
|
||||
super();
|
||||
this.logging = logging;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.logging.clear();
|
||||
this.show();
|
||||
}
|
||||
clear() {
|
||||
this.logging.clear();
|
||||
this.show();
|
||||
}
|
||||
|
||||
export() {
|
||||
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,
|
||||
download: `comfyui-logs-${Date.now()}.json`,
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
export() {
|
||||
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,
|
||||
download: `comfyui-logs-${Date.now()}.json`,
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
import() {
|
||||
fileInput.onchange = () => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
fileInput.remove();
|
||||
try {
|
||||
const obj = JSON.parse(reader.result as string);
|
||||
if (obj instanceof Array) {
|
||||
this.show(obj);
|
||||
} else {
|
||||
throw new Error("Invalid file selected.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Unable to load logs: " + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(fileInput.files[0]);
|
||||
};
|
||||
fileInput.click();
|
||||
}
|
||||
import() {
|
||||
fileInput.onchange = () => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
fileInput.remove();
|
||||
try {
|
||||
const obj = JSON.parse(reader.result as string);
|
||||
if (obj instanceof Array) {
|
||||
this.show(obj);
|
||||
} else {
|
||||
throw new Error("Invalid file selected.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Unable to load logs: " + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(fileInput.files[0]);
|
||||
};
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
return [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Clear",
|
||||
onclick: () => this.clear(),
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Export logs...",
|
||||
onclick: () => this.export(),
|
||||
}),
|
||||
$el("button", {
|
||||
createButtons() {
|
||||
return [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Clear",
|
||||
onclick: () => this.clear(),
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Export logs...",
|
||||
onclick: () => this.export(),
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "View exported logs...",
|
||||
onclick: () => this.import(),
|
||||
|
||||
@@ -1,504 +1,504 @@
|
||||
import { api } from "./api";
|
||||
|
||||
export function getPngMetadata(file) {
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
// Get the PNG data as a Uint8Array
|
||||
const pngData = new Uint8Array(event.target.result as ArrayBuffer);
|
||||
const dataView = new DataView(pngData.buffer);
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
// Get the PNG data as a Uint8Array
|
||||
const pngData = new Uint8Array(event.target.result as ArrayBuffer);
|
||||
const dataView = new DataView(pngData.buffer);
|
||||
|
||||
// Check that the PNG signature is present
|
||||
if (dataView.getUint32(0) !== 0x89504e47) {
|
||||
console.error("Not a valid PNG file");
|
||||
r({});
|
||||
return;
|
||||
}
|
||||
// Check that the PNG signature is present
|
||||
if (dataView.getUint32(0) !== 0x89504e47) {
|
||||
console.error("Not a valid PNG file");
|
||||
r({});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start searching for chunks after the PNG signature
|
||||
let offset = 8;
|
||||
let txt_chunks: Record<string, string> = {};
|
||||
// Loop through the chunks in the PNG file
|
||||
while (offset < pngData.length) {
|
||||
// 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));
|
||||
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));
|
||||
// Get the text
|
||||
const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
|
||||
const contentJson = new TextDecoder("utf-8").decode(contentArraySegment);
|
||||
txt_chunks[keyword] = contentJson;
|
||||
}
|
||||
// Start searching for chunks after the PNG signature
|
||||
let offset = 8;
|
||||
let txt_chunks: Record<string, string> = {};
|
||||
// Loop through the chunks in the PNG file
|
||||
while (offset < pngData.length) {
|
||||
// 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));
|
||||
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));
|
||||
// Get the text
|
||||
const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
|
||||
const contentJson = new TextDecoder("utf-8").decode(contentArraySegment);
|
||||
txt_chunks[keyword] = contentJson;
|
||||
}
|
||||
|
||||
offset += 12 + length;
|
||||
}
|
||||
offset += 12 + length;
|
||||
}
|
||||
|
||||
r(txt_chunks);
|
||||
};
|
||||
r(txt_chunks);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
function parseExifData(exifData) {
|
||||
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
|
||||
const isLittleEndian = new Uint16Array(exifData.slice(0, 2))[0] === 0x4949;
|
||||
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
|
||||
const isLittleEndian = new Uint16Array(exifData.slice(0, 2))[0] === 0x4949;
|
||||
|
||||
// Function to read 16-bit and 32-bit integers from binary data
|
||||
function readInt(offset, isLittleEndian, length) {
|
||||
let arr = exifData.slice(offset, offset + length)
|
||||
if (length === 2) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Function to read 16-bit and 32-bit integers from binary data
|
||||
function readInt(offset, isLittleEndian, length) {
|
||||
let arr = exifData.slice(offset, offset + length)
|
||||
if (length === 2) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the offset to the first IFD (Image File Directory)
|
||||
const ifdOffset = readInt(4, isLittleEndian, 4);
|
||||
// Read the offset to the first IFD (Image File Directory)
|
||||
const ifdOffset = readInt(4, isLittleEndian, 4);
|
||||
|
||||
function parseIFD(offset) {
|
||||
const numEntries = readInt(offset, isLittleEndian, 2);
|
||||
const result = {};
|
||||
function parseIFD(offset) {
|
||||
const numEntries = readInt(offset, isLittleEndian, 2);
|
||||
const result = {};
|
||||
|
||||
for (let i = 0; i < numEntries; i++) {
|
||||
const entryOffset = offset + 2 + i * 12;
|
||||
const tag = readInt(entryOffset, isLittleEndian, 2);
|
||||
const type = readInt(entryOffset + 2, isLittleEndian, 2);
|
||||
const numValues = readInt(entryOffset + 4, isLittleEndian, 4);
|
||||
const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4);
|
||||
for (let i = 0; i < numEntries; i++) {
|
||||
const entryOffset = offset + 2 + i * 12;
|
||||
const tag = readInt(entryOffset, isLittleEndian, 2);
|
||||
const type = readInt(entryOffset + 2, isLittleEndian, 2);
|
||||
const numValues = readInt(entryOffset + 4, isLittleEndian, 4);
|
||||
const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4);
|
||||
|
||||
// Read the value(s) based on the data type
|
||||
let value;
|
||||
if (type === 2) {
|
||||
// ASCII string
|
||||
value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1));
|
||||
}
|
||||
// Read the value(s) based on the data type
|
||||
let value;
|
||||
if (type === 2) {
|
||||
// ASCII string
|
||||
value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1));
|
||||
}
|
||||
|
||||
result[tag] = value;
|
||||
}
|
||||
result[tag] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse the first IFD
|
||||
const ifdData = parseIFD(ifdOffset);
|
||||
return ifdData;
|
||||
// Parse the first IFD
|
||||
const ifdData = parseIFD(ifdOffset);
|
||||
return ifdData;
|
||||
}
|
||||
|
||||
function splitValues(input) {
|
||||
var output = {};
|
||||
for (var key in input) {
|
||||
var value = input[key];
|
||||
var splitValues = value.split(':', 2);
|
||||
output[splitValues[0]] = splitValues[1];
|
||||
var value = input[key];
|
||||
var splitValues = value.split(':', 2);
|
||||
output[splitValues[0]] = splitValues[1];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function getWebpMetadata(file) {
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const webp = new Uint8Array(event.target.result as ArrayBuffer);
|
||||
const dataView = new DataView(webp.buffer);
|
||||
return new Promise<Record<string, string>>((r) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const webp = new Uint8Array(event.target.result as ArrayBuffer);
|
||||
const dataView = new DataView(webp.buffer);
|
||||
|
||||
// Check that the WEBP signature is present
|
||||
if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) {
|
||||
console.error("Not a valid WEBP file");
|
||||
r({});
|
||||
return;
|
||||
}
|
||||
// Check that the WEBP signature is present
|
||||
if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) {
|
||||
console.error("Not a valid WEBP file");
|
||||
r({});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start searching for chunks after the WEBP signature
|
||||
let offset = 12;
|
||||
let txt_chunks = {};
|
||||
// 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));
|
||||
if (chunk_type === "EXIF") {
|
||||
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));
|
||||
for (var key in data) {
|
||||
var value = data[key] as string;
|
||||
let index = value.indexOf(':');
|
||||
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
|
||||
}
|
||||
}
|
||||
// Start searching for chunks after the WEBP signature
|
||||
let offset = 12;
|
||||
let txt_chunks = {};
|
||||
// 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));
|
||||
if (chunk_type === "EXIF") {
|
||||
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));
|
||||
for (var key in data) {
|
||||
var value = data[key] as string;
|
||||
let index = value.indexOf(':');
|
||||
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
offset += 8 + chunk_length;
|
||||
}
|
||||
offset += 8 + chunk_length;
|
||||
}
|
||||
|
||||
r(txt_chunks);
|
||||
};
|
||||
r(txt_chunks);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
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 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)));
|
||||
r(header.__metadata__);
|
||||
};
|
||||
return new Promise((r) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
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)));
|
||||
r(header.__metadata__);
|
||||
};
|
||||
|
||||
var slice = file.slice(0, 1024 * 1024 * 4);
|
||||
reader.readAsArrayBuffer(slice);
|
||||
});
|
||||
var slice = file.slice(0, 1024 * 1024 * 4);
|
||||
reader.readAsArrayBuffer(slice);
|
||||
});
|
||||
}
|
||||
|
||||
function getString(dataView: DataView, offset: number, length: number): string {
|
||||
let string = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
string += String.fromCharCode(dataView.getUint8(offset + i));
|
||||
}
|
||||
return string;
|
||||
let string = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
string += String.fromCharCode(dataView.getUint8(offset + i));
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
// Function to parse the Vorbis Comment block
|
||||
function parseVorbisComment(dataView: DataView): Record<string, string> {
|
||||
let offset = 0;
|
||||
const vendorLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const vendorString = getString(dataView, offset, vendorLength);
|
||||
offset += vendorLength;
|
||||
let offset = 0;
|
||||
const vendorLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const vendorString = getString(dataView, offset, vendorLength);
|
||||
offset += vendorLength;
|
||||
|
||||
const userCommentListLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const comments = {};
|
||||
for (let i = 0; i < userCommentListLength; i++) {
|
||||
const commentLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const comment = getString(dataView, offset, commentLength);
|
||||
offset += commentLength;
|
||||
const userCommentListLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const comments = {};
|
||||
for (let i = 0; i < userCommentListLength; i++) {
|
||||
const commentLength = dataView.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const comment = getString(dataView, offset, commentLength);
|
||||
offset += commentLength;
|
||||
|
||||
const [key, value] = comment.split('=');
|
||||
const [key, value] = comment.split('=');
|
||||
|
||||
comments[key] = value;
|
||||
}
|
||||
comments[key] = value;
|
||||
}
|
||||
|
||||
return comments;
|
||||
return comments;
|
||||
}
|
||||
|
||||
// Function to read a FLAC file and parse Vorbis comments
|
||||
export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
|
||||
return new Promise((r) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
return new Promise((r) => {
|
||||
const reader = new FileReader();
|
||||
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');
|
||||
return;
|
||||
}
|
||||
// Verify the FLAC signature
|
||||
const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4));
|
||||
if (signature !== 'fLaC') {
|
||||
console.error('Not a valid FLAC file');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse metadata blocks
|
||||
let offset = 4;
|
||||
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;
|
||||
offset += 4;
|
||||
// Parse metadata blocks
|
||||
let offset = 4;
|
||||
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;
|
||||
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;
|
||||
if (isLastBlock) break;
|
||||
}
|
||||
offset += blockSize;
|
||||
if (isLastBlock) break;
|
||||
}
|
||||
|
||||
r(vorbisComment);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
r(vorbisComment);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function importA1111(graph, parameters) {
|
||||
const p = parameters.lastIndexOf("\nSteps:");
|
||||
if (p > -1) {
|
||||
const embeddings = await api.getEmbeddings();
|
||||
const opts = parameters
|
||||
.substr(p)
|
||||
.split("\n")[1]
|
||||
.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);
|
||||
}
|
||||
p[s[0].trim().toLowerCase()] = s[1].trim();
|
||||
return p;
|
||||
}, {});
|
||||
const p2 = parameters.lastIndexOf("\nNegative prompt:", p);
|
||||
if (p2 > -1) {
|
||||
let positive = parameters.substr(0, p2).trim();
|
||||
let negative = parameters.substring(p2 + 18, p).trim();
|
||||
const p = parameters.lastIndexOf("\nSteps:");
|
||||
if (p > -1) {
|
||||
const embeddings = await api.getEmbeddings();
|
||||
const opts = parameters
|
||||
.substr(p)
|
||||
.split("\n")[1]
|
||||
.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);
|
||||
}
|
||||
p[s[0].trim().toLowerCase()] = s[1].trim();
|
||||
return p;
|
||||
}, {});
|
||||
const p2 = parameters.lastIndexOf("\nNegative prompt:", p);
|
||||
if (p2 > -1) {
|
||||
let positive = parameters.substr(0, p2).trim();
|
||||
let negative = parameters.substring(p2 + 18, p).trim();
|
||||
|
||||
const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple");
|
||||
const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer");
|
||||
const positiveNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
const negativeNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
const samplerNode = LiteGraph.createNode("KSampler");
|
||||
const imageNode = LiteGraph.createNode("EmptyLatentImage");
|
||||
const vaeNode = LiteGraph.createNode("VAEDecode");
|
||||
const vaeLoaderNode = LiteGraph.createNode("VAELoader");
|
||||
const saveNode = LiteGraph.createNode("SaveImage");
|
||||
let hrSamplerNode = null;
|
||||
let hrSteps = null;
|
||||
const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple");
|
||||
const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer");
|
||||
const positiveNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
const negativeNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
const samplerNode = LiteGraph.createNode("KSampler");
|
||||
const imageNode = LiteGraph.createNode("EmptyLatentImage");
|
||||
const vaeNode = LiteGraph.createNode("VAEDecode");
|
||||
const vaeLoaderNode = LiteGraph.createNode("VAELoader");
|
||||
const saveNode = LiteGraph.createNode("SaveImage");
|
||||
let hrSamplerNode = null;
|
||||
let hrSteps = null;
|
||||
|
||||
const ceil64 = (v) => Math.ceil(v / 64) * 64;
|
||||
const ceil64 = (v) => Math.ceil(v / 64) * 64;
|
||||
|
||||
const getWidget = (node, name) => {
|
||||
return node.widgets.find((w) => w.name === name);
|
||||
}
|
||||
const getWidget = (node, name) => {
|
||||
return node.widgets.find((w) => w.name === name);
|
||||
}
|
||||
|
||||
const setWidgetValue = (node, name, value, isOptionPrefix?) => {
|
||||
const w = getWidget(node, name);
|
||||
if (isOptionPrefix) {
|
||||
const o = w.options.values.find((w) => w.startsWith(value));
|
||||
if (o) {
|
||||
w.value = o;
|
||||
} else {
|
||||
console.warn(`Unknown value '${value}' for widget '${name}'`, node);
|
||||
w.value = value;
|
||||
}
|
||||
} else {
|
||||
w.value = value;
|
||||
}
|
||||
}
|
||||
const setWidgetValue = (node, name, value, isOptionPrefix?) => {
|
||||
const w = getWidget(node, name);
|
||||
if (isOptionPrefix) {
|
||||
const o = w.options.values.find((w) => w.startsWith(value));
|
||||
if (o) {
|
||||
w.value = o;
|
||||
} else {
|
||||
console.warn(`Unknown value '${value}' for widget '${name}'`, node);
|
||||
w.value = value;
|
||||
}
|
||||
} else {
|
||||
w.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
|
||||
const loras = [];
|
||||
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function (m, c) {
|
||||
const s = c.split(":");
|
||||
const weight = parseFloat(s[1]);
|
||||
if (isNaN(weight)) {
|
||||
console.warn("Invalid LORA", m);
|
||||
} else {
|
||||
loras.push({ name: s[0], weight });
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
|
||||
const loras = [];
|
||||
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function (m, c) {
|
||||
const s = c.split(":");
|
||||
const weight = parseFloat(s[1]);
|
||||
if (isNaN(weight)) {
|
||||
console.warn("Invalid LORA", m);
|
||||
} else {
|
||||
loras.push({ name: s[0], weight });
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
for (const l of loras) {
|
||||
const loraNode = LiteGraph.createNode("LoraLoader");
|
||||
graph.add(loraNode);
|
||||
setWidgetValue(loraNode, "lora_name", l.name, true);
|
||||
setWidgetValue(loraNode, "strength_model", l.weight);
|
||||
setWidgetValue(loraNode, "strength_clip", l.weight);
|
||||
prevModel.node.connect(prevModel.index, loraNode, 0);
|
||||
prevClip.node.connect(prevClip.index, loraNode, 1);
|
||||
prevModel = { node: loraNode, index: 0 };
|
||||
prevClip = { node: loraNode, index: 1 };
|
||||
}
|
||||
for (const l of loras) {
|
||||
const loraNode = LiteGraph.createNode("LoraLoader");
|
||||
graph.add(loraNode);
|
||||
setWidgetValue(loraNode, "lora_name", l.name, true);
|
||||
setWidgetValue(loraNode, "strength_model", l.weight);
|
||||
setWidgetValue(loraNode, "strength_clip", l.weight);
|
||||
prevModel.node.connect(prevModel.index, loraNode, 0);
|
||||
prevClip.node.connect(prevClip.index, loraNode, 1);
|
||||
prevModel = { node: loraNode, index: 0 };
|
||||
prevClip = { node: loraNode, index: 1 };
|
||||
}
|
||||
|
||||
prevClip.node.connect(1, clipNode, 0);
|
||||
prevModel.node.connect(0, samplerNode, 0);
|
||||
if (hrSamplerNode) {
|
||||
prevModel.node.connect(0, hrSamplerNode, 0);
|
||||
}
|
||||
prevClip.node.connect(1, clipNode, 0);
|
||||
prevModel.node.connect(0, samplerNode, 0);
|
||||
if (hrSamplerNode) {
|
||||
prevModel.node.connect(0, hrSamplerNode, 0);
|
||||
}
|
||||
|
||||
return { text, prevModel, prevClip };
|
||||
}
|
||||
return { text, prevModel, prevClip };
|
||||
}
|
||||
|
||||
const replaceEmbeddings = (text) => {
|
||||
if(!embeddings.length) return text;
|
||||
return text.replaceAll(
|
||||
new RegExp(
|
||||
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
|
||||
"ig"
|
||||
),
|
||||
"embedding:$1"
|
||||
);
|
||||
}
|
||||
const replaceEmbeddings = (text) => {
|
||||
if(!embeddings.length) return text;
|
||||
return text.replaceAll(
|
||||
new RegExp(
|
||||
"\\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;
|
||||
}
|
||||
const popOpt = (name) => {
|
||||
const v = opts[name];
|
||||
delete opts[name];
|
||||
return v;
|
||||
}
|
||||
|
||||
graph.clear();
|
||||
graph.add(ckptNode);
|
||||
graph.add(clipSkipNode);
|
||||
graph.add(positiveNode);
|
||||
graph.add(negativeNode);
|
||||
graph.add(samplerNode);
|
||||
graph.add(imageNode);
|
||||
graph.add(vaeNode);
|
||||
graph.add(vaeLoaderNode);
|
||||
graph.add(saveNode);
|
||||
graph.clear();
|
||||
graph.add(ckptNode);
|
||||
graph.add(clipSkipNode);
|
||||
graph.add(positiveNode);
|
||||
graph.add(negativeNode);
|
||||
graph.add(samplerNode);
|
||||
graph.add(imageNode);
|
||||
graph.add(vaeNode);
|
||||
graph.add(vaeLoaderNode);
|
||||
graph.add(saveNode);
|
||||
|
||||
ckptNode.connect(1, clipSkipNode, 0);
|
||||
clipSkipNode.connect(0, positiveNode, 0);
|
||||
clipSkipNode.connect(0, negativeNode, 0);
|
||||
ckptNode.connect(0, samplerNode, 0);
|
||||
positiveNode.connect(0, samplerNode, 1);
|
||||
negativeNode.connect(0, samplerNode, 2);
|
||||
imageNode.connect(0, samplerNode, 3);
|
||||
vaeNode.connect(0, saveNode, 0);
|
||||
samplerNode.connect(0, vaeNode, 0);
|
||||
vaeLoaderNode.connect(0, vaeNode, 1);
|
||||
ckptNode.connect(1, clipSkipNode, 0);
|
||||
clipSkipNode.connect(0, positiveNode, 0);
|
||||
clipSkipNode.connect(0, negativeNode, 0);
|
||||
ckptNode.connect(0, samplerNode, 0);
|
||||
positiveNode.connect(0, samplerNode, 1);
|
||||
negativeNode.connect(0, samplerNode, 2);
|
||||
imageNode.connect(0, samplerNode, 3);
|
||||
vaeNode.connect(0, saveNode, 0);
|
||||
samplerNode.connect(0, vaeNode, 0);
|
||||
vaeLoaderNode.connect(0, vaeNode, 1);
|
||||
|
||||
const handlers = {
|
||||
model(v) {
|
||||
setWidgetValue(ckptNode, "ckpt_name", v, true);
|
||||
},
|
||||
"vae"(v) {
|
||||
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
|
||||
},
|
||||
"cfg scale"(v) {
|
||||
setWidgetValue(samplerNode, "cfg", +v);
|
||||
},
|
||||
"clip skip"(v) {
|
||||
setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v);
|
||||
},
|
||||
sampler(v) {
|
||||
let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
|
||||
if (name.includes("karras")) {
|
||||
name = name.replace("karras", "").replace(/_+$/, "");
|
||||
setWidgetValue(samplerNode, "scheduler", "karras");
|
||||
} else {
|
||||
setWidgetValue(samplerNode, "scheduler", "normal");
|
||||
}
|
||||
const w = getWidget(samplerNode, "sampler_name");
|
||||
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
|
||||
if (o) {
|
||||
setWidgetValue(samplerNode, "sampler_name", o);
|
||||
}
|
||||
},
|
||||
size(v) {
|
||||
const wxh = v.split("x");
|
||||
const w = ceil64(+wxh[0]);
|
||||
const h = ceil64(+wxh[1]);
|
||||
const hrUp = popOpt("hires upscale");
|
||||
const hrSz = popOpt("hires resize");
|
||||
hrSteps = popOpt("hires steps");
|
||||
let hrMethod = popOpt("hires upscaler");
|
||||
const handlers = {
|
||||
model(v) {
|
||||
setWidgetValue(ckptNode, "ckpt_name", v, true);
|
||||
},
|
||||
"vae"(v) {
|
||||
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
|
||||
},
|
||||
"cfg scale"(v) {
|
||||
setWidgetValue(samplerNode, "cfg", +v);
|
||||
},
|
||||
"clip skip"(v) {
|
||||
setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v);
|
||||
},
|
||||
sampler(v) {
|
||||
let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
|
||||
if (name.includes("karras")) {
|
||||
name = name.replace("karras", "").replace(/_+$/, "");
|
||||
setWidgetValue(samplerNode, "scheduler", "karras");
|
||||
} else {
|
||||
setWidgetValue(samplerNode, "scheduler", "normal");
|
||||
}
|
||||
const w = getWidget(samplerNode, "sampler_name");
|
||||
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
|
||||
if (o) {
|
||||
setWidgetValue(samplerNode, "sampler_name", o);
|
||||
}
|
||||
},
|
||||
size(v) {
|
||||
const wxh = v.split("x");
|
||||
const w = ceil64(+wxh[0]);
|
||||
const h = ceil64(+wxh[1]);
|
||||
const hrUp = popOpt("hires upscale");
|
||||
const hrSz = popOpt("hires resize");
|
||||
hrSteps = popOpt("hires steps");
|
||||
let hrMethod = popOpt("hires upscaler");
|
||||
|
||||
setWidgetValue(imageNode, "width", w);
|
||||
setWidgetValue(imageNode, "height", h);
|
||||
setWidgetValue(imageNode, "width", w);
|
||||
setWidgetValue(imageNode, "height", h);
|
||||
|
||||
if (hrUp || hrSz) {
|
||||
let uw, uh;
|
||||
if (hrUp) {
|
||||
uw = w * hrUp;
|
||||
uh = h * hrUp;
|
||||
} else {
|
||||
const s = hrSz.split("x");
|
||||
uw = +s[0];
|
||||
uh = +s[1];
|
||||
}
|
||||
if (hrUp || hrSz) {
|
||||
let uw, uh;
|
||||
if (hrUp) {
|
||||
uw = w * hrUp;
|
||||
uh = h * hrUp;
|
||||
} else {
|
||||
const s = hrSz.split("x");
|
||||
uw = +s[0];
|
||||
uh = +s[1];
|
||||
}
|
||||
|
||||
let upscaleNode;
|
||||
let latentNode;
|
||||
let upscaleNode;
|
||||
let latentNode;
|
||||
|
||||
if (hrMethod.startsWith("Latent")) {
|
||||
latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale");
|
||||
graph.add(upscaleNode);
|
||||
samplerNode.connect(0, upscaleNode, 0);
|
||||
if (hrMethod.startsWith("Latent")) {
|
||||
latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale");
|
||||
graph.add(upscaleNode);
|
||||
samplerNode.connect(0, upscaleNode, 0);
|
||||
|
||||
switch (hrMethod) {
|
||||
case "Latent (nearest-exact)":
|
||||
hrMethod = "nearest-exact";
|
||||
break;
|
||||
}
|
||||
setWidgetValue(upscaleNode, "upscale_method", hrMethod, true);
|
||||
} else {
|
||||
const decode = LiteGraph.createNode("VAEDecodeTiled");
|
||||
graph.add(decode);
|
||||
samplerNode.connect(0, decode, 0);
|
||||
vaeLoaderNode.connect(0, decode, 1);
|
||||
switch (hrMethod) {
|
||||
case "Latent (nearest-exact)":
|
||||
hrMethod = "nearest-exact";
|
||||
break;
|
||||
}
|
||||
setWidgetValue(upscaleNode, "upscale_method", hrMethod, true);
|
||||
} else {
|
||||
const decode = LiteGraph.createNode("VAEDecodeTiled");
|
||||
graph.add(decode);
|
||||
samplerNode.connect(0, decode, 0);
|
||||
vaeLoaderNode.connect(0, decode, 1);
|
||||
|
||||
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
|
||||
graph.add(upscaleLoaderNode);
|
||||
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
|
||||
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
|
||||
graph.add(upscaleLoaderNode);
|
||||
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
|
||||
|
||||
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
|
||||
graph.add(modelUpscaleNode);
|
||||
decode.connect(0, modelUpscaleNode, 1);
|
||||
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
|
||||
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
|
||||
graph.add(modelUpscaleNode);
|
||||
decode.connect(0, modelUpscaleNode, 1);
|
||||
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
|
||||
|
||||
upscaleNode = LiteGraph.createNode("ImageScale");
|
||||
graph.add(upscaleNode);
|
||||
modelUpscaleNode.connect(0, upscaleNode, 0);
|
||||
upscaleNode = LiteGraph.createNode("ImageScale");
|
||||
graph.add(upscaleNode);
|
||||
modelUpscaleNode.connect(0, upscaleNode, 0);
|
||||
|
||||
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
|
||||
graph.add(vaeEncodeNode);
|
||||
upscaleNode.connect(0, vaeEncodeNode, 0);
|
||||
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
|
||||
}
|
||||
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
|
||||
graph.add(vaeEncodeNode);
|
||||
upscaleNode.connect(0, vaeEncodeNode, 0);
|
||||
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
|
||||
}
|
||||
|
||||
setWidgetValue(upscaleNode, "width", ceil64(uw));
|
||||
setWidgetValue(upscaleNode, "height", ceil64(uh));
|
||||
setWidgetValue(upscaleNode, "width", ceil64(uw));
|
||||
setWidgetValue(upscaleNode, "height", ceil64(uh));
|
||||
|
||||
hrSamplerNode = LiteGraph.createNode("KSampler");
|
||||
graph.add(hrSamplerNode);
|
||||
ckptNode.connect(0, hrSamplerNode, 0);
|
||||
positiveNode.connect(0, hrSamplerNode, 1);
|
||||
negativeNode.connect(0, hrSamplerNode, 2);
|
||||
latentNode.connect(0, hrSamplerNode, 3);
|
||||
hrSamplerNode.connect(0, vaeNode, 0);
|
||||
}
|
||||
},
|
||||
steps(v) {
|
||||
setWidgetValue(samplerNode, "steps", +v);
|
||||
},
|
||||
seed(v) {
|
||||
setWidgetValue(samplerNode, "seed", +v);
|
||||
},
|
||||
};
|
||||
hrSamplerNode = LiteGraph.createNode("KSampler");
|
||||
graph.add(hrSamplerNode);
|
||||
ckptNode.connect(0, hrSamplerNode, 0);
|
||||
positiveNode.connect(0, hrSamplerNode, 1);
|
||||
negativeNode.connect(0, hrSamplerNode, 2);
|
||||
latentNode.connect(0, hrSamplerNode, 3);
|
||||
hrSamplerNode.connect(0, vaeNode, 0);
|
||||
}
|
||||
},
|
||||
steps(v) {
|
||||
setWidgetValue(samplerNode, "steps", +v);
|
||||
},
|
||||
seed(v) {
|
||||
setWidgetValue(samplerNode, "seed", +v);
|
||||
},
|
||||
};
|
||||
|
||||
for (const opt in opts) {
|
||||
if (opt in handlers) {
|
||||
handlers[opt](popOpt(opt));
|
||||
}
|
||||
}
|
||||
for (const opt in opts) {
|
||||
if (opt in handlers) {
|
||||
handlers[opt](popOpt(opt));
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
setWidgetValue(positiveNode, "text", replaceEmbeddings(positive));
|
||||
setWidgetValue(negativeNode, "text", replaceEmbeddings(negative));
|
||||
setWidgetValue(positiveNode, "text", replaceEmbeddings(positive));
|
||||
setWidgetValue(negativeNode, "text", replaceEmbeddings(negative));
|
||||
|
||||
graph.arrange();
|
||||
graph.arrange();
|
||||
|
||||
for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) {
|
||||
delete opts[opt];
|
||||
}
|
||||
for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) {
|
||||
delete opts[opt];
|
||||
}
|
||||
|
||||
console.warn("Unhandled parameters:", opts);
|
||||
}
|
||||
}
|
||||
console.warn("Unhandled parameters:", opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1110
src/scripts/ui.ts
1110
src/scripts/ui.ts
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,40 @@
|
||||
import { $el } from "../ui";
|
||||
|
||||
export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarget {
|
||||
element: T;
|
||||
textElement: HTMLElement;
|
||||
#buttons: HTMLButtonElement[] | null;
|
||||
element: T;
|
||||
textElement: HTMLElement;
|
||||
#buttons: HTMLButtonElement[] | null;
|
||||
|
||||
constructor(type = "div", buttons = null) {
|
||||
super();
|
||||
this.#buttons = buttons;
|
||||
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
|
||||
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
|
||||
]) as T;
|
||||
}
|
||||
constructor(type = "div", buttons = null) {
|
||||
super();
|
||||
this.#buttons = buttons;
|
||||
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
|
||||
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
|
||||
]) as T;
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
return (
|
||||
this.#buttons ?? [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => this.close(),
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
createButtons() {
|
||||
return (
|
||||
this.#buttons ?? [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => this.close(),
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.element.style.display = "none";
|
||||
}
|
||||
close() {
|
||||
this.element.style.display = "none";
|
||||
}
|
||||
|
||||
show(html) {
|
||||
if (typeof html === "string") {
|
||||
this.textElement.innerHTML = html;
|
||||
} else {
|
||||
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
|
||||
}
|
||||
this.element.style.display = "flex";
|
||||
}
|
||||
show(html) {
|
||||
if (typeof html === "string") {
|
||||
this.textElement.innerHTML = html;
|
||||
} else {
|
||||
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
|
||||
}
|
||||
this.element.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Original implementation:
|
||||
Original implementation:
|
||||
https://github.com/TahaSh/drag-to-reorder
|
||||
MIT License
|
||||
|
||||
@@ -44,243 +44,243 @@ $el("style", {
|
||||
});
|
||||
|
||||
export class DraggableList extends EventTarget {
|
||||
listContainer;
|
||||
draggableItem;
|
||||
pointerStartX;
|
||||
pointerStartY;
|
||||
scrollYMax;
|
||||
itemsGap = 0;
|
||||
items = [];
|
||||
itemSelector;
|
||||
handleClass = "drag-handle";
|
||||
off = [];
|
||||
offDrag = [];
|
||||
listContainer;
|
||||
draggableItem;
|
||||
pointerStartX;
|
||||
pointerStartY;
|
||||
scrollYMax;
|
||||
itemsGap = 0;
|
||||
items = [];
|
||||
itemSelector;
|
||||
handleClass = "drag-handle";
|
||||
off = [];
|
||||
offDrag = [];
|
||||
|
||||
constructor(element, itemSelector) {
|
||||
constructor(element, itemSelector) {
|
||||
super();
|
||||
this.listContainer = element;
|
||||
this.listContainer = element;
|
||||
this.itemSelector = itemSelector;
|
||||
|
||||
if (!this.listContainer) return;
|
||||
if (!this.listContainer) return;
|
||||
|
||||
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
|
||||
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
|
||||
this.off.push(this.on(document, "mouseup", this.dragEnd));
|
||||
this.off.push(this.on(document, "touchend", this.dragEnd));
|
||||
}
|
||||
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
|
||||
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
|
||||
this.off.push(this.on(document, "mouseup", this.dragEnd));
|
||||
this.off.push(this.on(document, "touchend", this.dragEnd));
|
||||
}
|
||||
|
||||
getAllItems() {
|
||||
if (!this.items?.length) {
|
||||
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
|
||||
this.items.forEach((element) => {
|
||||
element.classList.add("is-idle");
|
||||
});
|
||||
}
|
||||
return this.items;
|
||||
}
|
||||
getAllItems() {
|
||||
if (!this.items?.length) {
|
||||
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
|
||||
this.items.forEach((element) => {
|
||||
element.classList.add("is-idle");
|
||||
});
|
||||
}
|
||||
return this.items;
|
||||
}
|
||||
|
||||
getIdleItems() {
|
||||
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
|
||||
}
|
||||
getIdleItems() {
|
||||
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
|
||||
}
|
||||
|
||||
isItemAbove(item) {
|
||||
return item.hasAttribute("data-is-above");
|
||||
}
|
||||
isItemAbove(item) {
|
||||
return item.hasAttribute("data-is-above");
|
||||
}
|
||||
|
||||
isItemToggled(item) {
|
||||
return item.hasAttribute("data-is-toggled");
|
||||
}
|
||||
isItemToggled(item) {
|
||||
return item.hasAttribute("data-is-toggled");
|
||||
}
|
||||
|
||||
on(source, event, listener, options?) {
|
||||
listener = listener.bind(this);
|
||||
source.addEventListener(event, listener, options);
|
||||
return () => source.removeEventListener(event, listener);
|
||||
}
|
||||
on(source, event, listener, options?) {
|
||||
listener = listener.bind(this);
|
||||
source.addEventListener(event, listener, options);
|
||||
return () => source.removeEventListener(event, listener);
|
||||
}
|
||||
|
||||
dragStart(e) {
|
||||
if (e.target.classList.contains(this.handleClass)) {
|
||||
this.draggableItem = e.target.closest(this.itemSelector);
|
||||
}
|
||||
dragStart(e) {
|
||||
if (e.target.classList.contains(this.handleClass)) {
|
||||
this.draggableItem = e.target.closest(this.itemSelector);
|
||||
}
|
||||
|
||||
if (!this.draggableItem) return;
|
||||
if (!this.draggableItem) return;
|
||||
|
||||
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
||||
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
||||
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
||||
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
||||
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
||||
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
||||
|
||||
this.setItemsGap();
|
||||
this.initDraggableItem();
|
||||
this.initItemsState();
|
||||
this.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, "mousemove", this.drag));
|
||||
this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false }));
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragstart", {
|
||||
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
|
||||
})
|
||||
);
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragstart", {
|
||||
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setItemsGap() {
|
||||
if (this.getIdleItems().length <= 1) {
|
||||
this.itemsGap = 0;
|
||||
return;
|
||||
}
|
||||
setItemsGap() {
|
||||
if (this.getIdleItems().length <= 1) {
|
||||
this.itemsGap = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const item1 = this.getIdleItems()[0];
|
||||
const item2 = this.getIdleItems()[1];
|
||||
const item1 = this.getIdleItems()[0];
|
||||
const item2 = this.getIdleItems()[1];
|
||||
|
||||
const item1Rect = item1.getBoundingClientRect();
|
||||
const item2Rect = item2.getBoundingClientRect();
|
||||
const item1Rect = item1.getBoundingClientRect();
|
||||
const item2Rect = item2.getBoundingClientRect();
|
||||
|
||||
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
|
||||
}
|
||||
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
|
||||
}
|
||||
|
||||
initItemsState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
if (this.getAllItems().indexOf(this.draggableItem) > i) {
|
||||
item.dataset.isAbove = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
initItemsState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
if (this.getAllItems().indexOf(this.draggableItem) > i) {
|
||||
item.dataset.isAbove = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initDraggableItem() {
|
||||
this.draggableItem.classList.remove("is-idle");
|
||||
this.draggableItem.classList.add("is-draggable");
|
||||
}
|
||||
initDraggableItem() {
|
||||
this.draggableItem.classList.remove("is-idle");
|
||||
this.draggableItem.classList.add("is-draggable");
|
||||
}
|
||||
|
||||
drag(e) {
|
||||
if (!this.draggableItem) return;
|
||||
drag(e) {
|
||||
if (!this.draggableItem) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.preventDefault();
|
||||
|
||||
const clientX = e.clientX || e.touches[0].clientX;
|
||||
const clientY = e.clientY || e.touches[0].clientY;
|
||||
const clientX = e.clientX || e.touches[0].clientX;
|
||||
const clientY = e.clientY || e.touches[0].clientY;
|
||||
|
||||
const listRect = this.listContainer.getBoundingClientRect();
|
||||
const listRect = this.listContainer.getBoundingClientRect();
|
||||
|
||||
if (clientY > listRect.bottom) {
|
||||
if (this.listContainer.scrollTop < this.scrollYMax) {
|
||||
this.listContainer.scrollBy(0, 10);
|
||||
this.pointerStartY -= 10;
|
||||
}
|
||||
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
|
||||
this.pointerStartY += 10;
|
||||
this.listContainer.scrollBy(0, -10);
|
||||
}
|
||||
if (clientY > listRect.bottom) {
|
||||
if (this.listContainer.scrollTop < this.scrollYMax) {
|
||||
this.listContainer.scrollBy(0, 10);
|
||||
this.pointerStartY -= 10;
|
||||
}
|
||||
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
|
||||
this.pointerStartY += 10;
|
||||
this.listContainer.scrollBy(0, -10);
|
||||
}
|
||||
|
||||
const pointerOffsetX = clientX - this.pointerStartX;
|
||||
const pointerOffsetY = clientY - this.pointerStartY;
|
||||
const pointerOffsetX = clientX - this.pointerStartX;
|
||||
const pointerOffsetY = clientY - this.pointerStartY;
|
||||
|
||||
this.updateIdleItemsStateAndPosition();
|
||||
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
|
||||
}
|
||||
this.updateIdleItemsStateAndPosition();
|
||||
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
|
||||
}
|
||||
|
||||
updateIdleItemsStateAndPosition() {
|
||||
const draggableItemRect = this.draggableItem.getBoundingClientRect();
|
||||
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
|
||||
updateIdleItemsStateAndPosition() {
|
||||
const draggableItemRect = this.draggableItem.getBoundingClientRect();
|
||||
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
|
||||
|
||||
// Update state
|
||||
this.getIdleItems().forEach((item) => {
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const itemY = itemRect.top + itemRect.height / 2;
|
||||
if (this.isItemAbove(item)) {
|
||||
if (draggableItemY <= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
}
|
||||
} else {
|
||||
if (draggableItemY >= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Update state
|
||||
this.getIdleItems().forEach((item) => {
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const itemY = itemRect.top + itemRect.height / 2;
|
||||
if (this.isItemAbove(item)) {
|
||||
if (draggableItemY <= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
}
|
||||
} else {
|
||||
if (draggableItemY >= itemY) {
|
||||
item.dataset.isToggled = "";
|
||||
} else {
|
||||
delete item.dataset.isToggled;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update position
|
||||
this.getIdleItems().forEach((item) => {
|
||||
if (this.isItemToggled(item)) {
|
||||
const direction = this.isItemAbove(item) ? 1 : -1;
|
||||
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
|
||||
} else {
|
||||
item.style.transform = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
// Update position
|
||||
this.getIdleItems().forEach((item) => {
|
||||
if (this.isItemToggled(item)) {
|
||||
const direction = this.isItemAbove(item) ? 1 : -1;
|
||||
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
|
||||
} else {
|
||||
item.style.transform = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dragEnd() {
|
||||
if (!this.draggableItem) return;
|
||||
dragEnd() {
|
||||
if (!this.draggableItem) return;
|
||||
|
||||
this.applyNewItemsOrder();
|
||||
this.cleanup();
|
||||
}
|
||||
this.applyNewItemsOrder();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
applyNewItemsOrder() {
|
||||
const reorderedItems = [];
|
||||
applyNewItemsOrder() {
|
||||
const reorderedItems = [];
|
||||
|
||||
let oldPosition = -1;
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index;
|
||||
return;
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item;
|
||||
return;
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
|
||||
reorderedItems[newIndex] = item;
|
||||
});
|
||||
let oldPosition = -1;
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index;
|
||||
return;
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item;
|
||||
return;
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
|
||||
reorderedItems[newIndex] = item;
|
||||
});
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index];
|
||||
if (typeof item === "undefined") {
|
||||
reorderedItems[index] = this.draggableItem;
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index];
|
||||
if (typeof item === "undefined") {
|
||||
reorderedItems[index] = this.draggableItem;
|
||||
}
|
||||
}
|
||||
|
||||
reorderedItems.forEach((item) => {
|
||||
this.listContainer.appendChild(item);
|
||||
});
|
||||
reorderedItems.forEach((item) => {
|
||||
this.listContainer.appendChild(item);
|
||||
});
|
||||
|
||||
this.items = reorderedItems;
|
||||
this.items = reorderedItems;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragend", {
|
||||
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
|
||||
})
|
||||
);
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dragend", {
|
||||
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.itemsGap = 0;
|
||||
this.items = [];
|
||||
this.unsetDraggableItem();
|
||||
this.unsetItemState();
|
||||
cleanup() {
|
||||
this.itemsGap = 0;
|
||||
this.items = [];
|
||||
this.unsetDraggableItem();
|
||||
this.unsetItemState();
|
||||
|
||||
this.offDrag.forEach((f) => f());
|
||||
this.offDrag = [];
|
||||
}
|
||||
this.offDrag.forEach((f) => f());
|
||||
this.offDrag = [];
|
||||
}
|
||||
|
||||
unsetDraggableItem() {
|
||||
this.draggableItem.style = null;
|
||||
this.draggableItem.classList.remove("is-draggable");
|
||||
this.draggableItem.classList.add("is-idle");
|
||||
this.draggableItem = null;
|
||||
}
|
||||
unsetDraggableItem() {
|
||||
this.draggableItem.style = null;
|
||||
this.draggableItem.classList.remove("is-draggable");
|
||||
this.draggableItem.classList.add("is-idle");
|
||||
this.draggableItem = null;
|
||||
}
|
||||
|
||||
unsetItemState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
delete item.dataset.isAbove;
|
||||
delete item.dataset.isToggled;
|
||||
item.style.transform = "";
|
||||
});
|
||||
}
|
||||
unsetItemState() {
|
||||
this.getIdleItems().forEach((item, i) => {
|
||||
delete item.dataset.isAbove;
|
||||
delete item.dataset.isToggled;
|
||||
item.style.transform = "";
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.off.forEach((f) => f());
|
||||
}
|
||||
dispose() {
|
||||
this.off.forEach((f) => f());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,97 +2,97 @@ import { app } from "../app";
|
||||
import { $el } from "../ui";
|
||||
|
||||
export function calculateImageGrid(imgs, dw, dh) {
|
||||
let best = 0;
|
||||
let w = imgs[0].naturalWidth;
|
||||
let h = imgs[0].naturalHeight;
|
||||
const numImages = imgs.length;
|
||||
let best = 0;
|
||||
let w = imgs[0].naturalWidth;
|
||||
let h = imgs[0].naturalHeight;
|
||||
const numImages = imgs.length;
|
||||
|
||||
let cellWidth, cellHeight, cols, rows, shiftX;
|
||||
// compact style
|
||||
for (let c = 1; c <= numImages; c++) {
|
||||
const r = Math.ceil(numImages / c);
|
||||
const cW = dw / c;
|
||||
const cH = dh / r;
|
||||
const scaleX = cW / w;
|
||||
const scaleY = cH / h;
|
||||
let cellWidth, cellHeight, cols, rows, shiftX;
|
||||
// compact style
|
||||
for (let c = 1; c <= numImages; c++) {
|
||||
const r = Math.ceil(numImages / c);
|
||||
const cW = dw / c;
|
||||
const cH = dh / r;
|
||||
const scaleX = cW / w;
|
||||
const scaleY = cH / h;
|
||||
|
||||
const scale = Math.min(scaleX, scaleY, 1);
|
||||
const imageW = w * scale;
|
||||
const imageH = h * scale;
|
||||
const area = imageW * imageH * numImages;
|
||||
const scale = Math.min(scaleX, scaleY, 1);
|
||||
const imageW = w * scale;
|
||||
const imageH = h * scale;
|
||||
const area = imageW * imageH * numImages;
|
||||
|
||||
if (area > best) {
|
||||
best = area;
|
||||
cellWidth = imageW;
|
||||
cellHeight = imageH;
|
||||
cols = c;
|
||||
rows = r;
|
||||
shiftX = c * ((cW - imageW) / 2);
|
||||
}
|
||||
}
|
||||
if (area > best) {
|
||||
best = area;
|
||||
cellWidth = imageW;
|
||||
cellHeight = imageH;
|
||||
cols = c;
|
||||
rows = r;
|
||||
shiftX = c * ((cW - imageW) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
return { cellWidth, cellHeight, cols, rows, shiftX };
|
||||
return { cellWidth, cellHeight, cols, rows, shiftX };
|
||||
}
|
||||
|
||||
export function createImageHost(node) {
|
||||
const el = $el("div.comfy-img-preview");
|
||||
let currentImgs;
|
||||
let first = true;
|
||||
const el = $el("div.comfy-img-preview");
|
||||
let currentImgs;
|
||||
let first = true;
|
||||
|
||||
function updateSize() {
|
||||
let w = null;
|
||||
let h = null;
|
||||
function updateSize() {
|
||||
let w = null;
|
||||
let h = null;
|
||||
|
||||
if (currentImgs) {
|
||||
let elH = el.clientHeight;
|
||||
if (first) {
|
||||
first = false;
|
||||
// On first run, if we are small then grow a bit
|
||||
if (elH < 190) {
|
||||
elH = 190;
|
||||
}
|
||||
el.style.setProperty("--comfy-widget-min-height", elH.toString());
|
||||
} else {
|
||||
el.style.setProperty("--comfy-widget-min-height", null);
|
||||
}
|
||||
if (currentImgs) {
|
||||
let elH = el.clientHeight;
|
||||
if (first) {
|
||||
first = false;
|
||||
// On first run, if we are small then grow a bit
|
||||
if (elH < 190) {
|
||||
elH = 190;
|
||||
}
|
||||
el.style.setProperty("--comfy-widget-min-height", elH.toString());
|
||||
} else {
|
||||
el.style.setProperty("--comfy-widget-min-height", null);
|
||||
}
|
||||
|
||||
const nw = node.size[0];
|
||||
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
|
||||
w += "px";
|
||||
h += "px";
|
||||
const nw = node.size[0];
|
||||
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
|
||||
w += "px";
|
||||
h += "px";
|
||||
|
||||
el.style.setProperty("--comfy-img-preview-width", w);
|
||||
el.style.setProperty("--comfy-img-preview-height", h);
|
||||
}
|
||||
}
|
||||
return {
|
||||
el,
|
||||
updateImages(imgs) {
|
||||
if (imgs !== currentImgs) {
|
||||
if (currentImgs == null) {
|
||||
requestAnimationFrame(() => {
|
||||
updateSize();
|
||||
});
|
||||
}
|
||||
el.replaceChildren(...imgs);
|
||||
currentImgs = imgs;
|
||||
node.onResize(node.size);
|
||||
node.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
},
|
||||
getHeight() {
|
||||
updateSize();
|
||||
},
|
||||
onDraw() {
|
||||
// Element from point uses a hittest find elements so we need to toggle pointer events
|
||||
el.style.pointerEvents = "all";
|
||||
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
|
||||
el.style.pointerEvents = "none";
|
||||
el.style.setProperty("--comfy-img-preview-width", w);
|
||||
el.style.setProperty("--comfy-img-preview-height", h);
|
||||
}
|
||||
}
|
||||
return {
|
||||
el,
|
||||
updateImages(imgs) {
|
||||
if (imgs !== currentImgs) {
|
||||
if (currentImgs == null) {
|
||||
requestAnimationFrame(() => {
|
||||
updateSize();
|
||||
});
|
||||
}
|
||||
el.replaceChildren(...imgs);
|
||||
currentImgs = imgs;
|
||||
node.onResize(node.size);
|
||||
node.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
},
|
||||
getHeight() {
|
||||
updateSize();
|
||||
},
|
||||
onDraw() {
|
||||
// Element from point uses a hittest find elements so we need to toggle pointer events
|
||||
el.style.pointerEvents = "all";
|
||||
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
|
||||
el.style.pointerEvents = "none";
|
||||
|
||||
if(!over) return;
|
||||
// Set the overIndex so Open Image etc work
|
||||
const idx = currentImgs.indexOf(over);
|
||||
node.overIndex = idx;
|
||||
},
|
||||
};
|
||||
if(!over) return;
|
||||
// Set the overIndex so Open Image etc work
|
||||
const idx = currentImgs.indexOf(over);
|
||||
node.overIndex = idx;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,291 +13,291 @@ import { getInteruptButton } from "./interruptButton";
|
||||
import "./menu.css";
|
||||
|
||||
const collapseOnMobile = (t) => {
|
||||
(t.element ?? t).classList.add("comfyui-menu-mobile-collapse");
|
||||
return t;
|
||||
(t.element ?? t).classList.add("comfyui-menu-mobile-collapse");
|
||||
return t;
|
||||
};
|
||||
const showOnMobile = (t) => {
|
||||
(t.element ?? t).classList.add("lt-lg-show");
|
||||
return t;
|
||||
(t.element ?? t).classList.add("lt-lg-show");
|
||||
return t;
|
||||
};
|
||||
|
||||
export class ComfyAppMenu {
|
||||
#sizeBreak = "lg";
|
||||
#lastSizeBreaks = {
|
||||
lg: null,
|
||||
md: null,
|
||||
sm: null,
|
||||
xs: null,
|
||||
};
|
||||
#sizeBreaks = Object.keys(this.#lastSizeBreaks);
|
||||
#cachedInnerSize = null;
|
||||
#cacheTimeout = null;
|
||||
#sizeBreak = "lg";
|
||||
#lastSizeBreaks = {
|
||||
lg: null,
|
||||
md: null,
|
||||
sm: null,
|
||||
xs: null,
|
||||
};
|
||||
#sizeBreaks = Object.keys(this.#lastSizeBreaks);
|
||||
#cachedInnerSize = null;
|
||||
#cacheTimeout = null;
|
||||
|
||||
/**
|
||||
* @param { import("../../app").ComfyApp } app
|
||||
*/
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
/**
|
||||
* @param { import("../../app").ComfyApp } app
|
||||
*/
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.workflows = new ComfyWorkflowsMenu(app);
|
||||
const getSaveButton = (t) =>
|
||||
new ComfyButton({
|
||||
icon: "content-save",
|
||||
tooltip: "Save the current workflow",
|
||||
action: () => app.workflowManager.activeWorkflow.save(),
|
||||
content: t,
|
||||
});
|
||||
this.workflows = new ComfyWorkflowsMenu(app);
|
||||
const getSaveButton = (t) =>
|
||||
new ComfyButton({
|
||||
icon: "content-save",
|
||||
tooltip: "Save the current workflow",
|
||||
action: () => app.workflowManager.activeWorkflow.save(),
|
||||
content: t,
|
||||
});
|
||||
|
||||
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
|
||||
this.saveButton = new ComfySplitButton(
|
||||
{
|
||||
primary: getSaveButton(),
|
||||
mode: "hover",
|
||||
position: "absolute",
|
||||
},
|
||||
getSaveButton("Save"),
|
||||
new ComfyButton({
|
||||
icon: "content-save-edit",
|
||||
content: "Save As",
|
||||
tooltip: "Save the current graph as a new workflow",
|
||||
action: () => app.workflowManager.activeWorkflow.save(true),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "download",
|
||||
content: "Export",
|
||||
tooltip: "Export the current workflow as JSON",
|
||||
action: () => this.exportWorkflow("workflow", "workflow"),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "api",
|
||||
content: "Export (API Format)",
|
||||
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
|
||||
action: () => this.exportWorkflow("workflow_api", "output"),
|
||||
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
|
||||
app,
|
||||
})
|
||||
);
|
||||
this.actionsGroup = new ComfyButtonGroup(
|
||||
new ComfyButton({
|
||||
icon: "refresh",
|
||||
content: "Refresh",
|
||||
tooltip: "Refresh widgets in nodes to find new models or files",
|
||||
action: () => app.refreshComboInNodes(),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "clipboard-edit-outline",
|
||||
content: "Clipspace",
|
||||
tooltip: "Open Clipspace window",
|
||||
action: () => app["openClipspace"](),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "fit-to-page-outline",
|
||||
content: "Reset View",
|
||||
tooltip: "Reset the canvas view",
|
||||
action: () => app.resetView(),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "cancel",
|
||||
content: "Clear",
|
||||
tooltip: "Clears current workflow",
|
||||
action: () => {
|
||||
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
|
||||
app.clean();
|
||||
app.graph.clear();
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
this.settingsGroup = new ComfyButtonGroup(
|
||||
new ComfyButton({
|
||||
icon: "cog",
|
||||
content: "Settings",
|
||||
tooltip: "Open settings",
|
||||
action: () => {
|
||||
app.ui.settings.show();
|
||||
},
|
||||
})
|
||||
);
|
||||
this.viewGroup = new ComfyButtonGroup(
|
||||
new ComfyViewHistoryButton(app).element,
|
||||
new ComfyViewQueueButton(app).element,
|
||||
getInteruptButton("nlg-hide").element
|
||||
);
|
||||
this.mobileMenuButton = new ComfyButton({
|
||||
icon: "menu",
|
||||
action: (_, btn) => {
|
||||
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
classList: "comfyui-button comfyui-menu-button",
|
||||
});
|
||||
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
|
||||
this.saveButton = new ComfySplitButton(
|
||||
{
|
||||
primary: getSaveButton(),
|
||||
mode: "hover",
|
||||
position: "absolute",
|
||||
},
|
||||
getSaveButton("Save"),
|
||||
new ComfyButton({
|
||||
icon: "content-save-edit",
|
||||
content: "Save As",
|
||||
tooltip: "Save the current graph as a new workflow",
|
||||
action: () => app.workflowManager.activeWorkflow.save(true),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "download",
|
||||
content: "Export",
|
||||
tooltip: "Export the current workflow as JSON",
|
||||
action: () => this.exportWorkflow("workflow", "workflow"),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "api",
|
||||
content: "Export (API Format)",
|
||||
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
|
||||
action: () => this.exportWorkflow("workflow_api", "output"),
|
||||
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
|
||||
app,
|
||||
})
|
||||
);
|
||||
this.actionsGroup = new ComfyButtonGroup(
|
||||
new ComfyButton({
|
||||
icon: "refresh",
|
||||
content: "Refresh",
|
||||
tooltip: "Refresh widgets in nodes to find new models or files",
|
||||
action: () => app.refreshComboInNodes(),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "clipboard-edit-outline",
|
||||
content: "Clipspace",
|
||||
tooltip: "Open Clipspace window",
|
||||
action: () => app["openClipspace"](),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "fit-to-page-outline",
|
||||
content: "Reset View",
|
||||
tooltip: "Reset the canvas view",
|
||||
action: () => app.resetView(),
|
||||
}),
|
||||
new ComfyButton({
|
||||
icon: "cancel",
|
||||
content: "Clear",
|
||||
tooltip: "Clears current workflow",
|
||||
action: () => {
|
||||
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
|
||||
app.clean();
|
||||
app.graph.clear();
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
this.settingsGroup = new ComfyButtonGroup(
|
||||
new ComfyButton({
|
||||
icon: "cog",
|
||||
content: "Settings",
|
||||
tooltip: "Open settings",
|
||||
action: () => {
|
||||
app.ui.settings.show();
|
||||
},
|
||||
})
|
||||
);
|
||||
this.viewGroup = new ComfyButtonGroup(
|
||||
new ComfyViewHistoryButton(app).element,
|
||||
new ComfyViewQueueButton(app).element,
|
||||
getInteruptButton("nlg-hide").element
|
||||
);
|
||||
this.mobileMenuButton = new ComfyButton({
|
||||
icon: "menu",
|
||||
action: (_, btn) => {
|
||||
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
classList: "comfyui-button comfyui-menu-button",
|
||||
});
|
||||
|
||||
this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [
|
||||
this.logo,
|
||||
this.workflows.element,
|
||||
this.saveButton.element,
|
||||
collapseOnMobile(this.actionsGroup).element,
|
||||
$el("section.comfyui-menu-push"),
|
||||
collapseOnMobile(this.settingsGroup).element,
|
||||
collapseOnMobile(this.viewGroup).element,
|
||||
this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [
|
||||
this.logo,
|
||||
this.workflows.element,
|
||||
this.saveButton.element,
|
||||
collapseOnMobile(this.actionsGroup).element,
|
||||
$el("section.comfyui-menu-push"),
|
||||
collapseOnMobile(this.settingsGroup).element,
|
||||
collapseOnMobile(this.viewGroup).element,
|
||||
|
||||
getInteruptButton("lt-lg-show").element,
|
||||
new ComfyQueueButton(app).element,
|
||||
showOnMobile(this.mobileMenuButton).element,
|
||||
]);
|
||||
getInteruptButton("lt-lg-show").element,
|
||||
new ComfyQueueButton(app).element,
|
||||
showOnMobile(this.mobileMenuButton).element,
|
||||
]);
|
||||
|
||||
let resizeHandler;
|
||||
this.menuPositionSetting = app.ui.settings.addSetting({
|
||||
id: "Comfy.UseNewMenu",
|
||||
defaultValue: "Disabled",
|
||||
name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.",
|
||||
type: "combo",
|
||||
options: ["Disabled", "Top", "Bottom"],
|
||||
onChange: async (v) => {
|
||||
if (v && v !== "Disabled") {
|
||||
if (!resizeHandler) {
|
||||
resizeHandler = () => {
|
||||
this.calculateSizeBreak();
|
||||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
}
|
||||
this.updatePosition(v);
|
||||
} else {
|
||||
if (resizeHandler) {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
resizeHandler = null;
|
||||
}
|
||||
document.body.style.removeProperty("display");
|
||||
app.ui.menuContainer.style.removeProperty("display");
|
||||
this.element.style.display = "none";
|
||||
app.ui.restoreMenuPosition();
|
||||
}
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
});
|
||||
}
|
||||
let resizeHandler;
|
||||
this.menuPositionSetting = app.ui.settings.addSetting({
|
||||
id: "Comfy.UseNewMenu",
|
||||
defaultValue: "Disabled",
|
||||
name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.",
|
||||
type: "combo",
|
||||
options: ["Disabled", "Top", "Bottom"],
|
||||
onChange: async (v) => {
|
||||
if (v && v !== "Disabled") {
|
||||
if (!resizeHandler) {
|
||||
resizeHandler = () => {
|
||||
this.calculateSizeBreak();
|
||||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
}
|
||||
this.updatePosition(v);
|
||||
} else {
|
||||
if (resizeHandler) {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
resizeHandler = null;
|
||||
}
|
||||
document.body.style.removeProperty("display");
|
||||
app.ui.menuContainer.style.removeProperty("display");
|
||||
this.element.style.display = "none";
|
||||
app.ui.restoreMenuPosition();
|
||||
}
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updatePosition(v) {
|
||||
document.body.style.display = "grid";
|
||||
this.app.ui.menuContainer.style.display = "none";
|
||||
this.element.style.removeProperty("display");
|
||||
this.position = v;
|
||||
if (v === "Bottom") {
|
||||
this.app.bodyBottom.append(this.element);
|
||||
} else {
|
||||
this.app.bodyTop.prepend(this.element);
|
||||
}
|
||||
this.calculateSizeBreak();
|
||||
}
|
||||
updatePosition(v) {
|
||||
document.body.style.display = "grid";
|
||||
this.app.ui.menuContainer.style.display = "none";
|
||||
this.element.style.removeProperty("display");
|
||||
this.position = v;
|
||||
if (v === "Bottom") {
|
||||
this.app.bodyBottom.append(this.element);
|
||||
} else {
|
||||
this.app.bodyTop.prepend(this.element);
|
||||
}
|
||||
this.calculateSizeBreak();
|
||||
}
|
||||
|
||||
updateSizeBreak(idx, prevIdx, direction) {
|
||||
const newSize = this.#sizeBreaks[idx];
|
||||
if (newSize === this.#sizeBreak) return;
|
||||
this.#cachedInnerSize = null;
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
updateSizeBreak(idx, prevIdx, direction) {
|
||||
const newSize = this.#sizeBreaks[idx];
|
||||
if (newSize === this.#sizeBreak) return;
|
||||
this.#cachedInnerSize = null;
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
|
||||
this.#sizeBreak = this.#sizeBreaks[idx];
|
||||
for (let i = 0; i < this.#sizeBreaks.length; i++) {
|
||||
const sz = this.#sizeBreaks[i];
|
||||
if (sz === this.#sizeBreak) {
|
||||
this.element.classList.add(sz);
|
||||
} else {
|
||||
this.element.classList.remove(sz);
|
||||
}
|
||||
if (i < idx) {
|
||||
this.element.classList.add("lt-" + sz);
|
||||
} else {
|
||||
this.element.classList.remove("lt-" + sz);
|
||||
}
|
||||
}
|
||||
this.#sizeBreak = this.#sizeBreaks[idx];
|
||||
for (let i = 0; i < this.#sizeBreaks.length; i++) {
|
||||
const sz = this.#sizeBreaks[i];
|
||||
if (sz === this.#sizeBreak) {
|
||||
this.element.classList.add(sz);
|
||||
} else {
|
||||
this.element.classList.remove(sz);
|
||||
}
|
||||
if (i < idx) {
|
||||
this.element.classList.add("lt-" + sz);
|
||||
} else {
|
||||
this.element.classList.remove("lt-" + sz);
|
||||
}
|
||||
}
|
||||
|
||||
if (idx) {
|
||||
// We're on a small screen, force the menu at the top
|
||||
if (this.position !== "Top") {
|
||||
this.updatePosition("Top");
|
||||
}
|
||||
} else if (this.position != this.menuPositionSetting.value) {
|
||||
// Restore user position
|
||||
this.updatePosition(this.menuPositionSetting.value);
|
||||
}
|
||||
if (idx) {
|
||||
// We're on a small screen, force the menu at the top
|
||||
if (this.position !== "Top") {
|
||||
this.updatePosition("Top");
|
||||
}
|
||||
} else if (this.position != this.menuPositionSetting.value) {
|
||||
// Restore user position
|
||||
this.updatePosition(this.menuPositionSetting.value);
|
||||
}
|
||||
|
||||
// Allow multiple updates, but prevent bouncing
|
||||
if (!direction) {
|
||||
direction = prevIdx - idx;
|
||||
} else if (direction != prevIdx - idx) {
|
||||
return;
|
||||
}
|
||||
this.calculateSizeBreak(direction);
|
||||
}
|
||||
// Allow multiple updates, but prevent bouncing
|
||||
if (!direction) {
|
||||
direction = prevIdx - idx;
|
||||
} else if (direction != prevIdx - idx) {
|
||||
return;
|
||||
}
|
||||
this.calculateSizeBreak(direction);
|
||||
}
|
||||
|
||||
calculateSizeBreak(direction = 0) {
|
||||
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak);
|
||||
const currIdx = idx;
|
||||
const innerSize = this.calculateInnerSize(idx);
|
||||
if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) {
|
||||
if (idx > 0) {
|
||||
idx--;
|
||||
}
|
||||
} else if (innerSize > this.element.clientWidth) {
|
||||
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
|
||||
// We need to shrink
|
||||
if (idx < this.#sizeBreaks.length - 1) {
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
calculateSizeBreak(direction = 0) {
|
||||
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak);
|
||||
const currIdx = idx;
|
||||
const innerSize = this.calculateInnerSize(idx);
|
||||
if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) {
|
||||
if (idx > 0) {
|
||||
idx--;
|
||||
}
|
||||
} else if (innerSize > this.element.clientWidth) {
|
||||
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
|
||||
// We need to shrink
|
||||
if (idx < this.#sizeBreaks.length - 1) {
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSizeBreak(idx, currIdx, direction);
|
||||
}
|
||||
this.updateSizeBreak(idx, currIdx, direction);
|
||||
}
|
||||
|
||||
calculateInnerSize(idx) {
|
||||
// Cache the inner size to prevent too much calculation when resizing the window
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
if (this.#cachedInnerSize) {
|
||||
// Extend cache time
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
||||
} else {
|
||||
let innerSize = 0;
|
||||
let count = 1;
|
||||
for (const c of this.element.children) {
|
||||
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
|
||||
if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items
|
||||
innerSize += c.clientWidth;
|
||||
count++;
|
||||
}
|
||||
innerSize += 8 * count;
|
||||
this.#cachedInnerSize = innerSize;
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
||||
}
|
||||
return this.#cachedInnerSize;
|
||||
}
|
||||
calculateInnerSize(idx) {
|
||||
// Cache the inner size to prevent too much calculation when resizing the window
|
||||
clearTimeout(this.#cacheTimeout);
|
||||
if (this.#cachedInnerSize) {
|
||||
// Extend cache time
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
||||
} else {
|
||||
let innerSize = 0;
|
||||
let count = 1;
|
||||
for (const c of this.element.children) {
|
||||
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
|
||||
if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items
|
||||
innerSize += c.clientWidth;
|
||||
count++;
|
||||
}
|
||||
innerSize += 8 * count;
|
||||
this.#cachedInnerSize = innerSize;
|
||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
||||
}
|
||||
return this.#cachedInnerSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} defaultName
|
||||
*/
|
||||
getFilename(defaultName) {
|
||||
if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) {
|
||||
defaultName = prompt("Save workflow as:", defaultName);
|
||||
if (!defaultName) return;
|
||||
if (!defaultName.toLowerCase().endsWith(".json")) {
|
||||
defaultName += ".json";
|
||||
}
|
||||
}
|
||||
return defaultName;
|
||||
}
|
||||
/**
|
||||
* @param {string} defaultName
|
||||
*/
|
||||
getFilename(defaultName) {
|
||||
if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) {
|
||||
defaultName = prompt("Save workflow as:", defaultName);
|
||||
if (!defaultName) return;
|
||||
if (!defaultName.toLowerCase().endsWith(".json")) {
|
||||
defaultName += ".json";
|
||||
}
|
||||
}
|
||||
return defaultName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filename]
|
||||
* @param { "workflow" | "output" } [promptProperty]
|
||||
*/
|
||||
async exportWorkflow(filename, promptProperty) {
|
||||
if (this.app.workflowManager.activeWorkflow?.path) {
|
||||
filename = this.app.workflowManager.activeWorkflow.name;
|
||||
}
|
||||
const p = await this.app.graphToPrompt();
|
||||
const json = JSON.stringify(p[promptProperty], null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const file = this.getFilename(filename);
|
||||
if (!file) return;
|
||||
downloadBlob(file, blob);
|
||||
}
|
||||
/**
|
||||
* @param {string} [filename]
|
||||
* @param { "workflow" | "output" } [promptProperty]
|
||||
*/
|
||||
async exportWorkflow(filename, promptProperty) {
|
||||
if (this.app.workflowManager.activeWorkflow?.path) {
|
||||
filename = this.app.workflowManager.activeWorkflow.name;
|
||||
}
|
||||
const p = await this.app.graphToPrompt();
|
||||
const json = JSON.stringify(p[promptProperty], null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const file = this.getFilename(filename);
|
||||
if (!file) return;
|
||||
downloadBlob(file, blob);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ import { api } from "../../api";
|
||||
import { ComfyButton } from "../components/button";
|
||||
|
||||
export function getInteruptButton(visibility) {
|
||||
const btn = new ComfyButton({
|
||||
icon: "close",
|
||||
tooltip: "Cancel current generation",
|
||||
enabled: false,
|
||||
action: () => {
|
||||
api.interrupt();
|
||||
},
|
||||
classList: ["comfyui-button", "comfyui-interrupt-button", visibility],
|
||||
});
|
||||
const btn = new ComfyButton({
|
||||
icon: "close",
|
||||
tooltip: "Cancel current generation",
|
||||
enabled: false,
|
||||
action: () => {
|
||||
api.interrupt();
|
||||
},
|
||||
classList: ["comfyui-button", "comfyui-interrupt-button", visibility],
|
||||
});
|
||||
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
const sz = detail?.exec_info?.queue_remaining;
|
||||
btn.enabled = sz > 0;
|
||||
});
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
const sz = detail?.exec_info?.queue_remaining;
|
||||
btn.enabled = sz > 0;
|
||||
});
|
||||
|
||||
return btn;
|
||||
return btn;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,86 +8,86 @@ import { ComfyQueueOptions } from "./queueOptions";
|
||||
import { prop } from "../../utils";
|
||||
|
||||
export class ComfyQueueButton {
|
||||
element = $el("div.comfyui-queue-button");
|
||||
#internalQueueSize = 0;
|
||||
element = $el("div.comfyui-queue-button");
|
||||
#internalQueueSize = 0;
|
||||
|
||||
queuePrompt = async (e) => {
|
||||
this.#internalQueueSize += this.queueOptions.batchCount;
|
||||
// Hold shift to queue front, event is undefined when auto-queue is enabled
|
||||
await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount);
|
||||
};
|
||||
queuePrompt = async (e) => {
|
||||
this.#internalQueueSize += this.queueOptions.batchCount;
|
||||
// Hold shift to queue front, event is undefined when auto-queue is enabled
|
||||
await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount);
|
||||
};
|
||||
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
this.queueSizeElement = $el("span.comfyui-queue-count", {
|
||||
textContent: "?",
|
||||
});
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
this.queueSizeElement = $el("span.comfyui-queue-count", {
|
||||
textContent: "?",
|
||||
});
|
||||
|
||||
const queue = new ComfyButton({
|
||||
content: $el("div", [
|
||||
$el("span", {
|
||||
textContent: "Queue",
|
||||
}),
|
||||
this.queueSizeElement,
|
||||
]),
|
||||
icon: "play",
|
||||
classList: "comfyui-button",
|
||||
action: this.queuePrompt,
|
||||
});
|
||||
const queue = new ComfyButton({
|
||||
content: $el("div", [
|
||||
$el("span", {
|
||||
textContent: "Queue",
|
||||
}),
|
||||
this.queueSizeElement,
|
||||
]),
|
||||
icon: "play",
|
||||
classList: "comfyui-button",
|
||||
action: this.queuePrompt,
|
||||
});
|
||||
|
||||
this.queueOptions = new ComfyQueueOptions(app);
|
||||
this.queueOptions = new ComfyQueueOptions(app);
|
||||
|
||||
const btn = new ComfySplitButton(
|
||||
{
|
||||
primary: queue,
|
||||
mode: "click",
|
||||
position: "absolute",
|
||||
horizontal: "right",
|
||||
},
|
||||
this.queueOptions.element
|
||||
);
|
||||
btn.element.classList.add("primary");
|
||||
this.element.append(btn.element);
|
||||
const btn = new ComfySplitButton(
|
||||
{
|
||||
primary: queue,
|
||||
mode: "click",
|
||||
position: "absolute",
|
||||
horizontal: "right",
|
||||
},
|
||||
this.queueOptions.element
|
||||
);
|
||||
btn.element.classList.add("primary");
|
||||
this.element.append(btn.element);
|
||||
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "", () => {
|
||||
switch (this.autoQueueMode) {
|
||||
case "instant":
|
||||
queue.icon = "infinity";
|
||||
break;
|
||||
case "change":
|
||||
queue.icon = "auto-mode";
|
||||
break;
|
||||
default:
|
||||
queue.icon = "play";
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "", () => {
|
||||
switch (this.autoQueueMode) {
|
||||
case "instant":
|
||||
queue.icon = "infinity";
|
||||
break;
|
||||
case "change":
|
||||
queue.icon = "auto-mode";
|
||||
break;
|
||||
default:
|
||||
queue.icon = "play";
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"]));
|
||||
this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"]));
|
||||
|
||||
api.addEventListener("graphChanged", () => {
|
||||
if (this.autoQueueMode === "change") {
|
||||
if (this.#internalQueueSize) {
|
||||
this.graphHasChanged = true;
|
||||
} else {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
}
|
||||
}
|
||||
});
|
||||
api.addEventListener("graphChanged", () => {
|
||||
if (this.autoQueueMode === "change") {
|
||||
if (this.#internalQueueSize) {
|
||||
this.graphHasChanged = true;
|
||||
} else {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
|
||||
if (this.#internalQueueSize != null) {
|
||||
this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
|
||||
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
|
||||
if (!this.#internalQueueSize && !app.lastExecutionError) {
|
||||
if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
|
||||
if (this.#internalQueueSize != null) {
|
||||
this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
|
||||
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
|
||||
if (!this.#internalQueueSize && !app.lastExecutionError) {
|
||||
if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) {
|
||||
this.graphHasChanged = false;
|
||||
this.queuePrompt();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,74 +4,74 @@ import { $el } from "../../ui";
|
||||
import { prop } from "../../utils";
|
||||
|
||||
export class ComfyQueueOptions extends EventTarget {
|
||||
element = $el("div.comfyui-queue-options");
|
||||
element = $el("div.comfyui-queue-options");
|
||||
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
|
||||
this.batchCountInput = $el("input", {
|
||||
className: "comfyui-queue-batch-value",
|
||||
type: "number",
|
||||
min: "1",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountInput.value),
|
||||
});
|
||||
this.batchCountInput = $el("input", {
|
||||
className: "comfyui-queue-batch-value",
|
||||
type: "number",
|
||||
min: "1",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountInput.value),
|
||||
});
|
||||
|
||||
this.batchCountRange = $el("input", {
|
||||
type: "range",
|
||||
min: "1",
|
||||
max: "100",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountRange.value),
|
||||
});
|
||||
this.batchCountRange = $el("input", {
|
||||
type: "range",
|
||||
min: "1",
|
||||
max: "100",
|
||||
value: "1",
|
||||
oninput: () => (this.batchCount = +this.batchCountRange.value),
|
||||
});
|
||||
|
||||
this.element.append(
|
||||
$el("div.comfyui-queue-batch", [
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
textContent: "Batch count: ",
|
||||
},
|
||||
this.batchCountInput
|
||||
),
|
||||
this.batchCountRange,
|
||||
])
|
||||
);
|
||||
this.element.append(
|
||||
$el("div.comfyui-queue-batch", [
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
textContent: "Batch count: ",
|
||||
},
|
||||
this.batchCountInput
|
||||
),
|
||||
this.batchCountRange,
|
||||
])
|
||||
);
|
||||
|
||||
const createOption = (text, value, checked = false) =>
|
||||
$el(
|
||||
"label",
|
||||
{ textContent: text },
|
||||
$el("input", {
|
||||
type: "radio",
|
||||
name: "AutoQueueMode",
|
||||
checked,
|
||||
value,
|
||||
oninput: (e) => (this.autoQueueMode = e.target["value"]),
|
||||
})
|
||||
);
|
||||
const createOption = (text, value, checked = false) =>
|
||||
$el(
|
||||
"label",
|
||||
{ textContent: text },
|
||||
$el("input", {
|
||||
type: "radio",
|
||||
name: "AutoQueueMode",
|
||||
checked,
|
||||
value,
|
||||
oninput: (e) => (this.autoQueueMode = e.target["value"]),
|
||||
})
|
||||
);
|
||||
|
||||
this.autoQueueEl = $el("div.comfyui-queue-mode", [
|
||||
$el("span", "Auto Queue:"),
|
||||
createOption("Disabled", "", true),
|
||||
createOption("Instant", "instant"),
|
||||
createOption("On Change", "change"),
|
||||
]);
|
||||
this.autoQueueEl = $el("div.comfyui-queue-mode", [
|
||||
$el("span", "Auto Queue:"),
|
||||
createOption("Disabled", "", true),
|
||||
createOption("Instant", "instant"),
|
||||
createOption("On Change", "change"),
|
||||
]);
|
||||
|
||||
this.element.append(this.autoQueueEl);
|
||||
this.element.append(this.autoQueueEl);
|
||||
|
||||
this.batchCount = prop(this, "batchCount", 1, () => {
|
||||
this.batchCountInput.value = this.batchCount + "";
|
||||
this.batchCountRange.value = this.batchCount + "";
|
||||
});
|
||||
this.batchCount = prop(this, "batchCount", 1, () => {
|
||||
this.batchCountInput.value = this.batchCount + "";
|
||||
this.batchCountRange.value = this.batchCount + "";
|
||||
});
|
||||
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("autoQueueMode", {
|
||||
detail: this.autoQueueMode,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("autoQueueMode", {
|
||||
detail: this.autoQueueMode,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,24 @@ import { ComfyButton } from "../components/button";
|
||||
import { ComfyViewList, ComfyViewListButton } from "./viewList";
|
||||
|
||||
export class ComfyViewHistoryButton extends ComfyViewListButton {
|
||||
constructor(app) {
|
||||
super(app, {
|
||||
button: new ComfyButton({
|
||||
content: "View History",
|
||||
icon: "history",
|
||||
tooltip: "View history",
|
||||
classList: "comfyui-button comfyui-history-button",
|
||||
}),
|
||||
list: ComfyViewHistoryList,
|
||||
mode: "History",
|
||||
});
|
||||
}
|
||||
constructor(app) {
|
||||
super(app, {
|
||||
button: new ComfyButton({
|
||||
content: "View History",
|
||||
icon: "history",
|
||||
tooltip: "View history",
|
||||
classList: "comfyui-button comfyui-history-button",
|
||||
}),
|
||||
list: ComfyViewHistoryList,
|
||||
mode: "History",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyViewHistoryList extends ComfyViewList {
|
||||
async loadItems() {
|
||||
const items = await super.loadItems();
|
||||
items["History"].reverse();
|
||||
return items;
|
||||
}
|
||||
async loadItems() {
|
||||
const items = await super.loadItems();
|
||||
items["History"].reverse();
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,198 +6,198 @@ import { api } from "../../api";
|
||||
import { ComfyPopup } from "../components/popup";
|
||||
|
||||
export class ComfyViewListButton {
|
||||
get open() {
|
||||
return this.popup.open;
|
||||
}
|
||||
get open() {
|
||||
return this.popup.open;
|
||||
}
|
||||
|
||||
set open(open) {
|
||||
this.popup.open = open;
|
||||
}
|
||||
set open(open) {
|
||||
this.popup.open = open;
|
||||
}
|
||||
|
||||
constructor(app, { button, list, mode }) {
|
||||
this.app = app;
|
||||
this.button = button;
|
||||
this.element = $el("div.comfyui-button-wrapper", this.button.element);
|
||||
this.popup = new ComfyPopup({
|
||||
target: this.element,
|
||||
container: this.element,
|
||||
horizontal: "right",
|
||||
});
|
||||
this.list = new (list ?? ComfyViewList)(app, mode, this.popup);
|
||||
this.popup.children = [this.list.element];
|
||||
this.popup.addEventListener("open", () => {
|
||||
this.list.update();
|
||||
});
|
||||
this.popup.addEventListener("close", () => {
|
||||
this.list.close();
|
||||
});
|
||||
this.button.withPopup(this.popup);
|
||||
constructor(app, { button, list, mode }) {
|
||||
this.app = app;
|
||||
this.button = button;
|
||||
this.element = $el("div.comfyui-button-wrapper", this.button.element);
|
||||
this.popup = new ComfyPopup({
|
||||
target: this.element,
|
||||
container: this.element,
|
||||
horizontal: "right",
|
||||
});
|
||||
this.list = new (list ?? ComfyViewList)(app, mode, this.popup);
|
||||
this.popup.children = [this.list.element];
|
||||
this.popup.addEventListener("open", () => {
|
||||
this.list.update();
|
||||
});
|
||||
this.popup.addEventListener("close", () => {
|
||||
this.list.close();
|
||||
});
|
||||
this.button.withPopup(this.popup);
|
||||
|
||||
api.addEventListener("status", () => {
|
||||
if (this.popup.open) {
|
||||
this.popup.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
api.addEventListener("status", () => {
|
||||
if (this.popup.open) {
|
||||
this.popup.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyViewList {
|
||||
popup;
|
||||
popup;
|
||||
|
||||
constructor(app, mode, popup) {
|
||||
this.app = app;
|
||||
this.mode = mode;
|
||||
this.popup = popup;
|
||||
this.type = mode.toLowerCase();
|
||||
constructor(app, mode, popup) {
|
||||
this.app = app;
|
||||
this.mode = mode;
|
||||
this.popup = popup;
|
||||
this.type = mode.toLowerCase();
|
||||
|
||||
this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`);
|
||||
this.clear = new ComfyButton({
|
||||
icon: "cancel",
|
||||
content: "Clear",
|
||||
action: async () => {
|
||||
this.showSpinner(false);
|
||||
await api.clearItems(this.type);
|
||||
await this.update();
|
||||
},
|
||||
});
|
||||
this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`);
|
||||
this.clear = new ComfyButton({
|
||||
icon: "cancel",
|
||||
content: "Clear",
|
||||
action: async () => {
|
||||
this.showSpinner(false);
|
||||
await api.clearItems(this.type);
|
||||
await this.update();
|
||||
},
|
||||
});
|
||||
|
||||
this.refresh = new ComfyButton({
|
||||
icon: "refresh",
|
||||
content: "Refresh",
|
||||
action: async () => {
|
||||
await this.update(false);
|
||||
},
|
||||
});
|
||||
this.refresh = new ComfyButton({
|
||||
icon: "refresh",
|
||||
content: "Refresh",
|
||||
action: async () => {
|
||||
await this.update(false);
|
||||
},
|
||||
});
|
||||
|
||||
this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [
|
||||
$el("h3", mode),
|
||||
$el("header", [this.clear.element, this.refresh.element]),
|
||||
this.items,
|
||||
]);
|
||||
this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [
|
||||
$el("h3", mode),
|
||||
$el("header", [this.clear.element, this.refresh.element]),
|
||||
this.items,
|
||||
]);
|
||||
|
||||
api.addEventListener("status", () => {
|
||||
if (this.popup.open) {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
api.addEventListener("status", () => {
|
||||
if (this.popup.open) {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.items.replaceChildren();
|
||||
}
|
||||
async close() {
|
||||
this.items.replaceChildren();
|
||||
}
|
||||
|
||||
async update(resize = true) {
|
||||
this.showSpinner(resize);
|
||||
const res = await this.loadItems();
|
||||
let any = false;
|
||||
async update(resize = true) {
|
||||
this.showSpinner(resize);
|
||||
const res = await this.loadItems();
|
||||
let any = false;
|
||||
|
||||
const names = Object.keys(res);
|
||||
const sections = names
|
||||
.map((section) => {
|
||||
const items = res[section];
|
||||
if (items?.length) {
|
||||
any = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const names = Object.keys(res);
|
||||
const sections = names
|
||||
.map((section) => {
|
||||
const items = res[section];
|
||||
if (items?.length) {
|
||||
any = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
if (names.length > 1) {
|
||||
rows.push($el("h5", section));
|
||||
}
|
||||
rows.push(...items.flatMap((item) => this.createRow(item, section)));
|
||||
return $el("section", rows);
|
||||
})
|
||||
.filter(Boolean);
|
||||
const rows = [];
|
||||
if (names.length > 1) {
|
||||
rows.push($el("h5", section));
|
||||
}
|
||||
rows.push(...items.flatMap((item) => this.createRow(item, section)));
|
||||
return $el("section", rows);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (any) {
|
||||
this.items.replaceChildren(...sections);
|
||||
} else {
|
||||
this.items.replaceChildren($el("h5", "None"));
|
||||
}
|
||||
if (any) {
|
||||
this.items.replaceChildren(...sections);
|
||||
} else {
|
||||
this.items.replaceChildren($el("h5", "None"));
|
||||
}
|
||||
|
||||
this.popup.update();
|
||||
this.clear.enabled = this.refresh.enabled = true;
|
||||
this.element.style.removeProperty("height");
|
||||
}
|
||||
this.popup.update();
|
||||
this.clear.enabled = this.refresh.enabled = true;
|
||||
this.element.style.removeProperty("height");
|
||||
}
|
||||
|
||||
showSpinner(resize = true) {
|
||||
// if (!this.spinner) {
|
||||
// this.spinner = createSpinner();
|
||||
// }
|
||||
// if (!resize) {
|
||||
// this.element.style.height = this.element.clientHeight + "px";
|
||||
// }
|
||||
// this.clear.enabled = this.refresh.enabled = false;
|
||||
// this.items.replaceChildren(
|
||||
// $el(
|
||||
// "div",
|
||||
// {
|
||||
// style: {
|
||||
// fontSize: "18px",
|
||||
// },
|
||||
// },
|
||||
// this.spinner
|
||||
// )
|
||||
// );
|
||||
// this.popup.update();
|
||||
}
|
||||
showSpinner(resize = true) {
|
||||
// if (!this.spinner) {
|
||||
// this.spinner = createSpinner();
|
||||
// }
|
||||
// if (!resize) {
|
||||
// this.element.style.height = this.element.clientHeight + "px";
|
||||
// }
|
||||
// this.clear.enabled = this.refresh.enabled = false;
|
||||
// this.items.replaceChildren(
|
||||
// $el(
|
||||
// "div",
|
||||
// {
|
||||
// style: {
|
||||
// fontSize: "18px",
|
||||
// },
|
||||
// },
|
||||
// this.spinner
|
||||
// )
|
||||
// );
|
||||
// this.popup.update();
|
||||
}
|
||||
|
||||
async loadItems() {
|
||||
return await api.getItems(this.type);
|
||||
}
|
||||
async loadItems() {
|
||||
return await api.getItems(this.type);
|
||||
}
|
||||
|
||||
getRow(item, section) {
|
||||
return {
|
||||
text: item.prompt[0] + "",
|
||||
actions: [
|
||||
{
|
||||
text: "Load",
|
||||
action: async () => {
|
||||
try {
|
||||
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
||||
if (item.outputs) {
|
||||
this.app.nodeOutputs = item.outputs;
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error loading workflow: " + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Delete",
|
||||
action: async () => {
|
||||
try {
|
||||
await api.deleteItem(this.type, item.prompt[1]);
|
||||
this.update();
|
||||
} catch (error) {}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
getRow(item, section) {
|
||||
return {
|
||||
text: item.prompt[0] + "",
|
||||
actions: [
|
||||
{
|
||||
text: "Load",
|
||||
action: async () => {
|
||||
try {
|
||||
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
||||
if (item.outputs) {
|
||||
this.app.nodeOutputs = item.outputs;
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error loading workflow: " + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Delete",
|
||||
action: async () => {
|
||||
try {
|
||||
await api.deleteItem(this.type, item.prompt[1]);
|
||||
this.update();
|
||||
} catch (error) {}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
createRow = (item, section) => {
|
||||
const row = this.getRow(item, section);
|
||||
return [
|
||||
$el("span", row.text),
|
||||
...row.actions.map(
|
||||
(a) =>
|
||||
new ComfyButton({
|
||||
content: a.text,
|
||||
action: async (e, btn) => {
|
||||
btn.enabled = false;
|
||||
try {
|
||||
await a.action();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
btn.enabled = true;
|
||||
}
|
||||
},
|
||||
}).element
|
||||
),
|
||||
];
|
||||
};
|
||||
createRow = (item, section) => {
|
||||
const row = this.getRow(item, section);
|
||||
return [
|
||||
$el("span", row.text),
|
||||
...row.actions.map(
|
||||
(a) =>
|
||||
new ComfyButton({
|
||||
content: a.text,
|
||||
action: async (e, btn) => {
|
||||
btn.enabled = false;
|
||||
try {
|
||||
await a.action();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
btn.enabled = true;
|
||||
}
|
||||
},
|
||||
}).element
|
||||
),
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,51 +5,51 @@ import { ComfyViewList, ComfyViewListButton } from "./viewList";
|
||||
import { api } from "../../api";
|
||||
|
||||
export class ComfyViewQueueButton extends ComfyViewListButton {
|
||||
constructor(app) {
|
||||
super(app, {
|
||||
button: new ComfyButton({
|
||||
content: "View Queue",
|
||||
icon: "format-list-numbered",
|
||||
tooltip: "View queue",
|
||||
classList: "comfyui-button comfyui-queue-button",
|
||||
}),
|
||||
list: ComfyViewQueueList,
|
||||
mode: "Queue",
|
||||
});
|
||||
}
|
||||
constructor(app) {
|
||||
super(app, {
|
||||
button: new ComfyButton({
|
||||
content: "View Queue",
|
||||
icon: "format-list-numbered",
|
||||
tooltip: "View queue",
|
||||
classList: "comfyui-button comfyui-queue-button",
|
||||
}),
|
||||
list: ComfyViewQueueList,
|
||||
mode: "Queue",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyViewQueueList extends ComfyViewList {
|
||||
getRow = (item, section) => {
|
||||
if (section !== "Running") {
|
||||
return super.getRow(item, section);
|
||||
}
|
||||
return {
|
||||
text: item.prompt[0] + "",
|
||||
actions: [
|
||||
{
|
||||
text: "Load",
|
||||
action: async () => {
|
||||
try {
|
||||
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
||||
if (item.outputs) {
|
||||
this.app.nodeOutputs = item.outputs;
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error loading workflow: " + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Cancel",
|
||||
action: async () => {
|
||||
try {
|
||||
await api.interrupt();
|
||||
} catch (error) {}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
getRow = (item, section) => {
|
||||
if (section !== "Running") {
|
||||
return super.getRow(item, section);
|
||||
}
|
||||
return {
|
||||
text: item.prompt[0] + "",
|
||||
actions: [
|
||||
{
|
||||
text: "Load",
|
||||
action: async () => {
|
||||
try {
|
||||
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
||||
if (item.outputs) {
|
||||
this.app.nodeOutputs = item.outputs;
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error loading workflow: " + error.message);
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Cancel",
|
||||
action: async () => {
|
||||
try {
|
||||
await api.interrupt();
|
||||
} catch (error) {}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,359 +5,359 @@ import type { ComfyApp } from "../app";
|
||||
|
||||
/* The Setting entry stored in `ComfySettingsDialog` */
|
||||
interface Setting {
|
||||
id: string;
|
||||
onChange?: (value: any, oldValue?: any) => void;
|
||||
name: string;
|
||||
render: () => HTMLElement;
|
||||
id: string;
|
||||
onChange?: (value: any, oldValue?: any) => void;
|
||||
name: string;
|
||||
render: () => HTMLElement;
|
||||
}
|
||||
|
||||
interface SettingOption {
|
||||
text: string;
|
||||
value?: string;
|
||||
text: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface SettingParams {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string | ((name: string, setter: (v: any) => void, value: any, attrs: any) => HTMLElement);
|
||||
defaultValue: any;
|
||||
onChange?: (newValue: any, oldValue?: any) => void;
|
||||
attrs?: any;
|
||||
tooltip?: string;
|
||||
options?: SettingOption[] | ((value: any) => SettingOption[]);
|
||||
id: string;
|
||||
name: string;
|
||||
type: string | ((name: string, setter: (v: any) => void, value: any, attrs: any) => HTMLElement);
|
||||
defaultValue: any;
|
||||
onChange?: (newValue: any, oldValue?: any) => void;
|
||||
attrs?: any;
|
||||
tooltip?: string;
|
||||
options?: SettingOption[] | ((value: any) => SettingOption[]);
|
||||
}
|
||||
|
||||
export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
app: ComfyApp;
|
||||
settingsValues: any;
|
||||
settingsLookup: Record<string, Setting>;
|
||||
app: ComfyApp;
|
||||
settingsValues: any;
|
||||
settingsLookup: Record<string, Setting>;
|
||||
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.settingsValues = {};
|
||||
this.settingsLookup = {};
|
||||
this.element = $el(
|
||||
"dialog",
|
||||
{
|
||||
id: "comfy-settings-dialog",
|
||||
parent: document.body,
|
||||
},
|
||||
[
|
||||
$el("table.comfy-modal-content.comfy-table", [
|
||||
$el(
|
||||
"caption",
|
||||
{ textContent: "Settings" },
|
||||
$el("button.comfy-btn", {
|
||||
type: "button",
|
||||
textContent: "\u00d7",
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
})
|
||||
),
|
||||
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
style: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]
|
||||
) as HTMLDialogElement;
|
||||
}
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.settingsValues = {};
|
||||
this.settingsLookup = {};
|
||||
this.element = $el(
|
||||
"dialog",
|
||||
{
|
||||
id: "comfy-settings-dialog",
|
||||
parent: document.body,
|
||||
},
|
||||
[
|
||||
$el("table.comfy-modal-content.comfy-table", [
|
||||
$el(
|
||||
"caption",
|
||||
{ textContent: "Settings" },
|
||||
$el("button.comfy-btn", {
|
||||
type: "button",
|
||||
textContent: "\u00d7",
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
})
|
||||
),
|
||||
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
style: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
onclick: () => {
|
||||
this.element.close();
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]
|
||||
) as HTMLDialogElement;
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return Object.values(this.settingsLookup);
|
||||
}
|
||||
get settings() {
|
||||
return Object.values(this.settingsLookup);
|
||||
}
|
||||
|
||||
#dispatchChange(id, value, oldValue?) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(id + ".change", {
|
||||
detail: {
|
||||
value,
|
||||
oldValue
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
#dispatchChange(id, value, oldValue?) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(id + ".change", {
|
||||
detail: {
|
||||
value,
|
||||
oldValue
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
this.settingsValues = localStorage;
|
||||
} else {
|
||||
this.settingsValues = await api.getSettings();
|
||||
}
|
||||
async load() {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
this.settingsValues = localStorage;
|
||||
} else {
|
||||
this.settingsValues = await api.getSettings();
|
||||
}
|
||||
|
||||
// Trigger onChange for any settings added before load
|
||||
for (const id in this.settingsLookup) {
|
||||
const value = this.settingsValues[this.getId(id)];
|
||||
this.settingsLookup[id].onChange?.(value);
|
||||
this.#dispatchChange(id, value);
|
||||
}
|
||||
}
|
||||
// Trigger onChange for any settings added before load
|
||||
for (const id in this.settingsLookup) {
|
||||
const value = this.settingsValues[this.getId(id)];
|
||||
this.settingsLookup[id].onChange?.(value);
|
||||
this.#dispatchChange(id, value);
|
||||
}
|
||||
}
|
||||
|
||||
getId(id) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
id = "Comfy.Settings." + id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
getId(id) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
id = "Comfy.Settings." + id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
getSettingValue(id, defaultValue?) {
|
||||
let value = this.settingsValues[this.getId(id)];
|
||||
if (value != null) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return value ?? defaultValue;
|
||||
}
|
||||
getSettingValue(id, defaultValue?) {
|
||||
let value = this.settingsValues[this.getId(id)];
|
||||
if (value != null) {
|
||||
if (this.app.storageLocation === "browser") {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return value ?? defaultValue;
|
||||
}
|
||||
|
||||
async setSettingValueAsync(id, value) {
|
||||
const json = JSON.stringify(value);
|
||||
localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage
|
||||
async setSettingValueAsync(id, value) {
|
||||
const json = JSON.stringify(value);
|
||||
localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage
|
||||
|
||||
let oldValue = this.getSettingValue(id, undefined);
|
||||
this.settingsValues[this.getId(id)] = value;
|
||||
let oldValue = this.getSettingValue(id, undefined);
|
||||
this.settingsValues[this.getId(id)] = value;
|
||||
|
||||
if (id in this.settingsLookup) {
|
||||
this.settingsLookup[id].onChange?.(value, oldValue);
|
||||
}
|
||||
this.#dispatchChange(id, value, oldValue);
|
||||
if (id in this.settingsLookup) {
|
||||
this.settingsLookup[id].onChange?.(value, oldValue);
|
||||
}
|
||||
this.#dispatchChange(id, value, oldValue);
|
||||
|
||||
await api.storeSetting(id, value);
|
||||
}
|
||||
await api.storeSetting(id, value);
|
||||
}
|
||||
|
||||
setSettingValue(id, value) {
|
||||
this.setSettingValueAsync(id, value).catch((err) => {
|
||||
alert(`Error saving setting '${id}'`);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
setSettingValue(id, value) {
|
||||
this.setSettingValueAsync(id, value).catch((err) => {
|
||||
alert(`Error saving setting '${id}'`);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
addSetting(params: SettingParams) {
|
||||
const { id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined } = params;
|
||||
if (!id) {
|
||||
throw new Error("Settings must have an ID");
|
||||
}
|
||||
addSetting(params: SettingParams) {
|
||||
const { id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined } = params;
|
||||
if (!id) {
|
||||
throw new Error("Settings must have an ID");
|
||||
}
|
||||
|
||||
if (id in this.settingsLookup) {
|
||||
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`);
|
||||
}
|
||||
if (id in this.settingsLookup) {
|
||||
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`);
|
||||
}
|
||||
|
||||
let skipOnChange = false;
|
||||
let value = this.getSettingValue(id);
|
||||
if (value == null) {
|
||||
if (this.app.isNewUserSession) {
|
||||
// Check if we have a localStorage value but not a setting value and we are a new user
|
||||
const localValue = localStorage["Comfy.Settings." + id];
|
||||
if (localValue) {
|
||||
value = JSON.parse(localValue);
|
||||
this.setSettingValue(id, value); // Store on the server
|
||||
}
|
||||
}
|
||||
if (value == null) {
|
||||
value = defaultValue;
|
||||
}
|
||||
}
|
||||
let skipOnChange = false;
|
||||
let value = this.getSettingValue(id);
|
||||
if (value == null) {
|
||||
if (this.app.isNewUserSession) {
|
||||
// Check if we have a localStorage value but not a setting value and we are a new user
|
||||
const localValue = localStorage["Comfy.Settings." + id];
|
||||
if (localValue) {
|
||||
value = JSON.parse(localValue);
|
||||
this.setSettingValue(id, value); // Store on the server
|
||||
}
|
||||
}
|
||||
if (value == null) {
|
||||
value = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger initial setting of value
|
||||
if (!skipOnChange) {
|
||||
onChange?.(value, undefined);
|
||||
}
|
||||
// Trigger initial setting of value
|
||||
if (!skipOnChange) {
|
||||
onChange?.(value, undefined);
|
||||
}
|
||||
|
||||
this.settingsLookup[id] = {
|
||||
id,
|
||||
onChange,
|
||||
name,
|
||||
render: () => {
|
||||
if (type === "hidden") return;
|
||||
this.settingsLookup[id] = {
|
||||
id,
|
||||
onChange,
|
||||
name,
|
||||
render: () => {
|
||||
if (type === "hidden") return;
|
||||
|
||||
const setter = (v) => {
|
||||
if (onChange) {
|
||||
onChange(v, value);
|
||||
}
|
||||
const setter = (v) => {
|
||||
if (onChange) {
|
||||
onChange(v, value);
|
||||
}
|
||||
|
||||
this.setSettingValue(id, v);
|
||||
value = v;
|
||||
};
|
||||
value = this.getSettingValue(id, defaultValue);
|
||||
this.setSettingValue(id, v);
|
||||
value = v;
|
||||
};
|
||||
value = this.getSettingValue(id, defaultValue);
|
||||
|
||||
let element;
|
||||
const htmlID = id.replaceAll(".", "-");
|
||||
let element;
|
||||
const htmlID = id.replaceAll(".", "-");
|
||||
|
||||
const labelCell = $el("td", [
|
||||
$el("label", {
|
||||
for: htmlID,
|
||||
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
|
||||
textContent: name,
|
||||
}),
|
||||
]);
|
||||
const labelCell = $el("td", [
|
||||
$el("label", {
|
||||
for: htmlID,
|
||||
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
|
||||
textContent: name,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (typeof type === "function") {
|
||||
element = type(name, setter, value, attrs);
|
||||
} else {
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
id: htmlID,
|
||||
type: "checkbox",
|
||||
checked: value,
|
||||
onchange: (event) => {
|
||||
const isChecked = event.target.checked;
|
||||
if (onChange !== undefined) {
|
||||
onChange(isChecked);
|
||||
}
|
||||
this.setSettingValue(id, isChecked);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "number":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
type,
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "slider":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el(
|
||||
"div",
|
||||
{
|
||||
style: {
|
||||
display: "grid",
|
||||
gridAutoFlow: "column",
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("input", {
|
||||
...attrs,
|
||||
value,
|
||||
type: "range",
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.nextElementSibling.value = e.target.value;
|
||||
},
|
||||
}),
|
||||
$el("input", {
|
||||
...attrs,
|
||||
value,
|
||||
id: htmlID,
|
||||
type: "number",
|
||||
style: { maxWidth: "4rem" },
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.previousElementSibling.value = e.target.value;
|
||||
},
|
||||
}),
|
||||
]
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "combo":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el(
|
||||
"select",
|
||||
{
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
},
|
||||
},
|
||||
(typeof options === "function" ? options(value) : options || []).map((opt) => {
|
||||
if (typeof opt === "string") {
|
||||
opt = { text: opt };
|
||||
}
|
||||
const v = opt.value ?? opt.text;
|
||||
return $el("option", {
|
||||
value: v,
|
||||
textContent: opt.text,
|
||||
selected: value + "" === v + "",
|
||||
});
|
||||
})
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "text":
|
||||
default:
|
||||
if (type !== "text") {
|
||||
console.warn(`Unsupported setting type '${type}, defaulting to text`);
|
||||
}
|
||||
if (typeof type === "function") {
|
||||
element = type(name, setter, value, attrs);
|
||||
} else {
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
id: htmlID,
|
||||
type: "checkbox",
|
||||
checked: value,
|
||||
onchange: (event) => {
|
||||
const isChecked = event.target.checked;
|
||||
if (onChange !== undefined) {
|
||||
onChange(isChecked);
|
||||
}
|
||||
this.setSettingValue(id, isChecked);
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "number":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
type,
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "slider":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el(
|
||||
"div",
|
||||
{
|
||||
style: {
|
||||
display: "grid",
|
||||
gridAutoFlow: "column",
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("input", {
|
||||
...attrs,
|
||||
value,
|
||||
type: "range",
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.nextElementSibling.value = e.target.value;
|
||||
},
|
||||
}),
|
||||
$el("input", {
|
||||
...attrs,
|
||||
value,
|
||||
id: htmlID,
|
||||
type: "number",
|
||||
style: { maxWidth: "4rem" },
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
e.target.previousElementSibling.value = e.target.value;
|
||||
},
|
||||
}),
|
||||
]
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "combo":
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el(
|
||||
"select",
|
||||
{
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
},
|
||||
},
|
||||
(typeof options === "function" ? options(value) : options || []).map((opt) => {
|
||||
if (typeof opt === "string") {
|
||||
opt = { text: opt };
|
||||
}
|
||||
const v = opt.value ?? opt.text;
|
||||
return $el("option", {
|
||||
value: v,
|
||||
textContent: opt.text,
|
||||
selected: value + "" === v + "",
|
||||
});
|
||||
})
|
||||
),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
case "text":
|
||||
default:
|
||||
if (type !== "text") {
|
||||
console.warn(`Unsupported setting type '${type}, defaulting to text`);
|
||||
}
|
||||
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tooltip) {
|
||||
element.title = tooltip;
|
||||
}
|
||||
element = $el("tr", [
|
||||
labelCell,
|
||||
$el("td", [
|
||||
$el("input", {
|
||||
value,
|
||||
id: htmlID,
|
||||
oninput: (e) => {
|
||||
setter(e.target.value);
|
||||
},
|
||||
...attrs,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tooltip) {
|
||||
element.title = tooltip;
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
} as Setting;
|
||||
return element;
|
||||
},
|
||||
} as Setting;
|
||||
|
||||
const self = this;
|
||||
return {
|
||||
get value() {
|
||||
return self.getSettingValue(id, defaultValue);
|
||||
},
|
||||
set value(v) {
|
||||
self.setSettingValue(id, v);
|
||||
},
|
||||
};
|
||||
}
|
||||
const self = this;
|
||||
return {
|
||||
get value() {
|
||||
return self.getSettingValue(id, defaultValue);
|
||||
},
|
||||
set value(v) {
|
||||
self.setSettingValue(id, v);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
show() {
|
||||
this.textElement.replaceChildren(
|
||||
$el(
|
||||
"tr",
|
||||
{
|
||||
style: { display: "none" },
|
||||
},
|
||||
[$el("th"), $el("th", { style: { width: "33%" } })]
|
||||
),
|
||||
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean)
|
||||
);
|
||||
this.element.showModal();
|
||||
}
|
||||
show() {
|
||||
this.textElement.replaceChildren(
|
||||
$el(
|
||||
"tr",
|
||||
{
|
||||
style: { display: "none" },
|
||||
},
|
||||
[$el("th"), $el("th", { style: { width: "33%" } })]
|
||||
),
|
||||
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean)
|
||||
);
|
||||
this.element.showModal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0.15em solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0.15em solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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>`;
|
||||
return div.firstElementChild;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
|
||||
return div.firstElementChild;
|
||||
}
|
||||
|
||||
@@ -11,52 +11,52 @@ import { $el } from "../ui";
|
||||
* @param { (e: { item: ToggleSwitchItem, prev?: ToggleSwitchItem }) => void } [opts.onChange]
|
||||
*/
|
||||
export function toggleSwitch(name, items, e?) {
|
||||
const onChange = e?.onChange;
|
||||
const onChange = e?.onChange;
|
||||
|
||||
let selectedIndex;
|
||||
let elements;
|
||||
let selectedIndex;
|
||||
let elements;
|
||||
|
||||
function updateSelected(index) {
|
||||
if (selectedIndex != null) {
|
||||
elements[selectedIndex].classList.remove("comfy-toggle-selected");
|
||||
}
|
||||
onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] });
|
||||
selectedIndex = index;
|
||||
elements[selectedIndex].classList.add("comfy-toggle-selected");
|
||||
}
|
||||
function updateSelected(index) {
|
||||
if (selectedIndex != null) {
|
||||
elements[selectedIndex].classList.remove("comfy-toggle-selected");
|
||||
}
|
||||
onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] });
|
||||
selectedIndex = index;
|
||||
elements[selectedIndex].classList.add("comfy-toggle-selected");
|
||||
}
|
||||
|
||||
elements = items.map((item, i) => {
|
||||
if (typeof item === "string") item = { text: item };
|
||||
if (!item.value) item.value = item.text;
|
||||
elements = items.map((item, i) => {
|
||||
if (typeof item === "string") item = { text: item };
|
||||
if (!item.value) item.value = item.text;
|
||||
|
||||
const toggle = $el(
|
||||
"label",
|
||||
{
|
||||
textContent: item.text,
|
||||
title: item.tooltip ?? "",
|
||||
},
|
||||
$el("input", {
|
||||
name,
|
||||
type: "radio",
|
||||
value: item.value ?? item.text,
|
||||
checked: item.selected,
|
||||
onchange: () => {
|
||||
updateSelected(i);
|
||||
},
|
||||
})
|
||||
);
|
||||
if (item.selected) {
|
||||
updateSelected(i);
|
||||
}
|
||||
return toggle;
|
||||
});
|
||||
const toggle = $el(
|
||||
"label",
|
||||
{
|
||||
textContent: item.text,
|
||||
title: item.tooltip ?? "",
|
||||
},
|
||||
$el("input", {
|
||||
name,
|
||||
type: "radio",
|
||||
value: item.value ?? item.text,
|
||||
checked: item.selected,
|
||||
onchange: () => {
|
||||
updateSelected(i);
|
||||
},
|
||||
})
|
||||
);
|
||||
if (item.selected) {
|
||||
updateSelected(i);
|
||||
}
|
||||
return toggle;
|
||||
});
|
||||
|
||||
const container = $el("div.comfy-toggle-switch", elements);
|
||||
const container = $el("div.comfy-toggle-switch", elements);
|
||||
|
||||
if (selectedIndex == null) {
|
||||
elements[0].children[0].checked = true;
|
||||
updateSelected(0);
|
||||
}
|
||||
if (selectedIndex == null) {
|
||||
elements[0].children[0].checked = true;
|
||||
updateSelected(0);
|
||||
}
|
||||
|
||||
return container;
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -5,122 +5,122 @@ import "./userSelection.css";
|
||||
|
||||
|
||||
interface SelectedUser {
|
||||
username: string;
|
||||
userId: string;
|
||||
created: boolean;
|
||||
username: string;
|
||||
userId: string;
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
|
||||
export class UserSelectionScreen {
|
||||
async show(users, user): Promise<SelectedUser>{
|
||||
const userSelection = document.getElementById("comfy-user-selection");
|
||||
userSelection.style.display = "";
|
||||
return new Promise((resolve) => {
|
||||
const input = userSelection.getElementsByTagName("input")[0];
|
||||
const select = userSelection.getElementsByTagName("select")[0];
|
||||
const inputSection = input.closest("section");
|
||||
const selectSection = select.closest("section");
|
||||
const form = userSelection.getElementsByTagName("form")[0];
|
||||
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
|
||||
const button = userSelection.getElementsByClassName("comfy-user-button-next")[0];
|
||||
async show(users, user): Promise<SelectedUser>{
|
||||
const userSelection = document.getElementById("comfy-user-selection");
|
||||
userSelection.style.display = "";
|
||||
return new Promise((resolve) => {
|
||||
const input = userSelection.getElementsByTagName("input")[0];
|
||||
const select = userSelection.getElementsByTagName("select")[0];
|
||||
const inputSection = input.closest("section");
|
||||
const selectSection = select.closest("section");
|
||||
const form = userSelection.getElementsByTagName("form")[0];
|
||||
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
|
||||
const button = userSelection.getElementsByClassName("comfy-user-button-next")[0];
|
||||
|
||||
let inputActive = null;
|
||||
input.addEventListener("focus", () => {
|
||||
inputSection.classList.add("selected");
|
||||
selectSection.classList.remove("selected");
|
||||
inputActive = true;
|
||||
});
|
||||
select.addEventListener("focus", () => {
|
||||
inputSection.classList.remove("selected");
|
||||
selectSection.classList.add("selected");
|
||||
inputActive = false;
|
||||
select.style.color = "";
|
||||
});
|
||||
select.addEventListener("blur", () => {
|
||||
if (!select.value) {
|
||||
select.style.color = "var(--descrip-text)";
|
||||
}
|
||||
});
|
||||
let inputActive = null;
|
||||
input.addEventListener("focus", () => {
|
||||
inputSection.classList.add("selected");
|
||||
selectSection.classList.remove("selected");
|
||||
inputActive = true;
|
||||
});
|
||||
select.addEventListener("focus", () => {
|
||||
inputSection.classList.remove("selected");
|
||||
selectSection.classList.add("selected");
|
||||
inputActive = false;
|
||||
select.style.color = "";
|
||||
});
|
||||
select.addEventListener("blur", () => {
|
||||
if (!select.value) {
|
||||
select.style.color = "var(--descrip-text)";
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (inputActive == null) {
|
||||
error.textContent = "Please enter a username or select an existing user.";
|
||||
} else if (inputActive) {
|
||||
const username = input.value.trim();
|
||||
if (!username) {
|
||||
error.textContent = "Please enter a username.";
|
||||
return;
|
||||
}
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (inputActive == null) {
|
||||
error.textContent = "Please enter a username or select an existing user.";
|
||||
} else if (inputActive) {
|
||||
const username = input.value.trim();
|
||||
if (!username) {
|
||||
error.textContent = "Please enter a username.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new user
|
||||
// @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;
|
||||
const spinner = createSpinner();
|
||||
button.prepend(spinner);
|
||||
try {
|
||||
const resp = await api.createUser(username);
|
||||
if (resp.status >= 300) {
|
||||
let message = "Error creating user: " + resp.status + " " + resp.statusText;
|
||||
try {
|
||||
const res = await resp.json();
|
||||
if(res.error) {
|
||||
message = res.error;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
// 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;
|
||||
const spinner = createSpinner();
|
||||
button.prepend(spinner);
|
||||
try {
|
||||
const resp = await api.createUser(username);
|
||||
if (resp.status >= 300) {
|
||||
let message = "Error creating user: " + resp.status + " " + resp.statusText;
|
||||
try {
|
||||
const res = await resp.json();
|
||||
if(res.error) {
|
||||
message = res.error;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
resolve({ username, userId: await resp.json(), created: true });
|
||||
} catch (err) {
|
||||
spinner.remove();
|
||||
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred.";
|
||||
// @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 = 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, userId: await resp.json(), created: true });
|
||||
} catch (err) {
|
||||
spinner.remove();
|
||||
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred.";
|
||||
// @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 = false;
|
||||
return;
|
||||
}
|
||||
} else if (!select.value) {
|
||||
error.textContent = "Please select an existing user.";
|
||||
return;
|
||||
} else {
|
||||
resolve({ username: users[select.value], userId: select.value, created: false });
|
||||
}
|
||||
});
|
||||
|
||||
if (user) {
|
||||
const name = localStorage["Comfy.userName"];
|
||||
if (name) {
|
||||
input.value = name;
|
||||
}
|
||||
}
|
||||
if (input.value) {
|
||||
// Focus the input, do this separately as sometimes browsers like to fill in the value
|
||||
input.focus();
|
||||
}
|
||||
if (user) {
|
||||
const name = localStorage["Comfy.userName"];
|
||||
if (name) {
|
||||
input.value = name;
|
||||
}
|
||||
}
|
||||
if (input.value) {
|
||||
// Focus the input, do this separately as sometimes browsers like to fill in the value
|
||||
input.focus();
|
||||
}
|
||||
|
||||
const userIds = Object.keys(users ?? {});
|
||||
if (userIds.length) {
|
||||
for (const u of userIds) {
|
||||
$el("option", { textContent: users[u], value: u, parent: select });
|
||||
}
|
||||
select.style.color = "var(--descrip-text)";
|
||||
const userIds = Object.keys(users ?? {});
|
||||
if (userIds.length) {
|
||||
for (const u of userIds) {
|
||||
$el("option", { textContent: users[u], value: u, parent: select });
|
||||
}
|
||||
select.style.color = "var(--descrip-text)";
|
||||
|
||||
if (select.value) {
|
||||
// Focus the select, do this separately as sometimes browsers like to fill in the value
|
||||
select.focus();
|
||||
}
|
||||
} else {
|
||||
userSelection.classList.add("no-users");
|
||||
input.focus();
|
||||
}
|
||||
}).then((r: SelectedUser) => {
|
||||
userSelection.remove();
|
||||
return r;
|
||||
});
|
||||
}
|
||||
if (select.value) {
|
||||
// Focus the select, do this separately as sometimes browsers like to fill in the value
|
||||
select.focus();
|
||||
}
|
||||
} else {
|
||||
userSelection.classList.add("no-users");
|
||||
input.focus();
|
||||
}
|
||||
}).then((r: SelectedUser) => {
|
||||
userSelection.remove();
|
||||
return r;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
* @param { string[] } requiredClasses
|
||||
*/
|
||||
export function applyClasses(element, classList, ...requiredClasses) {
|
||||
classList ??= "";
|
||||
classList ??= "";
|
||||
|
||||
let str;
|
||||
if (typeof classList === "string") {
|
||||
str = classList;
|
||||
} else if (classList instanceof Array) {
|
||||
str = classList.join(" ");
|
||||
} else {
|
||||
str = Object.entries(classList).reduce((p, c) => {
|
||||
if (c[1]) {
|
||||
p += (p.length ? " " : "") + c[0];
|
||||
}
|
||||
return p;
|
||||
}, "");
|
||||
}
|
||||
element.className = str;
|
||||
if (requiredClasses) {
|
||||
element.classList.add(...requiredClasses);
|
||||
}
|
||||
let str;
|
||||
if (typeof classList === "string") {
|
||||
str = classList;
|
||||
} else if (classList instanceof Array) {
|
||||
str = classList.join(" ");
|
||||
} else {
|
||||
str = Object.entries(classList).reduce((p, c) => {
|
||||
if (c[1]) {
|
||||
p += (p.length ? " " : "") + c[0];
|
||||
}
|
||||
return p;
|
||||
}, "");
|
||||
}
|
||||
element.className = str;
|
||||
if (requiredClasses) {
|
||||
element.classList.add(...requiredClasses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,22 +35,22 @@ export function applyClasses(element, classList, ...requiredClasses) {
|
||||
* @returns
|
||||
*/
|
||||
export function toggleElement(element, { onHide, onShow } = {}) {
|
||||
let placeholder;
|
||||
let hidden;
|
||||
return (value) => {
|
||||
if (value) {
|
||||
if (hidden) {
|
||||
hidden = false;
|
||||
placeholder.replaceWith(element);
|
||||
}
|
||||
onShow?.(element, value);
|
||||
} else {
|
||||
if (!placeholder) {
|
||||
placeholder = document.createComment("");
|
||||
}
|
||||
hidden = true;
|
||||
element.replaceWith(placeholder);
|
||||
onHide?.(element);
|
||||
}
|
||||
};
|
||||
let placeholder;
|
||||
let hidden;
|
||||
return (value) => {
|
||||
if (value) {
|
||||
if (hidden) {
|
||||
hidden = false;
|
||||
placeholder.replaceWith(element);
|
||||
}
|
||||
onShow?.(element, value);
|
||||
} else {
|
||||
if (!placeholder) {
|
||||
placeholder = document.createComment("");
|
||||
}
|
||||
hidden = true;
|
||||
element.replaceWith(placeholder);
|
||||
onHide?.(element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,103 +4,103 @@ import { $el } from "./ui";
|
||||
|
||||
// Simple date formatter
|
||||
const parts = {
|
||||
d: (d) => d.getDate(),
|
||||
M: (d) => d.getMonth() + 1,
|
||||
h: (d) => d.getHours(),
|
||||
m: (d) => d.getMinutes(),
|
||||
s: (d) => d.getSeconds(),
|
||||
d: (d) => d.getDate(),
|
||||
M: (d) => d.getMonth() + 1,
|
||||
h: (d) => d.getHours(),
|
||||
m: (d) => d.getMinutes(),
|
||||
s: (d) => d.getSeconds(),
|
||||
};
|
||||
const format =
|
||||
Object.keys(parts)
|
||||
.map((k) => k + k + "?")
|
||||
.join("|") + "|yyy?y?";
|
||||
Object.keys(parts)
|
||||
.map((k) => k + k + "?")
|
||||
.join("|") + "|yyy?y?";
|
||||
|
||||
function formatDate(text: string, date: Date) {
|
||||
return text.replace(new RegExp(format, "g"), (text: string): string => {
|
||||
if (text === "yy") return (date.getFullYear() + "").substring(2);
|
||||
if (text === "yyyy") return date.getFullYear().toString();
|
||||
if (text[0] in parts) {
|
||||
const p = parts[text[0]](date);
|
||||
return (p + "").padStart(text.length, "0");
|
||||
}
|
||||
return text;
|
||||
});
|
||||
return text.replace(new RegExp(format, "g"), (text: string): string => {
|
||||
if (text === "yy") return (date.getFullYear() + "").substring(2);
|
||||
if (text === "yyyy") return date.getFullYear().toString();
|
||||
if (text[0] in parts) {
|
||||
const p = parts[text[0]](date);
|
||||
return (p + "").padStart(text.length, "0");
|
||||
}
|
||||
return text;
|
||||
});
|
||||
}
|
||||
|
||||
export function clone(obj) {
|
||||
try {
|
||||
if (typeof structuredClone !== "undefined") {
|
||||
return structuredClone(obj);
|
||||
}
|
||||
} catch (error) {
|
||||
// structuredClone is stricter than using JSON.parse/stringify so fallback to that
|
||||
}
|
||||
try {
|
||||
if (typeof structuredClone !== "undefined") {
|
||||
return structuredClone(obj);
|
||||
}
|
||||
} catch (error) {
|
||||
// structuredClone is stricter than using JSON.parse/stringify so fallback to that
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export function applyTextReplacements(app: ComfyApp, value: string): string {
|
||||
return value.replace(/%([^%]+)%/g, function (match, text) {
|
||||
const split = text.split(".");
|
||||
if (split.length !== 2) {
|
||||
// Special handling for dates
|
||||
if (split[0].startsWith("date:")) {
|
||||
return formatDate(split[0].substring(5), new Date());
|
||||
}
|
||||
return value.replace(/%([^%]+)%/g, function (match, text) {
|
||||
const split = text.split(".");
|
||||
if (split.length !== 2) {
|
||||
// Special handling for dates
|
||||
if (split[0].startsWith("date:")) {
|
||||
return formatDate(split[0].substring(5), new Date());
|
||||
}
|
||||
|
||||
if (text !== "width" && text !== "height") {
|
||||
// Dont warn on standard replacements
|
||||
console.warn("Invalid replacement pattern", text);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
if (text !== "width" && text !== "height") {
|
||||
// Dont warn on standard replacements
|
||||
console.warn("Invalid replacement pattern", text);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
// If we cant, see if there is a node with that title
|
||||
if (!nodes.length) {
|
||||
// @ts-ignore
|
||||
nodes = app.graph._nodes.filter((n) => n.title === split[0]);
|
||||
}
|
||||
if (!nodes.length) {
|
||||
console.warn("Unable to find node", split[0]);
|
||||
return match;
|
||||
}
|
||||
// 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]);
|
||||
// If we cant, see if there is a node with that title
|
||||
if (!nodes.length) {
|
||||
// @ts-ignore
|
||||
nodes = app.graph._nodes.filter((n) => n.title === split[0]);
|
||||
}
|
||||
if (!nodes.length) {
|
||||
console.warn("Unable to find node", split[0]);
|
||||
return match;
|
||||
}
|
||||
|
||||
if (nodes.length > 1) {
|
||||
console.warn("Multiple nodes matched", split[0], "using first match");
|
||||
}
|
||||
if (nodes.length > 1) {
|
||||
console.warn("Multiple nodes matched", split[0], "using first match");
|
||||
}
|
||||
|
||||
const node = nodes[0];
|
||||
const node = nodes[0];
|
||||
|
||||
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);
|
||||
return match;
|
||||
}
|
||||
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);
|
||||
return match;
|
||||
}
|
||||
|
||||
return ((widget.value ?? "") + "").replaceAll(/\/|\\/g, "_");
|
||||
});
|
||||
return ((widget.value ?? "") + "").replaceAll(/\/|\\/g, "_");
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
$el("link", {
|
||||
parent: document.head,
|
||||
rel: "stylesheet",
|
||||
type: "text/css",
|
||||
href: url,
|
||||
onload: res,
|
||||
onerror: rej,
|
||||
});
|
||||
});
|
||||
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();
|
||||
}
|
||||
$el("link", {
|
||||
parent: document.head,
|
||||
rel: "stylesheet",
|
||||
type: "text/css",
|
||||
href: url,
|
||||
onload: res,
|
||||
onerror: rej,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -109,18 +109,18 @@ export async function addStylesheet(urlOrFile: string, relativeTo?: string): Pro
|
||||
* @param { Blob } blob
|
||||
*/
|
||||
export function downloadBlob(filename, blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = $el("a", {
|
||||
href: url,
|
||||
download: filename,
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = $el("a", {
|
||||
href: url,
|
||||
download: filename,
|
||||
style: { display: "none" },
|
||||
parent: document.body,
|
||||
});
|
||||
a.click();
|
||||
setTimeout(function () {
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,29 +131,29 @@ export function downloadBlob(filename, blob) {
|
||||
* @returns {T}
|
||||
*/
|
||||
export function prop(target, name, defaultValue, onChanged) {
|
||||
let currentValue;
|
||||
Object.defineProperty(target, name, {
|
||||
get() {
|
||||
return currentValue;
|
||||
},
|
||||
set(newValue) {
|
||||
const prevValue = currentValue;
|
||||
currentValue = newValue;
|
||||
onChanged?.(currentValue, prevValue, target, name);
|
||||
},
|
||||
});
|
||||
return defaultValue;
|
||||
let currentValue;
|
||||
Object.defineProperty(target, name, {
|
||||
get() {
|
||||
return currentValue;
|
||||
},
|
||||
set(newValue) {
|
||||
const prevValue = currentValue;
|
||||
currentValue = newValue;
|
||||
onChanged?.(currentValue, prevValue, target, name);
|
||||
},
|
||||
});
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export function getStorageValue(id) {
|
||||
const clientId = api.clientId ?? api.initialClientId;
|
||||
return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id);
|
||||
const clientId = api.clientId ?? api.initialClientId;
|
||||
return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id);
|
||||
}
|
||||
|
||||
export function setStorageValue(id, value) {
|
||||
const clientId = api.clientId ?? api.initialClientId;
|
||||
if (clientId) {
|
||||
sessionStorage.setItem(`${id}:${clientId}`, value);
|
||||
}
|
||||
localStorage.setItem(id, value);
|
||||
const clientId = api.clientId ?? api.initialClientId;
|
||||
if (clientId) {
|
||||
sessionStorage.setItem(`${id}:${clientId}`, value);
|
||||
}
|
||||
localStorage.setItem(id, value);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,445 +6,445 @@ import { ComfyAsyncDialog } from "./ui/components/asyncDialog";
|
||||
import { getStorageValue, setStorageValue } from "./utils";
|
||||
|
||||
function appendJsonExt(path) {
|
||||
if (!path.toLowerCase().endsWith(".json")) {
|
||||
path += ".json";
|
||||
}
|
||||
return path;
|
||||
if (!path.toLowerCase().endsWith(".json")) {
|
||||
path += ".json";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function trimJsonExt(path) {
|
||||
return path?.replace(/\.json$/, "");
|
||||
return path?.replace(/\.json$/, "");
|
||||
}
|
||||
|
||||
export class ComfyWorkflowManager extends EventTarget {
|
||||
/** @type {string | null} */
|
||||
#activePromptId = null;
|
||||
#unsavedCount = 0;
|
||||
#activeWorkflow;
|
||||
/** @type {string | null} */
|
||||
#activePromptId = null;
|
||||
#unsavedCount = 0;
|
||||
#activeWorkflow;
|
||||
|
||||
/** @type {Record<string, ComfyWorkflow>} */
|
||||
workflowLookup = {};
|
||||
/** @type {Array<ComfyWorkflow>} */
|
||||
workflows = [];
|
||||
/** @type {Array<ComfyWorkflow>} */
|
||||
openWorkflows = [];
|
||||
/** @type {Record<string, {workflow?: ComfyWorkflow, nodes?: Record<string, boolean>}>} */
|
||||
queuedPrompts = {};
|
||||
/** @type {Record<string, ComfyWorkflow>} */
|
||||
workflowLookup = {};
|
||||
/** @type {Array<ComfyWorkflow>} */
|
||||
workflows = [];
|
||||
/** @type {Array<ComfyWorkflow>} */
|
||||
openWorkflows = [];
|
||||
/** @type {Record<string, {workflow?: ComfyWorkflow, nodes?: Record<string, boolean>}>} */
|
||||
queuedPrompts = {};
|
||||
|
||||
get activeWorkflow() {
|
||||
return this.#activeWorkflow ?? this.openWorkflows[0];
|
||||
}
|
||||
get activeWorkflow() {
|
||||
return this.#activeWorkflow ?? this.openWorkflows[0];
|
||||
}
|
||||
|
||||
get activePromptId() {
|
||||
return this.#activePromptId;
|
||||
}
|
||||
get activePromptId() {
|
||||
return this.#activePromptId;
|
||||
}
|
||||
|
||||
get activePrompt() {
|
||||
return this.queuedPrompts[this.#activePromptId];
|
||||
}
|
||||
get activePrompt() {
|
||||
return this.queuedPrompts[this.#activePromptId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./app").ComfyApp} app
|
||||
*/
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
ChangeTracker.init(app);
|
||||
/**
|
||||
* @param {import("./app").ComfyApp} app
|
||||
*/
|
||||
constructor(app) {
|
||||
super();
|
||||
this.app = app;
|
||||
ChangeTracker.init(app);
|
||||
|
||||
this.#bindExecutionEvents();
|
||||
}
|
||||
this.#bindExecutionEvents();
|
||||
}
|
||||
|
||||
#bindExecutionEvents() {
|
||||
// TODO: on reload, set active prompt based on the latest ws message
|
||||
#bindExecutionEvents() {
|
||||
// TODO: on reload, set active prompt based on the latest ws message
|
||||
|
||||
const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt }));
|
||||
let executing = null;
|
||||
api.addEventListener("execution_start", (e) => {
|
||||
this.#activePromptId = e.detail.prompt_id;
|
||||
const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt }));
|
||||
let executing = null;
|
||||
api.addEventListener("execution_start", (e) => {
|
||||
this.#activePromptId = e.detail.prompt_id;
|
||||
|
||||
// This event can fire before the event is stored, so put a placeholder
|
||||
this.queuedPrompts[this.#activePromptId] ??= { nodes: {} };
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("execution_cached", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
for (const n of e.detail.nodes) {
|
||||
this.activePrompt.nodes[n] = true;
|
||||
}
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("executed", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
this.activePrompt.nodes[e.detail.node] = true;
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("executing", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
// This event can fire before the event is stored, so put a placeholder
|
||||
this.queuedPrompts[this.#activePromptId] ??= { nodes: {} };
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("execution_cached", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
for (const n of e.detail.nodes) {
|
||||
this.activePrompt.nodes[n] = true;
|
||||
}
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("executed", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
this.activePrompt.nodes[e.detail.node] = true;
|
||||
emit();
|
||||
});
|
||||
api.addEventListener("executing", (e) => {
|
||||
if (!this.activePrompt) return;
|
||||
|
||||
if (executing) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
this.activePrompt.nodes[executing] = true;
|
||||
}
|
||||
executing = e.detail;
|
||||
if (!executing) {
|
||||
delete this.queuedPrompts[this.#activePromptId];
|
||||
this.#activePromptId = null;
|
||||
}
|
||||
emit();
|
||||
});
|
||||
}
|
||||
if (executing) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
this.activePrompt.nodes[executing] = true;
|
||||
}
|
||||
executing = e.detail;
|
||||
if (!executing) {
|
||||
delete this.queuedPrompts[this.#activePromptId];
|
||||
this.#activePromptId = null;
|
||||
}
|
||||
emit();
|
||||
});
|
||||
}
|
||||
|
||||
async loadWorkflows() {
|
||||
try {
|
||||
let favorites;
|
||||
const resp = await api.getUserData("workflows/.index.json");
|
||||
let info;
|
||||
if (resp.status === 200) {
|
||||
info = await resp.json();
|
||||
favorites = new Set(info?.favorites ?? []);
|
||||
} else {
|
||||
favorites = new Set();
|
||||
}
|
||||
async loadWorkflows() {
|
||||
try {
|
||||
let favorites;
|
||||
const resp = await api.getUserData("workflows/.index.json");
|
||||
let info;
|
||||
if (resp.status === 200) {
|
||||
info = await resp.json();
|
||||
favorites = new Set(info?.favorites ?? []);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
return 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;
|
||||
});
|
||||
|
||||
this.workflows = workflows;
|
||||
} catch (error) {
|
||||
alert("Error loading workflows: " + (error.message ?? error));
|
||||
this.workflows = [];
|
||||
}
|
||||
}
|
||||
this.workflows = workflows;
|
||||
} catch (error) {
|
||||
alert("Error loading workflows: " + (error.message ?? error));
|
||||
this.workflows = [];
|
||||
}
|
||||
}
|
||||
|
||||
async saveWorkflowMetadata() {
|
||||
await api.storeUserData("workflows/.index.json", {
|
||||
favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)],
|
||||
});
|
||||
}
|
||||
async saveWorkflowMetadata() {
|
||||
await api.storeUserData("workflows/.index.json", {
|
||||
favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | ComfyWorkflow | null} workflow
|
||||
*/
|
||||
setWorkflow(workflow) {
|
||||
if (workflow && typeof workflow === "string") {
|
||||
// Selected by path, i.e. on reload of last workflow
|
||||
const found = this.workflows.find((w) => w.path === workflow);
|
||||
if (found) {
|
||||
workflow = found;
|
||||
workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string | ComfyWorkflow | null} workflow
|
||||
*/
|
||||
setWorkflow(workflow) {
|
||||
if (workflow && typeof workflow === "string") {
|
||||
// Selected by path, i.e. on reload of last workflow
|
||||
const found = this.workflows.find((w) => w.path === workflow);
|
||||
if (found) {
|
||||
workflow = found;
|
||||
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})` : ""));
|
||||
}
|
||||
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})` : ""));
|
||||
}
|
||||
|
||||
const index = this.openWorkflows.indexOf(workflow);
|
||||
if (index === -1) {
|
||||
// Opening a new workflow
|
||||
this.openWorkflows.push(workflow);
|
||||
}
|
||||
const index = this.openWorkflows.indexOf(workflow);
|
||||
if (index === -1) {
|
||||
// Opening a new workflow
|
||||
this.openWorkflows.push(workflow);
|
||||
}
|
||||
|
||||
this.#activeWorkflow = workflow;
|
||||
this.#activeWorkflow = workflow;
|
||||
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.activeWorkflow.path ?? "");
|
||||
this.dispatchEvent(new CustomEvent("changeWorkflow"));
|
||||
}
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.activeWorkflow.path ?? "");
|
||||
this.dispatchEvent(new CustomEvent("changeWorkflow"));
|
||||
}
|
||||
|
||||
storePrompt({ nodes, id }) {
|
||||
this.queuedPrompts[id] ??= {};
|
||||
this.queuedPrompts[id].nodes = {
|
||||
...nodes.reduce((p, n) => {
|
||||
p[n] = false;
|
||||
return p;
|
||||
}, {}),
|
||||
...this.queuedPrompts[id].nodes,
|
||||
};
|
||||
this.queuedPrompts[id].workflow = this.activeWorkflow;
|
||||
}
|
||||
storePrompt({ nodes, id }) {
|
||||
this.queuedPrompts[id] ??= {};
|
||||
this.queuedPrompts[id].nodes = {
|
||||
...nodes.reduce((p, n) => {
|
||||
p[n] = false;
|
||||
return p;
|
||||
}, {}),
|
||||
...this.queuedPrompts[id].nodes,
|
||||
};
|
||||
this.queuedPrompts[id].workflow = this.activeWorkflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ComfyWorkflow} workflow
|
||||
*/
|
||||
async closeWorkflow(workflow, warnIfUnsaved = true) {
|
||||
if (!workflow.isOpen) {
|
||||
return true;
|
||||
}
|
||||
if (workflow.unsaved && warnIfUnsaved) {
|
||||
const res = await ComfyAsyncDialog.prompt({
|
||||
title: "Save Changes?",
|
||||
message: `Do you want to save changes to "${workflow.path ?? workflow.name}" before closing?`,
|
||||
actions: ["Yes", "No", "Cancel"],
|
||||
});
|
||||
if (res === "Yes") {
|
||||
const active = this.activeWorkflow;
|
||||
if (active !== workflow) {
|
||||
// We need to switch to the workflow to save it
|
||||
await workflow.load();
|
||||
}
|
||||
/**
|
||||
* @param {ComfyWorkflow} workflow
|
||||
*/
|
||||
async closeWorkflow(workflow, warnIfUnsaved = true) {
|
||||
if (!workflow.isOpen) {
|
||||
return true;
|
||||
}
|
||||
if (workflow.unsaved && warnIfUnsaved) {
|
||||
const res = await ComfyAsyncDialog.prompt({
|
||||
title: "Save Changes?",
|
||||
message: `Do you want to save changes to "${workflow.path ?? workflow.name}" before closing?`,
|
||||
actions: ["Yes", "No", "Cancel"],
|
||||
});
|
||||
if (res === "Yes") {
|
||||
const active = this.activeWorkflow;
|
||||
if (active !== workflow) {
|
||||
// We need to switch to the workflow to save it
|
||||
await workflow.load();
|
||||
}
|
||||
|
||||
if (!(await workflow.save())) {
|
||||
// Save was canceled, restore the previous workflow
|
||||
if (active !== workflow) {
|
||||
await active.load();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (res === "Cancel") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
workflow.changeTracker = null;
|
||||
this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1);
|
||||
if (this.openWorkflows.length) {
|
||||
this.#activeWorkflow = this.openWorkflows[0];
|
||||
await this.#activeWorkflow.load();
|
||||
} else {
|
||||
// Load default
|
||||
await this.app.loadGraphData();
|
||||
}
|
||||
}
|
||||
if (!(await workflow.save())) {
|
||||
// Save was canceled, restore the previous workflow
|
||||
if (active !== workflow) {
|
||||
await active.load();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (res === "Cancel") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
workflow.changeTracker = null;
|
||||
this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1);
|
||||
if (this.openWorkflows.length) {
|
||||
this.#activeWorkflow = this.openWorkflows[0];
|
||||
await this.#activeWorkflow.load();
|
||||
} else {
|
||||
// Load default
|
||||
await this.app.loadGraphData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyWorkflow {
|
||||
#name;
|
||||
#path;
|
||||
#pathParts;
|
||||
#isFavorite = false;
|
||||
/** @type {ChangeTracker | null} */
|
||||
changeTracker = null;
|
||||
unsaved = false;
|
||||
#name;
|
||||
#path;
|
||||
#pathParts;
|
||||
#isFavorite = false;
|
||||
/** @type {ChangeTracker | null} */
|
||||
changeTracker = null;
|
||||
unsaved = false;
|
||||
|
||||
get name() {
|
||||
return this.#name;
|
||||
}
|
||||
get name() {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.#path;
|
||||
}
|
||||
get path() {
|
||||
return this.#path;
|
||||
}
|
||||
|
||||
get pathParts() {
|
||||
return this.#pathParts;
|
||||
}
|
||||
get pathParts() {
|
||||
return this.#pathParts;
|
||||
}
|
||||
|
||||
get isFavorite() {
|
||||
return this.#isFavorite;
|
||||
}
|
||||
get isFavorite() {
|
||||
return this.#isFavorite;
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return !!this.changeTracker;
|
||||
}
|
||||
get isOpen() {
|
||||
return !!this.changeTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @param {ComfyWorkflowManager} manager
|
||||
* @param {string} path
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {ComfyWorkflowManager} manager
|
||||
* @param {string} path
|
||||
* @param {string[]} pathParts
|
||||
* @param {boolean} isFavorite
|
||||
*/
|
||||
/**
|
||||
* @param {ComfyWorkflowManager} manager
|
||||
* @param {string} path
|
||||
* @param {string[]} [pathParts]
|
||||
* @param {boolean} [isFavorite]
|
||||
*/
|
||||
constructor(manager, path, pathParts, isFavorite) {
|
||||
this.manager = manager;
|
||||
if (pathParts) {
|
||||
this.#updatePath(path, pathParts);
|
||||
this.#isFavorite = isFavorite;
|
||||
} else {
|
||||
this.#name = path;
|
||||
this.unsaved = true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @overload
|
||||
* @param {ComfyWorkflowManager} manager
|
||||
* @param {string} path
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {ComfyWorkflowManager} manager
|
||||
* @param {string} path
|
||||
* @param {string[]} pathParts
|
||||
* @param {boolean} isFavorite
|
||||
*/
|
||||
/**
|
||||
* @param {ComfyWorkflowManager} manager
|
||||
* @param {string} path
|
||||
* @param {string[]} [pathParts]
|
||||
* @param {boolean} [isFavorite]
|
||||
*/
|
||||
constructor(manager, path, pathParts, isFavorite) {
|
||||
this.manager = manager;
|
||||
if (pathParts) {
|
||||
this.#updatePath(path, pathParts);
|
||||
this.#isFavorite = isFavorite;
|
||||
} else {
|
||||
this.#name = path;
|
||||
this.unsaved = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {string[]} [pathParts]
|
||||
*/
|
||||
#updatePath(path, pathParts) {
|
||||
this.#path = path;
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {string[]} [pathParts]
|
||||
*/
|
||||
#updatePath(path, pathParts) {
|
||||
this.#path = path;
|
||||
|
||||
if (!pathParts) {
|
||||
if (!path.includes("\\")) {
|
||||
pathParts = path.split("/");
|
||||
} else {
|
||||
pathParts = path.split("\\");
|
||||
}
|
||||
}
|
||||
if (!pathParts) {
|
||||
if (!path.includes("\\")) {
|
||||
pathParts = path.split("/");
|
||||
} else {
|
||||
pathParts = path.split("\\");
|
||||
}
|
||||
}
|
||||
|
||||
this.#pathParts = pathParts;
|
||||
this.#name = trimJsonExt(pathParts[pathParts.length - 1]);
|
||||
}
|
||||
this.#pathParts = pathParts;
|
||||
this.#name = trimJsonExt(pathParts[pathParts.length - 1]);
|
||||
}
|
||||
|
||||
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}`);
|
||||
return;
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
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}`);
|
||||
return;
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
if (this.isOpen) {
|
||||
await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this);
|
||||
} else {
|
||||
const data = await this.getWorkflowData();
|
||||
if (!data) return;
|
||||
await this.manager.app.loadGraphData(data, true, true, this);
|
||||
}
|
||||
};
|
||||
load = async () => {
|
||||
if (this.isOpen) {
|
||||
await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this);
|
||||
} else {
|
||||
const data = await this.getWorkflowData();
|
||||
if (!data) return;
|
||||
await this.manager.app.loadGraphData(data, true, true, this);
|
||||
}
|
||||
};
|
||||
|
||||
async save(saveAs = false) {
|
||||
if (!this.path || saveAs) {
|
||||
return !!(await this.#save(null, false));
|
||||
} else {
|
||||
return !!(await this.#save(this.path, true));
|
||||
}
|
||||
}
|
||||
async save(saveAs = false) {
|
||||
if (!this.path || saveAs) {
|
||||
return !!(await this.#save(null, false));
|
||||
} else {
|
||||
return !!(await this.#save(this.path, true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
*/
|
||||
async favorite(value) {
|
||||
try {
|
||||
if (this.#isFavorite === value) return;
|
||||
this.#isFavorite = value;
|
||||
await this.manager.saveWorkflowMetadata();
|
||||
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
|
||||
} catch (error) {
|
||||
alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {boolean} value
|
||||
*/
|
||||
async favorite(value) {
|
||||
try {
|
||||
if (this.#isFavorite === value) return;
|
||||
this.#isFavorite = value;
|
||||
await this.manager.saveWorkflowMetadata();
|
||||
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
|
||||
} catch (error) {
|
||||
alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
*/
|
||||
async rename(path) {
|
||||
path = appendJsonExt(path);
|
||||
let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path);
|
||||
/**
|
||||
* @param {string} path
|
||||
*/
|
||||
async rename(path) {
|
||||
path = appendJsonExt(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 (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 (resp.status !== 200) {
|
||||
alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
|
||||
return;
|
||||
}
|
||||
if (resp.status !== 200) {
|
||||
alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isFav = this.isFavorite;
|
||||
if (isFav) {
|
||||
await this.favorite(false);
|
||||
}
|
||||
path = (await resp.json()).substring("workflows/".length);
|
||||
this.#updatePath(path, null);
|
||||
if (isFav) {
|
||||
await this.favorite(true);
|
||||
}
|
||||
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
|
||||
}
|
||||
const isFav = this.isFavorite;
|
||||
if (isFav) {
|
||||
await this.favorite(false);
|
||||
}
|
||||
path = (await resp.json()).substring("workflows/".length);
|
||||
this.#updatePath(path, null);
|
||||
if (isFav) {
|
||||
await this.favorite(true);
|
||||
}
|
||||
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
|
||||
}
|
||||
|
||||
async insert() {
|
||||
const data = await this.getWorkflowData();
|
||||
if (!data) return;
|
||||
async insert() {
|
||||
const data = await this.getWorkflowData();
|
||||
if (!data) return;
|
||||
|
||||
const old = localStorage.getItem("litegrapheditor_clipboard");
|
||||
const graph = new LGraph(data);
|
||||
const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true });
|
||||
canvas.selectNodes();
|
||||
canvas.copyToClipboard();
|
||||
this.manager.app.canvas.pasteFromClipboard();
|
||||
localStorage.setItem("litegrapheditor_clipboard", old);
|
||||
}
|
||||
const old = localStorage.getItem("litegrapheditor_clipboard");
|
||||
const graph = new LGraph(data);
|
||||
const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true });
|
||||
canvas.selectNodes();
|
||||
canvas.copyToClipboard();
|
||||
this.manager.app.canvas.pasteFromClipboard();
|
||||
localStorage.setItem("litegrapheditor_clipboard", old);
|
||||
}
|
||||
|
||||
async delete() {
|
||||
// TODO: fix delete of current workflow - should mark workflow as unsaved and when saving use old name by default
|
||||
async delete() {
|
||||
// TODO: fix delete of current workflow - should mark workflow as unsaved and when saving use old name by default
|
||||
|
||||
try {
|
||||
if (this.isFavorite) {
|
||||
await this.favorite(false);
|
||||
}
|
||||
await api.deleteUserData("workflows/" + this.path);
|
||||
this.unsaved = true;
|
||||
this.#path = null;
|
||||
this.#pathParts = null;
|
||||
this.manager.workflows.splice(this.manager.workflows.indexOf(this), 1);
|
||||
this.manager.dispatchEvent(new CustomEvent("delete", { detail: this }));
|
||||
} catch (error) {
|
||||
alert(`Error deleting workflow: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (this.isFavorite) {
|
||||
await this.favorite(false);
|
||||
}
|
||||
await api.deleteUserData("workflows/" + this.path);
|
||||
this.unsaved = true;
|
||||
this.#path = null;
|
||||
this.#pathParts = null;
|
||||
this.manager.workflows.splice(this.manager.workflows.indexOf(this), 1);
|
||||
this.manager.dispatchEvent(new CustomEvent("delete", { detail: this }));
|
||||
} catch (error) {
|
||||
alert(`Error deleting workflow: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
track() {
|
||||
if (this.changeTracker) {
|
||||
this.changeTracker.restore();
|
||||
} else {
|
||||
this.changeTracker = new ChangeTracker(this);
|
||||
}
|
||||
}
|
||||
track() {
|
||||
if (this.changeTracker) {
|
||||
this.changeTracker.restore();
|
||||
} else {
|
||||
this.changeTracker = new ChangeTracker(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null} path
|
||||
* @param {boolean} overwrite
|
||||
*/
|
||||
async #save(path, overwrite) {
|
||||
if (!path) {
|
||||
path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow");
|
||||
if (!path) return;
|
||||
}
|
||||
/**
|
||||
* @param {string|null} path
|
||||
* @param {boolean} overwrite
|
||||
*/
|
||||
async #save(path, overwrite) {
|
||||
if (!path) {
|
||||
path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow");
|
||||
if (!path) return;
|
||||
}
|
||||
|
||||
path = appendJsonExt(path);
|
||||
path = appendJsonExt(path);
|
||||
|
||||
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 });
|
||||
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 });
|
||||
}
|
||||
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 });
|
||||
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 (resp.status !== 200) {
|
||||
alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`);
|
||||
return;
|
||||
}
|
||||
if (resp.status !== 200) {
|
||||
alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
path = (await resp.json()).substring("workflows/".length);
|
||||
path = (await resp.json()).substring("workflows/".length);
|
||||
|
||||
if (!this.path) {
|
||||
// Saved new workflow, patch this instance
|
||||
this.#updatePath(path, null);
|
||||
await this.manager.loadWorkflows();
|
||||
this.unsaved = false;
|
||||
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
|
||||
} else if (path !== this.path) {
|
||||
// Saved as, open the new copy
|
||||
await this.manager.loadWorkflows();
|
||||
const workflow = this.manager.workflowLookup[path];
|
||||
await workflow.load();
|
||||
} else {
|
||||
// Normal save
|
||||
this.unsaved = false;
|
||||
this.manager.dispatchEvent(new CustomEvent("save", { detail: this }));
|
||||
}
|
||||
if (!this.path) {
|
||||
// Saved new workflow, patch this instance
|
||||
this.#updatePath(path, null);
|
||||
await this.manager.loadWorkflows();
|
||||
this.unsaved = false;
|
||||
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
|
||||
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
|
||||
} else if (path !== this.path) {
|
||||
// Saved as, open the new copy
|
||||
await this.manager.loadWorkflows();
|
||||
const workflow = this.manager.workflowLookup[path];
|
||||
await workflow.load();
|
||||
} else {
|
||||
// Normal save
|
||||
this.unsaved = false;
|
||||
this.manager.dispatchEvent(new CustomEvent("save", { detail: this }));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
24
src/types/litegraph.d.ts
vendored
24
src/types/litegraph.d.ts
vendored
@@ -53,7 +53,7 @@ export type WidgetCallback<T extends IWidget = IWidget> = (
|
||||
|
||||
export interface IWidget<TValue = any, TOptions = any> {
|
||||
// linked widgets, e.g. seed+seedControl
|
||||
linkedWidgets: IWidget[];
|
||||
linkedWidgets: IWidget[];
|
||||
|
||||
name: string | null;
|
||||
value: TValue;
|
||||
@@ -165,7 +165,7 @@ export declare class LGraph {
|
||||
static supported_types: string[];
|
||||
static STATUS_STOPPED: 1;
|
||||
static STATUS_RUNNING: 2;
|
||||
extra: any;
|
||||
extra: any;
|
||||
|
||||
constructor(o?: object);
|
||||
|
||||
@@ -411,12 +411,12 @@ export type SerializedLGraphNode<T extends LGraphNode = LGraphNode> = {
|
||||
|
||||
/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */
|
||||
export declare class LGraphNode {
|
||||
onResize?: Function;
|
||||
onResize?: Function;
|
||||
|
||||
// Used in group node
|
||||
setInnerNodes(nodes: any) {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
setInnerNodes(nodes: any) {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
static title_color: string;
|
||||
static title: string;
|
||||
@@ -857,7 +857,7 @@ export declare class LGraphNode {
|
||||
}
|
||||
|
||||
export type LGraphNodeConstructor<T extends LGraphNode = LGraphNode> = {
|
||||
nodeData: any; // Used by group node.
|
||||
nodeData: any; // Used by group node.
|
||||
new (): T;
|
||||
};
|
||||
|
||||
@@ -1341,11 +1341,11 @@ declare global {
|
||||
}
|
||||
|
||||
const LiteGraph: {
|
||||
DEFAULT_GROUP_FONT_SIZE: any;
|
||||
overlapBounding(visible_area: any, _bounding: any): unknown;
|
||||
release_link_on_empty_shows_menu: boolean;
|
||||
alt_drag_do_clone_nodes: boolean;
|
||||
GRID_SHAPE: number;
|
||||
DEFAULT_GROUP_FONT_SIZE: any;
|
||||
overlapBounding(visible_area: any, _bounding: any): unknown;
|
||||
release_link_on_empty_shows_menu: boolean;
|
||||
alt_drag_do_clone_nodes: boolean;
|
||||
GRID_SHAPE: number;
|
||||
VERSION: number;
|
||||
|
||||
CANVAS_GRID_SIZE: number;
|
||||
|
||||
@@ -3,7 +3,7 @@ import lg from "./utils/litegraph";
|
||||
|
||||
// Load things once per test file before to ensure its all warmed up for the tests
|
||||
beforeAll(async () => {
|
||||
lg.setup(global);
|
||||
await start({ resetEnv: true });
|
||||
lg.teardown(global);
|
||||
lg.setup(global);
|
||||
await start({ resetEnv: true });
|
||||
lg.teardown(global);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module.exports = async function () {
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const { nop } = require("./utils/nopProxy");
|
||||
global.enableWebGLCanvas = nop;
|
||||
const { nop } = require("./utils/nopProxy");
|
||||
global.enableWebGLCanvas = nop;
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = nop;
|
||||
HTMLCanvasElement.prototype.getContext = nop;
|
||||
|
||||
localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false";
|
||||
localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false";
|
||||
};
|
||||
|
||||
@@ -3,36 +3,36 @@ import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import http from "http";
|
||||
|
||||
async function setup() {
|
||||
await new Promise<void>((res, rej) => {
|
||||
http
|
||||
.get("http://127.0.0.1:8188/object_info", (resp) => {
|
||||
let data = "";
|
||||
resp.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
resp.on("end", () => {
|
||||
// Modify the response data to add some checkpoints
|
||||
const objectInfo = JSON.parse(data);
|
||||
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"];
|
||||
objectInfo.VAELoader.input.required.vae_name[0] = ["vae1.safetensors", "vae2.ckpt"];
|
||||
await new Promise<void>((res, rej) => {
|
||||
http
|
||||
.get("http://127.0.0.1:8188/object_info", (resp) => {
|
||||
let data = "";
|
||||
resp.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
resp.on("end", () => {
|
||||
// Modify the response data to add some checkpoints
|
||||
const objectInfo = JSON.parse(data);
|
||||
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"];
|
||||
objectInfo.VAELoader.input.required.vae_name[0] = ["vae1.safetensors", "vae2.ckpt"];
|
||||
|
||||
data = JSON.stringify(objectInfo, undefined, "\t");
|
||||
data = JSON.stringify(objectInfo, undefined, "\t");
|
||||
|
||||
const outDir = resolve("./tests-ui/data");
|
||||
if (!existsSync(outDir)) {
|
||||
mkdirSync(outDir);
|
||||
}
|
||||
const outDir = resolve("./tests-ui/data");
|
||||
if (!existsSync(outDir)) {
|
||||
mkdirSync(outDir);
|
||||
}
|
||||
|
||||
const outPath = resolve(outDir, "object_info.json");
|
||||
console.log(`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`);
|
||||
writeFileSync(outPath, data, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
res();
|
||||
});
|
||||
})
|
||||
.on("error", rej);
|
||||
});
|
||||
const outPath = resolve(outDir, "object_info.json");
|
||||
console.log(`Writing ${Object.keys(objectInfo).length} nodes to ${outPath}`);
|
||||
writeFileSync(outPath, data, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
res();
|
||||
});
|
||||
})
|
||||
.on("error", rej);
|
||||
});
|
||||
}
|
||||
|
||||
setup();
|
||||
@@ -2,193 +2,193 @@ import { start } from "../utils";
|
||||
import lg from "../utils/litegraph";
|
||||
|
||||
describe("extensions", () => {
|
||||
beforeEach(() => {
|
||||
lg.setup(global);
|
||||
});
|
||||
beforeEach(() => {
|
||||
lg.setup(global);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
lg.teardown(global);
|
||||
});
|
||||
afterEach(() => {
|
||||
lg.teardown(global);
|
||||
});
|
||||
|
||||
it("calls each extension hook", async () => {
|
||||
const mockExtension = {
|
||||
name: "TestExtension",
|
||||
init: jest.fn(),
|
||||
setup: jest.fn(),
|
||||
addCustomNodeDefs: jest.fn(),
|
||||
getCustomWidgets: jest.fn(),
|
||||
beforeRegisterNodeDef: jest.fn(),
|
||||
registerCustomNodes: jest.fn(),
|
||||
loadedGraphNode: jest.fn(),
|
||||
nodeCreated: jest.fn(),
|
||||
beforeConfigureGraph: jest.fn(),
|
||||
afterConfigureGraph: jest.fn(),
|
||||
};
|
||||
it("calls each extension hook", async () => {
|
||||
const mockExtension = {
|
||||
name: "TestExtension",
|
||||
init: jest.fn(),
|
||||
setup: jest.fn(),
|
||||
addCustomNodeDefs: jest.fn(),
|
||||
getCustomWidgets: jest.fn(),
|
||||
beforeRegisterNodeDef: jest.fn(),
|
||||
registerCustomNodes: jest.fn(),
|
||||
loadedGraphNode: jest.fn(),
|
||||
nodeCreated: jest.fn(),
|
||||
beforeConfigureGraph: jest.fn(),
|
||||
afterConfigureGraph: jest.fn(),
|
||||
};
|
||||
|
||||
const { app, ez, graph } = await start({
|
||||
async preSetup(app) {
|
||||
app.registerExtension(mockExtension);
|
||||
},
|
||||
});
|
||||
const { app, ez, graph } = await start({
|
||||
async preSetup(app) {
|
||||
app.registerExtension(mockExtension);
|
||||
},
|
||||
});
|
||||
|
||||
// Basic initialisation hooks should be called once, with app
|
||||
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.init).toHaveBeenCalledWith(app);
|
||||
// Basic initialisation hooks should be called once, with app
|
||||
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.init).toHaveBeenCalledWith(app);
|
||||
|
||||
// Adding custom node defs should be passed the full list of nodes
|
||||
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app);
|
||||
const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0];
|
||||
expect(defs).toHaveProperty("KSampler");
|
||||
expect(defs).toHaveProperty("LoadImage");
|
||||
// Adding custom node defs should be passed the full list of nodes
|
||||
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.addCustomNodeDefs.mock.calls[0][1]).toStrictEqual(app);
|
||||
const defs = mockExtension.addCustomNodeDefs.mock.calls[0][0];
|
||||
expect(defs).toHaveProperty("KSampler");
|
||||
expect(defs).toHaveProperty("LoadImage");
|
||||
|
||||
// Get custom widgets is called once and should return new widget types
|
||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app);
|
||||
// Get custom widgets is called once and should return new widget types
|
||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledWith(app);
|
||||
|
||||
// Before register node def will be called once per node type
|
||||
const nodeNames = Object.keys(defs);
|
||||
const nodeCount = nodeNames.length;
|
||||
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// It should be send the JS class and the original JSON definition
|
||||
const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0];
|
||||
const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1];
|
||||
// Before register node def will be called once per node type
|
||||
const nodeNames = Object.keys(defs);
|
||||
const nodeCount = nodeNames.length;
|
||||
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// It should be send the JS class and the original JSON definition
|
||||
const nodeClass = mockExtension.beforeRegisterNodeDef.mock.calls[i][0];
|
||||
const nodeDef = mockExtension.beforeRegisterNodeDef.mock.calls[i][1];
|
||||
|
||||
expect(nodeClass.name).toBe("ComfyNode");
|
||||
expect(nodeClass.comfyClass).toBe(nodeNames[i]);
|
||||
expect(nodeDef.name).toBe(nodeNames[i]);
|
||||
expect(nodeDef).toHaveProperty("input");
|
||||
expect(nodeDef).toHaveProperty("output");
|
||||
}
|
||||
expect(nodeClass.name).toBe("ComfyNode");
|
||||
expect(nodeClass.comfyClass).toBe(nodeNames[i]);
|
||||
expect(nodeDef.name).toBe(nodeNames[i]);
|
||||
expect(nodeDef).toHaveProperty("input");
|
||||
expect(nodeDef).toHaveProperty("output");
|
||||
}
|
||||
|
||||
// Register custom nodes is called once after registerNode defs to allow adding other frontend nodes
|
||||
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
||||
// Register custom nodes is called once after registerNode defs to allow adding other frontend nodes
|
||||
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Before configure graph will be called here as the default graph is being loaded
|
||||
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1);
|
||||
// it gets sent the graph data that is going to be loaded
|
||||
const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0];
|
||||
// Before configure graph will be called here as the default graph is being loaded
|
||||
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(1);
|
||||
// it gets sent the graph data that is going to be loaded
|
||||
const graphData = mockExtension.beforeConfigureGraph.mock.calls[0][0];
|
||||
|
||||
// A node created is fired for each node constructor that is called
|
||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length);
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
expect(mockExtension.nodeCreated.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
||||
}
|
||||
// A node created is fired for each node constructor that is called
|
||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length);
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
expect(mockExtension.nodeCreated.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
||||
}
|
||||
|
||||
// Each node then calls loadedGraphNode to allow them to be updated
|
||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
||||
}
|
||||
// Each node then calls loadedGraphNode to allow them to be updated
|
||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
expect(mockExtension.loadedGraphNode.mock.calls[i][0].type).toBe(graphData.nodes[i].type);
|
||||
}
|
||||
|
||||
// After configure is then called once all the setup is done
|
||||
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1);
|
||||
// After configure is then called once all the setup is done
|
||||
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.setup).toHaveBeenCalledWith(app);
|
||||
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.setup).toHaveBeenCalledWith(app);
|
||||
|
||||
// Ensure hooks are called in the correct order
|
||||
const callOrder = [
|
||||
"init",
|
||||
"addCustomNodeDefs",
|
||||
"getCustomWidgets",
|
||||
"beforeRegisterNodeDef",
|
||||
"registerCustomNodes",
|
||||
"beforeConfigureGraph",
|
||||
"nodeCreated",
|
||||
"loadedGraphNode",
|
||||
"afterConfigureGraph",
|
||||
"setup",
|
||||
];
|
||||
for (let i = 1; i < callOrder.length; i++) {
|
||||
const fn1 = mockExtension[callOrder[i - 1]];
|
||||
const fn2 = mockExtension[callOrder[i]];
|
||||
expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(fn2.mock.invocationCallOrder[0]);
|
||||
}
|
||||
// Ensure hooks are called in the correct order
|
||||
const callOrder = [
|
||||
"init",
|
||||
"addCustomNodeDefs",
|
||||
"getCustomWidgets",
|
||||
"beforeRegisterNodeDef",
|
||||
"registerCustomNodes",
|
||||
"beforeConfigureGraph",
|
||||
"nodeCreated",
|
||||
"loadedGraphNode",
|
||||
"afterConfigureGraph",
|
||||
"setup",
|
||||
];
|
||||
for (let i = 1; i < callOrder.length; i++) {
|
||||
const fn1 = mockExtension[callOrder[i - 1]];
|
||||
const fn2 = mockExtension[callOrder[i]];
|
||||
expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(fn2.mock.invocationCallOrder[0]);
|
||||
}
|
||||
|
||||
graph.clear();
|
||||
graph.clear();
|
||||
|
||||
// Ensure adding a new node calls the correct callback
|
||||
ez.LoadImage();
|
||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
||||
expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe("LoadImage");
|
||||
// Ensure adding a new node calls the correct callback
|
||||
ez.LoadImage();
|
||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length);
|
||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
||||
expect(mockExtension.nodeCreated.mock.lastCall[0].type).toBe("LoadImage");
|
||||
|
||||
// Reload the graph to ensure correct hooks are fired
|
||||
await graph.reload();
|
||||
// Reload the graph to ensure correct hooks are fired
|
||||
await graph.reload();
|
||||
|
||||
// These hooks should not be fired again
|
||||
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
||||
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
||||
// These hooks should not be fired again
|
||||
expect(mockExtension.init).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.addCustomNodeDefs).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.getCustomWidgets).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.registerCustomNodes).toHaveBeenCalledTimes(1);
|
||||
expect(mockExtension.beforeRegisterNodeDef).toHaveBeenCalledTimes(nodeCount);
|
||||
expect(mockExtension.setup).toHaveBeenCalledTimes(1);
|
||||
|
||||
// These should be called again
|
||||
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2);
|
||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 2);
|
||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
||||
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2);
|
||||
}, 15000);
|
||||
// These should be called again
|
||||
expect(mockExtension.beforeConfigureGraph).toHaveBeenCalledTimes(2);
|
||||
expect(mockExtension.nodeCreated).toHaveBeenCalledTimes(graphData.nodes.length + 2);
|
||||
expect(mockExtension.loadedGraphNode).toHaveBeenCalledTimes(graphData.nodes.length + 1);
|
||||
expect(mockExtension.afterConfigureGraph).toHaveBeenCalledTimes(2);
|
||||
}, 15000);
|
||||
|
||||
it("allows custom nodeDefs and widgets to be registered", async () => {
|
||||
const widgetMock = jest.fn((node, inputName, inputData, app) => {
|
||||
expect(node.constructor.comfyClass).toBe("TestNode");
|
||||
expect(inputName).toBe("test_input");
|
||||
expect(inputData[0]).toBe("CUSTOMWIDGET");
|
||||
expect(inputData[1]?.hello).toBe("world");
|
||||
expect(app).toStrictEqual(app);
|
||||
it("allows custom nodeDefs and widgets to be registered", async () => {
|
||||
const widgetMock = jest.fn((node, inputName, inputData, app) => {
|
||||
expect(node.constructor.comfyClass).toBe("TestNode");
|
||||
expect(inputName).toBe("test_input");
|
||||
expect(inputData[0]).toBe("CUSTOMWIDGET");
|
||||
expect(inputData[1]?.hello).toBe("world");
|
||||
expect(app).toStrictEqual(app);
|
||||
|
||||
return {
|
||||
widget: node.addWidget("button", inputName, "hello", () => {}),
|
||||
};
|
||||
});
|
||||
return {
|
||||
widget: node.addWidget("button", inputName, "hello", () => {}),
|
||||
};
|
||||
});
|
||||
|
||||
// Register our extension that adds a custom node + widget type
|
||||
const mockExtension = {
|
||||
name: "TestExtension",
|
||||
addCustomNodeDefs: (nodeDefs) => {
|
||||
nodeDefs["TestNode"] = {
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
name: "TestNode",
|
||||
display_name: "TestNode",
|
||||
category: "Test",
|
||||
input: {
|
||||
required: {
|
||||
test_input: ["CUSTOMWIDGET", { hello: "world" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getCustomWidgets: jest.fn(() => {
|
||||
return {
|
||||
CUSTOMWIDGET: widgetMock,
|
||||
};
|
||||
}),
|
||||
};
|
||||
// Register our extension that adds a custom node + widget type
|
||||
const mockExtension = {
|
||||
name: "TestExtension",
|
||||
addCustomNodeDefs: (nodeDefs) => {
|
||||
nodeDefs["TestNode"] = {
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
name: "TestNode",
|
||||
display_name: "TestNode",
|
||||
category: "Test",
|
||||
input: {
|
||||
required: {
|
||||
test_input: ["CUSTOMWIDGET", { hello: "world" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getCustomWidgets: jest.fn(() => {
|
||||
return {
|
||||
CUSTOMWIDGET: widgetMock,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const { graph, ez } = await start({
|
||||
async preSetup(app) {
|
||||
app.registerExtension(mockExtension);
|
||||
},
|
||||
});
|
||||
const { graph, ez } = await start({
|
||||
async preSetup(app) {
|
||||
app.registerExtension(mockExtension);
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockExtension.getCustomWidgets).toBeCalledTimes(1);
|
||||
expect(mockExtension.getCustomWidgets).toBeCalledTimes(1);
|
||||
|
||||
graph.clear();
|
||||
expect(widgetMock).toBeCalledTimes(0);
|
||||
const node = ez.TestNode();
|
||||
expect(widgetMock).toBeCalledTimes(1);
|
||||
graph.clear();
|
||||
expect(widgetMock).toBeCalledTimes(0);
|
||||
const node = ez.TestNode();
|
||||
expect(widgetMock).toBeCalledTimes(1);
|
||||
|
||||
// Ensure our custom widget is created
|
||||
expect(node.inputs.length).toBe(0);
|
||||
expect(node.widgets.length).toBe(1);
|
||||
const w = node.widgets[0].widget;
|
||||
expect(w.name).toBe("test_input");
|
||||
expect(w.type).toBe("button");
|
||||
});
|
||||
// Ensure our custom widget is created
|
||||
expect(node.inputs.length).toBe(0);
|
||||
expect(node.widgets.length).toBe(1);
|
||||
const w = node.widgets[0].widget;
|
||||
expect(w.name).toBe("test_input");
|
||||
expect(w.type).toBe("button");
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,291 +2,291 @@ import { start } from "../utils";
|
||||
import lg from "../utils/litegraph";
|
||||
|
||||
describe("users", () => {
|
||||
beforeEach(() => {
|
||||
lg.setup(global);
|
||||
});
|
||||
beforeEach(() => {
|
||||
lg.setup(global);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
lg.teardown(global);
|
||||
});
|
||||
afterEach(() => {
|
||||
lg.teardown(global);
|
||||
});
|
||||
|
||||
function expectNoUserScreen() {
|
||||
// Ensure login isnt visible
|
||||
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
||||
expect(selection["style"].display).toBe("none");
|
||||
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
||||
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
||||
}
|
||||
function expectNoUserScreen() {
|
||||
// Ensure login isnt visible
|
||||
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
||||
expect(selection["style"].display).toBe("none");
|
||||
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
||||
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
||||
}
|
||||
|
||||
describe("multi-user", () => {
|
||||
async function mockAddStylesheet() {
|
||||
const utils = await import("../../src/scripts/utils");
|
||||
utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve());
|
||||
}
|
||||
describe("multi-user", () => {
|
||||
async function mockAddStylesheet() {
|
||||
const utils = await import("../../src/scripts/utils");
|
||||
utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve());
|
||||
}
|
||||
|
||||
async function waitForUserScreenShow() {
|
||||
// Wait for "show" to be called
|
||||
const { UserSelectionScreen } = await import("../../src/scripts/ui/userSelection");
|
||||
let resolve, reject;
|
||||
const fn = UserSelectionScreen.prototype.show;
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
jest.spyOn(UserSelectionScreen.prototype, "show").mockImplementation(async (...args) => {
|
||||
const res = fn(...args);
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
resolve();
|
||||
return res;
|
||||
});
|
||||
// @ts-ignore
|
||||
setTimeout(() => reject("timeout waiting for UserSelectionScreen to be shown."), 500);
|
||||
await p;
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
}
|
||||
async function waitForUserScreenShow() {
|
||||
// Wait for "show" to be called
|
||||
const { UserSelectionScreen } = await import("../../src/scripts/ui/userSelection");
|
||||
let resolve, reject;
|
||||
const fn = UserSelectionScreen.prototype.show;
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
jest.spyOn(UserSelectionScreen.prototype, "show").mockImplementation(async (...args) => {
|
||||
const res = fn(...args);
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
resolve();
|
||||
return res;
|
||||
});
|
||||
// @ts-ignore
|
||||
setTimeout(() => reject("timeout waiting for UserSelectionScreen to be shown."), 500);
|
||||
await p;
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
}
|
||||
|
||||
async function testUserScreen(onShown, users?) {
|
||||
if (!users) {
|
||||
users = {};
|
||||
}
|
||||
const starting = start({
|
||||
resetEnv: true,
|
||||
userConfig: { storage: "server", users },
|
||||
preSetup: mockAddStylesheet,
|
||||
});
|
||||
async function testUserScreen(onShown, users?) {
|
||||
if (!users) {
|
||||
users = {};
|
||||
}
|
||||
const starting = start({
|
||||
resetEnv: true,
|
||||
userConfig: { storage: "server", users },
|
||||
preSetup: mockAddStylesheet,
|
||||
});
|
||||
|
||||
// Ensure no current user
|
||||
expect(localStorage["Comfy.userId"]).toBeFalsy();
|
||||
expect(localStorage["Comfy.userName"]).toBeFalsy();
|
||||
// Ensure no current user
|
||||
expect(localStorage["Comfy.userId"]).toBeFalsy();
|
||||
expect(localStorage["Comfy.userName"]).toBeFalsy();
|
||||
|
||||
await waitForUserScreenShow();
|
||||
await waitForUserScreenShow();
|
||||
|
||||
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
||||
expect(selection).toBeTruthy();
|
||||
const selection = document.querySelectorAll("#comfy-user-selection")?.[0];
|
||||
expect(selection).toBeTruthy();
|
||||
|
||||
// Ensure login is visible
|
||||
expect(window.getComputedStyle(selection)?.display).not.toBe("none");
|
||||
// Ensure menu is hidden
|
||||
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
||||
expect(window.getComputedStyle(menu)?.display).toBe("none");
|
||||
// Ensure login is visible
|
||||
expect(window.getComputedStyle(selection)?.display).not.toBe("none");
|
||||
// Ensure menu is hidden
|
||||
const menu = document.querySelectorAll(".comfy-menu")?.[0];
|
||||
expect(window.getComputedStyle(menu)?.display).toBe("none");
|
||||
|
||||
const isCreate = await onShown(selection);
|
||||
const isCreate = await onShown(selection);
|
||||
|
||||
// Submit form
|
||||
selection.querySelectorAll("form")[0].submit();
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
// Submit form
|
||||
selection.querySelectorAll("form")[0].submit();
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
|
||||
// Wait for start
|
||||
const s = await starting;
|
||||
// Wait for start
|
||||
const s = await starting;
|
||||
|
||||
// Ensure login is removed
|
||||
expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(0);
|
||||
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
||||
// Ensure login is removed
|
||||
expect(document.querySelectorAll("#comfy-user-selection")).toHaveLength(0);
|
||||
expect(window.getComputedStyle(menu)?.display).not.toBe("none");
|
||||
|
||||
// Ensure settings + templates are saved
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.createUser).toHaveBeenCalledTimes(+isCreate);
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate);
|
||||
if (isCreate) {
|
||||
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
||||
expect(s.app.isNewUserSession).toBeTruthy();
|
||||
} else {
|
||||
expect(s.app.isNewUserSession).toBeFalsy();
|
||||
}
|
||||
// Ensure settings + templates are saved
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.createUser).toHaveBeenCalledTimes(+isCreate);
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate);
|
||||
if (isCreate) {
|
||||
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
||||
expect(s.app.isNewUserSession).toBeTruthy();
|
||||
} else {
|
||||
expect(s.app.isNewUserSession).toBeFalsy();
|
||||
}
|
||||
|
||||
return { users, selection, ...s };
|
||||
}
|
||||
return { users, selection, ...s };
|
||||
}
|
||||
|
||||
it("allows user creation if no users", async () => {
|
||||
const { users } = await testUserScreen((selection) => {
|
||||
// Ensure we have no users flag added
|
||||
expect(selection.classList.contains("no-users")).toBeTruthy();
|
||||
it("allows user creation if no users", async () => {
|
||||
const { users } = await testUserScreen((selection) => {
|
||||
// Ensure we have no users flag added
|
||||
expect(selection.classList.contains("no-users")).toBeTruthy();
|
||||
|
||||
// Enter a username
|
||||
const input = selection.getElementsByTagName("input")[0];
|
||||
input.focus();
|
||||
input.value = "Test User";
|
||||
// Enter a username
|
||||
const input = selection.getElementsByTagName("input")[0];
|
||||
input.focus();
|
||||
input.value = "Test User";
|
||||
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(users).toStrictEqual({
|
||||
"Test User!": "Test User",
|
||||
});
|
||||
expect(users).toStrictEqual({
|
||||
"Test User!": "Test User",
|
||||
});
|
||||
|
||||
expect(localStorage["Comfy.userId"]).toBe("Test User!");
|
||||
expect(localStorage["Comfy.userName"]).toBe("Test User");
|
||||
});
|
||||
it("allows user creation if no current user but other users", async () => {
|
||||
const users = {
|
||||
"Test User 2!": "Test User 2",
|
||||
};
|
||||
expect(localStorage["Comfy.userId"]).toBe("Test User!");
|
||||
expect(localStorage["Comfy.userName"]).toBe("Test User");
|
||||
});
|
||||
it("allows user creation if no current user but other users", async () => {
|
||||
const users = {
|
||||
"Test User 2!": "Test User 2",
|
||||
};
|
||||
|
||||
await testUserScreen((selection) => {
|
||||
expect(selection.classList.contains("no-users")).toBeFalsy();
|
||||
await testUserScreen((selection) => {
|
||||
expect(selection.classList.contains("no-users")).toBeFalsy();
|
||||
|
||||
// Enter a username
|
||||
const input = selection.getElementsByTagName("input")[0];
|
||||
input.focus();
|
||||
input.value = "Test User 3";
|
||||
return true;
|
||||
}, users);
|
||||
// Enter a username
|
||||
const input = selection.getElementsByTagName("input")[0];
|
||||
input.focus();
|
||||
input.value = "Test User 3";
|
||||
return true;
|
||||
}, users);
|
||||
|
||||
expect(users).toStrictEqual({
|
||||
"Test User 2!": "Test User 2",
|
||||
"Test User 3!": "Test User 3",
|
||||
});
|
||||
expect(users).toStrictEqual({
|
||||
"Test User 2!": "Test User 2",
|
||||
"Test User 3!": "Test User 3",
|
||||
});
|
||||
|
||||
expect(localStorage["Comfy.userId"]).toBe("Test User 3!");
|
||||
expect(localStorage["Comfy.userName"]).toBe("Test User 3");
|
||||
});
|
||||
it("allows user selection if no current user but other users", async () => {
|
||||
const users = {
|
||||
"A!": "A",
|
||||
"B!": "B",
|
||||
"C!": "C",
|
||||
};
|
||||
expect(localStorage["Comfy.userId"]).toBe("Test User 3!");
|
||||
expect(localStorage["Comfy.userName"]).toBe("Test User 3");
|
||||
});
|
||||
it("allows user selection if no current user but other users", async () => {
|
||||
const users = {
|
||||
"A!": "A",
|
||||
"B!": "B",
|
||||
"C!": "C",
|
||||
};
|
||||
|
||||
await testUserScreen((selection) => {
|
||||
expect(selection.classList.contains("no-users")).toBeFalsy();
|
||||
await testUserScreen((selection) => {
|
||||
expect(selection.classList.contains("no-users")).toBeFalsy();
|
||||
|
||||
// Check user list
|
||||
const select = selection.getElementsByTagName("select")[0];
|
||||
const options = select.getElementsByTagName("option");
|
||||
expect(
|
||||
[...options]
|
||||
.filter((o) => !o.disabled)
|
||||
.reduce((p, n) => {
|
||||
p[n.getAttribute("value")] = n.textContent;
|
||||
return p;
|
||||
}, {})
|
||||
).toStrictEqual(users);
|
||||
// Check user list
|
||||
const select = selection.getElementsByTagName("select")[0];
|
||||
const options = select.getElementsByTagName("option");
|
||||
expect(
|
||||
[...options]
|
||||
.filter((o) => !o.disabled)
|
||||
.reduce((p, n) => {
|
||||
p[n.getAttribute("value")] = n.textContent;
|
||||
return p;
|
||||
}, {})
|
||||
).toStrictEqual(users);
|
||||
|
||||
// Select an option
|
||||
select.focus();
|
||||
select.value = options[2].value;
|
||||
// Select an option
|
||||
select.focus();
|
||||
select.value = options[2].value;
|
||||
|
||||
return false;
|
||||
}, users);
|
||||
return false;
|
||||
}, users);
|
||||
|
||||
expect(users).toStrictEqual(users);
|
||||
expect(users).toStrictEqual(users);
|
||||
|
||||
expect(localStorage["Comfy.userId"]).toBe("B!");
|
||||
expect(localStorage["Comfy.userName"]).toBe("B");
|
||||
});
|
||||
it("doesnt show user screen if current user", async () => {
|
||||
const starting = start({
|
||||
resetEnv: true,
|
||||
userConfig: {
|
||||
storage: "server",
|
||||
users: {
|
||||
"User!": "User",
|
||||
},
|
||||
},
|
||||
localStorage: {
|
||||
"Comfy.userId": "User!",
|
||||
"Comfy.userName": "User",
|
||||
},
|
||||
});
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
expect(localStorage["Comfy.userId"]).toBe("B!");
|
||||
expect(localStorage["Comfy.userName"]).toBe("B");
|
||||
});
|
||||
it("doesnt show user screen if current user", async () => {
|
||||
const starting = start({
|
||||
resetEnv: true,
|
||||
userConfig: {
|
||||
storage: "server",
|
||||
users: {
|
||||
"User!": "User",
|
||||
},
|
||||
},
|
||||
localStorage: {
|
||||
"Comfy.userId": "User!",
|
||||
"Comfy.userName": "User",
|
||||
},
|
||||
});
|
||||
await new Promise(process.nextTick); // wait for promises to resolve
|
||||
|
||||
expectNoUserScreen();
|
||||
expectNoUserScreen();
|
||||
|
||||
await starting;
|
||||
});
|
||||
it("allows user switching", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: {
|
||||
storage: "server",
|
||||
users: {
|
||||
"User!": "User",
|
||||
},
|
||||
},
|
||||
localStorage: {
|
||||
"Comfy.userId": "User!",
|
||||
"Comfy.userName": "User",
|
||||
},
|
||||
});
|
||||
await starting;
|
||||
});
|
||||
it("allows user switching", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: {
|
||||
storage: "server",
|
||||
users: {
|
||||
"User!": "User",
|
||||
},
|
||||
},
|
||||
localStorage: {
|
||||
"Comfy.userId": "User!",
|
||||
"Comfy.userName": "User",
|
||||
},
|
||||
});
|
||||
|
||||
// cant actually test switching user easily but can check the setting is present
|
||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe("single-user", () => {
|
||||
it("doesnt show user creation if no default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: false, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
// cant actually test switching user easily but can check the setting is present
|
||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe("single-user", () => {
|
||||
it("doesnt show user creation if no default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: false, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
||||
expect(app.isNewUserSession).toBeTruthy();
|
||||
});
|
||||
it("doesnt show user creation if default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(1);
|
||||
expect(api.storeUserData).toHaveBeenCalledWith("comfy.templates.json", null, { stringify: false });
|
||||
expect(app.isNewUserSession).toBeTruthy();
|
||||
});
|
||||
it("doesnt show user creation if default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
||||
expect(app.isNewUserSession).toBeFalsy();
|
||||
});
|
||||
it("doesnt allow user switching", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
||||
expect(app.isNewUserSession).toBeFalsy();
|
||||
});
|
||||
it("doesnt allow user switching", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
|
||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
describe("browser-user", () => {
|
||||
it("doesnt show user creation if no default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: false, storage: "browser" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
describe("browser-user", () => {
|
||||
it("doesnt show user creation if no default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: false, storage: "browser" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
||||
expect(app.isNewUserSession).toBeFalsy();
|
||||
});
|
||||
it("doesnt show user creation if default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
||||
expect(app.isNewUserSession).toBeFalsy();
|
||||
});
|
||||
it("doesnt show user creation if default user", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "server" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
||||
expect(app.isNewUserSession).toBeFalsy();
|
||||
});
|
||||
it("doesnt allow user switching", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "browser" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
// It should store the settings
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(0);
|
||||
expect(api.storeUserData).toHaveBeenCalledTimes(0);
|
||||
expect(app.isNewUserSession).toBeFalsy();
|
||||
});
|
||||
it("doesnt allow user switching", async () => {
|
||||
const { app } = await start({
|
||||
resetEnv: true,
|
||||
userConfig: { migrated: true, storage: "browser" },
|
||||
});
|
||||
expectNoUserScreen();
|
||||
|
||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
expect(app.ui.settings.settingsLookup["Comfy.SwitchUser"]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,441 +15,441 @@
|
||||
export type EzNameSpace = Record<string, (...args) => EzNode>;
|
||||
|
||||
export class EzConnection {
|
||||
/** @type { app } */
|
||||
app;
|
||||
/** @type { InstanceType<LG["LLink"]> } */
|
||||
link;
|
||||
/** @type { app } */
|
||||
app;
|
||||
/** @type { InstanceType<LG["LLink"]> } */
|
||||
link;
|
||||
|
||||
get originNode() {
|
||||
return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id));
|
||||
}
|
||||
get originNode() {
|
||||
return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id));
|
||||
}
|
||||
|
||||
get originOutput() {
|
||||
return this.originNode.outputs[this.link.origin_slot];
|
||||
}
|
||||
get originOutput() {
|
||||
return this.originNode.outputs[this.link.origin_slot];
|
||||
}
|
||||
|
||||
get targetNode() {
|
||||
return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id));
|
||||
}
|
||||
get targetNode() {
|
||||
return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id));
|
||||
}
|
||||
|
||||
get targetInput() {
|
||||
return this.targetNode.inputs[this.link.target_slot];
|
||||
}
|
||||
get targetInput() {
|
||||
return this.targetNode.inputs[this.link.target_slot];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { app } app
|
||||
* @param { InstanceType<LG["LLink"]> } link
|
||||
*/
|
||||
constructor(app, link) {
|
||||
this.app = app;
|
||||
this.link = link;
|
||||
}
|
||||
/**
|
||||
* @param { app } app
|
||||
* @param { InstanceType<LG["LLink"]> } link
|
||||
*/
|
||||
constructor(app, link) {
|
||||
this.app = app;
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.targetInput.disconnect();
|
||||
}
|
||||
disconnect() {
|
||||
this.targetInput.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export class EzSlot {
|
||||
/** @type { EzNode } */
|
||||
node;
|
||||
/** @type { number } */
|
||||
index;
|
||||
/** @type { EzNode } */
|
||||
node;
|
||||
/** @type { number } */
|
||||
index;
|
||||
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
*/
|
||||
constructor(node, index) {
|
||||
this.node = node;
|
||||
this.index = index;
|
||||
}
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
*/
|
||||
constructor(node, index) {
|
||||
this.node = node;
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
export class EzInput extends EzSlot {
|
||||
/** @type { INodeInputSlot } */
|
||||
input;
|
||||
/** @type { INodeInputSlot } */
|
||||
input;
|
||||
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { INodeInputSlot } input
|
||||
*/
|
||||
constructor(node, index, input) {
|
||||
super(node, index);
|
||||
this.input = input;
|
||||
}
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { INodeInputSlot } input
|
||||
*/
|
||||
constructor(node, index, input) {
|
||||
super(node, index);
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
get connection() {
|
||||
const link = this.node.node.inputs?.[this.index]?.link;
|
||||
if (link == null) {
|
||||
return null;
|
||||
}
|
||||
return new EzConnection(this.node.app, this.node.app.graph.links[link]);
|
||||
}
|
||||
get connection() {
|
||||
const link = this.node.node.inputs?.[this.index]?.link;
|
||||
if (link == null) {
|
||||
return null;
|
||||
}
|
||||
return new EzConnection(this.node.app, this.node.app.graph.links[link]);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.node.node.disconnectInput(this.index);
|
||||
}
|
||||
disconnect() {
|
||||
this.node.node.disconnectInput(this.index);
|
||||
}
|
||||
}
|
||||
|
||||
export class EzOutput extends EzSlot {
|
||||
/** @type { INodeOutputSlot } */
|
||||
output;
|
||||
/** @type { INodeOutputSlot } */
|
||||
output;
|
||||
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { INodeOutputSlot } output
|
||||
*/
|
||||
constructor(node, index, output) {
|
||||
super(node, index);
|
||||
this.output = output;
|
||||
}
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { INodeOutputSlot } output
|
||||
*/
|
||||
constructor(node, index, output) {
|
||||
super(node, index);
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
get connections() {
|
||||
return (this.node.node.outputs?.[this.index]?.links ?? []).map(
|
||||
(l) => new EzConnection(this.node.app, this.node.app.graph.links[l])
|
||||
);
|
||||
}
|
||||
get connections() {
|
||||
return (this.node.node.outputs?.[this.index]?.links ?? []).map(
|
||||
(l) => new EzConnection(this.node.app, this.node.app.graph.links[l])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { EzInput } input
|
||||
*/
|
||||
connectTo(input) {
|
||||
if (!input) throw new Error("Invalid input");
|
||||
/**
|
||||
* @param { EzInput } input
|
||||
*/
|
||||
connectTo(input) {
|
||||
if (!input) throw new Error("Invalid input");
|
||||
|
||||
/**
|
||||
* @type { LG["LLink"] | null }
|
||||
*/
|
||||
const link = this.node.node.connect(this.index, input.node.node, input.index);
|
||||
if (!link) {
|
||||
const inp = input.input;
|
||||
const inName = inp.name || inp.label || inp.type;
|
||||
throw new Error(
|
||||
`Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${
|
||||
this.output.name ?? this.output.type
|
||||
}#${this.index}] failed.`
|
||||
);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
/**
|
||||
* @type { LG["LLink"] | null }
|
||||
*/
|
||||
const link = this.node.node.connect(this.index, input.node.node, input.index);
|
||||
if (!link) {
|
||||
const inp = input.input;
|
||||
const inName = inp.name || inp.label || inp.type;
|
||||
throw new Error(
|
||||
`Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${
|
||||
this.output.name ?? this.output.type
|
||||
}#${this.index}] failed.`
|
||||
);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
export class EzNodeMenuItem {
|
||||
/** @type { EzNode } */
|
||||
node;
|
||||
/** @type { number } */
|
||||
index;
|
||||
/** @type { ContextMenuItem } */
|
||||
item;
|
||||
/** @type { EzNode } */
|
||||
node;
|
||||
/** @type { number } */
|
||||
index;
|
||||
/** @type { ContextMenuItem } */
|
||||
item;
|
||||
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { ContextMenuItem } item
|
||||
*/
|
||||
constructor(node, index, item) {
|
||||
this.node = node;
|
||||
this.index = index;
|
||||
this.item = item;
|
||||
}
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { ContextMenuItem } item
|
||||
*/
|
||||
constructor(node, index, item) {
|
||||
this.node = node;
|
||||
this.index = index;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
call(selectNode = true) {
|
||||
if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`);
|
||||
if (selectNode) {
|
||||
this.node.select();
|
||||
}
|
||||
return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
|
||||
}
|
||||
call(selectNode = true) {
|
||||
if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`);
|
||||
if (selectNode) {
|
||||
this.node.select();
|
||||
}
|
||||
return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
|
||||
}
|
||||
}
|
||||
|
||||
export class EzWidget {
|
||||
/** @type { EzNode } */
|
||||
node;
|
||||
/** @type { number } */
|
||||
index;
|
||||
/** @type { IWidget } */
|
||||
widget;
|
||||
/** @type { EzNode } */
|
||||
node;
|
||||
/** @type { number } */
|
||||
index;
|
||||
/** @type { IWidget } */
|
||||
widget;
|
||||
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { IWidget } widget
|
||||
*/
|
||||
constructor(node, index, widget) {
|
||||
this.node = node;
|
||||
this.index = index;
|
||||
this.widget = widget;
|
||||
}
|
||||
/**
|
||||
* @param { EzNode } node
|
||||
* @param { number } index
|
||||
* @param { IWidget } widget
|
||||
*/
|
||||
constructor(node, index, widget) {
|
||||
this.node = node;
|
||||
this.index = index;
|
||||
this.widget = widget;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.widget.value;
|
||||
}
|
||||
get value() {
|
||||
return this.widget.value;
|
||||
}
|
||||
|
||||
set value(v) {
|
||||
this.widget.value = v;
|
||||
this.widget.callback?.call?.(this.widget, v)
|
||||
}
|
||||
set value(v) {
|
||||
this.widget.value = v;
|
||||
this.widget.callback?.call?.(this.widget, v)
|
||||
}
|
||||
|
||||
get isConvertedToInput() {
|
||||
// @ts-ignore : this type is valid for converted widgets
|
||||
return this.widget.type === "converted-widget";
|
||||
}
|
||||
get isConvertedToInput() {
|
||||
// @ts-ignore : this type is valid for converted widgets
|
||||
return this.widget.type === "converted-widget";
|
||||
}
|
||||
|
||||
getConvertedInput() {
|
||||
if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
|
||||
getConvertedInput() {
|
||||
if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
|
||||
|
||||
return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name);
|
||||
}
|
||||
return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name);
|
||||
}
|
||||
|
||||
convertToWidget() {
|
||||
if (!this.isConvertedToInput)
|
||||
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already a widget.`);
|
||||
var menu = this.node.menu["Convert Input to Widget"].item.submenu.options;
|
||||
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to widget`);
|
||||
menu[index].callback.call();
|
||||
}
|
||||
convertToWidget() {
|
||||
if (!this.isConvertedToInput)
|
||||
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already a widget.`);
|
||||
var menu = this.node.menu["Convert Input to Widget"].item.submenu.options;
|
||||
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to widget`);
|
||||
menu[index].callback.call();
|
||||
}
|
||||
|
||||
convertToInput() {
|
||||
if (this.isConvertedToInput)
|
||||
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already an input.`);
|
||||
var menu = this.node.menu["Convert Widget to Input"].item.submenu.options;
|
||||
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to input`);
|
||||
menu[index].callback.call();
|
||||
}
|
||||
convertToInput() {
|
||||
if (this.isConvertedToInput)
|
||||
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already an input.`);
|
||||
var menu = this.node.menu["Convert Widget to Input"].item.submenu.options;
|
||||
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to input`);
|
||||
menu[index].callback.call();
|
||||
}
|
||||
}
|
||||
|
||||
export class EzNode {
|
||||
/** @type { app } */
|
||||
app;
|
||||
/** @type { LGNode } */
|
||||
node;
|
||||
/** @type { app } */
|
||||
app;
|
||||
/** @type { LGNode } */
|
||||
node;
|
||||
|
||||
/**
|
||||
* @param { app } app
|
||||
* @param { LGNode } node
|
||||
*/
|
||||
constructor(app, node) {
|
||||
this.app = app;
|
||||
this.node = node;
|
||||
}
|
||||
/**
|
||||
* @param { app } app
|
||||
* @param { LGNode } node
|
||||
*/
|
||||
constructor(app, node) {
|
||||
this.app = app;
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.node.id;
|
||||
}
|
||||
get id() {
|
||||
return this.node.id;
|
||||
}
|
||||
|
||||
get inputs() {
|
||||
return this.#makeLookupArray("inputs", "name", EzInput);
|
||||
}
|
||||
get inputs() {
|
||||
return this.#makeLookupArray("inputs", "name", EzInput);
|
||||
}
|
||||
|
||||
get outputs() {
|
||||
return this.#makeLookupArray("outputs", "name", EzOutput);
|
||||
}
|
||||
get outputs() {
|
||||
return this.#makeLookupArray("outputs", "name", EzOutput);
|
||||
}
|
||||
|
||||
get widgets() {
|
||||
return this.#makeLookupArray("widgets", "name", EzWidget);
|
||||
}
|
||||
get widgets() {
|
||||
return this.#makeLookupArray("widgets", "name", EzWidget);
|
||||
}
|
||||
|
||||
get menu() {
|
||||
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
|
||||
}
|
||||
get menu() {
|
||||
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
|
||||
}
|
||||
|
||||
get isRemoved() {
|
||||
return !this.app.graph.getNodeById(this.id);
|
||||
}
|
||||
get isRemoved() {
|
||||
return !this.app.graph.getNodeById(this.id);
|
||||
}
|
||||
|
||||
select(addToSelection = false) {
|
||||
this.app.canvas.selectNode(this.node, addToSelection);
|
||||
}
|
||||
select(addToSelection = false) {
|
||||
this.app.canvas.selectNode(this.node, addToSelection);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @template { "inputs" | "outputs" } T
|
||||
// * @param { T } type
|
||||
// * @returns { Record<string, type extends "inputs" ? EzInput : EzOutput> & (type extends "inputs" ? EzInput [] : EzOutput[]) }
|
||||
// */
|
||||
// #getSlotItems(type) {
|
||||
// // @ts-ignore : these items are correct
|
||||
// return (this.node[type] ?? []).reduce((p, s, i) => {
|
||||
// if (s.name in p) {
|
||||
// throw new Error(`Unable to store input ${s.name} on array as name conflicts.`);
|
||||
// }
|
||||
// // @ts-ignore
|
||||
// p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)));
|
||||
// return p;
|
||||
// }, Object.assign([], { $: this }));
|
||||
// }
|
||||
// /**
|
||||
// * @template { "inputs" | "outputs" } T
|
||||
// * @param { T } type
|
||||
// * @returns { Record<string, type extends "inputs" ? EzInput : EzOutput> & (type extends "inputs" ? EzInput [] : EzOutput[]) }
|
||||
// */
|
||||
// #getSlotItems(type) {
|
||||
// // @ts-ignore : these items are correct
|
||||
// return (this.node[type] ?? []).reduce((p, s, i) => {
|
||||
// if (s.name in p) {
|
||||
// throw new Error(`Unable to store input ${s.name} on array as name conflicts.`);
|
||||
// }
|
||||
// // @ts-ignore
|
||||
// p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)));
|
||||
// return p;
|
||||
// }, Object.assign([], { $: this }));
|
||||
// }
|
||||
|
||||
/**
|
||||
* @template { { new(node: EzNode, index: number, obj: any): any } } T
|
||||
* @param { "inputs" | "outputs" | "widgets" | (() => Array<unknown>) } nodeProperty
|
||||
* @param { string } nameProperty
|
||||
* @param { T } ctor
|
||||
* @returns { Record<string, InstanceType<T>> & Array<InstanceType<T>> }
|
||||
*/
|
||||
#makeLookupArray(nodeProperty, nameProperty, ctor) {
|
||||
const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty];
|
||||
// @ts-ignore
|
||||
return (items ?? []).reduce((p, s, i) => {
|
||||
if (!s) return p;
|
||||
/**
|
||||
* @template { { new(node: EzNode, index: number, obj: any): any } } T
|
||||
* @param { "inputs" | "outputs" | "widgets" | (() => Array<unknown>) } nodeProperty
|
||||
* @param { string } nameProperty
|
||||
* @param { T } ctor
|
||||
* @returns { Record<string, InstanceType<T>> & Array<InstanceType<T>> }
|
||||
*/
|
||||
#makeLookupArray(nodeProperty, nameProperty, ctor) {
|
||||
const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty];
|
||||
// @ts-ignore
|
||||
return (items ?? []).reduce((p, s, i) => {
|
||||
if (!s) return p;
|
||||
|
||||
const name = s[nameProperty];
|
||||
const item = new ctor(this, i, s);
|
||||
// @ts-ignore
|
||||
p.push(item);
|
||||
if (name) {
|
||||
// @ts-ignore
|
||||
if (name in p) {
|
||||
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
p[name] = item;
|
||||
return p;
|
||||
}, Object.assign([], { $: this }));
|
||||
}
|
||||
const name = s[nameProperty];
|
||||
const item = new ctor(this, i, s);
|
||||
// @ts-ignore
|
||||
p.push(item);
|
||||
if (name) {
|
||||
// @ts-ignore
|
||||
if (name in p) {
|
||||
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
p[name] = item;
|
||||
return p;
|
||||
}, Object.assign([], { $: this }));
|
||||
}
|
||||
}
|
||||
|
||||
export class EzGraph {
|
||||
/** @type { app } */
|
||||
app;
|
||||
/** @type { app } */
|
||||
app;
|
||||
|
||||
/**
|
||||
* @param { app } app
|
||||
*/
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
}
|
||||
/**
|
||||
* @param { app } app
|
||||
*/
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this.app.graph._nodes.map((n) => new EzNode(this.app, n));
|
||||
}
|
||||
get nodes() {
|
||||
return this.app.graph._nodes.map((n) => new EzNode(this.app, n));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.app.graph.clear();
|
||||
}
|
||||
clear() {
|
||||
this.app.graph.clear();
|
||||
}
|
||||
|
||||
arrange() {
|
||||
this.app.graph.arrange();
|
||||
}
|
||||
arrange() {
|
||||
this.app.graph.arrange();
|
||||
}
|
||||
|
||||
stringify() {
|
||||
return JSON.stringify(this.app.graph.serialize(), undefined);
|
||||
}
|
||||
stringify() {
|
||||
return JSON.stringify(this.app.graph.serialize(), undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { number | LGNode | EzNode } obj
|
||||
* @returns { EzNode }
|
||||
*/
|
||||
find(obj) {
|
||||
let match;
|
||||
let id;
|
||||
if (typeof obj === "number") {
|
||||
id = obj;
|
||||
} else {
|
||||
id = obj.id;
|
||||
}
|
||||
/**
|
||||
* @param { number | LGNode | EzNode } obj
|
||||
* @returns { EzNode }
|
||||
*/
|
||||
find(obj) {
|
||||
let match;
|
||||
let id;
|
||||
if (typeof obj === "number") {
|
||||
id = obj;
|
||||
} else {
|
||||
id = obj.id;
|
||||
}
|
||||
|
||||
match = this.app.graph.getNodeById(id);
|
||||
match = this.app.graph.getNodeById(id);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Unable to find node with ID ${id}.`);
|
||||
}
|
||||
if (!match) {
|
||||
throw new Error(`Unable to find node with ID ${id}.`);
|
||||
}
|
||||
|
||||
return new EzNode(this.app, match);
|
||||
}
|
||||
return new EzNode(this.app, match);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
reload() {
|
||||
const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()));
|
||||
return new Promise((r) => {
|
||||
this.app.graph.clear();
|
||||
setTimeout(async () => {
|
||||
await this.app.loadGraphData(graph);
|
||||
// @ts-ignore
|
||||
r();
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
reload() {
|
||||
const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()));
|
||||
return new Promise((r) => {
|
||||
this.app.graph.clear();
|
||||
setTimeout(async () => {
|
||||
await this.app.loadGraphData(graph);
|
||||
// @ts-ignore
|
||||
r();
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns { Promise<{
|
||||
* workflow: {},
|
||||
* output: Record<string, {
|
||||
* class_name: string,
|
||||
* inputs: Record<string, [string, number] | unknown>
|
||||
* }>}> }
|
||||
*/
|
||||
toPrompt() {
|
||||
// @ts-ignore
|
||||
return this.app.graphToPrompt();
|
||||
}
|
||||
/**
|
||||
* @returns { Promise<{
|
||||
* workflow: {},
|
||||
* output: Record<string, {
|
||||
* class_name: string,
|
||||
* inputs: Record<string, [string, number] | unknown>
|
||||
* }>}> }
|
||||
*/
|
||||
toPrompt() {
|
||||
// @ts-ignore
|
||||
return this.app.graphToPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
export const Ez = {
|
||||
/**
|
||||
* Quickly build and interact with a ComfyUI graph
|
||||
* @example
|
||||
* const { ez, graph } = Ez.graph(app);
|
||||
* graph.clear();
|
||||
* const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs;
|
||||
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs;
|
||||
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs;
|
||||
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs;
|
||||
* const [image] = ez.VAEDecode(latent, vae).outputs;
|
||||
* const saveNode = ez.SaveImage(image);
|
||||
* console.log(saveNode);
|
||||
* graph.arrange();
|
||||
* @param { app } app
|
||||
* @param { LG["LiteGraph"] } LiteGraph
|
||||
* @param { LG["LGraphCanvas"] } LGraphCanvas
|
||||
* @param { boolean } clearGraph
|
||||
* @returns { { graph: EzGraph, ez: Record<string, EzNodeFactory> } }
|
||||
*/
|
||||
graph(app, LiteGraph = window["LiteGraph"], LGraphCanvas = window["LGraphCanvas"], clearGraph = true) {
|
||||
// Always set the active canvas so things work
|
||||
LGraphCanvas.active_canvas = app.canvas;
|
||||
/**
|
||||
* Quickly build and interact with a ComfyUI graph
|
||||
* @example
|
||||
* const { ez, graph } = Ez.graph(app);
|
||||
* graph.clear();
|
||||
* const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs;
|
||||
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs;
|
||||
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs;
|
||||
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs;
|
||||
* const [image] = ez.VAEDecode(latent, vae).outputs;
|
||||
* const saveNode = ez.SaveImage(image);
|
||||
* console.log(saveNode);
|
||||
* graph.arrange();
|
||||
* @param { app } app
|
||||
* @param { LG["LiteGraph"] } LiteGraph
|
||||
* @param { LG["LGraphCanvas"] } LGraphCanvas
|
||||
* @param { boolean } clearGraph
|
||||
* @returns { { graph: EzGraph, ez: Record<string, EzNodeFactory> } }
|
||||
*/
|
||||
graph(app, LiteGraph = window["LiteGraph"], LGraphCanvas = window["LGraphCanvas"], clearGraph = true) {
|
||||
// Always set the active canvas so things work
|
||||
LGraphCanvas.active_canvas = app.canvas;
|
||||
|
||||
if (clearGraph) {
|
||||
app.graph.clear();
|
||||
}
|
||||
if (clearGraph) {
|
||||
app.graph.clear();
|
||||
}
|
||||
|
||||
// @ts-ignore : this proxy handles utility methods & node creation
|
||||
const factory = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, p) {
|
||||
if (typeof p !== "string") throw new Error("Invalid node");
|
||||
const node = LiteGraph.createNode(p);
|
||||
if (!node) throw new Error(`Unknown node "${p}"`);
|
||||
app.graph.add(node);
|
||||
// @ts-ignore : this proxy handles utility methods & node creation
|
||||
const factory = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, p) {
|
||||
if (typeof p !== "string") throw new Error("Invalid node");
|
||||
const node = LiteGraph.createNode(p);
|
||||
if (!node) throw new Error(`Unknown node "${p}"`);
|
||||
app.graph.add(node);
|
||||
|
||||
/**
|
||||
* @param {Parameters<EzNodeFactory>} args
|
||||
*/
|
||||
return function (...args) {
|
||||
const ezNode = new EzNode(app, node);
|
||||
const inputs = ezNode.inputs;
|
||||
/**
|
||||
* @param {Parameters<EzNodeFactory>} args
|
||||
*/
|
||||
return function (...args) {
|
||||
const ezNode = new EzNode(app, node);
|
||||
const inputs = ezNode.inputs;
|
||||
|
||||
let slot = 0;
|
||||
for (const arg of args) {
|
||||
if (arg instanceof EzOutput) {
|
||||
arg.connectTo(inputs[slot++]);
|
||||
} else {
|
||||
for (const k in arg) {
|
||||
ezNode.widgets[k].value = arg[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
let slot = 0;
|
||||
for (const arg of args) {
|
||||
if (arg instanceof EzOutput) {
|
||||
arg.connectTo(inputs[slot++]);
|
||||
} else {
|
||||
for (const k in arg) {
|
||||
ezNode.widgets[k].value = arg[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ezNode;
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
return ezNode;
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { graph: new EzGraph(app), ez: factory };
|
||||
},
|
||||
return { graph: new EzGraph(app), ez: factory };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,45 +7,45 @@ import path from "path";
|
||||
const html = fs.readFileSync(path.resolve(__dirname, "../../index.html"))
|
||||
|
||||
interface StartConfig extends APIConfig {
|
||||
resetEnv?: boolean;
|
||||
preSetup?(app): Promise<void>;
|
||||
localStorage?: Record<string, string>;
|
||||
resetEnv?: boolean;
|
||||
preSetup?(app): Promise<void>;
|
||||
localStorage?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface StartResult {
|
||||
app: any;
|
||||
graph: EzGraph;
|
||||
ez: EzNameSpace;
|
||||
app: any;
|
||||
graph: EzGraph;
|
||||
ez: EzNameSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param { Parameters<typeof mockApi>[0] & {
|
||||
* resetEnv?: boolean,
|
||||
* preSetup?(app): Promise<void>,
|
||||
* resetEnv?: boolean,
|
||||
* preSetup?(app): Promise<void>,
|
||||
* localStorage?: Record<string, string>
|
||||
* } } config
|
||||
* @returns
|
||||
*/
|
||||
export async function start(config: StartConfig = {}): Promise<StartResult> {
|
||||
if(config.resetEnv) {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
if(config.resetEnv) {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
lg.setup(global);
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
}
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
Object.assign(localStorage, config.localStorage ?? {});
|
||||
document.body.innerHTML = html.toString();
|
||||
Object.assign(localStorage, config.localStorage ?? {});
|
||||
document.body.innerHTML = html.toString();
|
||||
|
||||
mockApi(config);
|
||||
const { app } = await import("../../src/scripts/app");
|
||||
config.preSetup?.(app);
|
||||
await app.setup();
|
||||
mockApi(config);
|
||||
const { app } = await import("../../src/scripts/app");
|
||||
config.preSetup?.(app);
|
||||
await app.setup();
|
||||
|
||||
// @ts-ignore
|
||||
return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
|
||||
// @ts-ignore
|
||||
return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,9 +53,9 @@ export async function start(config: StartConfig = {}): Promise<StartResult> {
|
||||
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
|
||||
*/
|
||||
export async function checkBeforeAndAfterReload(graph, cb) {
|
||||
await cb(false);
|
||||
await graph.reload();
|
||||
await cb(true);
|
||||
await cb(false);
|
||||
await graph.reload();
|
||||
await cb(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,35 +65,35 @@ export async function checkBeforeAndAfterReload(graph, cb) {
|
||||
* @returns { Record<string, import("./src/types/comfy").ComfyObjectInfo> }
|
||||
*/
|
||||
export function makeNodeDef(name, input, output = {}) {
|
||||
const nodeDef = {
|
||||
name,
|
||||
category: "test",
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
input: {
|
||||
required: {},
|
||||
},
|
||||
};
|
||||
for (const k in input) {
|
||||
nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
|
||||
}
|
||||
if (output instanceof Array) {
|
||||
output = output.reduce((p, c) => {
|
||||
p[c] = c;
|
||||
return p;
|
||||
}, {});
|
||||
}
|
||||
for (const k in output) {
|
||||
// @ts-ignore
|
||||
nodeDef.output.push(output[k]);
|
||||
// @ts-ignore
|
||||
nodeDef.output_name.push(k);
|
||||
// @ts-ignore
|
||||
nodeDef.output_is_list.push(false);
|
||||
}
|
||||
const nodeDef = {
|
||||
name,
|
||||
category: "test",
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
input: {
|
||||
required: {},
|
||||
},
|
||||
};
|
||||
for (const k in input) {
|
||||
nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
|
||||
}
|
||||
if (output instanceof Array) {
|
||||
output = output.reduce((p, c) => {
|
||||
p[c] = c;
|
||||
return p;
|
||||
}, {});
|
||||
}
|
||||
for (const k in output) {
|
||||
// @ts-ignore
|
||||
nodeDef.output.push(output[k]);
|
||||
// @ts-ignore
|
||||
nodeDef.output_name.push(k);
|
||||
// @ts-ignore
|
||||
nodeDef.output_is_list.push(false);
|
||||
}
|
||||
|
||||
return { [name]: nodeDef };
|
||||
return { [name]: nodeDef };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,9 +103,9 @@ export function makeNodeDef(name, input, output = {}) {
|
||||
* @returns { x is Exclude<T, null | undefined> }
|
||||
*/
|
||||
export function assertNotNullOrUndefined(x) {
|
||||
expect(x).not.toEqual(null);
|
||||
expect(x).not.toEqual(undefined);
|
||||
return true;
|
||||
expect(x).not.toEqual(null);
|
||||
expect(x).not.toEqual(undefined);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,32 +114,32 @@ export function assertNotNullOrUndefined(x) {
|
||||
* @param { ReturnType<Ez["graph"]>["graph"] } graph
|
||||
*/
|
||||
export function createDefaultWorkflow(ez, graph) {
|
||||
graph.clear();
|
||||
const ckpt = ez.CheckpointLoaderSimple();
|
||||
graph.clear();
|
||||
const ckpt = ez.CheckpointLoaderSimple();
|
||||
|
||||
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
|
||||
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
|
||||
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
|
||||
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
|
||||
|
||||
const empty = ez.EmptyLatentImage();
|
||||
const sampler = ez.KSampler(
|
||||
ckpt.outputs.MODEL,
|
||||
pos.outputs.CONDITIONING,
|
||||
neg.outputs.CONDITIONING,
|
||||
empty.outputs.LATENT
|
||||
);
|
||||
const empty = ez.EmptyLatentImage();
|
||||
const sampler = ez.KSampler(
|
||||
ckpt.outputs.MODEL,
|
||||
pos.outputs.CONDITIONING,
|
||||
neg.outputs.CONDITIONING,
|
||||
empty.outputs.LATENT
|
||||
);
|
||||
|
||||
const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
|
||||
const save = ez.SaveImage(decode.outputs.IMAGE);
|
||||
graph.arrange();
|
||||
const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
|
||||
const save = ez.SaveImage(decode.outputs.IMAGE);
|
||||
graph.arrange();
|
||||
|
||||
return { ckpt, pos, neg, empty, sampler, decode, save };
|
||||
return { ckpt, pos, neg, empty, sampler, decode, save };
|
||||
}
|
||||
|
||||
export async function getNodeDefs() {
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
return api.getNodeDefs();
|
||||
const { api } = await import("../../src/scripts/api");
|
||||
return api.getNodeDefs();
|
||||
}
|
||||
|
||||
export async function getNodeDef(nodeId) {
|
||||
return (await getNodeDefs())[nodeId];
|
||||
return (await getNodeDefs())[nodeId];
|
||||
}
|
||||
@@ -3,37 +3,37 @@ import path from "path";
|
||||
import { nop } from "../utils/nopProxy";
|
||||
|
||||
function forEachKey(cb) {
|
||||
for (const k of [
|
||||
"LiteGraph",
|
||||
"LGraph",
|
||||
"LLink",
|
||||
"LGraphNode",
|
||||
"LGraphGroup",
|
||||
"DragAndScale",
|
||||
"LGraphCanvas",
|
||||
"ContextMenu",
|
||||
]) {
|
||||
cb(k);
|
||||
}
|
||||
for (const k of [
|
||||
"LiteGraph",
|
||||
"LGraph",
|
||||
"LLink",
|
||||
"LGraphNode",
|
||||
"LGraphGroup",
|
||||
"DragAndScale",
|
||||
"LGraphCanvas",
|
||||
"ContextMenu",
|
||||
]) {
|
||||
cb(k);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
setup(ctx) {
|
||||
const lg = fs.readFileSync(path.resolve("./src/lib/litegraph.core.js"), "utf-8");
|
||||
const globalTemp = {};
|
||||
(function (console) {
|
||||
eval(lg);
|
||||
}).call(globalTemp, nop);
|
||||
setup(ctx) {
|
||||
const lg = fs.readFileSync(path.resolve("./src/lib/litegraph.core.js"), "utf-8");
|
||||
const globalTemp = {};
|
||||
(function (console) {
|
||||
eval(lg);
|
||||
}).call(globalTemp, nop);
|
||||
|
||||
forEachKey((k) => (ctx[k] = globalTemp[k]));
|
||||
const lg_ext = fs.readFileSync(path.resolve("./src/lib/litegraph.extensions.js"), "utf-8");
|
||||
eval(lg_ext);
|
||||
},
|
||||
forEachKey((k) => (ctx[k] = globalTemp[k]));
|
||||
const lg_ext = fs.readFileSync(path.resolve("./src/lib/litegraph.extensions.js"), "utf-8");
|
||||
eval(lg_ext);
|
||||
},
|
||||
|
||||
teardown(ctx) {
|
||||
forEachKey((k) => delete ctx[k]);
|
||||
teardown(ctx) {
|
||||
forEachKey((k) => delete ctx[k]);
|
||||
|
||||
// Clear document after each run
|
||||
document.getElementsByTagName("html")[0].innerHTML = "";
|
||||
}
|
||||
// Clear document after each run
|
||||
document.getElementsByTagName("html")[0].innerHTML = "";
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export const nop = new Proxy(function () {}, {
|
||||
get: () => nop,
|
||||
set: () => true,
|
||||
apply: () => nop,
|
||||
construct: () => nop,
|
||||
get: () => nop,
|
||||
set: () => true,
|
||||
apply: () => nop,
|
||||
construct: () => nop,
|
||||
});
|
||||
|
||||
@@ -3,22 +3,22 @@ import "../../src/scripts/api";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
function* walkSync(dir: string): Generator<string> {
|
||||
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
if (file.isDirectory()) {
|
||||
yield* walkSync(path.join(dir, file.name));
|
||||
} else {
|
||||
yield path.join(dir, file.name);
|
||||
}
|
||||
}
|
||||
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
if (file.isDirectory()) {
|
||||
yield* walkSync(path.join(dir, file.name));
|
||||
} else {
|
||||
yield path.join(dir, file.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface APIConfig {
|
||||
mockExtensions?: string[];
|
||||
mockNodeDefs?: Record<string, any>;
|
||||
settings?: Record<string, string>;
|
||||
userConfig?: { storage: "server" | "browser"; users?: Record<string, any>; migrated?: boolean };
|
||||
userData?: Record<string, any>;
|
||||
mockExtensions?: string[];
|
||||
mockNodeDefs?: Record<string, any>;
|
||||
settings?: Record<string, string>;
|
||||
userConfig?: { storage: "server" | "browser"; users?: Record<string, any>; migrated?: boolean };
|
||||
userData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,66 +27,66 @@ export interface APIConfig {
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* mockExtensions?: string[],
|
||||
* mockNodeDefs?: Record<string, ComfyObjectInfo>,
|
||||
* settings?: Record<string, string>
|
||||
* userConfig?: {storage: "server" | "browser", users?: Record<string, any>, migrated?: boolean },
|
||||
* userData?: Record<string, any>
|
||||
* mockExtensions?: string[],
|
||||
* mockNodeDefs?: Record<string, ComfyObjectInfo>,
|
||||
* settings?: Record<string, string>
|
||||
* userConfig?: {storage: "server" | "browser", users?: Record<string, any>, migrated?: boolean },
|
||||
* userData?: Record<string, any>
|
||||
* }} config
|
||||
*/
|
||||
export function mockApi(config: APIConfig = {}) {
|
||||
let { mockExtensions, mockNodeDefs, userConfig, settings, userData } = {
|
||||
settings: {},
|
||||
userData: {},
|
||||
...config,
|
||||
};
|
||||
if (!mockExtensions) {
|
||||
mockExtensions = Array.from(walkSync(path.resolve("./src/extensions/core")))
|
||||
.filter((x) => x.endsWith(".js"))
|
||||
.map((x) => path.relative(path.resolve("./src/"), x).replace(/\\/g, "/"));
|
||||
}
|
||||
if (!mockNodeDefs) {
|
||||
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./tests-ui/data/object_info.json")));
|
||||
}
|
||||
let { mockExtensions, mockNodeDefs, userConfig, settings, userData } = {
|
||||
settings: {},
|
||||
userData: {},
|
||||
...config,
|
||||
};
|
||||
if (!mockExtensions) {
|
||||
mockExtensions = Array.from(walkSync(path.resolve("./src/extensions/core")))
|
||||
.filter((x) => x.endsWith(".js"))
|
||||
.map((x) => path.relative(path.resolve("./src/"), x).replace(/\\/g, "/"));
|
||||
}
|
||||
if (!mockNodeDefs) {
|
||||
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./tests-ui/data/object_info.json")));
|
||||
}
|
||||
|
||||
const events = new EventTarget();
|
||||
const mockApi = {
|
||||
addEventListener: events.addEventListener.bind(events),
|
||||
removeEventListener: events.removeEventListener.bind(events),
|
||||
dispatchEvent: events.dispatchEvent.bind(events),
|
||||
getSystemStats: jest.fn(),
|
||||
getExtensions: jest.fn(() => mockExtensions),
|
||||
getNodeDefs: jest.fn(() => mockNodeDefs),
|
||||
init: jest.fn(),
|
||||
apiURL: jest.fn((x) => "src/" + x),
|
||||
fileURL: jest.fn((x) => "src/" + x),
|
||||
createUser: jest.fn((username) => {
|
||||
// @ts-ignore
|
||||
if(username in userConfig.users) {
|
||||
return { status: 400, json: () => "Duplicate" }
|
||||
}
|
||||
// @ts-ignore
|
||||
userConfig.users[username + "!"] = username;
|
||||
return { status: 200, json: () => username + "!" }
|
||||
}),
|
||||
getUserConfig: jest.fn(() => userConfig ?? { storage: "browser", migrated: false }),
|
||||
getSettings: jest.fn(() => settings),
|
||||
storeSettings: jest.fn((v) => Object.assign(settings, v)),
|
||||
getUserData: jest.fn((f) => {
|
||||
if (f in userData) {
|
||||
return { status: 200, json: () => userData[f] };
|
||||
} else {
|
||||
return { status: 404 };
|
||||
}
|
||||
}),
|
||||
storeUserData: jest.fn((file, data) => {
|
||||
userData[file] = data;
|
||||
}),
|
||||
listUserData: jest.fn(() => []),
|
||||
};
|
||||
jest.mock("../../src/scripts/api", () => ({
|
||||
get api() {
|
||||
return mockApi;
|
||||
},
|
||||
}));
|
||||
const events = new EventTarget();
|
||||
const mockApi = {
|
||||
addEventListener: events.addEventListener.bind(events),
|
||||
removeEventListener: events.removeEventListener.bind(events),
|
||||
dispatchEvent: events.dispatchEvent.bind(events),
|
||||
getSystemStats: jest.fn(),
|
||||
getExtensions: jest.fn(() => mockExtensions),
|
||||
getNodeDefs: jest.fn(() => mockNodeDefs),
|
||||
init: jest.fn(),
|
||||
apiURL: jest.fn((x) => "src/" + x),
|
||||
fileURL: jest.fn((x) => "src/" + x),
|
||||
createUser: jest.fn((username) => {
|
||||
// @ts-ignore
|
||||
if(username in userConfig.users) {
|
||||
return { status: 400, json: () => "Duplicate" }
|
||||
}
|
||||
// @ts-ignore
|
||||
userConfig.users[username + "!"] = username;
|
||||
return { status: 200, json: () => username + "!" }
|
||||
}),
|
||||
getUserConfig: jest.fn(() => userConfig ?? { storage: "browser", migrated: false }),
|
||||
getSettings: jest.fn(() => settings),
|
||||
storeSettings: jest.fn((v) => Object.assign(settings, v)),
|
||||
getUserData: jest.fn((f) => {
|
||||
if (f in userData) {
|
||||
return { status: 200, json: () => userData[f] };
|
||||
} else {
|
||||
return { status: 404 };
|
||||
}
|
||||
}),
|
||||
storeUserData: jest.fn((file, data) => {
|
||||
userData[file] = data;
|
||||
}),
|
||||
listUserData: jest.fn(() => []),
|
||||
};
|
||||
jest.mock("../../src/scripts/api", () => ({
|
||||
get api() {
|
||||
return mockApi;
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
182
vite.config.mts
182
vite.config.mts
@@ -7,115 +7,115 @@ dotenv.config();
|
||||
const IS_DEV = process.env.NODE_ENV === 'development';
|
||||
|
||||
interface ShimResult {
|
||||
code: string;
|
||||
exports: string[];
|
||||
code: string;
|
||||
exports: string[];
|
||||
}
|
||||
|
||||
function comfyAPIPlugin(): Plugin {
|
||||
return {
|
||||
name: 'comfy-api-plugin',
|
||||
transform(code: string, id: string) {
|
||||
if (IS_DEV)
|
||||
return null;
|
||||
return {
|
||||
name: 'comfy-api-plugin',
|
||||
transform(code: string, id: string) {
|
||||
if (IS_DEV)
|
||||
return null;
|
||||
|
||||
// TODO: Remove second condition after all js files are converted to ts
|
||||
if (id.endsWith('.ts') || (id.endsWith('.js') && id.includes("extensions/core"))) {
|
||||
const result = transformExports(code, id);
|
||||
// TODO: Remove second condition after all js files are converted to ts
|
||||
if (id.endsWith('.ts') || (id.endsWith('.js') && id.includes("extensions/core"))) {
|
||||
const result = transformExports(code, id);
|
||||
|
||||
if (result.exports.length > 0) {
|
||||
const projectRoot = process.cwd();
|
||||
const relativePath = path.relative(path.join(projectRoot, 'src'), id);
|
||||
const shimFileName = relativePath.replace(/\.ts$/, '.js');
|
||||
if (result.exports.length > 0) {
|
||||
const projectRoot = process.cwd();
|
||||
const relativePath = path.relative(path.join(projectRoot, 'src'), id);
|
||||
const shimFileName = relativePath.replace(/\.ts$/, '.js');
|
||||
|
||||
const shimComment = `// Shim for ${relativePath}\n`;
|
||||
const shimComment = `// Shim for ${relativePath}\n`;
|
||||
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: shimFileName,
|
||||
source: shimComment + result.exports.join(""),
|
||||
});
|
||||
}
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: shimFileName,
|
||||
source: shimComment + result.exports.join(""),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
code: result.code,
|
||||
map: null // If you're not modifying the source map, return null
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
code: result.code,
|
||||
map: null // If you're not modifying the source map, return null
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformExports(code: string, id: string): ShimResult {
|
||||
const moduleName = getModuleName(id);
|
||||
const exports: string[] = [];
|
||||
let newCode = code;
|
||||
const moduleName = getModuleName(id);
|
||||
const exports: string[] = [];
|
||||
let newCode = code;
|
||||
|
||||
// Regex to match different types of exports
|
||||
const regex = /export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g;
|
||||
let match;
|
||||
// Regex to match different types of exports
|
||||
const regex = /export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(code)) !== null) {
|
||||
const name = match[2];
|
||||
// All exports should be bind to the window object as new API endpoint.
|
||||
if (exports.length == 0) {
|
||||
newCode += `\nwindow.comfyAPI = window.comfyAPI || {};`;
|
||||
newCode += `\nwindow.comfyAPI.${moduleName} = window.comfyAPI.${moduleName} || {};`;
|
||||
}
|
||||
newCode += `\nwindow.comfyAPI.${moduleName}.${name} = ${name};`;
|
||||
exports.push(`export const ${name} = window.comfyAPI.${moduleName}.${name};\n`);
|
||||
}
|
||||
while ((match = regex.exec(code)) !== null) {
|
||||
const name = match[2];
|
||||
// All exports should be bind to the window object as new API endpoint.
|
||||
if (exports.length == 0) {
|
||||
newCode += `\nwindow.comfyAPI = window.comfyAPI || {};`;
|
||||
newCode += `\nwindow.comfyAPI.${moduleName} = window.comfyAPI.${moduleName} || {};`;
|
||||
}
|
||||
newCode += `\nwindow.comfyAPI.${moduleName}.${name} = ${name};`;
|
||||
exports.push(`export const ${name} = window.comfyAPI.${moduleName}.${name};\n`);
|
||||
}
|
||||
|
||||
return {
|
||||
code: newCode,
|
||||
exports,
|
||||
};
|
||||
return {
|
||||
code: newCode,
|
||||
exports,
|
||||
};
|
||||
}
|
||||
|
||||
function getModuleName(id: string): string {
|
||||
// Simple example to derive a module name from the file path
|
||||
const parts = id.split('/');
|
||||
const fileName = parts[parts.length - 1];
|
||||
return fileName.replace(/\.\w+$/, ''); // Remove file extension
|
||||
// Simple example to derive a module name from the file path
|
||||
const parts = id.split('/');
|
||||
const fileName = parts[parts.length - 1];
|
||||
return fileName.replace(/\.\w+$/, ''); // Remove file extension
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188',
|
||||
// Return empty array for extensions API as these modules
|
||||
// are not on vite's dev server.
|
||||
bypass: (req, res, options) => {
|
||||
if (req.url === '/api/extensions') {
|
||||
res.end(JSON.stringify([]));
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
'/ws': {
|
||||
target: 'ws://127.0.0.1:8188',
|
||||
ws: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
comfyAPIPlugin(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: "src/lib/*", dest: "lib/" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
// Disabling tree-shaking
|
||||
// Prevent vite remove unused exports
|
||||
treeshake: false
|
||||
}
|
||||
},
|
||||
define: {
|
||||
'__COMFYUI_FRONTEND_VERSION__': JSON.stringify(process.env.npm_package_version),
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188',
|
||||
// Return empty array for extensions API as these modules
|
||||
// are not on vite's dev server.
|
||||
bypass: (req, res, options) => {
|
||||
if (req.url === '/api/extensions') {
|
||||
res.end(JSON.stringify([]));
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
'/ws': {
|
||||
target: 'ws://127.0.0.1:8188',
|
||||
ws: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
comfyAPIPlugin(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: "src/lib/*", dest: "lib/" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
// Disabling tree-shaking
|
||||
// Prevent vite remove unused exports
|
||||
treeshake: false
|
||||
}
|
||||
},
|
||||
define: {
|
||||
'__COMFYUI_FRONTEND_VERSION__': JSON.stringify(process.env.npm_package_version),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user