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.
NOTE:
Fetch Updates is not an update.
Please update from
");
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.
See terminal log.
');
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 = "
FAILED: "+update_info.failed.join(", ");
}
let updated_list = "";
if(update_info.updated.length > 0) {
updated_list = "
UPDATED: "+update_info.updated.join(", ");
}
show_message(
"ComfyUI and all extensions have been updated to the latest version.
To apply the updated custom node, please ComfyUI. And refresh browser.
"
+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 'flowt.ai'",
callback: () => {
const url = "https://flowt.ai/";
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();