Spaces:
Sleeping
Sleeping
| // @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); | |
| } | |
| } | |