diff --git a/.gitignore b/.gitignore
index fa803b09..4abc7136 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,6 @@ __pycache__/
.idea/
.vscode/
.tmp
-config.ini
\ No newline at end of file
+config.ini
+snapshots/**
+startup-scripts/**
diff --git a/__init__.py b/__init__.py
index 15ad8fba..89b8e01a 100644
--- a/__init__.py
+++ b/__init__.py
@@ -4,7 +4,8 @@ import folder_paths
import os
import sys
import threading
-import subprocess
+import datetime
+import re
def handle_stream(stream, prefix):
@@ -56,7 +57,7 @@ sys.path.append('../..')
from torchvision.datasets.utils import download_url
# ensure .js
-print("### Loading: ComfyUI-Manager (V0.36.1)")
+print("### Loading: ComfyUI-Manager (V0.37)")
comfy_ui_required_revision = 1240
comfy_ui_revision = "Unknown"
@@ -283,6 +284,14 @@ def __win_check_git_pull(path):
process.wait()
+def switch_to_default_branch(repo):
+ show_result = repo.git.remote("show", "origin")
+ matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result)
+ if matches:
+ default_branch = matches.group(1)
+ repo.git.checkout(default_branch)
+
+
def git_repo_has_updates(path, do_fetch=False, do_update=False):
if do_fetch:
print(f"\x1b[2K\rFetching: {path}", end='')
@@ -299,9 +308,6 @@ def git_repo_has_updates(path, do_fetch=False, do_update=False):
# Fetch the latest commits from the remote repository
repo = git.Repo(path)
- current_branch = repo.active_branch
- branch_name = current_branch.name
-
remote_name = 'origin'
remote = repo.remote(name=remote_name)
@@ -312,8 +318,11 @@ def git_repo_has_updates(path, do_fetch=False, do_update=False):
remote.fetch()
if do_update:
+ if repo.head.is_detached:
+ switch_to_default_branch(repo)
+
try:
- remote.pull(rebase=True)
+ remote.pull()
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha
@@ -326,7 +335,13 @@ def git_repo_has_updates(path, do_fetch=False, do_update=False):
except Exception as e:
print(f"\nUpdating failed: {path}\n{e}", file=sys.stderr)
+ if repo.head.is_detached:
+ return True
+
# Get commit hash of the remote branch
+ current_branch = repo.active_branch
+ branch_name = current_branch.name
+
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
# Compare the commit hashes to determine if the local repository is behind the remote repository
@@ -352,11 +367,17 @@ def git_pull(path):
return __win_check_git_pull(path)
else:
repo = git.Repo(path)
+
+ print(f"path={path} / repo.is_dirty: {repo.is_dirty()}")
+
if repo.is_dirty():
repo.git.stash()
+ if repo.head.is_detached:
+ switch_to_default_branch(repo)
+
origin = repo.remote(name='origin')
- origin.pull(rebase=True)
+ origin.pull()
repo.git.submodule('update', '--init', '--recursive')
repo.close()
@@ -569,6 +590,8 @@ async def fetch_updates(request):
@server.PromptServer.instance.routes.get("/customnode/update_all")
async def update_all(request):
try:
+ save_snapshot_with_postfix('autosave')
+
if request.rel_url.query["mode"] == "local":
uri = local_db_custom_node_list
else:
@@ -663,6 +686,125 @@ async def fetch_externalmodel_list(request):
return web.json_response(json_obj, content_type='application/json')
+@server.PromptServer.instance.routes.get("/snapshot/getlist")
+async def get_snapshot_list(request):
+ snapshots_directory = os.path.join(os.path.dirname(__file__), 'snapshots')
+ items = [f[:-5] for f in os.listdir(snapshots_directory) if f.endswith('.json')]
+ items.sort(reverse=True)
+ return web.json_response({'items': items}, content_type='application/json')
+
+
+@server.PromptServer.instance.routes.get("/snapshot/remove")
+async def remove_snapshot(request):
+ try:
+ target = request.rel_url.query["target"]
+
+ path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json")
+ if os.path.exists(path):
+ os.remove(path)
+
+ return web.Response(status=200)
+ except:
+ return web.Response(status=400)
+
+
+@server.PromptServer.instance.routes.get("/snapshot/restore")
+async def remove_snapshot(request):
+ try:
+ target = request.rel_url.query["target"]
+
+ path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json")
+ if os.path.exists(path):
+ if not os.path.exists(startup_script_path):
+ os.makedirs(startup_script_path)
+
+ target_path = os.path.join(startup_script_path, "restore-snapshot.json")
+ shutil.copy(path, target_path)
+
+ print(f"Snapshot restore scheduled: `{target}`")
+ return web.Response(status=200)
+
+ print(f"Snapshot file not found: `{path}`")
+ return web.Response(status=400)
+ except:
+ return web.Response(status=400)
+
+
+def get_current_snapshot():
+ # Get ComfyUI hash
+ repo_path = os.path.dirname(folder_paths.__file__)
+
+ if not os.path.exists(os.path.join(repo_path, '.git')):
+ print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.")
+ return web.Response(status=400)
+
+ repo = git.Repo(repo_path)
+ comfyui_commit_hash = repo.head.commit.hexsha
+
+ git_custom_nodes = {}
+ file_custom_nodes = []
+
+ # Get custom nodes hash
+ for path in os.listdir(custom_nodes_path):
+ fullpath = os.path.join(custom_nodes_path, path)
+
+ if os.path.isdir(fullpath):
+ is_disabled = path.endswith(".disabled")
+
+ try:
+ git_dir = os.path.join(fullpath, '.git')
+
+ if not os.path.exists(git_dir):
+ continue
+
+ repo = git.Repo(fullpath)
+ commit_hash = repo.head.commit.hexsha
+ url = repo.remotes.origin.url
+ git_custom_nodes[url] = {
+ 'hash': commit_hash,
+ 'disabled': is_disabled
+ }
+
+ except:
+ print(f"Failed to extract snapshots for the custom node '{path}'.")
+
+ elif path.endswith('.py'):
+ is_disabled = path.endswith(".py.disabled")
+ filename = os.path.basename(path)
+ item = {
+ 'filename': filename,
+ 'disabled': is_disabled
+ }
+
+ file_custom_nodes.append(item)
+
+ return {
+ 'comfyui': comfyui_commit_hash,
+ 'git_custom_nodes': git_custom_nodes,
+ 'file_custom_nodes': file_custom_nodes,
+ }
+
+
+def save_snapshot_with_postfix(postfix):
+ now = datetime.datetime.now()
+
+ date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
+ file_name = f"{date_time_format}_{postfix}"
+
+ path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{file_name}.json")
+ with open(path, "w") as json_file:
+ json.dump(get_current_snapshot(), json_file, indent=4)
+
+
+@server.PromptServer.instance.routes.get("/snapshot/save")
+async def save_snapshot(request):
+ try:
+ save_snapshot_with_postfix('snapshot')
+ return web.Response(status=200)
+ except:
+ return web.Response(status=400)
+
+
def unzip_install(files):
temp_filename = 'manager-temp.zip'
for url in files:
@@ -1073,6 +1215,9 @@ async def update_comfyui(request):
# version check
repo = git.Repo(repo_path)
+ if repo.head.is_detached:
+ switch_to_default_branch(repo)
+
current_branch = repo.active_branch
branch_name = current_branch.name
@@ -1202,6 +1347,8 @@ async def channel_url_list(request):
return web.Response(status=200)
+
WEB_DIRECTORY = "js"
NODE_CLASS_MAPPINGS = {}
__all__ = ['NODE_CLASS_MAPPINGS']
+
diff --git a/git_helper.py b/git_helper.py
index 5603fad1..2fbcf078 100644
--- a/git_helper.py
+++ b/git_helper.py
@@ -2,23 +2,39 @@ import sys
import os
import git
import configparser
+import re
+import json
+from torchvision.datasets.utils import download_url
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
+nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json")
+working_directory = os.getcwd()
-def gitclone(custom_nodes_path, url):
+
+def gitclone(custom_nodes_path, url, target_hash=None):
repo_name = os.path.splitext(os.path.basename(url))[0]
repo_path = os.path.join(custom_nodes_path, repo_name)
# Clone the repository from the remote URL
repo = git.Repo.clone_from(url, repo_path, recursive=True)
+
+ if target_hash is not None:
+ print(f"CHECKOUT: {repo_name} [{target_hash}]")
+ repo.git.checkout(target_hash)
+
repo.git.clear_cache()
repo.close()
+
def gitcheck(path, do_fetch=False):
try:
# Fetch the latest commits from the remote repository
repo = git.Repo(path)
+ if repo.head.is_detached:
+ print("CUSTOM NODE CHECK: True")
+ return
+
current_branch = repo.active_branch
branch_name = current_branch.name
@@ -48,6 +64,14 @@ def gitcheck(path, do_fetch=False):
print("CUSTOM NODE CHECK: Error")
+def switch_to_default_branch(repo):
+ show_result = repo.git.remote("show", "origin")
+ matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result)
+ if matches:
+ default_branch = matches.group(1)
+ repo.git.checkout(default_branch)
+
+
def gitpull(path):
# Check if the path is a git repository
if not os.path.exists(os.path.join(path, '.git')):
@@ -60,8 +84,12 @@ def gitpull(path):
commit_hash = repo.head.commit.hexsha
try:
+ if repo.head.is_detached:
+ switch_to_default_branch(repo)
+
origin = repo.remote(name='origin')
- origin.pull(rebase=True)
+ origin.pull()
+
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha
@@ -76,6 +104,165 @@ def gitpull(path):
repo.close()
+def checkout_comfyui_hash(target_hash):
+ repo_path = os.path.join(working_directory, '..') # ComfyUI dir
+
+ repo = git.Repo(repo_path)
+ commit_hash = repo.head.commit.hexsha
+
+ if commit_hash != target_hash:
+ try:
+ print(f"CHECKOUT: ComfyUI [{target_hash}]")
+ repo.git.checkout(target_hash)
+ except git.GitCommandError as e:
+ print(f"Error checking out the ComfyUI: {str(e)}")
+
+
+def checkout_custom_node_hash(git_custom_node_infos):
+ repo_name_to_url = {}
+
+ for url in git_custom_node_infos.keys():
+ repo_name = url.split('/')[-1]
+
+ if repo_name.endswith('.git'):
+ repo_name = repo_name[:-4]
+
+ repo_name_to_url[repo_name] = url
+
+ for path in os.listdir(working_directory):
+ if path.endswith("ComfyUI-Manager"):
+ continue
+
+ fullpath = os.path.join(working_directory, path)
+
+ if os.path.isdir(fullpath):
+ is_disabled = path.endswith(".disabled")
+
+ try:
+ git_dir = os.path.join(fullpath, '.git')
+ if not os.path.exists(git_dir):
+ continue
+
+ need_checkout = False
+ repo_name = os.path.basename(fullpath)
+
+ if repo_name.endswith('.disabled'):
+ repo_name = repo_name[:-9]
+
+ item = git_custom_node_infos[repo_name_to_url[repo_name]]
+ if item['disabled'] and is_disabled:
+ pass
+ elif item['disabled'] and not is_disabled:
+ # disable
+ print(f"DISABLE: {repo_name}")
+ new_path = fullpath + ".disabled"
+ os.rename(fullpath, new_path)
+ pass
+ elif not item['disabled'] and is_disabled:
+ # enable
+ print(f"ENABLE: {repo_name}")
+ new_path = fullpath[:-9]
+ os.rename(fullpath, new_path)
+ fullpath = new_path
+ need_checkout = True
+ else:
+ need_checkout = True
+
+ if need_checkout:
+ repo = git.Repo(fullpath)
+ commit_hash = repo.head.commit.hexsha
+
+ if commit_hash != item['hash']:
+ print(f"CHECKOUT: {repo_name} [{item['hash']}]")
+ repo.git.checkout(item['hash'])
+ except Exception:
+ print(f"Failed to restore snapshots for the custom node '{path}'")
+
+ # clone missing
+ for k, v in git_custom_node_infos.items():
+ if not v['disabled']:
+ repo_name = k.split('/')[-1]
+ if repo_name.endswith('.git'):
+ repo_name = repo_name[:-4]
+
+ path = os.path.join(working_directory, repo_name)
+ if not os.path.exists(path):
+ print(f"CLONE: {path}")
+ gitclone(working_directory, k, v['hash'])
+
+
+def invalidate_custom_node_file(file_custom_node_infos):
+ global nodelist_path
+
+ enabled_set = set()
+ for item in file_custom_node_infos:
+ if not item['disabled']:
+ enabled_set.add(item['filename'])
+
+ for path in os.listdir(working_directory):
+ fullpath = os.path.join(working_directory, path)
+
+ if not os.path.isdir(fullpath) and fullpath.endswith('.py'):
+ if path not in enabled_set:
+ print(f"DISABLE: {path}")
+ new_path = fullpath+'.disabled'
+ os.rename(fullpath, new_path)
+
+ elif not os.path.isdir(fullpath) and fullpath.endswith('.py.disabled'):
+ path = path[:-9]
+ if path in enabled_set:
+ print(f"ENABLE: {path}")
+ new_path = fullpath[:-9]
+ os.rename(fullpath, new_path)
+
+ # download missing: just support for 'copy' style
+ py_to_url = {}
+
+ with open(nodelist_path, 'r', encoding="UTF-8") as json_file:
+ info = json.load(json_file)
+ for item in info['custom_nodes']:
+ if item['install_type'] == 'copy':
+ for url in item['files']:
+ if url.endswith('.py'):
+ py = url.split('/')[-1]
+ py_to_url[py] = url
+
+ for item in file_custom_node_infos:
+ filename = item['filename']
+ if not item['disabled']:
+ target_path = os.path.join(working_directory, filename)
+
+ if not os.path.exists(target_path) and filename in py_to_url:
+ url = py_to_url[filename]
+ print(f"DOWNLOAD: {filename}")
+ download_url(url, working_directory)
+
+
+def apply_snapshot(target):
+ try:
+ path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}")
+ if os.path.exists(path):
+ with open(path, 'r', encoding="UTF-8") as json_file:
+ info = json.load(json_file)
+
+ comfyui_hash = info['comfyui']
+ git_custom_node_infos = info['git_custom_nodes']
+ file_custom_node_infos = info['file_custom_nodes']
+
+ checkout_comfyui_hash(comfyui_hash)
+ checkout_custom_node_hash(git_custom_node_infos)
+ invalidate_custom_node_file(file_custom_node_infos)
+
+ print("APPLY SNAPSHOT: True")
+ return
+
+ print(f"Snapshot file not found: `{path}`")
+ print("APPLY SNAPSHOT: False")
+ except Exception as e:
+ print(e)
+ print("APPLY SNAPSHOT: False")
+
+
def setup_environment():
config = configparser.ConfigParser()
config.read(config_path)
@@ -95,8 +282,11 @@ try:
gitcheck(sys.argv[2], True)
elif sys.argv[1] == "--pull":
gitpull(sys.argv[2])
+ elif sys.argv[1] == "--apply-snapshot":
+ apply_snapshot(sys.argv[2])
sys.exit(0)
-except:
+except Exception as e:
+ print(e)
sys.exit(-1)
diff --git a/js/a1111-alter-downloader.js b/js/a1111-alter-downloader.js
new file mode 100644
index 00000000..47bc28a8
--- /dev/null
+++ b/js/a1111-alter-downloader.js
@@ -0,0 +1,554 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js"
+import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { install_checked_custom_node, manager_instance } from "./common.js";
+
+async function getAlterList() {
+ var mode = "url";
+ if(manager_instance.local_mode_checkbox.checked)
+ mode = "local";
+
+ var skip_update = "";
+ if(manager_instance.update_check_checkbox.checked)
+ skip_update = "&skip_update=true";
+
+ const response = await api.fetchApi(`/alternatives/getlist?mode=${mode}${skip_update}`);
+
+ const data = await response.json();
+ return data;
+}
+
+export class AlternativesInstaller extends ComfyDialog {
+ static instance = null;
+
+ install_buttons = [];
+ message_box = null;
+ data = null;
+
+ clear() {
+ this.install_buttons = [];
+ this.message_box = null;
+ this.data = null;
+ }
+
+ constructor() {
+ super();
+ this.search_keyword = '';
+ this.element = $el("div.comfy-modal", { parent: document.body }, []);
+ }
+
+ startInstall(target) {
+ const self = AlternativesInstaller.instance;
+
+ self.updateMessage(`
Installing '${target.title}'`);
+ }
+
+ disableButtons() {
+ for(let i in this.install_buttons) {
+ this.install_buttons[i].disabled = true;
+ this.install_buttons[i].style.backgroundColor = 'gray';
+ }
+ }
+
+ apply_searchbox(data) {
+ let keyword = this.search_box.value.toLowerCase();
+ for(let i in this.grid_rows) {
+ let data1 = this.grid_rows[i].data;
+ let data2 = data1.custom_node;
+
+ if(!data2)
+ continue;
+
+ let content = data1.tags.toLowerCase() + data1.description.toLowerCase() + data2.author.toLowerCase() + data2.description.toLowerCase() + data2.title.toLowerCase();
+
+ if(this.filter && this.filter != '*') {
+ if(this.filter != data2.installed) {
+ this.grid_rows[i].control.style.display = 'none';
+ continue;
+ }
+ }
+
+ if(keyword == "")
+ this.grid_rows[i].control.style.display = null;
+ else if(content.includes(keyword)) {
+ this.grid_rows[i].control.style.display = null;
+ }
+ else {
+ this.grid_rows[i].control.style.display = 'none';
+ }
+ }
+ }
+
+ async invalidateControl() {
+ this.clear();
+
+ // splash
+ while (this.element.children.length) {
+ this.element.removeChild(this.element.children[0]);
+ }
+
+ const msg = $el('div', {id:'custom-message'},
+ [$el('br'),
+ 'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
+ $el('br'),
+ 'NOTE: Update only checks for extensions that have been fetched.',
+ $el('br')]);
+ msg.style.height = '100px';
+ msg.style.verticalAlign = 'middle';
+ this.element.appendChild(msg);
+
+ // invalidate
+ this.data = (await getAlterList()).items;
+
+ this.element.removeChild(msg);
+
+ while (this.element.children.length) {
+ this.element.removeChild(this.element.children[0]);
+ }
+
+ this.createHeaderControls();
+ await this.createGrid();
+ this.apply_searchbox(this.data);
+ this.createBottomControls();
+ }
+
+ updateMessage(msg) {
+ this.message_box.innerHTML = msg;
+ }
+
+ invalidate_checks(is_checked, install_state) {
+ if(is_checked) {
+ for(let i in this.grid_rows) {
+ let data = this.grid_rows[i].data;
+ let checkbox = this.grid_rows[i].checkbox;
+ let buttons = this.grid_rows[i].buttons;
+
+ checkbox.disabled = data.custom_node.installed != install_state;
+
+ if(checkbox.disabled) {
+ for(let j in buttons) {
+ buttons[j].style.display = 'none';
+ }
+ }
+ else {
+ for(let j in buttons) {
+ buttons[j].style.display = null;
+ }
+ }
+ }
+
+ this.checkbox_all.disabled = false;
+ }
+ else {
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ if(checkbox.check)
+ return; // do nothing
+ }
+
+ // every checkbox is unchecked -> enable all checkbox
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ let buttons = this.grid_rows[i].buttons;
+ checkbox.disabled = false;
+
+ for(let j in buttons) {
+ buttons[j].style.display = null;
+ }
+ }
+
+ this.checkbox_all.checked = false;
+ this.checkbox_all.disabled = true;
+ }
+ }
+
+ check_all(is_checked) {
+ if(is_checked) {
+ // lookup first checked item's state
+ let check_state = null;
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ if(checkbox.checked) {
+ check_state = this.grid_rows[i].data.custom_node.installed;
+ }
+ }
+
+ if(check_state == null)
+ return;
+
+ // check only same state items
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ if(this.grid_rows[i].data.custom_node.installed == check_state)
+ checkbox.checked = true;
+ }
+ }
+ else {
+ // uncheck all
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ let buttons = this.grid_rows[i].buttons;
+ checkbox.checked = false;
+ checkbox.disabled = false;
+
+ for(let j in buttons) {
+ buttons[j].style.display = null;
+ }
+ }
+
+ this.checkbox_all.disabled = true;
+ }
+ }
+
+ async createGrid() {
+ var grid = document.createElement('table');
+ grid.setAttribute('id', 'alternatives-grid');
+
+ this.grid_rows = {};
+
+ let self = this;
+
+ var thead = document.createElement('thead');
+ var tbody = document.createElement('tbody');
+
+ var headerRow = document.createElement('tr');
+ thead.style.position = "sticky";
+ thead.style.top = "0px";
+ thead.style.borderCollapse = "collapse";
+ thead.style.tableLayout = "fixed";
+
+ var header0 = document.createElement('th');
+ header0.style.width = "20px";
+ this.checkbox_all = $el("input",{type:'checkbox', id:'check_all'},[]);
+ header0.appendChild(this.checkbox_all);
+ this.checkbox_all.checked = false;
+ this.checkbox_all.disabled = true;
+ this.checkbox_all.addEventListener('change', function() { self.check_all.call(self, self.checkbox_all.checked); });
+
+ var header1 = document.createElement('th');
+ header1.innerHTML = ' ID ';
+ header1.style.width = "20px";
+ var header2 = document.createElement('th');
+ header2.innerHTML = 'Tags';
+ header2.style.width = "10%";
+ var header3 = document.createElement('th');
+ header3.innerHTML = 'Author';
+ header3.style.width = "150px";
+ var header4 = document.createElement('th');
+ header4.innerHTML = 'Title';
+ header4.style.width = "20%";
+ var header5 = document.createElement('th');
+ header5.innerHTML = 'Description';
+ header5.style.width = "50%";
+ var header6 = document.createElement('th');
+ header6.innerHTML = 'Install';
+ header6.style.width = "130px";
+
+ header1.style.position = "sticky";
+ header1.style.top = "0px";
+ header2.style.position = "sticky";
+ header2.style.top = "0px";
+ header3.style.position = "sticky";
+ header3.style.top = "0px";
+ header4.style.position = "sticky";
+ header4.style.top = "0px";
+ header5.style.position = "sticky";
+ header5.style.top = "0px";
+
+ thead.appendChild(headerRow);
+ headerRow.appendChild(header0);
+ headerRow.appendChild(header1);
+ headerRow.appendChild(header2);
+ headerRow.appendChild(header3);
+ headerRow.appendChild(header4);
+ headerRow.appendChild(header5);
+ headerRow.appendChild(header6);
+
+ headerRow.style.backgroundColor = "Black";
+ headerRow.style.color = "White";
+ headerRow.style.textAlign = "center";
+ headerRow.style.width = "100%";
+ headerRow.style.padding = "0";
+
+ grid.appendChild(thead);
+ grid.appendChild(tbody);
+
+ if(this.data)
+ for (var i = 0; i < this.data.length; i++) {
+ const data = this.data[i];
+ var dataRow = document.createElement('tr');
+
+ let data0 = document.createElement('td');
+ let checkbox = $el("input",{type:'checkbox', id:`check_${i}`},[]);
+ data0.appendChild(checkbox);
+ checkbox.checked = false;
+ checkbox.addEventListener('change', function() { self.invalidate_checks.call(self, checkbox.checked, data.custom_node?.installed); });
+
+ var data1 = document.createElement('td');
+ data1.style.textAlign = "center";
+ data1.innerHTML = i+1;
+ var data2 = document.createElement('td');
+ data2.innerHTML = ` ${data.tags}`;
+ var data3 = document.createElement('td');
+ var data4 = document.createElement('td');
+ if(data.custom_node) {
+ data3.innerHTML = ` ${data.custom_node.author}`;
+ data4.innerHTML = ` ${data.custom_node.title}`;
+ }
+ else {
+ data3.innerHTML = ` Unknown`;
+ data4.innerHTML = ` Unknown`;
+ }
+ var data5 = document.createElement('td');
+ data5.innerHTML = data.description;
+ var data6 = document.createElement('td');
+ data6.style.textAlign = "center";
+
+ var installBtn = document.createElement('button');
+ var installBtn2 = null;
+ var installBtn3 = null;
+
+ if(data.custom_node) {
+ this.install_buttons.push(installBtn);
+
+ switch(data.custom_node.installed) {
+ case 'Disabled':
+ installBtn3 = document.createElement('button');
+ installBtn3.innerHTML = 'Enable';
+ installBtn3.style.backgroundColor = 'blue';
+ installBtn3.style.color = 'white';
+ this.install_buttons.push(installBtn3);
+
+ installBtn.innerHTML = 'Uninstall';
+ installBtn.style.backgroundColor = 'red';
+ installBtn.style.color = 'white';
+ break;
+ case 'Update':
+ installBtn2 = document.createElement('button');
+ installBtn2.innerHTML = 'Update';
+ installBtn2.style.backgroundColor = 'blue';
+ installBtn2.style.color = 'white';
+ this.install_buttons.push(installBtn2);
+
+ installBtn3 = document.createElement('button');
+ installBtn3.innerHTML = 'Disable';
+ installBtn3.style.backgroundColor = 'MediumSlateBlue';
+ installBtn3.style.color = 'white';
+ this.install_buttons.push(installBtn3);
+
+ installBtn.innerHTML = 'Uninstall';
+ installBtn.style.backgroundColor = 'red';
+ installBtn.style.color = 'white';
+ break;
+ case 'True':
+ installBtn3 = document.createElement('button');
+ installBtn3.innerHTML = 'Disable';
+ installBtn3.style.backgroundColor = 'MediumSlateBlue';
+ installBtn3.style.color = 'white';
+ this.install_buttons.push(installBtn3);
+
+ installBtn.innerHTML = 'Uninstall';
+ installBtn.style.backgroundColor = 'red';
+ installBtn.style.color = 'white';
+ break;
+ case 'False':
+ installBtn.innerHTML = 'Install';
+ installBtn.style.backgroundColor = 'black';
+ installBtn.style.color = 'white';
+ break;
+ default:
+ installBtn.innerHTML = 'Try Install';
+ installBtn.style.backgroundColor = 'Gray';
+ installBtn.style.color = 'white';
+ }
+
+ let j = i;
+ if(installBtn2 != null) {
+ installBtn2.style.width = "120px";
+ installBtn2.addEventListener('click', function() {
+ install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'update');
+ });
+
+ data6.appendChild(installBtn2);
+ }
+
+ if(installBtn3 != null) {
+ installBtn3.style.width = "120px";
+ installBtn3.addEventListener('click', function() {
+ install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'toggle_active');
+ });
+
+ data6.appendChild(installBtn3);
+ }
+
+
+ installBtn.style.width = "120px";
+ installBtn.addEventListener('click', function() {
+ if(this.innerHTML == 'Uninstall') {
+ if (confirm(`Are you sure uninstall ${data.title}?`)) {
+ install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'uninstall');
+ }
+ }
+ else {
+ install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'install');
+ }
+ });
+
+ data6.appendChild(installBtn);
+ }
+
+ dataRow.style.backgroundColor = "var(--bg-color)";
+ dataRow.style.color = "var(--fg-color)";
+ dataRow.style.textAlign = "left";
+
+ dataRow.appendChild(data0);
+ dataRow.appendChild(data1);
+ dataRow.appendChild(data2);
+ dataRow.appendChild(data3);
+ dataRow.appendChild(data4);
+ dataRow.appendChild(data5);
+ dataRow.appendChild(data6);
+ tbody.appendChild(dataRow);
+
+ let buttons = [];
+ if(installBtn) {
+ buttons.push(installBtn);
+ }
+ if(installBtn2) {
+ buttons.push(installBtn2);
+ }
+ if(installBtn3) {
+ buttons.push(installBtn3);
+ }
+
+ this.grid_rows[i] = {data:data, buttons:buttons, checkbox:checkbox, control:dataRow};
+ }
+
+ const panel = document.createElement('div');
+ panel.style.width = "100%";
+ panel.appendChild(grid);
+
+ function handleResize() {
+ const parentHeight = self.element.clientHeight;
+ const gridHeight = parentHeight - 200;
+
+ grid.style.height = gridHeight + "px";
+ }
+ window.addEventListener("resize", handleResize);
+
+ grid.style.position = "relative";
+ grid.style.display = "inline-block";
+ grid.style.width = "100%";
+ grid.style.height = "100%";
+ grid.style.overflowY = "scroll";
+ this.element.style.height = "85%";
+ this.element.style.width = "80%";
+ this.element.appendChild(panel);
+
+ handleResize();
+ }
+
+ createFilterCombo() {
+ let combo = document.createElement("select");
+
+ combo.style.cssFloat = "left";
+ combo.style.fontSize = "14px";
+ combo.style.padding = "4px";
+ combo.style.background = "black";
+ combo.style.marginLeft = "2px";
+ combo.style.width = "199px";
+ combo.id = `combo-manger-filter`;
+ combo.style.borderRadius = "15px";
+
+ let items =
+ [
+ { value:'*', text:'Filter: all' },
+ { value:'Disabled', text:'Filter: disabled' },
+ { value:'Update', text:'Filter: update' },
+ { value:'True', text:'Filter: installed' },
+ { value:'False', text:'Filter: not-installed' },
+ ];
+
+ items.forEach(item => {
+ const option = document.createElement("option");
+ option.value = item.value;
+ option.text = item.text;
+ combo.appendChild(option);
+ });
+
+ let self = this;
+ combo.addEventListener('change', function(event) {
+ self.filter = event.target.value;
+ self.apply_searchbox();
+ });
+
+ if(self.filter) {
+ combo.value = self.filter;
+ }
+
+ return combo;
+ }
+
+ createHeaderControls() {
+ let self = this;
+ this.search_box = $el('input', {type:'text', id:'manager-alternode-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
+ this.search_box.style.height = "25px";
+ this.search_box.onkeydown = (event) => {
+ if (event.key === 'Enter') {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ }
+ if (event.key === 'Escape') {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ }
+ };
+
+ let search_button = document.createElement("button");
+ search_button.innerHTML = "Search";
+ search_button.onclick = () => {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ };
+ search_button.style.display = "inline-block";
+
+ let filter_control = this.createFilterCombo();
+ filter_control.style.display = "inline-block";
+
+ let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
+ let search_control = $el('table', {width:'100%'},
+ [
+ $el('tr', {}, [cell])
+ ]
+ );
+
+ cell.style.textAlign = "right";
+ this.element.appendChild(search_control);
+ }
+
+ async createBottomControls() {
+ var close_button = document.createElement("button");
+ close_button.innerHTML = "Close";
+ close_button.onclick = () => { this.close(); }
+ close_button.style.display = "inline-block";
+
+ this.message_box = $el('div', {id:'alternatives-installer-message'}, [$el('br'), '']);
+ this.message_box.style.height = '60px';
+ this.message_box.style.verticalAlign = 'middle';
+
+ this.element.appendChild(this.message_box);
+ this.element.appendChild(close_button);
+ }
+
+ async show() {
+ try {
+ this.invalidateControl();
+ this.element.style.display = "block";
+ this.element.style.zIndex = 10001;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Failed to get alternatives list. / ${exception}`);
+ console.error(exception);
+ }
+ }
+}
diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js
index 28ac95a0..239de383 100644
--- a/js/comfyui-manager.js
+++ b/js/comfyui-manager.js
@@ -1,7 +1,35 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { ComfyDialog, $el } from "../../scripts/ui.js";
-import {ComfyWidgets} from "../../scripts/widgets.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 } from "./common.js";
+
+var style = document.createElement('style');
+style.innerHTML = `
+.cm-menu-container {
+ column-gap: 20px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.cm-menu-column {
+ display: flex;
+ flex-direction: column;
+}
+
+.cm-title {
+ padding: 10px 10px 0 10p;
+ background-color: black;
+ text-align: center;
+ height: 45px;
+}
+`;
+
+document.head.appendChild(style);
var update_comfyui_button = null;
var fetch_updates_button = null;
@@ -16,47 +44,7 @@ async function init_badge_mode() {
await init_badge_mode();
-async function getCustomnodeMappings() {
- var mode = "url";
- if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
- mode = "local";
- const response = await api.fetchApi(`/customnode/getmappings?mode=${mode}`);
-
- const data = await response.json();
- return data;
-}
-
-async function getUnresolvedNodesInComponent() {
- try {
- var mode = "url";
- if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
- mode = "local";
-
- const response = await api.fetchApi(`/component/get_unresolved`);
-
- const data = await response.json();
- return data.nodes;
- }
- catch {
- return [];
- }
-}
-
-async function getCustomNodes() {
- var mode = "url";
- if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
- mode = "local";
-
- var skip_update = "";
- if(ManagerMenuDialog.instance.update_check_checkbox.checked)
- skip_update = "&skip_update=true";
-
- const response = await api.fetchApi(`/customnode/getlist?mode=${mode}${skip_update}`);
-
- const data = await response.json();
- return data;
-}
async function fetchNicknames() {
const response1 = await api.fetchApi(`/customnode/getmappings?mode=local`);
@@ -84,85 +72,6 @@ async function fetchNicknames() {
let nicknames = await fetchNicknames();
-async function getAlterList() {
- var mode = "url";
- if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
- mode = "local";
-
- var skip_update = "";
- if(ManagerMenuDialog.instance.update_check_checkbox.checked)
- skip_update = "&skip_update=true";
-
- const response = await api.fetchApi(`/alternatives/getlist?mode=${mode}${skip_update}`);
-
- const data = await response.json();
- return data;
-}
-
-async function getModelList() {
- var mode = "url";
- if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
- mode = "local";
-
- const response = await api.fetchApi(`/externalmodel/getlist?mode=${mode}`);
-
- const data = await response.json();
- return data;
-}
-
-async function install_checked_custom_node(grid_rows, target_i, caller, mode) {
- if(caller) {
- let failed = '';
-
- caller.disableButtons();
-
- for(let i in grid_rows) {
- if(!grid_rows[i].checkbox.checked && i != target_i)
- continue;
-
- var target;
-
- if(grid_rows[i].data.custom_node) {
- target = grid_rows[i].data.custom_node;
- }
- else {
- target = grid_rows[i].data;
- }
-
- caller.startInstall(target);
-
- try {
- const response = await api.fetchApi(`/customnode/${mode}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(target)
- });
-
- if(response.status == 400) {
- app.ui.dialog.show(`${mode} failed: ${target.title}`);
- app.ui.dialog.element.style.zIndex = 9999;
- continue;
- }
-
- const status = await response.json();
- app.ui.dialog.close();
- target.installed = 'True';
- continue;
- }
- catch(exception) {
- failed += `
${target.title}`;
- }
- }
-
- if(failed != '') {
- app.ui.dialog.show(`${mode} failed: ${failed}`);
- app.ui.dialog.element.style.zIndex = 9999;
- }
-
- await caller.invalidateControl();
- caller.updateMessage('
To apply the installed/disabled/enabled custom node, please restart ComfyUI.');
- }
-}
async function updateComfyUI() {
let prev_text = update_comfyui_button.innerText;
@@ -210,7 +119,7 @@ async function fetchUpdates(update_check_checkbox) {
try {
var mode = "url";
- if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
+ if(manager_instance.local_mode_checkbox.checked)
mode = "local";
const response = await api.fetchApi(`/customnode/fetch_updates?mode=${mode}`);
@@ -253,7 +162,7 @@ async function updateAll(update_check_checkbox) {
try {
var mode = "url";
- if(ManagerMenuDialog.instance.local_mode_checkbox.checked)
+ if(manager_instance.local_mode_checkbox.checked)
mode = "local";
update_all_button.innerText = "Updating all...";
@@ -288,1514 +197,12 @@ async function updateAll(update_check_checkbox) {
}
}
-async function install_model(target) {
- if(ModelInstaller.instance) {
- ModelInstaller.instance.startInstall(target);
-
- try {
- const response = await api.fetchApi('/model/install', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(target)
- });
-
- const status = await response.json();
- app.ui.dialog.close();
- target.installed = 'True';
- return true;
- }
- catch(exception) {
- app.ui.dialog.show(`Install failed: ${target.title} / ${exception}`);
- app.ui.dialog.element.style.zIndex = 9999;
- return false;
- }
- finally {
- await ModelInstaller.instance.invalidateControl();
- ModelInstaller.instance.updateMessage("
To apply the installed model, please click the 'Refresh' button on the main menu.");
- }
- }
-}
-
-
-// -----
-class CustomNodesInstaller extends ComfyDialog {
- static instance = null;
-
- install_buttons = [];
- message_box = null;
- data = null;
-
- clear() {
- this.install_buttons = [];
- this.message_box = null;
- this.data = null;
- }
-
- constructor() {
- super();
- this.search_keyword = '';
- this.element = $el("div.comfy-modal", { parent: document.body }, []);
- }
-
- startInstall(target) {
- const self = CustomNodesInstaller.instance;
-
- self.updateMessage(`
Installing '${target.title}'`);
- }
-
- disableButtons() {
- for(let i in this.install_buttons) {
- this.install_buttons[i].disabled = true;
- this.install_buttons[i].style.backgroundColor = 'gray';
- }
- }
-
- apply_searchbox(data) {
- let keyword = this.search_box.value.toLowerCase();
- for(let i in this.grid_rows) {
- let data = this.grid_rows[i].data;
- let content = data.author.toLowerCase() + data.description.toLowerCase() + data.title.toLowerCase();
-
- if(this.filter && this.filter != '*') {
- if(this.filter != data.installed) {
- this.grid_rows[i].control.style.display = 'none';
- continue;
- }
- }
-
- if(keyword == "")
- this.grid_rows[i].control.style.display = null;
- else if(content.includes(keyword)) {
- this.grid_rows[i].control.style.display = null;
- }
- else {
- this.grid_rows[i].control.style.display = 'none';
- }
- }
- }
-
- async filter_missing_node(data) {
- const mappings = await getCustomnodeMappings();
-
-
- // build regex->url map
- const regex_to_url = [];
- for (let i in data) {
- if(data[i]['nodename_pattern']) {
- let item = {regex: new RegExp(data[i].nodename_pattern), url: data[i].files[0]};
- regex_to_url.push(item);
- }
- }
-
- // build name->url map
- const name_to_url = {};
- for (const url in mappings) {
- const names = mappings[url];
- for(const name in names[0]) {
- name_to_url[names[0][name]] = url;
- }
- }
-
- const registered_nodes = new Set();
- for (let i in LiteGraph.registered_node_types) {
- registered_nodes.add(LiteGraph.registered_node_types[i].type);
- }
-
- const missing_nodes = new Set();
- const nodes = app.graph.serialize().nodes;
- for (let i in nodes) {
- const node_type = nodes[i].type;
- if (!registered_nodes.has(node_type)) {
- const url = name_to_url[node_type.trim()];
- if(url)
- missing_nodes.add(url);
- else {
- for(let j in regex_to_url) {
- if(regex_to_url[j].regex.test(node_type)) {
- missing_nodes.add(regex_to_url[j].url);
- }
- }
- }
- }
- }
-
- let unresolved_nodes = await getUnresolvedNodesInComponent();
- for (let i in unresolved_nodes) {
- let node_type = unresolved_nodes[i];
- const url = name_to_url[node_type];
- if(url)
- missing_nodes.add(url);
- }
-
- return data.filter(node => node.files.some(file => missing_nodes.has(file)));
- }
-
- async invalidateControl() {
- this.clear();
-
- // splash
- while (this.element.children.length) {
- this.element.removeChild(this.element.children[0]);
- }
-
- const msg = $el('div', {id:'custom-message'},
- [$el('br'),
- 'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
- $el('br'),
- 'NOTE: Update only checks for extensions that have been fetched.',
- $el('br')]);
- msg.style.height = '100px';
- msg.style.verticalAlign = 'middle';
- msg.style.color = "var(--fg-color)";
-
- this.element.appendChild(msg);
-
- // invalidate
- this.data = (await getCustomNodes()).custom_nodes;
-
- if(this.is_missing_node_mode)
- this.data = await this.filter_missing_node(this.data);
-
- this.element.removeChild(msg);
-
- while (this.element.children.length) {
- this.element.removeChild(this.element.children[0]);
- }
-
- this.createHeaderControls();
- await this.createGrid();
- this.apply_searchbox(this.data);
- this.createBottomControls();
- }
-
- updateMessage(msg) {
- this.message_box.innerHTML = msg;
- }
-
- invalidate_checks(is_checked, install_state) {
- if(is_checked) {
- for(let i in this.grid_rows) {
- let data = this.grid_rows[i].data;
- let checkbox = this.grid_rows[i].checkbox;
- let buttons = this.grid_rows[i].buttons;
-
- checkbox.disabled = data.installed != install_state;
-
- if(checkbox.disabled) {
- for(let j in buttons) {
- buttons[j].style.display = 'none';
- }
- }
- else {
- for(let j in buttons) {
- buttons[j].style.display = null;
- }
- }
- }
-
- this.checkbox_all.disabled = false;
- }
- else {
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- if(checkbox.check)
- return; // do nothing
- }
-
- // every checkbox is unchecked -> enable all checkbox
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- let buttons = this.grid_rows[i].buttons;
- checkbox.disabled = false;
-
- for(let j in buttons) {
- buttons[j].style.display = null;
- }
- }
-
- this.checkbox_all.checked = false;
- this.checkbox_all.disabled = true;
- }
- }
-
- check_all(is_checked) {
- if(is_checked) {
- // lookup first checked item's state
- let check_state = null;
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- if(checkbox.checked) {
- check_state = this.grid_rows[i].data.installed;
- }
- }
-
- if(check_state == null)
- return;
-
- // check only same state items
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- if(this.grid_rows[i].data.installed == check_state)
- checkbox.checked = true;
- }
- }
- else {
- // uncheck all
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- let buttons = this.grid_rows[i].buttons;
- checkbox.checked = false;
- checkbox.disabled = false;
-
- for(let j in buttons) {
- buttons[j].style.display = null;
- }
- }
-
- this.checkbox_all.disabled = true;
- }
- }
-
- async createGrid() {
- var grid = document.createElement('table');
- grid.setAttribute('id', 'custom-nodes-grid');
-
- this.grid_rows = {};
-
- let self = this;
-
- var thead = document.createElement('thead');
- var tbody = document.createElement('tbody');
-
- var headerRow = document.createElement('tr');
- thead.style.position = "sticky";
- thead.style.top = "0px";
- thead.style.borderCollapse = "collapse";
- thead.style.tableLayout = "fixed";
-
- var header0 = document.createElement('th');
- header0.style.width = "20px";
- this.checkbox_all = $el("input",{type:'checkbox', id:'check_all'},[]);
- header0.appendChild(this.checkbox_all);
- this.checkbox_all.checked = false;
- this.checkbox_all.disabled = true;
- this.checkbox_all.addEventListener('change', function() { self.check_all.call(self, self.checkbox_all.checked); });
-
- var header1 = document.createElement('th');
- header1.innerHTML = ' ID ';
- header1.style.width = "20px";
- var header2 = document.createElement('th');
- header2.innerHTML = 'Author';
- header2.style.width = "150px";
- var header3 = document.createElement('th');
- header3.innerHTML = 'Name';
- header3.style.width = "20%";
- var header4 = document.createElement('th');
- header4.innerHTML = 'Description';
- header4.style.width = "60%";
-// header4.classList.add('expandable-column');
- var header5 = document.createElement('th');
- header5.innerHTML = 'Install';
- header5.style.width = "130px";
-
- header0.style.position = "sticky";
- header0.style.top = "0px";
- header1.style.position = "sticky";
- header1.style.top = "0px";
- header2.style.position = "sticky";
- header2.style.top = "0px";
- header3.style.position = "sticky";
- header3.style.top = "0px";
- header4.style.position = "sticky";
- header4.style.top = "0px";
- header5.style.position = "sticky";
- header5.style.top = "0px";
-
- thead.appendChild(headerRow);
- headerRow.appendChild(header0);
- headerRow.appendChild(header1);
- headerRow.appendChild(header2);
- headerRow.appendChild(header3);
- headerRow.appendChild(header4);
- headerRow.appendChild(header5);
-
- headerRow.style.backgroundColor = "Black";
- headerRow.style.color = "White";
- headerRow.style.textAlign = "center";
- headerRow.style.width = "100%";
- headerRow.style.padding = "0";
-
- grid.appendChild(thead);
- grid.appendChild(tbody);
-
- if(this.data)
- for (var i = 0; i < this.data.length; i++) {
- const data = this.data[i];
- let dataRow = document.createElement('tr');
-
- let data0 = document.createElement('td');
- let checkbox = $el("input",{type:'checkbox', id:`check_${i}`},[]);
- data0.appendChild(checkbox);
- checkbox.checked = false;
- checkbox.addEventListener('change', function() { self.invalidate_checks.call(self, checkbox.checked, data.installed); });
-
- var data1 = document.createElement('td');
- data1.style.textAlign = "center";
- data1.innerHTML = i+1;
- var data2 = document.createElement('td');
- data2.style.maxWidth = "100px";
- data2.className = "cm-node-author"
- data2.textContent = ` ${data.author}`;
- data2.style.whiteSpace = "nowrap";
- data2.style.overflow = "hidden";
- data2.style.textOverflow = "ellipsis";
- var data3 = document.createElement('td');
- data3.style.maxWidth = "200px";
- data3.style.wordWrap = "break-word";
- data3.className = "cm-node-name"
- data3.innerHTML = ` ${data.title}`;
- var data4 = document.createElement('td');
- data4.innerHTML = data.description;
- data4.className = "cm-node-desc"
- var data5 = document.createElement('td');
- data5.style.textAlign = "center";
-
- var installBtn = document.createElement('button');
- installBtn.className = "cm-btn-install";
- var installBtn2 = null;
- var installBtn3 = null;
-
- this.install_buttons.push(installBtn);
-
- switch(data.installed) {
- case 'Disabled':
- installBtn3 = document.createElement('button');
- installBtn3.innerHTML = 'Enable';
- installBtn3.className = "cm-btn-enable";
- installBtn3.style.backgroundColor = 'blue';
- installBtn3.style.color = 'white';
- this.install_buttons.push(installBtn3);
-
- installBtn.innerHTML = 'Uninstall';
- installBtn.style.backgroundColor = 'red';
- break;
- case 'Update':
- installBtn2 = document.createElement('button');
- installBtn2.innerHTML = 'Update';
- installBtn2.className = "cm-btn-update";
- installBtn2.style.backgroundColor = 'blue';
- installBtn2.style.color = 'white';
- this.install_buttons.push(installBtn2);
-
- installBtn3 = document.createElement('button');
- installBtn3.innerHTML = 'Disable';
- installBtn3.className = "cm-btn-disable";
- installBtn3.style.backgroundColor = 'MediumSlateBlue';
- installBtn3.style.color = 'white';
- this.install_buttons.push(installBtn3);
-
- installBtn.innerHTML = 'Uninstall';
- installBtn.style.backgroundColor = 'red';
- break;
- case 'True':
- installBtn3 = document.createElement('button');
- installBtn3.innerHTML = 'Disable';
- installBtn3.className = "cm-btn-disable";
- installBtn3.style.backgroundColor = 'MediumSlateBlue';
- installBtn3.style.color = 'white';
- this.install_buttons.push(installBtn3);
-
- installBtn.innerHTML = 'Uninstall';
- installBtn.style.backgroundColor = 'red';
- break;
- case 'False':
- installBtn.innerHTML = 'Install';
- installBtn.style.backgroundColor = 'black';
- installBtn.style.color = 'white';
- break;
- default:
- installBtn.innerHTML = 'Try Install';
- installBtn.style.backgroundColor = 'Gray';
- installBtn.style.color = 'white';
- }
-
- let j = i;
- if(installBtn2 != null) {
- installBtn2.style.width = "120px";
- installBtn2.addEventListener('click', function() {
- install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'update');
- });
-
- data5.appendChild(installBtn2);
- }
-
- if(installBtn3 != null) {
- installBtn3.style.width = "120px";
- installBtn3.addEventListener('click', function() {
- install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'toggle_active');
- });
-
- data5.appendChild(installBtn3);
- }
-
- installBtn.style.width = "120px";
- installBtn.addEventListener('click', function() {
- if(this.innerHTML == 'Uninstall') {
- if (confirm(`Are you sure uninstall ${data.title}?`)) {
- install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'uninstall');
- }
- }
- else {
- install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'install');
- }
- });
-
- data5.appendChild(installBtn);
-
- dataRow.style.backgroundColor = "var(--bg-color)";
- dataRow.style.color = "var(--fg-color)";
- dataRow.style.textAlign = "left";
-
- dataRow.appendChild(data0);
- dataRow.appendChild(data1);
- dataRow.appendChild(data2);
- dataRow.appendChild(data3);
- dataRow.appendChild(data4);
- dataRow.appendChild(data5);
- tbody.appendChild(dataRow);
-
- let buttons = [];
- if(installBtn) {
- buttons.push(installBtn);
- }
- if(installBtn2) {
- buttons.push(installBtn2);
- }
- if(installBtn3) {
- buttons.push(installBtn3);
- }
-
- this.grid_rows[i] = {data:data, buttons:buttons, checkbox:checkbox, control:dataRow};
- }
-
- const panel = document.createElement('div');
- panel.style.width = "100%";
- panel.appendChild(grid);
-
- function handleResize() {
- const parentHeight = self.element.clientHeight;
- const gridHeight = parentHeight - 200;
-
- grid.style.height = gridHeight + "px";
- }
- window.addEventListener("resize", handleResize);
-
- grid.style.position = "relative";
- grid.style.display = "inline-block";
- grid.style.width = "100%";
- grid.style.height = "100%";
- grid.style.overflowY = "scroll";
- this.element.style.height = "85%";
- this.element.style.width = "80%";
- this.element.appendChild(panel);
-
- handleResize();
- }
-
- createFilterCombo() {
- let combo = document.createElement("select");
-
- combo.style.cssFloat = "left";
- combo.style.fontSize = "14px";
- combo.style.padding = "4px";
- combo.style.background = "black";
- combo.style.marginLeft = "2px";
- combo.style.width = "199px";
- combo.id = `combo-manger-filter`;
- combo.style.borderRadius = "15px";
-
- let items =
- [
- { value:'*', text:'Filter: all' },
- { value:'Disabled', text:'Filter: disabled' },
- { value:'Update', text:'Filter: update' },
- { value:'True', text:'Filter: installed' },
- { value:'False', text:'Filter: not-installed' },
- ];
-
- items.forEach(item => {
- const option = document.createElement("option");
- option.value = item.value;
- option.text = item.text;
- combo.appendChild(option);
- });
-
- let self = this;
- combo.addEventListener('change', function(event) {
- self.filter = event.target.value;
- self.apply_searchbox();
- });
-
- if(self.filter) {
- combo.value = self.filter;
- }
-
- return combo;
- }
-
- createHeaderControls() {
- let self = this;
- this.search_box = $el('input', {type:'text', id:'manager-customnode-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
- this.search_box.style.height = "25px";
- this.search_box.onkeydown = (event) => {
- if (event.key === 'Enter') {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- }
- if (event.key === 'Escape') {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- }
- };
-
-
- let search_button = document.createElement("button");
- search_button.innerHTML = "Search";
- search_button.onclick = () => {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- };
- search_button.style.display = "inline-block";
-
- let filter_control = this.createFilterCombo();
- filter_control.style.display = "inline-block";
-
- let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
- let search_control = $el('table', {width:'100%'},
- [
- $el('tr', {}, [cell])
- ]
- );
-
- cell.style.textAlign = "right";
-
- this.element.appendChild(search_control);
- }
-
- async createBottomControls() {
- let close_button = document.createElement("button");
- close_button.innerHTML = "Close";
- close_button.onclick = () => { this.close(); }
- close_button.style.display = "inline-block";
-
- this.message_box = $el('div', {id:'custom-installer-message'}, [$el('br'), '']);
- this.message_box.style.height = '60px';
- this.message_box.style.verticalAlign = 'middle';
-
- this.element.appendChild(this.message_box);
- this.element.appendChild(close_button);
- }
-
- async show(is_missing_node_mode) {
- this.is_missing_node_mode = is_missing_node_mode;
- try {
- this.invalidateControl();
-
- this.element.style.display = "block";
- }
- catch(exception) {
- app.ui.dialog.show(`Failed to get custom node list. / ${exception}`);
- }
- }
-}
-
-// -----
-class AlternativesInstaller extends ComfyDialog {
- static instance = null;
-
- install_buttons = [];
- message_box = null;
- data = null;
-
- clear() {
- this.install_buttons = [];
- this.message_box = null;
- this.data = null;
- }
-
- constructor() {
- super();
- this.search_keyword = '';
- this.element = $el("div.comfy-modal", { parent: document.body }, []);
- }
-
- startInstall(target) {
- const self = AlternativesInstaller.instance;
-
- self.updateMessage(`
Installing '${target.title}'`);
- }
-
- disableButtons() {
- for(let i in this.install_buttons) {
- this.install_buttons[i].disabled = true;
- this.install_buttons[i].style.backgroundColor = 'gray';
- }
- }
-
- apply_searchbox(data) {
- let keyword = this.search_box.value.toLowerCase();
- for(let i in this.grid_rows) {
- let data1 = this.grid_rows[i].data;
- let data2 = data1.custom_node;
-
- if(!data2)
- continue;
-
- let content = data1.tags.toLowerCase() + data1.description.toLowerCase() + data2.author.toLowerCase() + data2.description.toLowerCase() + data2.title.toLowerCase();
-
- if(this.filter && this.filter != '*') {
- if(this.filter != data2.installed) {
- this.grid_rows[i].control.style.display = 'none';
- continue;
- }
- }
-
- if(keyword == "")
- this.grid_rows[i].control.style.display = null;
- else if(content.includes(keyword)) {
- this.grid_rows[i].control.style.display = null;
- }
- else {
- this.grid_rows[i].control.style.display = 'none';
- }
- }
- }
-
- async invalidateControl() {
- this.clear();
-
- // splash
- while (this.element.children.length) {
- this.element.removeChild(this.element.children[0]);
- }
-
- const msg = $el('div', {id:'custom-message'},
- [$el('br'),
- 'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
- $el('br'),
- 'NOTE: Update only checks for extensions that have been fetched.',
- $el('br')]);
- msg.style.height = '100px';
- msg.style.verticalAlign = 'middle';
- this.element.appendChild(msg);
-
- // invalidate
- this.data = (await getAlterList()).items;
-
- this.element.removeChild(msg);
-
- while (this.element.children.length) {
- this.element.removeChild(this.element.children[0]);
- }
-
- this.createHeaderControls();
- await this.createGrid();
- this.apply_searchbox(this.data);
- this.createBottomControls();
- }
-
- updateMessage(msg) {
- this.message_box.innerHTML = msg;
- }
-
- invalidate_checks(is_checked, install_state) {
- if(is_checked) {
- for(let i in this.grid_rows) {
- let data = this.grid_rows[i].data;
- let checkbox = this.grid_rows[i].checkbox;
- let buttons = this.grid_rows[i].buttons;
-
- checkbox.disabled = data.custom_node.installed != install_state;
-
- if(checkbox.disabled) {
- for(let j in buttons) {
- buttons[j].style.display = 'none';
- }
- }
- else {
- for(let j in buttons) {
- buttons[j].style.display = null;
- }
- }
- }
-
- this.checkbox_all.disabled = false;
- }
- else {
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- if(checkbox.check)
- return; // do nothing
- }
-
- // every checkbox is unchecked -> enable all checkbox
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- let buttons = this.grid_rows[i].buttons;
- checkbox.disabled = false;
-
- for(let j in buttons) {
- buttons[j].style.display = null;
- }
- }
-
- this.checkbox_all.checked = false;
- this.checkbox_all.disabled = true;
- }
- }
-
- check_all(is_checked) {
- if(is_checked) {
- // lookup first checked item's state
- let check_state = null;
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- if(checkbox.checked) {
- check_state = this.grid_rows[i].data.custom_node.installed;
- }
- }
-
- if(check_state == null)
- return;
-
- // check only same state items
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- if(this.grid_rows[i].data.custom_node.installed == check_state)
- checkbox.checked = true;
- }
- }
- else {
- // uncheck all
- for(let i in this.grid_rows) {
- let checkbox = this.grid_rows[i].checkbox;
- let buttons = this.grid_rows[i].buttons;
- checkbox.checked = false;
- checkbox.disabled = false;
-
- for(let j in buttons) {
- buttons[j].style.display = null;
- }
- }
-
- this.checkbox_all.disabled = true;
- }
- }
-
- async createGrid() {
- var grid = document.createElement('table');
- grid.setAttribute('id', 'alternatives-grid');
-
- this.grid_rows = {};
-
- let self = this;
-
- var thead = document.createElement('thead');
- var tbody = document.createElement('tbody');
-
- var headerRow = document.createElement('tr');
- thead.style.position = "sticky";
- thead.style.top = "0px";
- thead.style.borderCollapse = "collapse";
- thead.style.tableLayout = "fixed";
-
- var header0 = document.createElement('th');
- header0.style.width = "20px";
- this.checkbox_all = $el("input",{type:'checkbox', id:'check_all'},[]);
- header0.appendChild(this.checkbox_all);
- this.checkbox_all.checked = false;
- this.checkbox_all.disabled = true;
- this.checkbox_all.addEventListener('change', function() { self.check_all.call(self, self.checkbox_all.checked); });
-
- var header1 = document.createElement('th');
- header1.innerHTML = ' ID ';
- header1.style.width = "20px";
- var header2 = document.createElement('th');
- header2.innerHTML = 'Tags';
- header2.style.width = "10%";
- var header3 = document.createElement('th');
- header3.innerHTML = 'Author';
- header3.style.width = "150px";
- var header4 = document.createElement('th');
- header4.innerHTML = 'Title';
- header4.style.width = "20%";
- var header5 = document.createElement('th');
- header5.innerHTML = 'Description';
- header5.style.width = "50%";
- var header6 = document.createElement('th');
- header6.innerHTML = 'Install';
- header6.style.width = "130px";
-
- header1.style.position = "sticky";
- header1.style.top = "0px";
- header2.style.position = "sticky";
- header2.style.top = "0px";
- header3.style.position = "sticky";
- header3.style.top = "0px";
- header4.style.position = "sticky";
- header4.style.top = "0px";
- header5.style.position = "sticky";
- header5.style.top = "0px";
-
- thead.appendChild(headerRow);
- headerRow.appendChild(header0);
- headerRow.appendChild(header1);
- headerRow.appendChild(header2);
- headerRow.appendChild(header3);
- headerRow.appendChild(header4);
- headerRow.appendChild(header5);
- headerRow.appendChild(header6);
-
- headerRow.style.backgroundColor = "Black";
- headerRow.style.color = "White";
- headerRow.style.textAlign = "center";
- headerRow.style.width = "100%";
- headerRow.style.padding = "0";
-
- grid.appendChild(thead);
- grid.appendChild(tbody);
-
- if(this.data)
- for (var i = 0; i < this.data.length; i++) {
- const data = this.data[i];
- var dataRow = document.createElement('tr');
-
- let data0 = document.createElement('td');
- let checkbox = $el("input",{type:'checkbox', id:`check_${i}`},[]);
- data0.appendChild(checkbox);
- checkbox.checked = false;
- checkbox.addEventListener('change', function() { self.invalidate_checks.call(self, checkbox.checked, data.custom_node?.installed); });
-
- var data1 = document.createElement('td');
- data1.style.textAlign = "center";
- data1.innerHTML = i+1;
- var data2 = document.createElement('td');
- data2.innerHTML = ` ${data.tags}`;
- var data3 = document.createElement('td');
- var data4 = document.createElement('td');
- if(data.custom_node) {
- data3.innerHTML = ` ${data.custom_node.author}`;
- data4.innerHTML = ` ${data.custom_node.title}`;
- }
- else {
- data3.innerHTML = ` Unknown`;
- data4.innerHTML = ` Unknown`;
- }
- var data5 = document.createElement('td');
- data5.innerHTML = data.description;
- var data6 = document.createElement('td');
- data6.style.textAlign = "center";
-
- var installBtn = document.createElement('button');
- var installBtn2 = null;
- var installBtn3 = null;
-
- if(data.custom_node) {
- this.install_buttons.push(installBtn);
-
- switch(data.custom_node.installed) {
- case 'Disabled':
- installBtn3 = document.createElement('button');
- installBtn3.innerHTML = 'Enable';
- installBtn3.style.backgroundColor = 'blue';
- installBtn3.style.color = 'white';
- this.install_buttons.push(installBtn3);
-
- installBtn.innerHTML = 'Uninstall';
- installBtn.style.backgroundColor = 'red';
- installBtn.style.color = 'white';
- break;
- case 'Update':
- installBtn2 = document.createElement('button');
- installBtn2.innerHTML = 'Update';
- installBtn2.style.backgroundColor = 'blue';
- installBtn2.style.color = 'white';
- this.install_buttons.push(installBtn2);
-
- installBtn3 = document.createElement('button');
- installBtn3.innerHTML = 'Disable';
- installBtn3.style.backgroundColor = 'MediumSlateBlue';
- installBtn3.style.color = 'white';
- this.install_buttons.push(installBtn3);
-
- installBtn.innerHTML = 'Uninstall';
- installBtn.style.backgroundColor = 'red';
- installBtn.style.color = 'white';
- break;
- case 'True':
- installBtn3 = document.createElement('button');
- installBtn3.innerHTML = 'Disable';
- installBtn3.style.backgroundColor = 'MediumSlateBlue';
- installBtn3.style.color = 'white';
- this.install_buttons.push(installBtn3);
-
- installBtn.innerHTML = 'Uninstall';
- installBtn.style.backgroundColor = 'red';
- installBtn.style.color = 'white';
- break;
- case 'False':
- installBtn.innerHTML = 'Install';
- installBtn.style.backgroundColor = 'black';
- installBtn.style.color = 'white';
- break;
- default:
- installBtn.innerHTML = 'Try Install';
- installBtn.style.backgroundColor = 'Gray';
- installBtn.style.color = 'white';
- }
-
- let j = i;
- if(installBtn2 != null) {
- installBtn2.style.width = "120px";
- installBtn2.addEventListener('click', function() {
- install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'update');
- });
-
- data6.appendChild(installBtn2);
- }
-
- if(installBtn3 != null) {
- installBtn3.style.width = "120px";
- installBtn3.addEventListener('click', function() {
- install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'toggle_active');
- });
-
- data6.appendChild(installBtn3);
- }
-
-
- installBtn.style.width = "120px";
- installBtn.addEventListener('click', function() {
- if(this.innerHTML == 'Uninstall') {
- if (confirm(`Are you sure uninstall ${data.title}?`)) {
- install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'uninstall');
- }
- }
- else {
- install_checked_custom_node(self.grid_rows, j, AlternativesInstaller.instance, 'install');
- }
- });
-
- data6.appendChild(installBtn);
- }
-
- dataRow.style.backgroundColor = "var(--bg-color)";
- dataRow.style.color = "var(--fg-color)";
- dataRow.style.textAlign = "left";
-
- dataRow.appendChild(data0);
- dataRow.appendChild(data1);
- dataRow.appendChild(data2);
- dataRow.appendChild(data3);
- dataRow.appendChild(data4);
- dataRow.appendChild(data5);
- dataRow.appendChild(data6);
- tbody.appendChild(dataRow);
-
- let buttons = [];
- if(installBtn) {
- buttons.push(installBtn);
- }
- if(installBtn2) {
- buttons.push(installBtn2);
- }
- if(installBtn3) {
- buttons.push(installBtn3);
- }
-
- this.grid_rows[i] = {data:data, buttons:buttons, checkbox:checkbox, control:dataRow};
- }
-
- const panel = document.createElement('div');
- panel.style.width = "100%";
- panel.appendChild(grid);
-
- function handleResize() {
- const parentHeight = self.element.clientHeight;
- const gridHeight = parentHeight - 200;
-
- grid.style.height = gridHeight + "px";
- }
- window.addEventListener("resize", handleResize);
-
- grid.style.position = "relative";
- grid.style.display = "inline-block";
- grid.style.width = "100%";
- grid.style.height = "100%";
- grid.style.overflowY = "scroll";
- this.element.style.height = "85%";
- this.element.style.width = "80%";
- this.element.appendChild(panel);
-
- handleResize();
- }
-
- createFilterCombo() {
- let combo = document.createElement("select");
-
- combo.style.cssFloat = "left";
- combo.style.fontSize = "14px";
- combo.style.padding = "4px";
- combo.style.background = "black";
- combo.style.marginLeft = "2px";
- combo.style.width = "199px";
- combo.id = `combo-manger-filter`;
- combo.style.borderRadius = "15px";
-
- let items =
- [
- { value:'*', text:'Filter: all' },
- { value:'Disabled', text:'Filter: disabled' },
- { value:'Update', text:'Filter: update' },
- { value:'True', text:'Filter: installed' },
- { value:'False', text:'Filter: not-installed' },
- ];
-
- items.forEach(item => {
- const option = document.createElement("option");
- option.value = item.value;
- option.text = item.text;
- combo.appendChild(option);
- });
-
- let self = this;
- combo.addEventListener('change', function(event) {
- self.filter = event.target.value;
- self.apply_searchbox();
- });
-
- if(self.filter) {
- combo.value = self.filter;
- }
-
- return combo;
- }
-
- createHeaderControls() {
- let self = this;
- this.search_box = $el('input', {type:'text', id:'manager-alternode-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
- this.search_box.style.height = "25px";
- this.search_box.onkeydown = (event) => {
- if (event.key === 'Enter') {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- }
- if (event.key === 'Escape') {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- }
- };
-
- let search_button = document.createElement("button");
- search_button.innerHTML = "Search";
- search_button.onclick = () => {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- };
- search_button.style.display = "inline-block";
-
- let filter_control = this.createFilterCombo();
- filter_control.style.display = "inline-block";
-
- let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
- let search_control = $el('table', {width:'100%'},
- [
- $el('tr', {}, [cell])
- ]
- );
-
- cell.style.textAlign = "right";
- this.element.appendChild(search_control);
- }
-
- async createBottomControls() {
- var close_button = document.createElement("button");
- close_button.innerHTML = "Close";
- close_button.onclick = () => { this.close(); }
- close_button.style.display = "inline-block";
-
- this.message_box = $el('div', {id:'alternatives-installer-message'}, [$el('br'), '']);
- this.message_box.style.height = '60px';
- this.message_box.style.verticalAlign = 'middle';
-
- this.element.appendChild(this.message_box);
- this.element.appendChild(close_button);
- }
-
- async show() {
- try {
- this.invalidateControl();
- this.element.style.display = "block";
- }
- catch(exception) {
- app.ui.dialog.show(`Failed to get alternatives list. / ${exception}`);
- console.error(exception);
- }
- }
-}
-
-
-// -----------
-class ModelInstaller extends ComfyDialog {
- static instance = null;
-
- install_buttons = [];
- message_box = null;
- data = null;
-
- clear() {
- this.install_buttons = [];
- this.message_box = null;
- this.data = null;
- }
-
- constructor() {
- super();
- this.search_keyword = '';
- this.element = $el("div.comfy-modal", { parent: document.body }, []);
- }
-
- createControls() {
- return [
- $el("button", {
- type: "button",
- textContent: "Close",
- onclick: () => { this.close(); }
- })
- ];
- }
-
- startInstall(target) {
- const self = ModelInstaller.instance;
-
- self.updateMessage(`
Installing '${target.name}'`);
-
- for(let i in self.install_buttons) {
- self.install_buttons[i].disabled = true;
- self.install_buttons[i].style.backgroundColor = 'gray';
- }
- }
-
- apply_searchbox(data) {
- let keyword = this.search_box.value.toLowerCase();
- for(let i in this.grid_rows) {
- let data = this.grid_rows[i].data;
- let content = data.name.toLowerCase() + data.type.toLowerCase() + data.base.toLowerCase() + data.description.toLowerCase();
-
- if(this.filter && this.filter != '*') {
- if(this.filter != data.installed) {
- this.grid_rows[i].control.style.display = 'none';
- continue;
- }
- }
-
- if(keyword == "")
- this.grid_rows[i].control.style.display = null;
- else if(content.includes(keyword)) {
- this.grid_rows[i].control.style.display = null;
- }
- else {
- this.grid_rows[i].control.style.display = 'none';
- }
- }
- }
-
- async invalidateControl() {
- this.clear();
- this.data = (await getModelList()).models;
-
- while (this.element.children.length) {
- this.element.removeChild(this.element.children[0]);
- }
-
- await this.createHeaderControls();
-
- if(this.search_keyword) {
- this.search_box.value = this.search_keyword;
- }
-
- await this.createGrid();
- await this.createBottomControls();
-
- this.apply_searchbox(this.data);
- }
-
- updateMessage(msg) {
- this.message_box.innerHTML = msg;
- }
-
- async createGrid(models_json) {
- var grid = document.createElement('table');
- grid.setAttribute('id', 'external-models-grid');
-
- var thead = document.createElement('thead');
- var tbody = document.createElement('tbody');
-
- var headerRow = document.createElement('tr');
- thead.style.position = "sticky";
- thead.style.top = "0px";
- thead.style.borderCollapse = "collapse";
- thead.style.tableLayout = "fixed";
-
- var header1 = document.createElement('th');
- header1.innerHTML = ' ID ';
- header1.style.width = "20px";
- var header2 = document.createElement('th');
- header2.innerHTML = 'Type';
- header2.style.width = "100px";
- var header3 = document.createElement('th');
- header3.innerHTML = 'Base';
- header3.style.width = "100px";
- var header4 = document.createElement('th');
- header4.innerHTML = 'Name';
- header4.style.width = "30%";
- var header5 = document.createElement('th');
- header5.innerHTML = 'Filename';
- header5.style.width = "20%";
- header5.style.tableLayout = "fixed";
- var header6 = document.createElement('th');
- header6.innerHTML = 'Description';
- header6.style.width = "50%";
- var header_down = document.createElement('th');
- header_down.innerHTML = 'Download';
- header_down.style.width = "50px";
-
- thead.appendChild(headerRow);
- headerRow.appendChild(header1);
- headerRow.appendChild(header2);
- headerRow.appendChild(header3);
- headerRow.appendChild(header4);
- headerRow.appendChild(header5);
- headerRow.appendChild(header6);
- headerRow.appendChild(header_down);
-
- headerRow.style.backgroundColor = "Black";
- headerRow.style.color = "White";
- headerRow.style.textAlign = "center";
- headerRow.style.width = "100%";
- headerRow.style.padding = "0";
-
- grid.appendChild(thead);
- grid.appendChild(tbody);
-
- this.grid_rows = {};
-
- if(this.data)
- for (var i = 0; i < this.data.length; i++) {
- const data = this.data[i];
- var dataRow = document.createElement('tr');
- var data1 = document.createElement('td');
- data1.style.textAlign = "center";
- data1.innerHTML = i+1;
- var data2 = document.createElement('td');
- data2.innerHTML = ` ${data.type}`;
- var data3 = document.createElement('td');
- data3.innerHTML = ` ${data.base}`;
- var data4 = document.createElement('td');
- data4.className = "cm-node-name";
- data4.innerHTML = ` ${data.name}`;
- var data5 = document.createElement('td');
- data5.className = "cm-node-filename";
- data5.innerHTML = ` ${data.filename}`;
- data5.style.wordBreak = "break-all";
- var data6 = document.createElement('td');
- data6.className = "cm-node-desc";
- data6.innerHTML = data.description;
- data6.style.wordBreak = "break-all";
- var data_install = document.createElement('td');
- var installBtn = document.createElement('button');
- data_install.style.textAlign = "center";
-
- installBtn.innerHTML = 'Install';
- this.install_buttons.push(installBtn);
-
- switch(data.installed) {
- case 'True':
- installBtn.innerHTML = 'Installed';
- installBtn.style.backgroundColor = 'green';
- installBtn.style.color = 'white';
- installBtn.disabled = true;
- break;
- default:
- installBtn.innerHTML = 'Install';
- installBtn.style.backgroundColor = 'black';
- installBtn.style.color = 'white';
- break;
- }
-
- installBtn.style.width = "100px";
-
- installBtn.addEventListener('click', function() {
- install_model(data);
- });
-
- data_install.appendChild(installBtn);
-
- dataRow.style.backgroundColor = "var(--bg-color)";
- dataRow.style.color = "var(--fg-color)";
- dataRow.style.textAlign = "left";
-
- dataRow.appendChild(data1);
- dataRow.appendChild(data2);
- dataRow.appendChild(data3);
- dataRow.appendChild(data4);
- dataRow.appendChild(data5);
- dataRow.appendChild(data6);
- dataRow.appendChild(data_install);
- tbody.appendChild(dataRow);
-
- this.grid_rows[i] = {data:data, control:dataRow};
- }
-
- let self = this;
- const panel = document.createElement('div');
- panel.style.width = "100%";
- panel.appendChild(grid);
-
- function handleResize() {
- const parentHeight = self.element.clientHeight;
- const gridHeight = parentHeight - 200;
-
- grid.style.height = gridHeight + "px";
- }
- window.addEventListener("resize", handleResize);
-
- grid.style.position = "relative";
- grid.style.display = "inline-block";
- grid.style.width = "100%";
- grid.style.height = "100%";
- grid.style.overflowY = "scroll";
- this.element.style.height = "85%";
- this.element.style.width = "80%";
- this.element.appendChild(panel);
-
- handleResize();
- }
-
- createFilterCombo() {
- let combo = document.createElement("select");
-
- combo.style.cssFloat = "left";
- combo.style.fontSize = "14px";
- combo.style.padding = "4px";
- combo.style.background = "black";
- combo.style.marginLeft = "2px";
- combo.style.width = "199px";
- combo.id = `combo-manger-filter`;
- combo.style.borderRadius = "15px";
-
- let items =
- [
- { value:'*', text:'Filter: all' },
- { value:'True', text:'Filter: installed' },
- { value:'False', text:'Filter: not-installed' },
- ];
-
- items.forEach(item => {
- const option = document.createElement("option");
- option.value = item.value;
- option.text = item.text;
- combo.appendChild(option);
- });
-
- let self = this;
- combo.addEventListener('change', function(event) {
- self.filter = event.target.value;
- self.apply_searchbox();
- });
-
- return combo;
- }
-
- createHeaderControls() {
- let self = this;
- this.search_box = $el('input', {type:'text', id:'manager-model-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
- this.search_box.style.height = "25px";
- this.search_box.onkeydown = (event) => {
- if (event.key === 'Enter') {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- }
- if (event.key === 'Escape') {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- }
- };
-
- let search_button = document.createElement("button");
- search_button.innerHTML = "Search";
- search_button.onclick = () => {
- self.search_keyword = self.search_box.value;
- self.apply_searchbox();
- };
- search_button.style.display = "inline-block";
-
- let filter_control = this.createFilterCombo();
- filter_control.style.display = "inline-block";
-
- let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
- let search_control = $el('table', {width:'100%'},
- [
- $el('tr', {}, [cell])
- ]
- );
-
- cell.style.textAlign = "right";
- this.element.appendChild(search_control);
- }
-
- async createBottomControls() {
- var close_button = document.createElement("button");
- close_button.innerHTML = "Close";
- close_button.onclick = () => { this.close(); }
- close_button.style.display = "inline-block";
-
- this.message_box = $el('div', {id:'custom-download-message'}, [$el('br'), '']);
- this.message_box.style.height = '60px';
- this.message_box.style.verticalAlign = 'middle';
-
- this.element.appendChild(this.message_box);
- this.element.appendChild(close_button);
- }
-
- async show() {
- try {
- this.invalidateControl();
- this.element.style.display = "block";
- }
- catch(exception) {
- app.ui.dialog.show(`Failed to get external model list. / ${exception}`);
- }
- }
-}
-
// -----------
class ManagerMenuDialog extends ComfyDialog {
- static instance = null;
local_mode_checkbox = null;
- createButtons() {
- this.local_mode_checkbox = $el("input",{type:'checkbox', id:"use_local_db"},[])
- const checkbox_text = $el("label",{},[" Use local DB"])
- checkbox_text.style.color = "var(--fg-color)";
- checkbox_text.style.marginRight = "10px";
-
- this.update_check_checkbox = $el("input",{type:'checkbox', id:"skip_update_check"},[])
- const uc_checkbox_text = $el("label",{},[" Skip update check"])
- uc_checkbox_text.style.color = "var(--fg-color)";
- this.update_check_checkbox.checked = true;
-
+ createControlsMid() {
update_comfyui_button =
$el("button", {
type: "button",
@@ -1820,7 +227,76 @@ class ManagerMenuDialog extends ComfyDialog {
() => updateAll(this.update_check_checkbox)
});
- // preview method
+ const res =
+ [
+ $el("button", {
+ type: "button",
+ textContent: "Install Custom Nodes",
+ onclick:
+ () => {
+ if(!CustomNodesInstaller.instance)
+ CustomNodesInstaller.instance = new CustomNodesInstaller(app);
+ CustomNodesInstaller.instance.show(false);
+ }
+ }),
+
+ $el("button", {
+ type: "button",
+ textContent: "Install Missing Custom Nodes",
+ onclick:
+ () => {
+ if(!CustomNodesInstaller.instance)
+ CustomNodesInstaller.instance = new CustomNodesInstaller(app);
+ CustomNodesInstaller.instance.show(true);
+ }
+ }),
+
+ $el("button", {
+ type: "button",
+ textContent: "Install Models",
+ onclick:
+ () => {
+ if(!ModelInstaller.instance)
+ ModelInstaller.instance = new ModelInstaller(app);
+ 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);
+ AlternativesInstaller.instance.show();
+ }
+ }),
+
+ $el("br", {}, []),
+ ];
+
+ return res;
+ }
+
+ createControlsLeft() {
+ this.local_mode_checkbox = $el("input",{type:'checkbox', id:"use_local_db"},[])
+ const checkbox_text = $el("label",{},[" Use local DB"])
+ checkbox_text.style.color = "var(--fg-color)";
+ checkbox_text.style.marginRight = "10px";
+
+ this.update_check_checkbox = $el("input",{type:'checkbox', id:"skip_update_check"},[])
+ const uc_checkbox_text = $el("label",{},[" Skip update check"])
+ uc_checkbox_text.style.color = "var(--fg-color)";
+ this.update_check_checkbox.checked = true;
+
+ // preview method
let preview_combo = document.createElement("select");
preview_combo.appendChild($el('option', {value:'auto', text:'Preview method: Auto'}, []));
preview_combo.appendChild($el('option', {value:'taesd', text:'Preview method: TAESD (slow)'}, []));
@@ -1876,68 +352,37 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
- const res =
- [
- $el("tr.td", {width:"100%"}, [$el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])]),
- $el("br", {}, []),
- $el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]),
- $el("br", {}, []),
- $el("button", {
- type: "button",
- textContent: "Install Custom Nodes",
- onclick:
- () => {
- if(!CustomNodesInstaller.instance)
- CustomNodesInstaller.instance = new CustomNodesInstaller(app);
- CustomNodesInstaller.instance.show(false);
- }
- }),
+ return [
+ $el("div", {}, [this.local_mode_checkbox, checkbox_text, this.update_check_checkbox, uc_checkbox_text]),
+ $el("br", {}, []),
+ preview_combo,
+ badge_combo,
+ channel_combo,
- $el("button", {
- type: "button",
- textContent: "Install Missing Custom Nodes",
- onclick:
- () => {
- if(!CustomNodesInstaller.instance)
- CustomNodesInstaller.instance = new CustomNodesInstaller(app);
- CustomNodesInstaller.instance.show(true);
- }
- }),
+ $el("hr", {}, []),
+ $el("center", {}, ["!! EXPERIMENTAL !!"]),
+ $el("br", {}, []),
+ $el("button", {
+ type: "button",
+ textContent: "Snapshot Manager",
+ onclick:
+ () => {
+ if(!SnapshotManager.instance)
+ SnapshotManager.instance = new SnapshotManager(app);
+ SnapshotManager.instance.show();
+ }
+ }),
+ ];
+ }
- $el("button", {
- type: "button",
- textContent: "Install Models",
- onclick:
- () => {
- if(!ModelInstaller.instance)
- ModelInstaller.instance = new ModelInstaller(app);
- 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);
- AlternativesInstaller.instance.show();
- }
- }),
-
- $el("br", {}, []),
+ createControlsRight() {
+ return [
$el("button", {
type: "button",
textContent: "ComfyUI Community Manual",
onclick: () => { window.open("https://blenderneko.github.io/ComfyUI-docs/", "comfyui-community-manual"); }
}),
+
$el("button", {
type: "button",
textContent: "ComfyUI Workflow Gallery",
@@ -1949,36 +394,41 @@ class ManagerMenuDialog extends ComfyDialog {
textContent: "ComfyUI Nodes Info",
onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); }
}),
-
- $el("br", {}, []),
- $el("hr", {width: "100%"}, []),
- preview_combo,
- badge_combo,
- channel_combo,
- $el("hr", {width: "100%"}, []),
- $el("br", {}, []),
-
- $el("button", {
- type: "button",
- textContent: "Close",
- onclick: () => this.close()
- }),
- $el("br", {}, []),
- ];
-
- res[0].style.padding = "10px 10px 0 10px";
- res[0].style.backgroundColor = "black";
- res[0].style.textAlign = "center";
- res[0].style.height = "45px";
- return res;
+ ];
}
constructor() {
super();
- this.element = $el("div.comfy-modal", { parent: document.body },
- [ $el("div.comfy-modal-content",
- [...this.createButtons()]),
- ]);
+
+ 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 = '400px';
+ this.element.style.zIndex = 10000;
}
show() {
@@ -2000,9 +450,9 @@ app.registerExtension({
const managerButton = document.createElement("button");
managerButton.textContent = "Manager";
managerButton.onclick = () => {
- if(!ManagerMenuDialog.instance)
- ManagerMenuDialog.instance = new ManagerMenuDialog();
- ManagerMenuDialog.instance.show();
+ if(!manager_instance)
+ setManagerInstance(new ManagerMenuDialog());
+ manager_instance.show();
}
menu.append(managerButton);
},
diff --git a/js/common.js b/js/common.js
new file mode 100644
index 00000000..b5562691
--- /dev/null
+++ b/js/common.js
@@ -0,0 +1,62 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js"
+
+export async function install_checked_custom_node(grid_rows, target_i, caller, mode) {
+ if(caller) {
+ let failed = '';
+
+ caller.disableButtons();
+
+ for(let i in grid_rows) {
+ if(!grid_rows[i].checkbox.checked && i != target_i)
+ continue;
+
+ var target;
+
+ if(grid_rows[i].data.custom_node) {
+ target = grid_rows[i].data.custom_node;
+ }
+ else {
+ target = grid_rows[i].data;
+ }
+
+ caller.startInstall(target);
+
+ try {
+ const response = await api.fetchApi(`/customnode/${mode}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(target)
+ });
+
+ if(response.status == 400) {
+ app.ui.dialog.show(`${mode} failed: ${target.title}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ continue;
+ }
+
+ const status = await response.json();
+ app.ui.dialog.close();
+ target.installed = 'True';
+ continue;
+ }
+ catch(exception) {
+ failed += `
${target.title}`;
+ }
+ }
+
+ if(failed != '') {
+ app.ui.dialog.show(`${mode} failed: ${failed}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ }
+
+ await caller.invalidateControl();
+ caller.updateMessage('
To apply the installed/disabled/enabled custom node, please restart ComfyUI.');
+ }
+};
+
+export var manager_instance = null;
+
+export function setManagerInstance(obj) {
+ manager_instance = obj;
+}
\ No newline at end of file
diff --git a/js/custom-nodes-downloader.js b/js/custom-nodes-downloader.js
new file mode 100644
index 00000000..fd8553e5
--- /dev/null
+++ b/js/custom-nodes-downloader.js
@@ -0,0 +1,638 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js"
+import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { install_checked_custom_node, manager_instance } from "./common.js";
+
+async function getCustomNodes() {
+ var mode = "url";
+ if(manager_instance.local_mode_checkbox.checked)
+ mode = "local";
+
+ var skip_update = "";
+ if(manager_instance.update_check_checkbox.checked)
+ skip_update = "&skip_update=true";
+
+ const response = await api.fetchApi(`/customnode/getlist?mode=${mode}${skip_update}`);
+
+ const data = await response.json();
+ return data;
+}
+
+async function getCustomnodeMappings() {
+ var mode = "url";
+ if(manager_instance.local_mode_checkbox.checked)
+ mode = "local";
+
+ const response = await api.fetchApi(`/customnode/getmappings?mode=${mode}`);
+
+ const data = await response.json();
+ return data;
+}
+
+async function getUnresolvedNodesInComponent() {
+ try {
+ var mode = "url";
+ if(manager_instance.local_mode_checkbox.checked)
+ mode = "local";
+
+ const response = await api.fetchApi(`/component/get_unresolved`);
+
+ const data = await response.json();
+ return data.nodes;
+ }
+ catch {
+ return [];
+ }
+}
+
+export class CustomNodesInstaller extends ComfyDialog {
+ static instance = null;
+
+ install_buttons = [];
+ message_box = null;
+ data = null;
+
+ clear() {
+ this.install_buttons = [];
+ this.message_box = null;
+ this.data = null;
+ }
+
+ constructor() {
+ super();
+ this.search_keyword = '';
+ this.element = $el("div.comfy-modal", { parent: document.body }, []);
+ }
+
+ startInstall(target) {
+ const self = CustomNodesInstaller.instance;
+
+ self.updateMessage(`
Installing '${target.title}'`);
+ }
+
+ disableButtons() {
+ for(let i in this.install_buttons) {
+ this.install_buttons[i].disabled = true;
+ this.install_buttons[i].style.backgroundColor = 'gray';
+ }
+ }
+
+ apply_searchbox(data) {
+ let keyword = this.search_box.value.toLowerCase();
+ for(let i in this.grid_rows) {
+ let data = this.grid_rows[i].data;
+ let content = data.author.toLowerCase() + data.description.toLowerCase() + data.title.toLowerCase();
+
+ if(this.filter && this.filter != '*') {
+ if(this.filter != data.installed) {
+ this.grid_rows[i].control.style.display = 'none';
+ continue;
+ }
+ }
+
+ if(keyword == "")
+ this.grid_rows[i].control.style.display = null;
+ else if(content.includes(keyword)) {
+ this.grid_rows[i].control.style.display = null;
+ }
+ else {
+ this.grid_rows[i].control.style.display = 'none';
+ }
+ }
+ }
+
+ async filter_missing_node(data) {
+ const mappings = await getCustomnodeMappings();
+
+
+ // build regex->url map
+ const regex_to_url = [];
+ for (let i in data) {
+ if(data[i]['nodename_pattern']) {
+ let item = {regex: new RegExp(data[i].nodename_pattern), url: data[i].files[0]};
+ regex_to_url.push(item);
+ }
+ }
+
+ // build name->url map
+ const name_to_url = {};
+ for (const url in mappings) {
+ const names = mappings[url];
+ for(const name in names[0]) {
+ name_to_url[names[0][name]] = url;
+ }
+ }
+
+ const registered_nodes = new Set();
+ for (let i in LiteGraph.registered_node_types) {
+ registered_nodes.add(LiteGraph.registered_node_types[i].type);
+ }
+
+ const missing_nodes = new Set();
+ const nodes = app.graph.serialize().nodes;
+ for (let i in nodes) {
+ const node_type = nodes[i].type;
+ if (!registered_nodes.has(node_type)) {
+ const url = name_to_url[node_type.trim()];
+ if(url)
+ missing_nodes.add(url);
+ else {
+ for(let j in regex_to_url) {
+ if(regex_to_url[j].regex.test(node_type)) {
+ missing_nodes.add(regex_to_url[j].url);
+ }
+ }
+ }
+ }
+ }
+
+ let unresolved_nodes = await getUnresolvedNodesInComponent();
+ for (let i in unresolved_nodes) {
+ let node_type = unresolved_nodes[i];
+ const url = name_to_url[node_type];
+ if(url)
+ missing_nodes.add(url);
+ }
+
+ return data.filter(node => node.files.some(file => missing_nodes.has(file)));
+ }
+
+ async invalidateControl() {
+ this.clear();
+
+ // splash
+ while (this.element.children.length) {
+ this.element.removeChild(this.element.children[0]);
+ }
+
+ const msg = $el('div', {id:'custom-message'},
+ [$el('br'),
+ 'The custom node DB is currently being updated, and updates to custom nodes are being checked for.',
+ $el('br'),
+ 'NOTE: Update only checks for extensions that have been fetched.',
+ $el('br')]);
+ msg.style.height = '100px';
+ msg.style.verticalAlign = 'middle';
+ msg.style.color = "var(--fg-color)";
+
+ this.element.appendChild(msg);
+
+ // invalidate
+ this.data = (await getCustomNodes()).custom_nodes;
+
+ if(this.is_missing_node_mode)
+ this.data = await this.filter_missing_node(this.data);
+
+ this.element.removeChild(msg);
+
+ while (this.element.children.length) {
+ this.element.removeChild(this.element.children[0]);
+ }
+
+ this.createHeaderControls();
+ await this.createGrid();
+ this.apply_searchbox(this.data);
+ this.createBottomControls();
+ }
+
+ updateMessage(msg) {
+ this.message_box.innerHTML = msg;
+ }
+
+ invalidate_checks(is_checked, install_state) {
+ if(is_checked) {
+ for(let i in this.grid_rows) {
+ let data = this.grid_rows[i].data;
+ let checkbox = this.grid_rows[i].checkbox;
+ let buttons = this.grid_rows[i].buttons;
+
+ checkbox.disabled = data.installed != install_state;
+
+ if(checkbox.disabled) {
+ for(let j in buttons) {
+ buttons[j].style.display = 'none';
+ }
+ }
+ else {
+ for(let j in buttons) {
+ buttons[j].style.display = null;
+ }
+ }
+ }
+
+ this.checkbox_all.disabled = false;
+ }
+ else {
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ if(checkbox.check)
+ return; // do nothing
+ }
+
+ // every checkbox is unchecked -> enable all checkbox
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ let buttons = this.grid_rows[i].buttons;
+ checkbox.disabled = false;
+
+ for(let j in buttons) {
+ buttons[j].style.display = null;
+ }
+ }
+
+ this.checkbox_all.checked = false;
+ this.checkbox_all.disabled = true;
+ }
+ }
+
+ check_all(is_checked) {
+ if(is_checked) {
+ // lookup first checked item's state
+ let check_state = null;
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ if(checkbox.checked) {
+ check_state = this.grid_rows[i].data.installed;
+ }
+ }
+
+ if(check_state == null)
+ return;
+
+ // check only same state items
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ if(this.grid_rows[i].data.installed == check_state)
+ checkbox.checked = true;
+ }
+ }
+ else {
+ // uncheck all
+ for(let i in this.grid_rows) {
+ let checkbox = this.grid_rows[i].checkbox;
+ let buttons = this.grid_rows[i].buttons;
+ checkbox.checked = false;
+ checkbox.disabled = false;
+
+ for(let j in buttons) {
+ buttons[j].style.display = null;
+ }
+ }
+
+ this.checkbox_all.disabled = true;
+ }
+ }
+
+ async createGrid() {
+ var grid = document.createElement('table');
+ grid.setAttribute('id', 'custom-nodes-grid');
+
+ this.grid_rows = {};
+
+ let self = this;
+
+ var thead = document.createElement('thead');
+ var tbody = document.createElement('tbody');
+
+ var headerRow = document.createElement('tr');
+ thead.style.position = "sticky";
+ thead.style.top = "0px";
+ thead.style.borderCollapse = "collapse";
+ thead.style.tableLayout = "fixed";
+
+ var header0 = document.createElement('th');
+ header0.style.width = "20px";
+ this.checkbox_all = $el("input",{type:'checkbox', id:'check_all'},[]);
+ header0.appendChild(this.checkbox_all);
+ this.checkbox_all.checked = false;
+ this.checkbox_all.disabled = true;
+ this.checkbox_all.addEventListener('change', function() { self.check_all.call(self, self.checkbox_all.checked); });
+
+ var header1 = document.createElement('th');
+ header1.innerHTML = ' ID ';
+ header1.style.width = "20px";
+ var header2 = document.createElement('th');
+ header2.innerHTML = 'Author';
+ header2.style.width = "150px";
+ var header3 = document.createElement('th');
+ header3.innerHTML = 'Name';
+ header3.style.width = "20%";
+ var header4 = document.createElement('th');
+ header4.innerHTML = 'Description';
+ header4.style.width = "60%";
+// header4.classList.add('expandable-column');
+ var header5 = document.createElement('th');
+ header5.innerHTML = 'Install';
+ header5.style.width = "130px";
+
+ header0.style.position = "sticky";
+ header0.style.top = "0px";
+ header1.style.position = "sticky";
+ header1.style.top = "0px";
+ header2.style.position = "sticky";
+ header2.style.top = "0px";
+ header3.style.position = "sticky";
+ header3.style.top = "0px";
+ header4.style.position = "sticky";
+ header4.style.top = "0px";
+ header5.style.position = "sticky";
+ header5.style.top = "0px";
+
+ thead.appendChild(headerRow);
+ headerRow.appendChild(header0);
+ headerRow.appendChild(header1);
+ headerRow.appendChild(header2);
+ headerRow.appendChild(header3);
+ headerRow.appendChild(header4);
+ headerRow.appendChild(header5);
+
+ headerRow.style.backgroundColor = "Black";
+ headerRow.style.color = "White";
+ headerRow.style.textAlign = "center";
+ headerRow.style.width = "100%";
+ headerRow.style.padding = "0";
+
+ grid.appendChild(thead);
+ grid.appendChild(tbody);
+
+ if(this.data)
+ for (var i = 0; i < this.data.length; i++) {
+ const data = this.data[i];
+ let dataRow = document.createElement('tr');
+
+ let data0 = document.createElement('td');
+ let checkbox = $el("input",{type:'checkbox', id:`check_${i}`},[]);
+ data0.appendChild(checkbox);
+ checkbox.checked = false;
+ checkbox.addEventListener('change', function() { self.invalidate_checks.call(self, checkbox.checked, data.installed); });
+
+ var data1 = document.createElement('td');
+ data1.style.textAlign = "center";
+ data1.innerHTML = i+1;
+ var data2 = document.createElement('td');
+ data2.style.maxWidth = "100px";
+ data2.className = "cm-node-author"
+ data2.textContent = ` ${data.author}`;
+ data2.style.whiteSpace = "nowrap";
+ data2.style.overflow = "hidden";
+ data2.style.textOverflow = "ellipsis";
+ var data3 = document.createElement('td');
+ data3.style.maxWidth = "200px";
+ data3.style.wordWrap = "break-word";
+ data3.className = "cm-node-name"
+ data3.innerHTML = ` ${data.title}`;
+ var data4 = document.createElement('td');
+ data4.innerHTML = data.description;
+ data4.className = "cm-node-desc"
+ var data5 = document.createElement('td');
+ data5.style.textAlign = "center";
+
+ var installBtn = document.createElement('button');
+ installBtn.className = "cm-btn-install";
+ var installBtn2 = null;
+ var installBtn3 = null;
+
+ this.install_buttons.push(installBtn);
+
+ switch(data.installed) {
+ case 'Disabled':
+ installBtn3 = document.createElement('button');
+ installBtn3.innerHTML = 'Enable';
+ installBtn3.className = "cm-btn-enable";
+ installBtn3.style.backgroundColor = 'blue';
+ installBtn3.style.color = 'white';
+ this.install_buttons.push(installBtn3);
+
+ installBtn.innerHTML = 'Uninstall';
+ installBtn.style.backgroundColor = 'red';
+ break;
+ case 'Update':
+ installBtn2 = document.createElement('button');
+ installBtn2.innerHTML = 'Update';
+ installBtn2.className = "cm-btn-update";
+ installBtn2.style.backgroundColor = 'blue';
+ installBtn2.style.color = 'white';
+ this.install_buttons.push(installBtn2);
+
+ installBtn3 = document.createElement('button');
+ installBtn3.innerHTML = 'Disable';
+ installBtn3.className = "cm-btn-disable";
+ installBtn3.style.backgroundColor = 'MediumSlateBlue';
+ installBtn3.style.color = 'white';
+ this.install_buttons.push(installBtn3);
+
+ installBtn.innerHTML = 'Uninstall';
+ installBtn.style.backgroundColor = 'red';
+ break;
+ case 'True':
+ installBtn3 = document.createElement('button');
+ installBtn3.innerHTML = 'Disable';
+ installBtn3.className = "cm-btn-disable";
+ installBtn3.style.backgroundColor = 'MediumSlateBlue';
+ installBtn3.style.color = 'white';
+ this.install_buttons.push(installBtn3);
+
+ installBtn.innerHTML = 'Uninstall';
+ installBtn.style.backgroundColor = 'red';
+ break;
+ case 'False':
+ installBtn.innerHTML = 'Install';
+ installBtn.style.backgroundColor = 'black';
+ installBtn.style.color = 'white';
+ break;
+ default:
+ installBtn.innerHTML = 'Try Install';
+ installBtn.style.backgroundColor = 'Gray';
+ installBtn.style.color = 'white';
+ }
+
+ let j = i;
+ if(installBtn2 != null) {
+ installBtn2.style.width = "120px";
+ installBtn2.addEventListener('click', function() {
+ install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'update');
+ });
+
+ data5.appendChild(installBtn2);
+ }
+
+ if(installBtn3 != null) {
+ installBtn3.style.width = "120px";
+ installBtn3.addEventListener('click', function() {
+ install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'toggle_active');
+ });
+
+ data5.appendChild(installBtn3);
+ }
+
+ installBtn.style.width = "120px";
+ installBtn.addEventListener('click', function() {
+ if(this.innerHTML == 'Uninstall') {
+ if (confirm(`Are you sure uninstall ${data.title}?`)) {
+ install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'uninstall');
+ }
+ }
+ else {
+ install_checked_custom_node(self.grid_rows, j, CustomNodesInstaller.instance, 'install');
+ }
+ });
+
+ data5.appendChild(installBtn);
+
+ dataRow.style.backgroundColor = "var(--bg-color)";
+ dataRow.style.color = "var(--fg-color)";
+ dataRow.style.textAlign = "left";
+
+ dataRow.appendChild(data0);
+ dataRow.appendChild(data1);
+ dataRow.appendChild(data2);
+ dataRow.appendChild(data3);
+ dataRow.appendChild(data4);
+ dataRow.appendChild(data5);
+ tbody.appendChild(dataRow);
+
+ let buttons = [];
+ if(installBtn) {
+ buttons.push(installBtn);
+ }
+ if(installBtn2) {
+ buttons.push(installBtn2);
+ }
+ if(installBtn3) {
+ buttons.push(installBtn3);
+ }
+
+ this.grid_rows[i] = {data:data, buttons:buttons, checkbox:checkbox, control:dataRow};
+ }
+
+ const panel = document.createElement('div');
+ panel.style.width = "100%";
+ panel.appendChild(grid);
+
+ function handleResize() {
+ const parentHeight = self.element.clientHeight;
+ const gridHeight = parentHeight - 200;
+
+ grid.style.height = gridHeight + "px";
+ }
+ window.addEventListener("resize", handleResize);
+
+ grid.style.position = "relative";
+ grid.style.display = "inline-block";
+ grid.style.width = "100%";
+ grid.style.height = "100%";
+ grid.style.overflowY = "scroll";
+ this.element.style.height = "85%";
+ this.element.style.width = "80%";
+ this.element.appendChild(panel);
+
+ handleResize();
+ }
+
+ createFilterCombo() {
+ let combo = document.createElement("select");
+
+ combo.style.cssFloat = "left";
+ combo.style.fontSize = "14px";
+ combo.style.padding = "4px";
+ combo.style.background = "black";
+ combo.style.marginLeft = "2px";
+ combo.style.width = "199px";
+ combo.id = `combo-manger-filter`;
+ combo.style.borderRadius = "15px";
+
+ let items =
+ [
+ { value:'*', text:'Filter: all' },
+ { value:'Disabled', text:'Filter: disabled' },
+ { value:'Update', text:'Filter: update' },
+ { value:'True', text:'Filter: installed' },
+ { value:'False', text:'Filter: not-installed' },
+ ];
+
+ items.forEach(item => {
+ const option = document.createElement("option");
+ option.value = item.value;
+ option.text = item.text;
+ combo.appendChild(option);
+ });
+
+ let self = this;
+ combo.addEventListener('change', function(event) {
+ self.filter = event.target.value;
+ self.apply_searchbox();
+ });
+
+ if(self.filter) {
+ combo.value = self.filter;
+ }
+
+ return combo;
+ }
+
+ createHeaderControls() {
+ let self = this;
+ this.search_box = $el('input', {type:'text', id:'manager-customnode-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
+ this.search_box.style.height = "25px";
+ this.search_box.onkeydown = (event) => {
+ if (event.key === 'Enter') {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ }
+ if (event.key === 'Escape') {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ }
+ };
+
+
+ let search_button = document.createElement("button");
+ search_button.innerHTML = "Search";
+ search_button.onclick = () => {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ };
+ search_button.style.display = "inline-block";
+
+ let filter_control = this.createFilterCombo();
+ filter_control.style.display = "inline-block";
+
+ let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
+ let search_control = $el('table', {width:'100%'},
+ [
+ $el('tr', {}, [cell])
+ ]
+ );
+
+ cell.style.textAlign = "right";
+
+ this.element.appendChild(search_control);
+ }
+
+ async createBottomControls() {
+ let close_button = document.createElement("button");
+ close_button.innerHTML = "Close";
+ close_button.onclick = () => { this.close(); }
+ close_button.style.display = "inline-block";
+
+ this.message_box = $el('div', {id:'custom-installer-message'}, [$el('br'), '']);
+ this.message_box.style.height = '60px';
+ this.message_box.style.verticalAlign = 'middle';
+
+ this.element.appendChild(this.message_box);
+ this.element.appendChild(close_button);
+ }
+
+ async show(is_missing_node_mode) {
+ this.is_missing_node_mode = is_missing_node_mode;
+ try {
+ this.invalidateControl();
+
+ this.element.style.display = "block";
+ this.element.style.zIndex = 10001;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Failed to get custom node list. / ${exception}`);
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/model-downloader.js b/js/model-downloader.js
new file mode 100644
index 00000000..c83950ff
--- /dev/null
+++ b/js/model-downloader.js
@@ -0,0 +1,377 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js"
+import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { install_checked_custom_node, manager_instance } from "./common.js";
+
+async function install_model(target) {
+ if(ModelInstaller.instance) {
+ ModelInstaller.instance.startInstall(target);
+
+ try {
+ const response = await api.fetchApi('/model/install', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(target)
+ });
+
+ const status = await response.json();
+ app.ui.dialog.close();
+ target.installed = 'True';
+ return true;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Install failed: ${target.title} / ${exception}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ return false;
+ }
+ finally {
+ await ModelInstaller.instance.invalidateControl();
+ ModelInstaller.instance.updateMessage("
To apply the installed model, please click the 'Refresh' button on the main menu.");
+ }
+ }
+}
+
+async function getModelList() {
+ var mode = "url";
+ if(manager_instance.local_mode_checkbox.checked)
+ mode = "local";
+
+ const response = await api.fetchApi(`/externalmodel/getlist?mode=${mode}`);
+
+ const data = await response.json();
+ return data;
+}
+
+export class ModelInstaller extends ComfyDialog {
+ static instance = null;
+
+ install_buttons = [];
+ message_box = null;
+ data = null;
+
+ clear() {
+ this.install_buttons = [];
+ this.message_box = null;
+ this.data = null;
+ }
+
+ constructor() {
+ super();
+ this.search_keyword = '';
+ this.element = $el("div.comfy-modal", { parent: document.body }, []);
+ }
+
+ createControls() {
+ return [
+ $el("button", {
+ type: "button",
+ textContent: "Close",
+ onclick: () => { this.close(); }
+ })
+ ];
+ }
+
+ startInstall(target) {
+ const self = ModelInstaller.instance;
+
+ self.updateMessage(`
Installing '${target.name}'`);
+
+ for(let i in self.install_buttons) {
+ self.install_buttons[i].disabled = true;
+ self.install_buttons[i].style.backgroundColor = 'gray';
+ }
+ }
+
+ apply_searchbox(data) {
+ let keyword = this.search_box.value.toLowerCase();
+ for(let i in this.grid_rows) {
+ let data = this.grid_rows[i].data;
+ let content = data.name.toLowerCase() + data.type.toLowerCase() + data.base.toLowerCase() + data.description.toLowerCase();
+
+ if(this.filter && this.filter != '*') {
+ if(this.filter != data.installed) {
+ this.grid_rows[i].control.style.display = 'none';
+ continue;
+ }
+ }
+
+ if(keyword == "")
+ this.grid_rows[i].control.style.display = null;
+ else if(content.includes(keyword)) {
+ this.grid_rows[i].control.style.display = null;
+ }
+ else {
+ this.grid_rows[i].control.style.display = 'none';
+ }
+ }
+ }
+
+ async invalidateControl() {
+ this.clear();
+ this.data = (await getModelList()).models;
+
+ while (this.element.children.length) {
+ this.element.removeChild(this.element.children[0]);
+ }
+
+ await this.createHeaderControls();
+
+ if(this.search_keyword) {
+ this.search_box.value = this.search_keyword;
+ }
+
+ await this.createGrid();
+ await this.createBottomControls();
+
+ this.apply_searchbox(this.data);
+ }
+
+ updateMessage(msg) {
+ this.message_box.innerHTML = msg;
+ }
+
+ async createGrid(models_json) {
+ var grid = document.createElement('table');
+ grid.setAttribute('id', 'external-models-grid');
+
+ var thead = document.createElement('thead');
+ var tbody = document.createElement('tbody');
+
+ var headerRow = document.createElement('tr');
+ thead.style.position = "sticky";
+ thead.style.top = "0px";
+ thead.style.borderCollapse = "collapse";
+ thead.style.tableLayout = "fixed";
+
+ var header1 = document.createElement('th');
+ header1.innerHTML = ' ID ';
+ header1.style.width = "20px";
+ var header2 = document.createElement('th');
+ header2.innerHTML = 'Type';
+ header2.style.width = "100px";
+ var header3 = document.createElement('th');
+ header3.innerHTML = 'Base';
+ header3.style.width = "100px";
+ var header4 = document.createElement('th');
+ header4.innerHTML = 'Name';
+ header4.style.width = "30%";
+ var header5 = document.createElement('th');
+ header5.innerHTML = 'Filename';
+ header5.style.width = "20%";
+ header5.style.tableLayout = "fixed";
+ var header6 = document.createElement('th');
+ header6.innerHTML = 'Description';
+ header6.style.width = "50%";
+ var header_down = document.createElement('th');
+ header_down.innerHTML = 'Download';
+ header_down.style.width = "50px";
+
+ thead.appendChild(headerRow);
+ headerRow.appendChild(header1);
+ headerRow.appendChild(header2);
+ headerRow.appendChild(header3);
+ headerRow.appendChild(header4);
+ headerRow.appendChild(header5);
+ headerRow.appendChild(header6);
+ headerRow.appendChild(header_down);
+
+ headerRow.style.backgroundColor = "Black";
+ headerRow.style.color = "White";
+ headerRow.style.textAlign = "center";
+ headerRow.style.width = "100%";
+ headerRow.style.padding = "0";
+
+ grid.appendChild(thead);
+ grid.appendChild(tbody);
+
+ this.grid_rows = {};
+
+ if(this.data)
+ for (var i = 0; i < this.data.length; i++) {
+ const data = this.data[i];
+ var dataRow = document.createElement('tr');
+ var data1 = document.createElement('td');
+ data1.style.textAlign = "center";
+ data1.innerHTML = i+1;
+ var data2 = document.createElement('td');
+ data2.innerHTML = ` ${data.type}`;
+ var data3 = document.createElement('td');
+ data3.innerHTML = ` ${data.base}`;
+ var data4 = document.createElement('td');
+ data4.className = "cm-node-name";
+ data4.innerHTML = ` ${data.name}`;
+ var data5 = document.createElement('td');
+ data5.className = "cm-node-filename";
+ data5.innerHTML = ` ${data.filename}`;
+ data5.style.wordBreak = "break-all";
+ var data6 = document.createElement('td');
+ data6.className = "cm-node-desc";
+ data6.innerHTML = data.description;
+ data6.style.wordBreak = "break-all";
+ var data_install = document.createElement('td');
+ var installBtn = document.createElement('button');
+ data_install.style.textAlign = "center";
+
+ installBtn.innerHTML = 'Install';
+ this.install_buttons.push(installBtn);
+
+ switch(data.installed) {
+ case 'True':
+ installBtn.innerHTML = 'Installed';
+ installBtn.style.backgroundColor = 'green';
+ installBtn.style.color = 'white';
+ installBtn.disabled = true;
+ break;
+ default:
+ installBtn.innerHTML = 'Install';
+ installBtn.style.backgroundColor = 'black';
+ installBtn.style.color = 'white';
+ break;
+ }
+
+ installBtn.style.width = "100px";
+
+ installBtn.addEventListener('click', function() {
+ install_model(data);
+ });
+
+ data_install.appendChild(installBtn);
+
+ dataRow.style.backgroundColor = "var(--bg-color)";
+ dataRow.style.color = "var(--fg-color)";
+ dataRow.style.textAlign = "left";
+
+ dataRow.appendChild(data1);
+ dataRow.appendChild(data2);
+ dataRow.appendChild(data3);
+ dataRow.appendChild(data4);
+ dataRow.appendChild(data5);
+ dataRow.appendChild(data6);
+ dataRow.appendChild(data_install);
+ tbody.appendChild(dataRow);
+
+ this.grid_rows[i] = {data:data, control:dataRow};
+ }
+
+ let self = this;
+ const panel = document.createElement('div');
+ panel.style.width = "100%";
+ panel.appendChild(grid);
+
+ function handleResize() {
+ const parentHeight = self.element.clientHeight;
+ const gridHeight = parentHeight - 200;
+
+ grid.style.height = gridHeight + "px";
+ }
+ window.addEventListener("resize", handleResize);
+
+ grid.style.position = "relative";
+ grid.style.display = "inline-block";
+ grid.style.width = "100%";
+ grid.style.height = "100%";
+ grid.style.overflowY = "scroll";
+ this.element.style.height = "85%";
+ this.element.style.width = "80%";
+ this.element.appendChild(panel);
+
+ handleResize();
+ }
+
+ createFilterCombo() {
+ let combo = document.createElement("select");
+
+ combo.style.cssFloat = "left";
+ combo.style.fontSize = "14px";
+ combo.style.padding = "4px";
+ combo.style.background = "black";
+ combo.style.marginLeft = "2px";
+ combo.style.width = "199px";
+ combo.id = `combo-manger-filter`;
+ combo.style.borderRadius = "15px";
+
+ let items =
+ [
+ { value:'*', text:'Filter: all' },
+ { value:'True', text:'Filter: installed' },
+ { value:'False', text:'Filter: not-installed' },
+ ];
+
+ items.forEach(item => {
+ const option = document.createElement("option");
+ option.value = item.value;
+ option.text = item.text;
+ combo.appendChild(option);
+ });
+
+ let self = this;
+ combo.addEventListener('change', function(event) {
+ self.filter = event.target.value;
+ self.apply_searchbox();
+ });
+
+ return combo;
+ }
+
+ createHeaderControls() {
+ let self = this;
+ this.search_box = $el('input', {type:'text', id:'manager-model-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
+ this.search_box.style.height = "25px";
+ this.search_box.onkeydown = (event) => {
+ if (event.key === 'Enter') {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ }
+ if (event.key === 'Escape') {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ }
+ };
+
+ let search_button = document.createElement("button");
+ search_button.innerHTML = "Search";
+ search_button.onclick = () => {
+ self.search_keyword = self.search_box.value;
+ self.apply_searchbox();
+ };
+ search_button.style.display = "inline-block";
+
+ let filter_control = this.createFilterCombo();
+ filter_control.style.display = "inline-block";
+
+ let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
+ let search_control = $el('table', {width:'100%'},
+ [
+ $el('tr', {}, [cell])
+ ]
+ );
+
+ cell.style.textAlign = "right";
+ this.element.appendChild(search_control);
+ }
+
+ async createBottomControls() {
+ var close_button = document.createElement("button");
+ close_button.innerHTML = "Close";
+ close_button.onclick = () => { this.close(); }
+ close_button.style.display = "inline-block";
+
+ this.message_box = $el('div', {id:'custom-download-message'}, [$el('br'), '']);
+ this.message_box.style.height = '60px';
+ this.message_box.style.verticalAlign = 'middle';
+
+ this.element.appendChild(this.message_box);
+ this.element.appendChild(close_button);
+ }
+
+ async show() {
+ try {
+ this.invalidateControl();
+ this.element.style.display = "block";
+ this.element.style.zIndex = 10001;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Failed to get external model list. / ${exception}`);
+ }
+ }
+}
diff --git a/js/snapshot.js b/js/snapshot.js
new file mode 100644
index 00000000..ff009543
--- /dev/null
+++ b/js/snapshot.js
@@ -0,0 +1,279 @@
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js"
+import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { manager_instance } from "./common.js";
+
+
+async function restore_snapshot(target) {
+ if(SnapshotManager.instance) {
+ try {
+ const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" });
+ if(response.status == 400) {
+ app.ui.dialog.show(`Restore snapshot failed: ${target.title} / ${exception}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ }
+
+ app.ui.dialog.close();
+ return true;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Restore snapshot failed: ${target.title} / ${exception}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ return false;
+ }
+ finally {
+ await SnapshotManager.instance.invalidateControl();
+ SnapshotManager.instance.updateMessage("
To apply the snapshot, please restart ComfyUI.");
+ }
+ }
+}
+
+async function remove_snapshot(target) {
+ if(SnapshotManager.instance) {
+ try {
+ const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" });
+ if(response.status == 400) {
+ app.ui.dialog.show(`Remove snapshot failed: ${target.title} / ${exception}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ }
+
+ app.ui.dialog.close();
+ return true;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Restore snapshot failed: ${target.title} / ${exception}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ return false;
+ }
+ finally {
+ await SnapshotManager.instance.invalidateControl();
+ }
+ }
+}
+
+async function save_current_snapshot() {
+ try {
+ const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
+ app.ui.dialog.close();
+ return true;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Backup snapshot failed: ${exception}`);
+ app.ui.dialog.element.style.zIndex = 10010;
+ return false;
+ }
+ finally {
+ await SnapshotManager.instance.invalidateControl();
+ SnapshotManager.instance.updateMessage("
Current snapshot saved.");
+ }
+}
+
+async function getSnapshotList() {
+ const response = await api.fetchApi(`/snapshot/getlist`);
+ const data = await response.json();
+ return data;
+}
+
+export class SnapshotManager extends ComfyDialog {
+ static instance = null;
+
+ restore_buttons = [];
+ message_box = null;
+ data = null;
+
+ clear() {
+ this.restore_buttons = [];
+ this.message_box = null;
+ this.data = null;
+ }
+
+ constructor() {
+ super();
+ this.search_keyword = '';
+ this.element = $el("div.comfy-modal", { parent: document.body }, []);
+ }
+
+ async remove_item() {
+ caller.disableButtons();
+
+ await caller.invalidateControl();
+ }
+
+ createControls() {
+ return [
+ $el("button", {
+ type: "button",
+ textContent: "Close",
+ onclick: () => { this.close(); }
+ })
+ ];
+ }
+
+ startRestore(target) {
+ const self = SnapshotManager.instance;
+
+ self.updateMessage(`
Restore snapshot '${target.name}'`);
+
+ for(let i in self.restore_buttons) {
+ self.restore_buttons[i].disabled = true;
+ self.restore_buttons[i].style.backgroundColor = 'gray';
+ }
+ }
+
+ async invalidateControl() {
+ this.clear();
+ this.data = (await getSnapshotList()).items;
+
+ while (this.element.children.length) {
+ this.element.removeChild(this.element.children[0]);
+ }
+
+ await this.createGrid();
+ await this.createBottomControls();
+ }
+
+ updateMessage(msg) {
+ this.message_box.innerHTML = msg;
+ }
+
+ async createGrid(models_json) {
+ var grid = document.createElement('table');
+ grid.setAttribute('id', 'snapshot-list-grid');
+
+ var thead = document.createElement('thead');
+ var tbody = document.createElement('tbody');
+
+ var headerRow = document.createElement('tr');
+ thead.style.position = "sticky";
+ thead.style.top = "0px";
+ thead.style.borderCollapse = "collapse";
+ thead.style.tableLayout = "fixed";
+
+ var header1 = document.createElement('th');
+ header1.innerHTML = ' ID ';
+ header1.style.width = "20px";
+ var header2 = document.createElement('th');
+ header2.innerHTML = 'Datetime';
+ header2.style.width = "100%";
+ var header_button = document.createElement('th');
+ header_button.innerHTML = 'Action';
+ header_button.style.width = "100px";
+
+ thead.appendChild(headerRow);
+ headerRow.appendChild(header1);
+ headerRow.appendChild(header2);
+ headerRow.appendChild(header_button);
+
+ headerRow.style.backgroundColor = "Black";
+ headerRow.style.color = "White";
+ headerRow.style.textAlign = "center";
+ headerRow.style.width = "100%";
+ headerRow.style.padding = "0";
+
+ grid.appendChild(thead);
+ grid.appendChild(tbody);
+
+ this.grid_rows = {};
+
+ if(this.data)
+ for (var i = 0; i < this.data.length; i++) {
+ const data = this.data[i];
+ var dataRow = document.createElement('tr');
+ var data1 = document.createElement('td');
+ data1.style.textAlign = "center";
+ data1.innerHTML = i+1;
+ var data2 = document.createElement('td');
+ data2.innerHTML = ` ${data}`;
+ var data_button = document.createElement('td');
+ data_button.style.textAlign = "center";
+
+ var restoreBtn = document.createElement('button');
+ restoreBtn.innerHTML = 'Restore';
+ restoreBtn.style.width = "100px";
+ restoreBtn.style.backgroundColor = 'blue';
+
+ restoreBtn.addEventListener('click', function() {
+ restore_snapshot(data);
+ });
+
+ var removeBtn = document.createElement('button');
+ removeBtn.innerHTML = 'Remove';
+ removeBtn.style.width = "100px";
+ removeBtn.style.backgroundColor = 'red';
+
+ removeBtn.addEventListener('click', function() {
+ remove_snapshot(data);
+ });
+
+ data_button.appendChild(restoreBtn);
+ data_button.appendChild(removeBtn);
+
+ dataRow.style.backgroundColor = "var(--bg-color)";
+ dataRow.style.color = "var(--fg-color)";
+ dataRow.style.textAlign = "left";
+
+ dataRow.appendChild(data1);
+ dataRow.appendChild(data2);
+ dataRow.appendChild(data_button);
+ tbody.appendChild(dataRow);
+
+ this.grid_rows[i] = {data:data, control:dataRow};
+ }
+
+ let self = this;
+ const panel = document.createElement('div');
+ panel.style.width = "100%";
+ panel.appendChild(grid);
+
+ function handleResize() {
+ const parentHeight = self.element.clientHeight;
+ const gridHeight = parentHeight - 200;
+
+ grid.style.height = gridHeight + "px";
+ }
+ window.addEventListener("resize", handleResize);
+
+ grid.style.position = "relative";
+ grid.style.display = "inline-block";
+ grid.style.width = "100%";
+ grid.style.height = "100%";
+ grid.style.overflowY = "scroll";
+ this.element.style.height = "85%";
+ this.element.style.width = "80%";
+ this.element.appendChild(panel);
+
+ handleResize();
+ }
+
+ async createBottomControls() {
+ var close_button = document.createElement("button");
+ close_button.innerHTML = "Close";
+ close_button.onclick = () => { this.close(); }
+ close_button.style.display = "inline-block";
+
+ var save_button = document.createElement("button");
+ save_button.innerHTML = "Save snapshot";
+ save_button.onclick = () => { save_current_snapshot(); }
+ save_button.style.display = "inline-block";
+ save_button.style.horizontalAlign = "right";
+
+ this.message_box = $el('div', {id:'custom-download-message'}, [$el('br'), '']);
+ this.message_box.style.height = '60px';
+ this.message_box.style.verticalAlign = 'middle';
+
+ this.element.appendChild(this.message_box);
+ this.element.appendChild(close_button);
+ this.element.appendChild(save_button);
+ }
+
+ async show() {
+ try {
+ this.invalidateControl();
+ this.element.style.display = "block";
+ this.element.style.zIndex = 10001;
+ }
+ catch(exception) {
+ app.ui.dialog.show(`Failed to get external model list. / ${exception}`);
+ }
+ }
+}
diff --git a/prestartup_script.py b/prestartup_script.py
index 43d7c862..374e62ba 100644
--- a/prestartup_script.py
+++ b/prestartup_script.py
@@ -17,6 +17,35 @@ def register_message_collapse(f):
sys.__comfyui_manager_register_message_collapse = register_message_collapse
+comfyui_manager_path = os.path.dirname(__file__)
+custom_nodes_path = os.path.join(comfyui_manager_path, "..")
+startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
+restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
+git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
+
+
+def handle_stream(stream, prefix):
+ for msg in stream:
+ print(prefix, msg, end="")
+
+
+def process_wrap(cmd_str, cwd_path, handler=None):
+ process = subprocess.Popen(cmd_str, cwd=cwd_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
+
+ if handler is None:
+ handler = handle_stream
+
+ stdout_thread = threading.Thread(target=handler, args=(process.stdout, ""))
+ stderr_thread = threading.Thread(target=handler, args=(process.stderr, "[!]"))
+
+ stdout_thread.start()
+ stderr_thread.start()
+
+ stdout_thread.join()
+ stderr_thread.join()
+
+ return process.wait()
+
try:
if '--port' in sys.argv:
@@ -96,11 +125,6 @@ try:
original_stderr.flush()
- def handle_stream(stream, prefix):
- for line in stream:
- print(prefix, line, end="")
-
-
def close_log():
log_file.close()
@@ -116,6 +140,66 @@ except Exception as e:
print("** ComfyUI start up time:", datetime.datetime.now())
+if os.path.exists(restore_snapshot_path):
+ try:
+ import json
+
+ cloned_repos = []
+
+ def msg_capture(stream, prefix):
+ for msg in stream:
+ if msg.startswith("CLONE: "):
+ cloned_repos.append(msg[7:])
+
+ print(prefix, msg, end="")
+
+ print(f"[ComfyUI-Manager] Restore snapshot.")
+ cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path]
+ exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture)
+
+ with open(restore_snapshot_path, 'r', encoding="UTF-8") as json_file:
+ info = json.load(json_file)
+ for url in cloned_repos:
+ try:
+ repository_name = url.split("/")[-1].strip()
+ repo_path = os.path.join(custom_nodes_path, repository_name)
+
+ requirements_path = os.path.join(repo_path, 'requirements.txt')
+ install_script_path = os.path.join(repo_path, 'install.py')
+
+ this_exit_code = 0
+
+ if os.path.exists(requirements_path):
+ with open(requirements_path, 'r', encoding="UTF-8") as file:
+ for line in file:
+ package_name = line.strip()
+ if package_name:
+ install_cmd = [sys.executable, "-m", "pip", "install", package_name]
+ this_exit_code += process_wrap(install_cmd, repo_path)
+
+ if os.path.exists(install_script_path):
+ install_cmd = [sys.executable, install_script_path]
+ this_exit_code += process_wrap(install_cmd, repo_path)
+
+ if this_exit_code != 0:
+ print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
+
+ except Exception as e:
+ print(e)
+ print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
+
+ if exit_code != 0:
+ print(f"[ComfyUI-Manager] Restore snapshot failed.")
+ else:
+ print(f"[ComfyUI-Manager] Restore snapshot done.")
+
+ except Exception as e:
+ print(e)
+ print(f"[ComfyUI-Manager] Restore snapshot failed.")
+
+ os.remove(restore_snapshot_path)
+
+
# Perform install
script_list_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "startup-scripts", "install-scripts.txt")
@@ -126,7 +210,7 @@ if os.path.exists(script_list_path):
executed = set()
# Read each line from the file and convert it to a list using eval
- with open(script_list_path, 'r') as file:
+ with open(script_list_path, 'r', encoding="UTF-8") as file:
for line in file:
if line in executed:
continue
@@ -135,24 +219,17 @@ if os.path.exists(script_list_path):
try:
script = eval(line)
- print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
+ if os.path.exists(script[0]):
+ print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
- print(f"\n## Execute install/(de)activation script for '{script[0]}'")
- process = subprocess.Popen(script[1:], cwd=script[0], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
+ print(f"\n## Execute install/(de)activation script for '{script[0]}'")
+ exit_code = process_wrap(script[1:], script[0])
- stdout_thread = threading.Thread(target=handle_stream, args=(process.stdout, ""))
- stderr_thread = threading.Thread(target=handle_stream, args=(process.stderr, "[!]"))
+ if exit_code != 0:
+ print(f"install/(de)activation script failed: {script[0]}")
+ else:
+ print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}")
- stdout_thread.start()
- stderr_thread.start()
-
- stdout_thread.join()
- stderr_thread.join()
-
- exit_code = process.wait()
-
- if exit_code != 0:
- print(f"install/(de)activation script failed: {script[0]}")
except Exception as e:
print(f"[ERROR] Failed to execute install/(de)activation script: {line} / {e}")