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);
}
}
},
});