|
|
|
|
| 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);
|
| }
|
| }
|
|
|