import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js" import { ComfyDialog, $el } from "../../scripts/ui.js"; import { ShareDialog, SUPPORTED_OUTPUT_NODE_TYPES, getPotentialOutputsAndOutputNodes, ShareDialogChooser, showOpenArtShareDialog, showShareDialog } from "./comfyui-share-common.js"; import { OpenArtShareDialog } from "./comfyui-share-openart.js"; import { CustomNodesInstaller } from "./custom-nodes-downloader.js"; import { AlternativesInstaller } from "./a1111-alter-downloader.js"; import { SnapshotManager } from "./snapshot.js"; import { ModelInstaller } from "./model-downloader.js"; import { manager_instance, setManagerInstance, install_via_git_url, rebootAPI } from "./common.js"; var docStyle = document.createElement('style'); docStyle.innerHTML = ` .cm-menu-container { column-gap: 20px; display: flex; flex-wrap: wrap; justify-content: center; } .cm-menu-column { display: flex; flex-direction: column; flex: 1 1 auto; } .cm-title { padding: 10px 10px 0 10p; background-color: black; text-align: center; height: 45px; } .cm-notice-board { width: 360px; height: 160px; overflow: auto; color: var(--input-text); border: 1px solid #ccc; padding: 10px; overflow-x: hidden; } .cm-conflicted-nodes-text { background-color: #CCCC55 !important; color: #AA3333 !important; font-size: 10px; border-radius: 5px; padding: 5px; } `; document.head.appendChild(docStyle); var update_comfyui_button = null; var fetch_updates_button = null; var update_all_button = null; var badge_mode = "none"; let share_option = 'all'; // copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts const style = ` #comfyworkflows-button { position: relative; overflow: hidden; } .pysssss-workflow-arrow-2 { position: absolute; top: 0; bottom: 0; right: 0; font-size: 12px; display: flex; align-items: center; width: 24px; justify-content: center; background: rgba(255,255,255,0.1); content: "▼"; } .pysssss-workflow-arrow-2:after { content: "▼"; } .pysssss-workflow-arrow-2:hover { filter: brightness(1.6); background-color: var(--comfy-menu-bg); } .pysssss-workflow-popup-2 ~ .litecontextmenu { transform: scale(1.3); } #comfyworkflows-button-menu { z-index: 10000000000 !important; } `; async function init_badge_mode() { api.fetchApi('/manager/badge_mode') .then(response => response.text()) .then(data => { badge_mode = data; }) } async function init_share_option() { api.fetchApi('/manager/share_option') .then(response => response.text()) .then(data => { share_option = data || 'all'; }); } async function init_notice(notice) { api.fetchApi('/manager/notice') .then(response => response.text()) .then(data => { notice.innerHTML = data; }) } await init_badge_mode(); await init_share_option(); async function fetchNicknames() { const response1 = await api.fetchApi(`/customnode/getmappings?mode=local`); const mappings = await response1.json(); let result = {}; for (let i in mappings) { let item = mappings[i]; var nickname; if (item[1].title) { nickname = item[1].title; } else { nickname = item[1].title_aux; } for (let j in item[0]) { result[item[0][j]] = nickname; } } return result; } let nicknames = await fetchNicknames(); async function updateComfyUI() { let prev_text = update_comfyui_button.innerText; update_comfyui_button.innerText = "Updating ComfyUI..."; update_comfyui_button.disabled = true; update_comfyui_button.style.backgroundColor = "gray"; try { const response = await api.fetchApi('/comfyui_manager/update_comfyui'); if (response.status == 400) { app.ui.dialog.show('Failed to update ComfyUI.'); app.ui.dialog.element.style.zIndex = 10010; return false; } if (response.status == 201) { app.ui.dialog.show('ComfyUI has been successfully updated.'); app.ui.dialog.element.style.zIndex = 10010; } else { app.ui.dialog.show('ComfyUI is already up to date with the latest version.'); app.ui.dialog.element.style.zIndex = 10010; } return true; } catch (exception) { app.ui.dialog.show(`Failed to update ComfyUI / ${exception}`); app.ui.dialog.element.style.zIndex = 10010; return false; } finally { update_comfyui_button.disabled = false; update_comfyui_button.innerText = prev_text; update_comfyui_button.style.backgroundColor = ""; } } async function fetchUpdates(update_check_checkbox) { let prev_text = fetch_updates_button.innerText; fetch_updates_button.innerText = "Fetching updates..."; fetch_updates_button.disabled = true; fetch_updates_button.style.backgroundColor = "gray"; try { var mode = "url"; if(manager_instance.local_mode_checkbox.checked) mode = "local"; const response = await api.fetchApi(`/customnode/fetch_updates?mode=${mode}`); if (response.status != 200 && response.status != 201) { app.ui.dialog.show('Failed to fetch updates.'); app.ui.dialog.element.style.zIndex = 10010; return false; } if (response.status == 201) { app.ui.dialog.show('There is an updated extension available.'); app.ui.dialog.element.style.zIndex = 10010; update_check_checkbox.checked = false; } else { app.ui.dialog.show('All extensions are already up-to-date with the latest versions.'); app.ui.dialog.element.style.zIndex = 10010; } return true; } catch (exception) { app.ui.dialog.show(`Failed to update custom nodes / ${exception}`); app.ui.dialog.element.style.zIndex = 10010; return false; } finally { fetch_updates_button.disabled = false; fetch_updates_button.innerText = prev_text; fetch_updates_button.style.backgroundColor = ""; } } async function updateAll(update_check_checkbox, manager_dialog) { let prev_text = update_all_button.innerText; update_all_button.innerText = "Updating all...(ComfyUI)"; update_all_button.disabled = true; update_all_button.style.backgroundColor = "gray"; try { var mode = "url"; if(manager_instance.local_mode_checkbox.checked) mode = "local"; update_all_button.innerText = "Updating all..."; const response1 = await api.fetchApi('/comfyui_manager/update_comfyui'); const response2 = await api.fetchApi(`/customnode/update_all?mode=${mode}`); if (response1.status != 200 && response2.status != 201) { app.ui.dialog.show('Failed to update ComfyUI or several extensions.

See terminal log.
'); app.ui.dialog.element.style.zIndex = 10010; return false; } if(response1.status == 201 || response2.status == 201) { app.ui.dialog.show("ComfyUI and all extensions have been updated to the latest version.
To apply the updated custom node, please ComfyUI. And refresh browser."); const rebootButton = document.getElementById('cm-reboot-button'); rebootButton.onclick = function() { if(rebootAPI()) { manager_dialog.close(); } }; app.ui.dialog.element.style.zIndex = 10010; } else { app.ui.dialog.show('ComfyUI and all extensions are already up-to-date with the latest versions.'); app.ui.dialog.element.style.zIndex = 10010; } return true; } catch (exception) { app.ui.dialog.show(`Failed to update ComfyUI or several extensions / ${exception}`); app.ui.dialog.element.style.zIndex = 10010; return false; } finally { update_all_button.disabled = false; update_all_button.innerText = prev_text; update_all_button.style.backgroundColor = ""; } } function newDOMTokenList(initialTokens) { const tmp = document.createElement(`div`); const classList = tmp.classList; if (initialTokens) { initialTokens.forEach(token => { classList.add(token); }); } return classList; } /** * Check whether the node is a potential output node (img, gif or video output) */ const isOutputNode = (node) => { return [ "VHS_VideoCombine", "PreviewImage", "SaveImage", "ADE_AnimateDiffCombine", "SaveAnimatedWEBP", ].includes(node.type); } // ----------- class ManagerMenuDialog extends ComfyDialog { local_mode_checkbox = null; createControlsMid() { let self = this; update_comfyui_button = $el("button", { type: "button", textContent: "Update ComfyUI", onclick: () => updateComfyUI() }); fetch_updates_button = $el("button", { type: "button", textContent: "Fetch Updates", onclick: () => fetchUpdates(this.update_check_checkbox) }); update_all_button = $el("button", { type: "button", textContent: "Update All", onclick: () => updateAll(this.update_check_checkbox, self) }); const res = [ $el("button", { type: "button", textContent: "Install Custom Nodes", onclick: () => { if(!CustomNodesInstaller.instance) CustomNodesInstaller.instance = new CustomNodesInstaller(app, self); CustomNodesInstaller.instance.show(false); } }), $el("button", { type: "button", textContent: "Install Missing Custom Nodes", onclick: () => { if(!CustomNodesInstaller.instance) CustomNodesInstaller.instance = new CustomNodesInstaller(app, self); CustomNodesInstaller.instance.show(true); } }), $el("button", { type: "button", textContent: "Install Models", onclick: () => { if(!ModelInstaller.instance) ModelInstaller.instance = new ModelInstaller(app, self); ModelInstaller.instance.show(); } }), $el("br", {}, []), update_all_button, update_comfyui_button, fetch_updates_button, $el("br", {}, []), $el("button", { type: "button", textContent: "Alternatives of A1111", onclick: () => { if(!AlternativesInstaller.instance) AlternativesInstaller.instance = new AlternativesInstaller(app, self); AlternativesInstaller.instance.show(); } }), $el("br", {}, []), ]; return res; } createControlsLeft() { let self = this; this.local_mode_checkbox = $el("input",{type:'checkbox', id:"use_local_db"},[]) const checkbox_text = $el("label",{for: "use_local_db"},[" Use local DB"]) checkbox_text.style.color = "var(--fg-color)"; checkbox_text.style.cursor = "pointer"; checkbox_text.style.marginRight = "10px"; this.update_check_checkbox = $el("input",{type:'checkbox', id:"skip_update_check"},[]) const uc_checkbox_text = $el("label",{for:"skip_update_check"},[" Skip update check"]) uc_checkbox_text.style.color = "var(--fg-color)"; uc_checkbox_text.style.cursor = "pointer"; this.update_check_checkbox.checked = true; // preview method let preview_combo = document.createElement("select"); preview_combo.style.cursor = "pointer"; preview_combo.appendChild($el('option', { value: 'auto', text: 'Preview method: Auto' }, [])); preview_combo.appendChild($el('option', { value: 'taesd', text: 'Preview method: TAESD (slow)' }, [])); preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Preview method: Latent2RGB (fast)' }, [])); preview_combo.appendChild($el('option', { value: 'none', text: 'Preview method: None (very fast)' }, [])); api.fetchApi('/manager/preview_method') .then(response => response.text()) .then(data => { preview_combo.value = data; }) preview_combo.addEventListener('change', function (event) { api.fetchApi(`/manager/preview_method?value=${event.target.value}`); }); // nickname let badge_combo = document.createElement("select"); badge_combo.style.cursor = "pointer"; badge_combo.appendChild($el('option', { value: 'none', text: 'Badge: None' }, [])); badge_combo.appendChild($el('option', { value: 'nick', text: 'Badge: Nickname' }, [])); badge_combo.appendChild($el('option', { value: 'nick_hide', text: 'Badge: Nickname (hide built-in)' }, [])); badge_combo.appendChild($el('option', { value: 'id_nick', text: 'Badge: #ID Nickname' }, [])); badge_combo.appendChild($el('option', { value: 'id_nick_hide', text: 'Badge: #ID Nickname (hide built-in)' }, [])); api.fetchApi('/manager/badge_mode') .then(response => response.text()) .then(data => { badge_combo.value = data; badge_mode = data; }); badge_combo.addEventListener('change', function (event) { api.fetchApi(`/manager/badge_mode?value=${event.target.value}`); badge_mode = event.target.value; app.graph.setDirtyCanvas(true); }); // channel let channel_combo = document.createElement("select"); channel_combo.style.cursor = "pointer"; api.fetchApi('/manager/channel_url_list') .then(response => response.json()) .then(async data => { try { let urls = data.list; for (let i in urls) { if (urls[i] != '') { let name_url = urls[i].split('::'); channel_combo.appendChild($el('option', { value: name_url[0], text: `Channel: ${name_url[0]}` }, [])); } } channel_combo.addEventListener('change', function (event) { api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`); }); channel_combo.value = data.selected; } catch (exception) { } }); // share let share_combo = document.createElement("select"); share_combo.style.cursor = "pointer"; const share_options = [ ['none', 'None'], ['openart', 'OpenArt AI'], ['matrix', 'Matrix Server'], ['comfyworkflows', 'ComfyWorkflows'], ['all', 'All'], ]; for (const option of share_options) { share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, [])); } api.fetchApi('/manager/share_option') .then(response => response.text()) .then(data => { share_combo.value = data || 'all'; share_option = data || 'all'; }); share_combo.addEventListener('change', function (event) { const value = event.target.value; share_option = value; api.fetchApi(`/manager/share_option?value=${value}`); const shareButton = document.getElementById("shareButton"); if (value === 'none') { shareButton.style.display = "none"; } else { shareButton.style.display = "inline-block"; } }); return [ $el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]), $el("br", {}, []), preview_combo, badge_combo, channel_combo, share_combo, $el("hr", {}, []), $el("center", {}, ["!! EXPERIMENTAL !!"]), $el("br", {}, []), $el("button", { type: "button", textContent: "Snapshot Manager", onclick: () => { if(!SnapshotManager.instance) SnapshotManager.instance = new SnapshotManager(app, self); SnapshotManager.instance.show(); } }), $el("button", { type: "button", textContent: "Install via Git URL", onclick: () => { var url = prompt("Please enter the URL of the Git repository to install", ""); if (url !== null) { install_via_git_url(url); } } }), ]; } createControlsRight() { const elts = [ $el("button", { type: "button", textContent: "ComfyUI Community Manual", onclick: () => { window.open("https://blenderneko.github.io/ComfyUI-docs/", "comfyui-community-manual"); } }), $el("button", { id: 'comfyworkflows-button', type: "button", textContent: "ComfyUI Workflow Gallery", onclick: () => { window.open("https://comfyworkflows.com/", "comfyui-workflow-gallery"); } }, [ $el("div.pysssss-workflow-arrow-2", { id: `comfyworkflows-button-arrow`, // parent: document.getElementById(`comfyworkflows-button`), onclick: (e) => { e.preventDefault(); e.stopPropagation(); LiteGraph.closeAllContextMenus(); const menu = new LiteGraph.ContextMenu( [ { title: "Share your art", callback: () => { this.close(); if (!ShareDialog.instance) { ShareDialog.instance = new ShareDialog(); } app.graphToPrompt().then(prompt => { // console.log({ prompt }) return app.graph._nodes; }).then(nodes => { // console.log({ nodes }); const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes); if (potential_outputs.length === 0) { if (potential_output_nodes.length === 0) { // todo: add support for other output node types (animatediff combine, etc.) const supported_nodes_string = SUPPORTED_OUTPUT_NODE_TYPES.join(", "); alert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`); } else { alert("To share this, first run a prompt. Once it's done, click 'Share'."); } return; } ShareDialog.instance.show({ potential_outputs, potential_output_nodes }); }); }, }, { title: "Close", callback: () => { this.close(); }, } ], { event: e, scale: 1.3, }, window ); // set the id so that we can override the context menu's z-index to be above the comfyui manager menu menu.root.id = "comfyworkflows-button-menu"; menu.root.classList.add("pysssss-workflow-popup-2"); }, }) ]), $el("button", { type: "button", textContent: "ComfyUI Nodes Info", onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); } }), $el("br", {}, []), ]; var textarea = document.createElement("div"); textarea.className = "cm-notice-board"; elts.push(textarea); init_notice(textarea); return elts; } constructor() { super(); const close_button = $el("button", { type: "button", textContent: "Close", onclick: () => this.close() }); close_button.style.position = "absolute"; close_button.style.bottom = "20px"; close_button.style.width = "calc(100% - 60px)"; const content = $el("div.comfy-modal-content", [ $el("tr.cm-title", {width:"100%"}, [ $el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])] ), $el("br", {}, []), $el("div.cm-menu-container", [ $el("div.cm-menu-column", [...this.createControlsLeft()]), $el("div.cm-menu-column", [...this.createControlsMid()]), $el("div.cm-menu-column", [...this.createControlsRight()]) ]), close_button, ] ); content.style.width = '100%'; content.style.height = '100%'; this.element = $el("div.comfy-modal", { parent: document.body }, [ content ]); this.element.style.width = '1000px'; this.element.style.height = '420px'; this.element.style.zIndex = 10000; } show() { this.element.style.display = "block"; } } app.registerExtension({ name: "Comfy.ManagerMenu", init() { $el("style", { textContent: style, parent: document.head, }); }, async setup() { const menu = document.querySelector(".comfy-menu"); const separator = document.createElement("hr"); separator.style.margin = "20px 0"; separator.style.width = "100%"; menu.append(separator); const managerButton = document.createElement("button"); managerButton.textContent = "Manager"; managerButton.onclick = () => { if(!manager_instance) setManagerInstance(new ManagerMenuDialog()); manager_instance.show(); } menu.append(managerButton); const shareButton = document.createElement("button"); shareButton.id = "shareButton"; shareButton.textContent = "Share"; shareButton.onclick = () => { if (share_option === 'openart') { showOpenArtShareDialog(); return; } else if (share_option === 'matrix' || share_option === 'comfyworkflows') { showShareDialog(share_option); return; } if(!ShareDialogChooser.instance) { ShareDialogChooser.instance = new ShareDialogChooser(); } ShareDialogChooser.instance.show(); } // make the background color a gradient of blue to green shareButton.style.background = "linear-gradient(90deg, #00C9FF 0%, #92FE9D 100%)"; shareButton.style.color = "black"; // Load share option from local storage to determine whether to show // the share button. const shouldShowShareButton = share_option !== 'none'; shareButton.style.display = shouldShowShareButton ? "inline-block" : "none"; menu.append(shareButton); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { const onDrawForeground = nodeType.prototype.onDrawForeground; nodeType.prototype.onDrawForeground = function (ctx) { const r = onDrawForeground?.apply?.(this, arguments); if (!this.flags.collapsed && badge_mode != 'none' && nodeType.title_mode != LiteGraph.NO_TITLE) { let text = ""; if (badge_mode.startsWith('id_nick')) text = `#${this.id} `; if (nicknames[nodeData.name.trim()]) { let nick = nicknames[nodeData.name.trim()]; if (nick == 'ComfyUI') { if(badge_mode.endsWith('hide')) { nick = ""; } else { nick = "🦊" } } if (nick.length > 25) { text += nick.substring(0, 23) + ".."; } else { text += nick; } } if (text != "") { let fgColor = "white"; let bgColor = "#0F1F0F"; let visible = true; ctx.save(); ctx.font = "12px sans-serif"; const sz = ctx.measureText(text); ctx.fillStyle = bgColor; ctx.beginPath(); ctx.roundRect(this.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); ctx.fill(); ctx.fillStyle = fgColor; ctx.fillText(text, this.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); ctx.restore(); } } return r; }; this._addExtraNodeContextMenu(nodeType, app); }, async loadedGraphNode(node, app) { if (node.has_errors) { const onDrawForeground = node.onDrawForeground; node.onDrawForeground = function (ctx) { const r = onDrawForeground?.apply?.(this, arguments); if (!this.flags.collapsed && badge_mode != 'none') { let text = ""; if (badge_mode.startsWith('id_nick')) text = `#${this.id} `; if (nicknames[node.type.trim()]) { let nick = nicknames[node.type.trim()]; if (nick == 'ComfyUI') { if(badge_mode.endsWith('hide')) { nick = ""; } else { nick = "🦊" } } if (nick.length > 25) { text += nick.substring(0, 23) + ".."; } else { text += nick; } } if (text != "") { let fgColor = "white"; let bgColor = "#0F1F0F"; let visible = true; ctx.save(); ctx.font = "12px sans-serif"; const sz = ctx.measureText(text); ctx.fillStyle = bgColor; ctx.beginPath(); ctx.roundRect(this.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); ctx.fill(); ctx.fillStyle = fgColor; ctx.fillText(text, this.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); ctx.restore(); ctx.save(); ctx.font = "bold 14px sans-serif"; const sz2 = ctx.measureText(node.type); ctx.fillStyle = 'white'; ctx.fillText(node.type, this.size[0] / 2 - sz2.width / 2, this.size[1] / 2); ctx.restore(); } } return r; }; } }, _addExtraNodeContextMenu(node, app) { const origGetExtraMenuOptions = node.prototype.getExtraMenuOptions; node.prototype.getExtraMenuOptions = function (_, options) { origGetExtraMenuOptions?.apply?.(this, arguments); if (isOutputNode(node)) { const { potential_outputs } = getPotentialOutputsAndOutputNodes([this]); const hasOutput = potential_outputs.length > 0; // Check if the previous menu option is `null`. If it's not, // then we need to add a `null` as a separator. if (options[options.length - 1] !== null) { options.push(null); } options.push({ content: "🏞️ Share Output", disabled: !hasOutput, callback: (obj) => { if (!ShareDialog.instance) { ShareDialog.instance = new ShareDialog(); } const shareButton = document.getElementById("shareButton"); if (shareButton) { const currentNode = this; if (!OpenArtShareDialog.instance) { OpenArtShareDialog.instance = new OpenArtShareDialog(); } OpenArtShareDialog.instance.selectedNodeId = currentNode.id; if (!ShareDialog.instance) { ShareDialog.instance = new ShareDialog(share_option); } ShareDialog.instance.selectedNodeId = currentNode.id; shareButton.click(); } } }, null); } } }, });