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