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