| |
|
| |
|
| | 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;
|
| |
|
| | |
| | |
| |
|
| | 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) {
|
| |
|
| | if (this.position !== "Top") {
|
| | this.updatePosition("Top");
|
| | }
|
| | } else if (this.position != this.menuPositionSetting.value) {
|
| |
|
| | this.updatePosition(this.menuPositionSetting.value);
|
| | }
|
| |
|
| |
|
| | 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);
|
| |
|
| | if (idx < this.#sizeBreaks.length - 1) {
|
| | idx++;
|
| | }
|
| | }
|
| |
|
| | this.updateSizeBreak(idx, currIdx, direction);
|
| | }
|
| |
|
| | calculateInnerSize(idx) {
|
| |
|
| | clearTimeout(this.#cacheTimeout);
|
| | if (this.#cachedInnerSize) {
|
| |
|
| | 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;
|
| | if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue;
|
| | innerSize += c.clientWidth;
|
| | count++;
|
| | }
|
| | innerSize += 8 * count;
|
| | this.#cachedInnerSize = innerSize;
|
| | this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
| | }
|
| | return this.#cachedInnerSize;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | 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;
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | 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);
|
| | }
|
| | }
|
| |
|