From 7fbe34f8dbf95e913b2854c5d28f5605cda0af68 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Sat, 4 Nov 2023 17:17:55 +0900 Subject: [PATCH] feat: snapshot --- .gitignore | 4 +- __init__.py | 161 ++- git_helper.py | 196 +++- js/a1111-alter-downloader.js | 554 +++++++++ js/comfyui-manager.js | 2020 ++++----------------------------- js/common.js | 62 + js/custom-nodes-downloader.js | 638 +++++++++++ js/model-downloader.js | 377 ++++++ js/snapshot.js | 279 +++++ prestartup_script.py | 119 +- 10 files changed, 2593 insertions(+), 1817 deletions(-) create mode 100644 js/a1111-alter-downloader.js create mode 100644 js/common.js create mode 100644 js/custom-nodes-downloader.js create mode 100644 js/model-downloader.js create mode 100644 js/snapshot.js diff --git a/.gitignore b/.gitignore index fa803b0..4abc713 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 15ad8fb..89b8e01 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 5603fad..2fbcf07 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 0000000..47bc28a --- /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 28ac95a..239de38 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,1697 +197,238 @@ 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'; - } - } +// ----------- +class ManagerMenuDialog extends ComfyDialog { + local_mode_checkbox = null; - 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; - } - } + createControlsMid() { + update_comfyui_button = + $el("button", { + type: "button", + textContent: "Update ComfyUI", + onclick: + () => updateComfyUI() + }); - 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'; - } - } - } + fetch_updates_button = + $el("button", { + type: "button", + textContent: "Fetch Updates", + onclick: + () => fetchUpdates(this.update_check_checkbox) + }); - async filter_missing_node(data) { - const mappings = await getCustomnodeMappings(); + update_all_button = + $el("button", { + type: "button", + textContent: "Update All", + onclick: + () => updateAll(this.update_check_checkbox) + }); + const res = + [ + $el("button", { + type: "button", + textContent: "Install Custom Nodes", + onclick: + () => { + if(!CustomNodesInstaller.instance) + CustomNodesInstaller.instance = new CustomNodesInstaller(app); + CustomNodesInstaller.instance.show(false); + } + }), - // 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); - } - } + $el("button", { + type: "button", + textContent: "Install Missing Custom Nodes", + onclick: + () => { + if(!CustomNodesInstaller.instance) + CustomNodesInstaller.instance = new CustomNodesInstaller(app); + CustomNodesInstaller.instance.show(true); + } + }), - // 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; - } - } + $el("button", { + type: "button", + textContent: "Install Models", + onclick: + () => { + if(!ModelInstaller.instance) + ModelInstaller.instance = new ModelInstaller(app); + ModelInstaller.instance.show(); + } + }), - const registered_nodes = new Set(); - for (let i in LiteGraph.registered_node_types) { - registered_nodes.add(LiteGraph.registered_node_types[i].type); - } + $el("br", {}, []), + update_all_button, + update_comfyui_button, + fetch_updates_button, - 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); + $el("br", {}, []), + $el("button", { + type: "button", + textContent: "Alternatives of A1111", + onclick: + () => { + if(!AlternativesInstaller.instance) + AlternativesInstaller.instance = new AlternativesInstaller(app); + AlternativesInstaller.instance.show(); } - } - } - } - } + }), - 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); - } + $el("br", {}, []), + ]; - return data.filter(node => node.files.some(file => missing_nodes.has(file))); + return res; } - 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)"; + 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.element.appendChild(msg); + 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; - // invalidate - this.data = (await getCustomNodes()).custom_nodes; + // 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)'}, [])); + preview_combo.appendChild($el('option', {value:'latent2rgb', text:'Preview method: Latent2RGB (fast)'}, [])); + preview_combo.appendChild($el('option', {value:'none', text:'Preview method: None (very fast)'}, [])); - if(this.is_missing_node_mode) - this.data = await this.filter_missing_node(this.data); + api.fetchApi('/manager/preview_method') + .then(response => response.text()) + .then(data => { preview_combo.value = data; }) - this.element.removeChild(msg); + preview_combo.addEventListener('change', function(event) { + api.fetchApi(`/manager/preview_method?value=${event.target.value}`); + }); - while (this.element.children.length) { - this.element.removeChild(this.element.children[0]); - } + // nickname + let badge_combo = document.createElement("select"); + badge_combo.appendChild($el('option', {value:'none', text:'Badge: None'}, [])); + badge_combo.appendChild($el('option', {value:'nick', text:'Badge: Nickname'}, [])); + badge_combo.appendChild($el('option', {value:'id_nick', text:'Badge: #ID Nickname'}, [])); - this.createHeaderControls(); - await this.createGrid(); - this.apply_searchbox(this.data); - this.createBottomControls(); - } + api.fetchApi('/manager/badge_mode') + .then(response => response.text()) + .then(data => { badge_combo.value = data; badge_mode = data; }); - updateMessage(msg) { - this.message_box.innerHTML = msg; - } + badge_combo.addEventListener('change', function(event) { + api.fetchApi(`/manager/badge_mode?value=${event.target.value}`); + badge_mode = event.target.value; + app.graph.setDirtyCanvas(true); + }); - 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; + // channel + let channel_combo = document.createElement("select"); + api.fetchApi('/manager/channel_url_list') + .then(response => response.json()) + .then(async data => { + try { + let urls = data.list; + for(let i in urls) { + if(urls[i] != '') { + let name_url = urls[i].split('::'); + channel_combo.appendChild($el('option', {value:name_url[0], text:`Channel: ${name_url[0]}`}, [])); + } + } - checkbox.disabled = data.installed != install_state; + channel_combo.addEventListener('change', function(event) { + api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`); + }); - if(checkbox.disabled) { - for(let j in buttons) { - buttons[j].style.display = 'none'; - } - } - else { - for(let j in buttons) { - buttons[j].style.display = null; - } - } - } + channel_combo.value = data.selected; + } + catch(exception) { - 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; + 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("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(); + } + }), + ]; + } - for(let j in buttons) { - buttons[j].style.display = null; - } - } + createControlsRight() { + return [ + $el("button", { + type: "button", + textContent: "ComfyUI Community Manual", + onclick: () => { window.open("https://blenderneko.github.io/ComfyUI-docs/", "comfyui-community-manual"); } + }), - this.checkbox_all.checked = false; - this.checkbox_all.disabled = true; - } - } + $el("button", { + type: "button", + textContent: "ComfyUI Workflow Gallery", + onclick: () => { window.open("https://comfyworkflows.com/", "comfyui-workflow-gallery"); } + }), - 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; - } - } + $el("button", { + type: "button", + textContent: "ComfyUI Nodes Info", + onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); } + }), + ]; + } - if(check_state == null) - return; + constructor() { + super(); - // 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; - - update_comfyui_button = - $el("button", { - type: "button", - textContent: "Update ComfyUI", - onclick: - () => updateComfyUI() - }); - - fetch_updates_button = - $el("button", { - type: "button", - textContent: "Fetch Updates", - onclick: - () => fetchUpdates(this.update_check_checkbox) - }); - - update_all_button = - $el("button", { - type: "button", - textContent: "Update All", - onclick: - () => updateAll(this.update_check_checkbox) - }); - - // 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)'}, [])); - preview_combo.appendChild($el('option', {value:'latent2rgb', text:'Preview method: Latent2RGB (fast)'}, [])); - preview_combo.appendChild($el('option', {value:'none', text:'Preview method: None (very fast)'}, [])); - - api.fetchApi('/manager/preview_method') - .then(response => response.text()) - .then(data => { preview_combo.value = data; }) - - preview_combo.addEventListener('change', function(event) { - api.fetchApi(`/manager/preview_method?value=${event.target.value}`); - }); - - // nickname - let badge_combo = document.createElement("select"); - badge_combo.appendChild($el('option', {value:'none', text:'Badge: None'}, [])); - badge_combo.appendChild($el('option', {value:'nick', text:'Badge: Nickname'}, [])); - badge_combo.appendChild($el('option', {value:'id_nick', text:'Badge: #ID Nickname'}, [])); - - api.fetchApi('/manager/badge_mode') - .then(response => response.text()) - .then(data => { badge_combo.value = data; badge_mode = data; }); - - badge_combo.addEventListener('change', function(event) { - api.fetchApi(`/manager/badge_mode?value=${event.target.value}`); - badge_mode = event.target.value; - app.graph.setDirtyCanvas(true); - }); - - // channel - let channel_combo = document.createElement("select"); - api.fetchApi('/manager/channel_url_list') - .then(response => response.json()) - .then(async data => { - try { - let urls = data.list; - for(let i in urls) { - if(urls[i] != '') { - let name_url = urls[i].split('::'); - channel_combo.appendChild($el('option', {value:name_url[0], text:`Channel: ${name_url[0]}`}, [])); - } - } - - channel_combo.addEventListener('change', function(event) { - api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`); - }); - - channel_combo.value = data.selected; - } - catch(exception) { - - } - }); - - 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); - } - }), - - $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", {}, []), - $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", - onclick: () => { window.open("https://comfyworkflows.com/", "comfyui-workflow-gallery"); } - }), - - $el("button", { - type: "button", - 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 0000000..b556269 --- /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 0000000..fd8553e --- /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 0000000..c83950f --- /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 0000000..ff00954 --- /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 43d7c86..374e62b 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, "[!]")) - - 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]}") + else: + print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}") - 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}")