Spaces:
Sleeping
Sleeping
import { api } from "../../scripts/api.js"; | |
import { app } from "../../scripts/app.js"; | |
import { $el, ComfyDialog } from "../../scripts/ui.js"; | |
import { | |
SUPPORTED_OUTPUT_NODE_TYPES, | |
ShareDialog, | |
ShareDialogChooser, | |
getPotentialOutputsAndOutputNodes, | |
showOpenArtShareDialog, | |
showShareDialog, | |
showYouMLShareDialog | |
} from "./comfyui-share-common.js"; | |
import { OpenArtShareDialog } from "./comfyui-share-openart.js"; | |
import { free_models, install_pip, install_via_git_url, manager_instance, rebootAPI, setManagerInstance, show_message } from "./common.js"; | |
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js"; | |
import { CustomNodesManager } from "./custom-nodes-manager.js"; | |
import { ModelManager } from "./model-manager.js"; | |
import { set_double_click_policy } from "./node_fixer.js"; | |
import { SnapshotManager } from "./snapshot.js"; | |
var docStyle = document.createElement('style'); | |
docStyle.innerHTML = ` | |
.comfy-toast { | |
position: fixed; | |
bottom: 20px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(0, 0, 0, 0.7); | |
color: white; | |
padding: 10px 20px; | |
border-radius: 5px; | |
z-index: 1000; | |
transition: opacity 0.5s; | |
} | |
.comfy-toast-fadeout { | |
opacity: 0; | |
} | |
#cm-manager-dialog { | |
width: 1000px; | |
height: 520px; | |
box-sizing: content-box; | |
z-index: 10000; | |
overflow-y: auto; | |
} | |
.cb-widget { | |
width: 400px; | |
height: 25px; | |
box-sizing: border-box; | |
z-index: 10000; | |
margin-top: 10px; | |
margin-bottom: 5px; | |
} | |
.cb-widget-input { | |
width: 305px; | |
height: 25px; | |
box-sizing: border-box; | |
} | |
.cb-widget-input:disabled { | |
background-color: #444444; | |
color: white; | |
} | |
.cb-widget-input-label { | |
width: 90px; | |
height: 25px; | |
box-sizing: border-box; | |
color: white; | |
text-align: right; | |
display: inline-block; | |
margin-right: 5px; | |
} | |
.cm-menu-container { | |
column-gap: 20px; | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
box-sizing: content-box; | |
} | |
.cm-menu-column { | |
display: flex; | |
flex-direction: column; | |
flex: 1 1 auto; | |
width: 300px; | |
box-sizing: content-box; | |
} | |
.cm-title { | |
background-color: black; | |
text-align: center; | |
height: 40px; | |
width: calc(100% - 10px); | |
font-weight: bold; | |
justify-content: center; | |
align-content: center; | |
vertical-align: middle; | |
} | |
#cm-channel-badge { | |
color: white; | |
background-color: #AA0000; | |
width: 220px; | |
height: 23px; | |
font-size: 13px; | |
border-radius: 5px; | |
left: 5px; | |
top: 5px; | |
align-content: center; | |
justify-content: center; | |
text-align: center; | |
font-weight: bold; | |
float: left; | |
vertical-align: middle; | |
position: relative; | |
} | |
#custom-nodes-grid a { | |
color: #5555FF; | |
font-weight: bold; | |
text-decoration: none; | |
} | |
#custom-nodes-grid a:hover { | |
color: #7777FF; | |
text-decoration: underline; | |
} | |
#external-models-grid a { | |
color: #5555FF; | |
font-weight: bold; | |
text-decoration: none; | |
} | |
#external-models-grid a:hover { | |
color: #7777FF; | |
text-decoration: underline; | |
} | |
#alternatives-grid a { | |
color: #5555FF; | |
font-weight: bold; | |
text-decoration: none; | |
} | |
#alternatives-grid a:hover { | |
color: #7777FF; | |
text-decoration: underline; | |
} | |
.cm-notice-board { | |
width: 290px; | |
height: 270px; | |
overflow: auto; | |
color: var(--input-text); | |
border: 1px solid var(--descrip-text); | |
padding: 5px 10px; | |
overflow-x: hidden; | |
box-sizing: content-box; | |
} | |
.cm-notice-board > ul { | |
display: block; | |
list-style-type: disc; | |
margin-block-start: 1em; | |
margin-block-end: 1em; | |
margin-inline-start: 0px; | |
margin-inline-end: 0px; | |
padding-inline-start: 40px; | |
} | |
.cm-conflicted-nodes-text { | |
background-color: #CCCC55 !important; | |
color: #AA3333 !important; | |
font-size: 10px; | |
border-radius: 5px; | |
padding: 10px; | |
} | |
.cm-warn-note { | |
background-color: #101010 !important; | |
color: #FF3800 !important; | |
font-size: 13px; | |
border-radius: 5px; | |
padding: 10px; | |
overflow-x: hidden; | |
overflow: auto; | |
} | |
.cm-info-note { | |
background-color: #101010 !important; | |
color: #FF3800 !important; | |
font-size: 13px; | |
border-radius: 5px; | |
padding: 10px; | |
overflow-x: hidden; | |
overflow: auto; | |
} | |
`; | |
function is_legacy_front() { | |
let compareVersion = '1.2.49'; | |
try { | |
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__']; | |
if (typeof frontendVersion !== 'string') { | |
return false; | |
} | |
function parseVersion(versionString) { | |
const parts = versionString.split('.').map(Number); | |
return parts.length === 3 && parts.every(part => !isNaN(part)) ? parts : null; | |
} | |
const currentVersion = parseVersion(frontendVersion); | |
const comparisonVersion = parseVersion(compareVersion); | |
if (!currentVersion || !comparisonVersion) { | |
return false; | |
} | |
for (let i = 0; i < 3; i++) { | |
if (currentVersion[i] > comparisonVersion[i]) { | |
return false; | |
} else if (currentVersion[i] < comparisonVersion[i]) { | |
return true; | |
} | |
} | |
return false; | |
} catch { | |
return true; | |
} | |
} | |
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 = ` | |
#workflowgallery-button { | |
width: 310px; | |
height: 27px; | |
padding: 0px !important; | |
position: relative; | |
overflow: hidden; | |
font-size: 17px !important; | |
} | |
#cm-nodeinfo-button { | |
width: 310px; | |
height: 27px; | |
padding: 0px !important; | |
position: relative; | |
overflow: hidden; | |
font-size: 17px !important; | |
} | |
#cm-manual-button { | |
width: 310px; | |
height: 27px; | |
position: relative; | |
overflow: hidden; | |
} | |
.cm-button { | |
width: 310px; | |
height: 30px; | |
position: relative; | |
overflow: hidden; | |
font-size: 17px !important; | |
} | |
.cm-button-red { | |
width: 310px; | |
height: 30px; | |
position: relative; | |
overflow: hidden; | |
font-size: 17px !important; | |
background-color: #500000 !important; | |
color: white !important; | |
} | |
.cm-experimental-button { | |
width: 290px; | |
height: 30px; | |
position: relative; | |
overflow: hidden; | |
font-size: 17px !important; | |
} | |
.cm-experimental { | |
width: 310px; | |
border: 1px solid #555; | |
border-radius: 5px; | |
padding: 10px; | |
align-items: center; | |
text-align: center; | |
justify-content: center; | |
box-sizing: border-box; | |
} | |
.cm-experimental-legend { | |
margin-top: -20px; | |
margin-left: 50%; | |
width:auto; | |
height:20px; | |
font-size: 13px; | |
font-weight: bold; | |
background-color: #990000; | |
color: #CCFFFF; | |
border-radius: 5px; | |
text-align: center; | |
transform: translateX(-50%); | |
display: block; | |
} | |
.cm-menu-combo { | |
cursor: pointer; | |
width: 310px; | |
box-sizing: border-box; | |
} | |
.cm-small-button { | |
width: 120px; | |
height: 30px; | |
position: relative; | |
overflow: hidden; | |
box-sizing: border-box; | |
font-size: 17px !important; | |
} | |
#cm-install-customnodes-button { | |
width: 200px; | |
height: 30px; | |
position: relative; | |
overflow: hidden; | |
box-sizing: border-box; | |
font-size: 17px !important; | |
} | |
.cm-search-filter { | |
width: 200px; | |
height: 30px !important; | |
position: relative; | |
overflow: hidden; | |
box-sizing: border-box; | |
} | |
.cb-node-label { | |
width: 400px; | |
height:28px; | |
color: black; | |
background-color: #777777; | |
font-size: 18px; | |
text-align: center; | |
font-weight: bold; | |
} | |
#cm-close-button { | |
width: calc(100% - 65px); | |
bottom: 10px; | |
position: absolute; | |
overflow: hidden; | |
} | |
#cm-save-button { | |
width: calc(100% - 65px); | |
bottom:40px; | |
position: absolute; | |
overflow: hidden; | |
} | |
#cm-save-button:disabled { | |
background-color: #444444; | |
} | |
.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); | |
} | |
#workflowgallery-button-menu { | |
z-index: 10000000000 !important; | |
} | |
#cm-manual-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=nickname`); | |
const mappings = await response1.json(); | |
let result = {}; | |
let nickname_patterns = []; | |
for (let i in mappings) { | |
let item = mappings[i]; | |
var nickname; | |
if (item[1].nickname) { | |
nickname = item[1].nickname; | |
} | |
else 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; | |
} | |
if(item[1].nodename_pattern) { | |
nickname_patterns.push([item[1].nodename_pattern, nickname]); | |
} | |
} | |
return [result, nickname_patterns]; | |
} | |
const [nicknames, nickname_patterns] = await fetchNicknames(); | |
function getNickname(node, nodename) { | |
if(node.nickname) { | |
return node.nickname; | |
} | |
else { | |
if (nicknames[nodename]) { | |
node.nickname = nicknames[nodename]; | |
} | |
else if(node.getInnerNodes) { | |
let pure_name = getPureName(node); | |
let groupNode = app.graph.extra?.groupNodes?.[pure_name]; | |
if(groupNode) { | |
let packname = groupNode.packname; | |
node.nickname = packname; | |
} | |
return node.nickname; | |
} | |
else { | |
for(let i in nickname_patterns) { | |
let item = nickname_patterns[i]; | |
if(nodename.match(item[0])) { | |
node.nickname = item[1]; | |
} | |
} | |
} | |
return node.nickname; | |
} | |
} | |
function drawBadge(node, orig, restArgs) { | |
let ctx = restArgs[0]; | |
const r = orig?.apply?.(node, restArgs); | |
if (!node.flags.collapsed && badge_mode != 'none' && node.constructor.title_mode != LiteGraph.NO_TITLE) { | |
let text = ""; | |
if (badge_mode.startsWith('id_nick')) | |
text = `#${node.id} `; | |
let nick = node.getNickname(); | |
if (nick) { | |
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(node.size[0] - sz.width - 12, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5); | |
ctx.fill(); | |
ctx.fillStyle = fgColor; | |
ctx.fillText(text, node.size[0] - sz.width - 6, -LiteGraph.NODE_TITLE_HEIGHT - 6); | |
ctx.restore(); | |
if (node.has_errors) { | |
ctx.save(); | |
ctx.font = "bold 14px sans-serif"; | |
const sz2 = ctx.measureText(node.type); | |
ctx.fillStyle = 'white'; | |
ctx.fillText(node.type, node.size[0] / 2 - sz2.width / 2, node.size[1] / 2); | |
ctx.restore(); | |
} | |
} | |
} | |
return r; | |
} | |
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) { | |
show_message('Failed to update ComfyUI.'); | |
return false; | |
} | |
if (response.status == 201) { | |
show_message('ComfyUI has been successfully updated.'); | |
} | |
else { | |
show_message('ComfyUI is already up to date with the latest version.'); | |
} | |
return true; | |
} | |
catch (exception) { | |
show_message(`Failed to update ComfyUI / ${exception}`); | |
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 = manager_instance.datasrc_combo.value; | |
const response = await api.fetchApi(`/customnode/fetch_updates?mode=${mode}`); | |
if (response.status != 200 && response.status != 201) { | |
show_message('Failed to fetch updates.'); | |
return false; | |
} | |
if (response.status == 201) { | |
show_message("There is an updated extension available.<BR><BR><P><B>NOTE:<BR>Fetch Updates is not an update.<BR>Please update from <button id='cm-install-customnodes-button'>Install Custom Nodes</button> </B></P>"); | |
const button = document.getElementById('cm-install-customnodes-button'); | |
button.addEventListener("click", | |
async function() { | |
app.ui.dialog.close(); | |
if(!CustomNodesManager.instance) { | |
CustomNodesManager.instance = new CustomNodesManager(app, self); | |
} | |
await CustomNodesManager.instance.show(CustomNodesManager.ShowMode.UPDATE); | |
} | |
); | |
update_check_checkbox.checked = false; | |
} | |
else { | |
show_message('All extensions are already up-to-date with the latest versions.'); | |
} | |
return true; | |
} | |
catch (exception) { | |
show_message(`Failed to update custom nodes / ${exception}`); | |
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 = manager_instance.datasrc_combo.value; | |
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 (response2.status == 403) { | |
show_message('This action is not allowed with this security level configuration.'); | |
return false; | |
} | |
if (response1.status == 400 || response2.status == 400) { | |
show_message('Failed to update ComfyUI or several extensions.<BR><BR>See terminal log.<BR>'); | |
return false; | |
} | |
if(response1.status == 201 || response2.status == 201) { | |
const update_info = await response2.json(); | |
let failed_list = ""; | |
if(update_info.failed.length > 0) { | |
failed_list = "<BR>FAILED: "+update_info.failed.join(", "); | |
} | |
let updated_list = ""; | |
if(update_info.updated.length > 0) { | |
updated_list = "<BR>UPDATED: "+update_info.updated.join(", "); | |
} | |
show_message( | |
"ComfyUI and all extensions have been updated to the latest version.<BR>To apply the updated custom node, please <button class='cm-small-button' id='cm-reboot-button5'>RESTART</button> ComfyUI. And refresh browser.<BR>" | |
+failed_list | |
+updated_list | |
); | |
const rebootButton = document.getElementById('cm-reboot-button5'); | |
rebootButton.addEventListener("click", | |
function() { | |
if(rebootAPI()) { | |
manager_dialog.close(); | |
} | |
}); | |
} | |
else { | |
show_message('ComfyUI and all extensions are already up-to-date with the latest versions.'); | |
} | |
return true; | |
} | |
catch (exception) { | |
show_message(`Failed to update ComfyUI or several extensions / ${exception}`); | |
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 SUPPORTED_OUTPUT_NODE_TYPES.includes(node.type); | |
} | |
// ----------- | |
class ManagerMenuDialog extends ComfyDialog { | |
createControlsMid() { | |
let self = this; | |
update_comfyui_button = | |
$el("button.cm-button", { | |
type: "button", | |
textContent: "Update ComfyUI", | |
onclick: | |
() => updateComfyUI() | |
}); | |
fetch_updates_button = | |
$el("button.cm-button", { | |
type: "button", | |
textContent: "Fetch Updates", | |
onclick: | |
() => fetchUpdates(this.update_check_checkbox) | |
}); | |
update_all_button = | |
$el("button.cm-button", { | |
type: "button", | |
textContent: "Update All", | |
onclick: | |
() => updateAll(this.update_check_checkbox, self) | |
}); | |
const res = | |
[ | |
$el("button.cm-button", { | |
type: "button", | |
textContent: "Custom Nodes Manager", | |
onclick: | |
() => { | |
if(!CustomNodesManager.instance) { | |
CustomNodesManager.instance = new CustomNodesManager(app, self); | |
} | |
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.NORMAL); | |
} | |
}), | |
$el("button.cm-button", { | |
type: "button", | |
textContent: "Install Missing Custom Nodes", | |
onclick: | |
() => { | |
if(!CustomNodesManager.instance) { | |
CustomNodesManager.instance = new CustomNodesManager(app, self); | |
} | |
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.MISSING); | |
} | |
}), | |
$el("button.cm-button", { | |
type: "button", | |
textContent: "Model Manager", | |
onclick: | |
() => { | |
if(!ModelManager.instance) { | |
ModelManager.instance = new ModelManager(app, self); | |
} | |
ModelManager.instance.show(); | |
} | |
}), | |
$el("button.cm-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, self); | |
} | |
} | |
}), | |
$el("br", {}, []), | |
update_all_button, | |
update_comfyui_button, | |
fetch_updates_button, | |
$el("br", {}, []), | |
$el("button.cm-button", { | |
type: "button", | |
textContent: "Alternatives of A1111", | |
onclick: | |
() => { | |
if(!CustomNodesManager.instance) { | |
CustomNodesManager.instance = new CustomNodesManager(app, self); | |
} | |
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.ALTERNATIVES); | |
} | |
}), | |
$el("br", {}, []), | |
$el("button.cm-button-red", { | |
type: "button", | |
textContent: "Restart", | |
onclick: () => rebootAPI() | |
}), | |
]; | |
return res; | |
} | |
createControlsLeft() { | |
let self = this; | |
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; | |
// db mode | |
this.datasrc_combo = document.createElement("select"); | |
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened."); | |
this.datasrc_combo.className = "cm-menu-combo"; | |
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Channel (1day cache)' }, [])); | |
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, [])); | |
this.datasrc_combo.appendChild($el('option', { value: 'url', text: 'DB: Channel (remote)' }, [])); | |
// preview method | |
let preview_combo = document.createElement("select"); | |
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process."); | |
preview_combo.className = "cm-menu-combo"; | |
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 = ""; | |
if(is_legacy_front()) { | |
badge_combo = document.createElement("select"); | |
badge_combo.setAttribute("title", "Configure the content to be displayed on the badge at the top right corner of the node. The ID is the identifier of the node. If 'hide built-in' is selected, both unknown nodes and built-in nodes will be omitted, making them indistinguishable"); | |
badge_combo.className = "cm-menu-combo"; | |
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.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list. Note that the badge utilizes local information."); | |
channel_combo.className = "cm-menu-combo"; | |
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) { | |
} | |
}); | |
// default ui state | |
let default_ui_combo = document.createElement("select"); | |
default_ui_combo.setAttribute("title", "Set the default state to be displayed in the main menu when the browser starts."); | |
default_ui_combo.className = "cm-menu-combo"; | |
default_ui_combo.appendChild($el('option', { value: 'none', text: 'Default UI: None' }, [])); | |
default_ui_combo.appendChild($el('option', { value: 'history', text: 'Default UI: History' }, [])); | |
default_ui_combo.appendChild($el('option', { value: 'queue', text: 'Default UI: Queue' }, [])); | |
api.fetchApi('/manager/default_ui') | |
.then(response => response.text()) | |
.then(data => { default_ui_combo.value = data; }); | |
default_ui_combo.addEventListener('change', function (event) { | |
api.fetchApi(`/manager/default_ui?value=${event.target.value}`); | |
}); | |
// share | |
let share_combo = document.createElement("select"); | |
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button."); | |
share_combo.className = "cm-menu-combo"; | |
const share_options = [ | |
['none', 'None'], | |
['openart', 'OpenArt AI'], | |
['youml', 'YouML'], | |
['matrix', 'Matrix Server'], | |
['comfyworkflows', 'ComfyWorkflows'], | |
['copus', 'Copus'], | |
['all', 'All'], | |
]; | |
for (const option of share_options) { | |
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, [])); | |
} | |
// default ui state | |
let component_policy_combo = document.createElement("select"); | |
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use."); | |
component_policy_combo.className = "cm-menu-combo"; | |
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, [])); | |
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, [])); | |
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, [])); | |
api.fetchApi('/manager/component/policy') | |
.then(response => response.text()) | |
.then(data => { | |
component_policy_combo.value = data; | |
set_component_policy(data); | |
}); | |
component_policy_combo.addEventListener('change', function (event) { | |
api.fetchApi(`/manager/component/policy?value=${event.target.value}`); | |
set_component_policy(event.target.value); | |
}); | |
let dbl_click_policy_combo = document.createElement("select"); | |
dbl_click_policy_combo.setAttribute("title", "Sets the behavior when you double-click the title area of a node."); | |
dbl_click_policy_combo.className = "cm-menu-combo"; | |
dbl_click_policy_combo.appendChild($el('option', { value: 'none', text: 'Double-Click: None' }, [])); | |
dbl_click_policy_combo.appendChild($el('option', { value: 'copy-all', text: 'Double-Click: Copy All Connections' }, [])); | |
dbl_click_policy_combo.appendChild($el('option', { value: 'copy-full', text: 'Double-Click: Copy All Connections and shape' }, [])); | |
dbl_click_policy_combo.appendChild($el('option', { value: 'copy-input', text: 'Double-Click: Copy Input Connections' }, [])); | |
dbl_click_policy_combo.appendChild($el('option', { value: 'possible-input', text: 'Double-Click: Possible Input Connections' }, [])); | |
dbl_click_policy_combo.appendChild($el('option', { value: 'dual', text: 'Double-Click: Possible(left) + Copy(right)' }, [])); | |
api.fetchApi('/manager/dbl_click/policy') | |
.then(response => response.text()) | |
.then(data => { | |
dbl_click_policy_combo.value = data; | |
set_double_click_policy(data); | |
}); | |
dbl_click_policy_combo.addEventListener('change', function (event) { | |
api.fetchApi(`/manager/dbl_click/policy?value=${event.target.value}`); | |
set_double_click_policy(event.target.value); | |
}); | |
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.update_check_checkbox, uc_checkbox_text]), | |
$el("br", {}, []), | |
this.datasrc_combo, | |
channel_combo, | |
preview_combo, | |
badge_combo, | |
default_ui_combo, | |
share_combo, | |
component_policy_combo, | |
dbl_click_policy_combo, | |
$el("br", {}, []), | |
$el("br", {}, []), | |
$el("filedset.cm-experimental", {}, [ | |
$el("legend.cm-experimental-legend", {}, ["EXPERIMENTAL"]), | |
$el("button.cm-experimental-button", { | |
type: "button", | |
textContent: "Snapshot Manager", | |
onclick: | |
() => { | |
if(!SnapshotManager.instance) | |
SnapshotManager.instance = new SnapshotManager(app, self); | |
SnapshotManager.instance.show(); | |
} | |
}), | |
$el("button.cm-experimental-button", { | |
type: "button", | |
textContent: "Install PIP packages", | |
onclick: | |
() => { | |
var url = prompt("Please enumerate the pip packages to be installed.\n\nExample: insightface opencv-python-headless>=4.1.1\n", ""); | |
if (url !== null) { | |
install_pip(url, self); | |
} | |
} | |
}), | |
$el("button.cm-experimental-button", { | |
type: "button", | |
textContent: "Unload models", | |
onclick: () => { free_models(); } | |
}) | |
]), | |
]; | |
} | |
createControlsRight() { | |
const elts = [ | |
$el("button.cm-button", { | |
id: 'cm-manual-button', | |
type: "button", | |
textContent: "Community Manual", | |
onclick: () => { window.open("https://blenderneko.github.io/ComfyUI-docs/", "comfyui-community-manual"); } | |
}, [ | |
$el("div.pysssss-workflow-arrow-2", { | |
id: `cm-manual-button-arrow`, | |
onclick: (e) => { | |
e.preventDefault(); | |
e.stopPropagation(); | |
LiteGraph.closeAllContextMenus(); | |
const menu = new LiteGraph.ContextMenu( | |
[ | |
{ | |
title: "ComfyUI Docs", | |
callback: () => { window.open("https://docs.comfy.org/", "comfyui-official-manual"); }, | |
}, | |
{ | |
title: "Comfy Custom Node How To", | |
callback: () => { window.open("https://github.com/chrisgoringe/Comfy-Custom-Node-How-To/wiki/aaa_index", "comfyui-community-manual1"); }, | |
}, | |
{ | |
title: "ComfyUI Guide To Making Custom Nodes", | |
callback: () => { window.open("https://github.com/Suzie1/ComfyUI_Guide_To_Making_Custom_Nodes/wiki", "comfyui-community-manual2"); }, | |
}, | |
{ | |
title: "ComfyUI Examples", | |
callback: () => { window.open("https://comfyanonymous.github.io/ComfyUI_examples", "comfyui-community-manual3"); }, | |
}, | |
{ | |
title: "Close", | |
callback: () => { | |
LiteGraph.closeAllContextMenus(); | |
}, | |
} | |
], | |
{ | |
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 = "cm-manual-button-menu"; | |
menu.root.classList.add("pysssss-workflow-popup-2"); | |
}, | |
}) | |
]), | |
$el("button", { | |
id: 'workflowgallery-button', | |
type: "button", | |
style: { | |
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {}) | |
}, | |
onclick: (e) => { | |
const last_visited_site = localStorage.getItem("wg_last_visited") | |
if (!!last_visited_site) { | |
window.open(last_visited_site, last_visited_site); | |
} else { | |
this.handleWorkflowGalleryButtonClick(e) | |
} | |
}, | |
}, [ | |
$el("p", { | |
textContent: 'Workflow Gallery', | |
style: { | |
'text-align': 'center', | |
'color': 'var(--input-text)', | |
'font-size': '18px', | |
'margin': 0, | |
'padding': 0, | |
} | |
}, [ | |
$el("p", { | |
id: 'workflowgallery-button-last-visited-label', | |
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : ''})`, | |
style: { | |
'text-align': 'center', | |
'color': 'var(--input-text)', | |
'font-size': '12px', | |
'margin': 0, | |
'padding': 0, | |
} | |
}) | |
]), | |
$el("div.pysssss-workflow-arrow-2", { | |
id: `comfyworkflows-button-arrow`, | |
onclick: this.handleWorkflowGalleryButtonClick | |
}) | |
]), | |
$el("button.cm-button", { | |
id: 'cm-nodeinfo-button', | |
type: "button", | |
textContent: "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", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => this.close() }); | |
const content = | |
$el("div.comfy-modal-content", | |
[ | |
$el("tr.cm-title", {}, [ | |
$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()]) | |
]), | |
$el("br", {}, []), | |
close_button, | |
] | |
); | |
content.style.width = '100%'; | |
content.style.height = '100%'; | |
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]); | |
} | |
show() { | |
this.element.style.display = "block"; | |
} | |
handleWorkflowGalleryButtonClick(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
LiteGraph.closeAllContextMenus(); | |
// Modify the style of the button so that the UI can indicate the last | |
// visited site right away. | |
const modifyButtonStyle = (url) => { | |
const workflowGalleryButton = document.getElementById('workflowgallery-button'); | |
workflowGalleryButton.style.height = '50px'; | |
const lastVisitedLabel = document.getElementById('workflowgallery-button-last-visited-label'); | |
lastVisitedLabel.textContent = `(${url.split('/')[2]})`; | |
} | |
const menu = new LiteGraph.ContextMenu( | |
[ | |
{ | |
title: "Share your art", | |
callback: () => { | |
if (share_option === 'openart') { | |
showOpenArtShareDialog(); | |
return; | |
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') { | |
showShareDialog(share_option); | |
return; | |
} else if (share_option === 'youml') { | |
showYouMLShareDialog(); | |
return; | |
} | |
if (!ShareDialogChooser.instance) { | |
ShareDialogChooser.instance = new ShareDialogChooser(); | |
} | |
ShareDialogChooser.instance.show(); | |
}, | |
}, | |
{ | |
title: "Open 'openart.ai'", | |
callback: () => { | |
const url = "https://openart.ai/workflows/dev"; | |
localStorage.setItem("wg_last_visited", url); | |
window.open(url, url); | |
modifyButtonStyle(url); | |
}, | |
}, | |
{ | |
title: "Open 'youml.com'", | |
callback: () => { | |
const url = "https://youml.com/?from=comfyui-share"; | |
localStorage.setItem("wg_last_visited", url); | |
window.open(url, url); | |
modifyButtonStyle(url); | |
}, | |
}, | |
{ | |
title: "Open 'comfyworkflows.com'", | |
callback: () => { | |
const url = "https://comfyworkflows.com/"; | |
localStorage.setItem("wg_last_visited", url); | |
window.open(url, url); | |
modifyButtonStyle(url); | |
}, | |
}, | |
{ | |
title: "Open 'esheep'", | |
callback: () => { | |
const url = "https://www.esheep.com"; | |
localStorage.setItem("wg_last_visited", url); | |
window.open(url, url); | |
modifyButtonStyle(url); | |
}, | |
}, | |
{ | |
title: "Open 'Copus.io'", | |
callback: () => { | |
const url = "https://www.copus.io"; | |
localStorage.setItem("wg_last_visited", url); | |
window.open(url, url); | |
modifyButtonStyle(url); | |
}, | |
}, | |
{ | |
title: "Close", | |
callback: () => { | |
LiteGraph.closeAllContextMenus(); | |
}, | |
} | |
], | |
{ | |
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 = "workflowgallery-button-menu"; | |
menu.root.classList.add("pysssss-workflow-popup-2"); | |
} | |
} | |
app.registerExtension({ | |
name: "Comfy.ManagerMenu", | |
init() { | |
$el("style", { | |
textContent: style, | |
parent: document.head, | |
}); | |
}, | |
async setup() { | |
let orig_clear = app.graph.clear; | |
app.graph.clear = function () { | |
orig_clear.call(app.graph); | |
load_components(); | |
}; | |
load_components(); | |
const menu = document.querySelector(".comfy-menu"); | |
const separator = document.createElement("hr"); | |
separator.style.margin = "20px 0"; | |
separator.style.width = "100%"; | |
menu.append(separator); | |
try { | |
// new style Manager buttons | |
// unload models button into new style Manager button | |
let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup( | |
new(await import("../../scripts/ui/components/button.js")).ComfyButton({ | |
icon: "puzzle", | |
action: () => { | |
if(!manager_instance) | |
setManagerInstance(new ManagerMenuDialog()); | |
manager_instance.show(); | |
}, | |
tooltip: "ComfyUI Manager", | |
content: "Manager", | |
classList: "comfyui-button comfyui-menu-mobile-collapse primary" | |
}).element, | |
new(await import("../../scripts/ui/components/button.js")).ComfyButton({ | |
icon: "vacuum-outline", | |
action: () => { | |
free_models(); | |
}, | |
tooltip: "Unload Models" | |
}).element, | |
new(await import("../../scripts/ui/components/button.js")).ComfyButton({ | |
icon: "vacuum", | |
action: () => { | |
free_models(true); | |
}, | |
tooltip: "Free model and node cache" | |
}).element, | |
new(await import("../../scripts/ui/components/button.js")).ComfyButton({ | |
icon: "share", | |
action: () => { | |
if (share_option === 'openart') { | |
showOpenArtShareDialog(); | |
return; | |
} else if (share_option === 'matrix' || share_option === 'comfyworkflows') { | |
showShareDialog(share_option); | |
return; | |
} else if (share_option === 'youml') { | |
showYouMLShareDialog(); | |
return; | |
} | |
if(!ShareDialogChooser.instance) { | |
ShareDialogChooser.instance = new ShareDialogChooser(); | |
} | |
ShareDialogChooser.instance.show(); | |
}, | |
tooltip: "Share" | |
}).element | |
); | |
app.menu?.settingsGroup.element.before(cmGroup.element); | |
} | |
catch(exception) { | |
console.log('ComfyUI is outdated. New style menu based features are disabled.'); | |
} | |
// old style Manager button | |
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; | |
} else if (share_option === 'youml') { | |
showYouMLShareDialog(); | |
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) { | |
this._addExtraNodeContextMenu(nodeType, app); | |
}, | |
async nodeCreated(node, app) { | |
if(is_legacy_front()) { | |
if(!node.badge_enabled) { | |
node.getNickname = function () { return getNickname(node, node.comfyClass.trim()) }; | |
let orig = node.onDrawForeground; | |
if(!orig) | |
orig = node.__proto__.onDrawForeground; | |
node.onDrawForeground = function (ctx) { | |
drawBadge(node, orig, arguments) | |
}; | |
node.badge_enabled = true; | |
} | |
} | |
}, | |
async loadedGraphNode(node, app) { | |
if(is_legacy_front()) { | |
if(!node.badge_enabled) { | |
const orig = node.onDrawForeground; | |
node.getNickname = function () { return getNickname(node, node.type.trim()) }; | |
node.onDrawForeground = function (ctx) { drawBadge(node, orig, arguments) }; | |
} | |
} | |
}, | |
_addExtraNodeContextMenu(node, app) { | |
const origGetExtraMenuOptions = node.prototype.getExtraMenuOptions; | |
node.prototype.cm_menu_added = true; | |
node.prototype.getExtraMenuOptions = function (_, options) { | |
origGetExtraMenuOptions?.apply?.(this, arguments); | |
if (node.category.startsWith('group nodes>')) { | |
options.push({ | |
content: "Save As Component", | |
callback: (obj) => { | |
if (!ComponentBuilderDialog.instance) { | |
ComponentBuilderDialog.instance = new ComponentBuilderDialog(); | |
} | |
ComponentBuilderDialog.instance.target_node = node; | |
ComponentBuilderDialog.instance.show(); | |
} | |
}, null); | |
} | |
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); | |
} | |
} | |
}, | |
}); | |
async function set_default_ui() | |
{ | |
let res = await api.fetchApi('/manager/default_ui'); | |
if(res.status == 200) { | |
let mode = await res.text(); | |
switch(mode) { | |
case 'history': | |
app.ui.queue.hide(); | |
app.ui.history.show(); | |
break; | |
case 'queue': | |
app.ui.queue.show(); | |
app.ui.history.hide(); | |
break; | |
default: | |
// do nothing | |
break; | |
} | |
} | |
} | |
set_default_ui(); |