mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2025-12-28 15:50:52 +08:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3103fc9864 | ||
|
|
637678db20 | ||
|
|
e97407a286 | ||
|
|
e494abb779 | ||
|
|
44093a42fa | ||
|
|
8e1481ae78 | ||
|
|
9c59e7498f | ||
|
|
0a202dd506 | ||
|
|
7eb4a3f961 | ||
|
|
1ce5603379 | ||
|
|
97b86b02ad | ||
|
|
f2da1635f2 | ||
|
|
f0ed5c3433 | ||
|
|
aca5925e57 | ||
|
|
b8d78174a5 | ||
|
|
edf2a43122 | ||
|
|
21de993546 | ||
|
|
49bc24b66e | ||
|
|
771d627c5a | ||
|
|
98967de31b | ||
|
|
c87c07dbd5 | ||
|
|
2478d20e76 | ||
|
|
cc3428eb3b | ||
|
|
6001bd4940 | ||
|
|
f8709f4091 | ||
|
|
3cff881b5b | ||
|
|
b79e997a14 | ||
|
|
ed2c34143c | ||
|
|
639b17ef6b | ||
|
|
7834411ef3 | ||
|
|
d8ea83a44c | ||
|
|
6b9818b748 | ||
|
|
b4d5b228ae | ||
|
|
29b4824ee2 | ||
|
|
e3a8b669b2 | ||
|
|
80e5c8a987 | ||
|
|
e0e4886e63 | ||
|
|
c0947f4192 | ||
|
|
7706b047ce | ||
|
|
a44c6ff27c | ||
|
|
f4fdd51ce9 | ||
|
|
ae6c7dd673 | ||
|
|
0cbc773126 | ||
|
|
45bd3473fa | ||
|
|
02175844da | ||
|
|
fd60f7ee70 | ||
|
|
9eb4c3ab23 | ||
|
|
72d1aa7d97 | ||
|
|
57628ead80 | ||
|
|
9733c2328b | ||
|
|
70663cecc3 | ||
|
|
7c77942a92 | ||
|
|
04cf18e149 | ||
|
|
1825edda7e | ||
|
|
045f91c411 | ||
|
|
96d24f548c | ||
|
|
c7f03ad64e | ||
|
|
1232989d7d | ||
|
|
8f66a7997f | ||
|
|
f32dd80c24 | ||
|
|
a06ba343de | ||
|
|
bba55d4d5a | ||
|
|
87111bd889 | ||
|
|
3661ffd3ab | ||
|
|
d8f111a5e3 | ||
|
|
ae5565ce68 | ||
|
|
e4c370a7d9 | ||
|
|
891005bcd3 | ||
|
|
d3a4a7a0fa | ||
|
|
10211d1a93 | ||
|
|
7f019a932b | ||
|
|
fae909de2f | ||
|
|
d8455ef6e5 | ||
|
|
934c994783 | ||
|
|
d0961d596d | ||
|
|
382df24764 | ||
|
|
bfcfa42125 | ||
|
|
2333886c34 | ||
|
|
0cdad3c886 | ||
|
|
eee23c543b | ||
|
|
f0a8812f5e | ||
|
|
a8d603f753 | ||
|
|
22acaa1d2c | ||
|
|
fe791ccee9 | ||
|
|
414557eee0 | ||
|
|
97d2741360 | ||
|
|
b95e5f1eae | ||
|
|
43b200dc91 | ||
|
|
29014699bb | ||
|
|
5576672957 | ||
|
|
5002606861 | ||
|
|
ba0fb343ff | ||
|
|
17e5ae6bc2 | ||
|
|
7a0186efc8 | ||
|
|
de64af4a68 | ||
|
|
4a852ac8a8 | ||
|
|
6784bfb98c | ||
|
|
c8f246d344 | ||
|
|
8b3d31a936 | ||
|
|
5e88d6445b | ||
|
|
fd7dff88df | ||
|
|
8cfee1f483 | ||
|
|
cf4d8e6125 | ||
|
|
c0e8a41d2a | ||
|
|
a02c27b1af | ||
|
|
712e1bac0d | ||
|
|
513ea46cbe | ||
|
|
b1919b6f95 | ||
|
|
43561d209b | ||
|
|
16dcbc5412 | ||
|
|
c8dd2d5cad | ||
|
|
4b37777066 | ||
|
|
95ecd85a12 | ||
|
|
5c475e3c15 | ||
|
|
f705ee6863 | ||
|
|
1f67c18989 | ||
|
|
de6d451c5b | ||
|
|
580296d6f3 | ||
|
|
a9e28fbce3 | ||
|
|
311779cb20 | ||
|
|
d2f8a89e87 | ||
|
|
84c95bf322 | ||
|
|
f75c801955 | ||
|
|
faa2f54371 | ||
|
|
4249ac193a | ||
|
|
c709274a28 | ||
|
|
c8f05e79db | ||
|
|
4d2887e99f | ||
|
|
29256a5154 | ||
|
|
82d42e4094 | ||
|
|
53850fb627 | ||
|
|
34b4c8ce46 | ||
|
|
e944841054 | ||
|
|
f6a5ff5552 | ||
|
|
01763b59d4 | ||
|
|
044173b2a1 | ||
|
|
99e7a88dbd | ||
|
|
01cd9fbb0e |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
import time
|
||||||
|
|
||||||
import git
|
import git
|
||||||
import json
|
import json
|
||||||
@ -219,7 +220,14 @@ def gitpull(path):
|
|||||||
repo.close()
|
repo.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
remote.pull()
|
try:
|
||||||
|
repo.git.pull('--ff-only')
|
||||||
|
except git.GitCommandError:
|
||||||
|
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
|
||||||
|
repo.create_head(backup_name)
|
||||||
|
print(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
||||||
|
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
||||||
|
print(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
||||||
|
|
||||||
repo.git.submodule('update', '--init', '--recursive')
|
repo.git.submodule('update', '--init', '--recursive')
|
||||||
new_commit_hash = repo.head.commit.hexsha
|
new_commit_hash = repo.head.commit.hexsha
|
||||||
|
|||||||
22910
github-stats-cache.json
Normal file
22910
github-stats-cache.json
Normal file
File diff suppressed because it is too large
Load Diff
11599
github-stats.json
11599
github-stats.json
File diff suppressed because it is too large
Load Diff
@ -44,7 +44,7 @@ import manager_migration
|
|||||||
from node_package import InstalledNodePackage
|
from node_package import InstalledNodePackage
|
||||||
|
|
||||||
|
|
||||||
version_code = [3, 38]
|
version_code = [3, 39]
|
||||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||||
|
|
||||||
|
|
||||||
@ -2253,9 +2253,17 @@ def git_pull(path):
|
|||||||
|
|
||||||
current_branch = repo.active_branch
|
current_branch = repo.active_branch
|
||||||
remote_name = current_branch.tracking_branch().remote_name
|
remote_name = current_branch.tracking_branch().remote_name
|
||||||
remote = repo.remote(name=remote_name)
|
|
||||||
|
|
||||||
remote.pull()
|
try:
|
||||||
|
repo.git.pull('--ff-only')
|
||||||
|
except git.GitCommandError:
|
||||||
|
branch_name = current_branch.name
|
||||||
|
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
|
||||||
|
repo.create_head(backup_name)
|
||||||
|
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
||||||
|
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
||||||
|
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
||||||
|
|
||||||
repo.git.submodule('update', '--init', '--recursive')
|
repo.git.submodule('update', '--init', '--recursive')
|
||||||
|
|
||||||
repo.close()
|
repo.close()
|
||||||
@ -2523,22 +2531,22 @@ def update_to_stable_comfyui(repo_path):
|
|||||||
logging.error('\t'+branch.name)
|
logging.error('\t'+branch.name)
|
||||||
return "fail", None
|
return "fail", None
|
||||||
|
|
||||||
versions, current_tag, _ = get_comfyui_versions(repo)
|
versions, current_tag, latest_tag = get_comfyui_versions(repo)
|
||||||
|
|
||||||
if len(versions) == 0 or (len(versions) == 1 and versions[0] == 'nightly'):
|
if latest_tag is None:
|
||||||
logging.info("[ComfyUI-Manager] Unable to update to the stable ComfyUI version.")
|
logging.info("[ComfyUI-Manager] Unable to update to the stable ComfyUI version.")
|
||||||
return "fail", None
|
return "fail", None
|
||||||
|
|
||||||
if versions[0] == 'nightly':
|
|
||||||
latest_tag = versions[1]
|
|
||||||
else:
|
|
||||||
latest_tag = versions[0]
|
|
||||||
|
|
||||||
if current_tag == latest_tag:
|
tag_ref = next((t for t in repo.tags if t.name == latest_tag), None)
|
||||||
|
if tag_ref is None:
|
||||||
|
logging.info(f"[ComfyUI-Manager] Unable to locate tag '{latest_tag}' in repository.")
|
||||||
|
return "fail", None
|
||||||
|
|
||||||
|
if repo.head.commit == tag_ref.commit:
|
||||||
return "skip", None
|
return "skip", None
|
||||||
else:
|
else:
|
||||||
logging.info(f"[ComfyUI-Manager] Updating ComfyUI: {current_tag} -> {latest_tag}")
|
logging.info(f"[ComfyUI-Manager] Updating ComfyUI: {current_tag} -> {latest_tag}")
|
||||||
repo.git.checkout(latest_tag)
|
repo.git.checkout(tag_ref.name)
|
||||||
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
|
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
|
||||||
return 'updated', latest_tag
|
return 'updated', latest_tag
|
||||||
except:
|
except:
|
||||||
@ -3362,36 +3370,80 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_comfyui_versions(repo=None):
|
def get_comfyui_versions(repo=None):
|
||||||
if repo is None:
|
repo = repo or git.Repo(comfy_path)
|
||||||
repo = git.Repo(comfy_path)
|
|
||||||
|
|
||||||
|
remote_name = None
|
||||||
try:
|
try:
|
||||||
remote = get_remote_name(repo)
|
remote_name = get_remote_name(repo)
|
||||||
repo.remotes[remote].fetch()
|
repo.remotes[remote_name].fetch()
|
||||||
except:
|
except:
|
||||||
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
|
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
|
||||||
|
|
||||||
versions = [x.name for x in repo.tags if x.name.startswith('v')]
|
def parse_semver(tag_name):
|
||||||
|
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', tag_name)
|
||||||
|
return tuple(int(x) for x in match.groups()) if match else None
|
||||||
|
|
||||||
# nearest tag
|
def normalize_describe(tag_name):
|
||||||
versions = sorted(versions, key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
|
if not tag_name:
|
||||||
versions = versions[:4]
|
return None
|
||||||
|
base = tag_name.split('-', 1)[0]
|
||||||
|
return base if parse_semver(base) else None
|
||||||
|
|
||||||
current_tag = repo.git.describe('--tags')
|
# Collect semver tags and sort descending (highest first)
|
||||||
|
semver_tags = []
|
||||||
|
for tag in repo.tags:
|
||||||
|
semver = parse_semver(tag.name)
|
||||||
|
if semver:
|
||||||
|
semver_tags.append((semver, tag.name))
|
||||||
|
semver_tags.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
semver_tags = [name for _, name in semver_tags]
|
||||||
|
|
||||||
if current_tag not in versions:
|
latest_tag = semver_tags[0] if semver_tags else None
|
||||||
versions = sorted(versions + [current_tag], key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
|
|
||||||
versions = versions[:4]
|
|
||||||
|
|
||||||
main_branch = repo.heads.master
|
try:
|
||||||
latest_commit = main_branch.commit
|
described = repo.git.describe('--tags')
|
||||||
latest_tag = repo.git.describe('--tags', latest_commit.hexsha)
|
except Exception:
|
||||||
|
described = ''
|
||||||
|
|
||||||
if latest_tag != versions[0]:
|
try:
|
||||||
versions.insert(0, 'nightly')
|
exact_tag = repo.git.describe('--tags', '--exact-match')
|
||||||
else:
|
except Exception:
|
||||||
versions[0] = 'nightly'
|
exact_tag = ''
|
||||||
|
|
||||||
|
head_is_default = False
|
||||||
|
if remote_name:
|
||||||
|
try:
|
||||||
|
default_head_ref = repo.refs[f'{remote_name}/HEAD']
|
||||||
|
default_commit = default_head_ref.reference.commit
|
||||||
|
head_is_default = repo.head.commit == default_commit
|
||||||
|
except Exception:
|
||||||
|
head_is_default = False
|
||||||
|
|
||||||
|
nearest_semver = normalize_describe(described)
|
||||||
|
exact_semver = exact_tag if parse_semver(exact_tag) else None
|
||||||
|
|
||||||
|
if head_is_default and not exact_tag:
|
||||||
current_tag = 'nightly'
|
current_tag = 'nightly'
|
||||||
|
else:
|
||||||
|
current_tag = exact_tag or described or 'nightly'
|
||||||
|
|
||||||
|
# Prepare semver list for display: top 4 plus the current/nearest semver if missing
|
||||||
|
display_semver_tags = semver_tags[:4]
|
||||||
|
if exact_semver and exact_semver not in display_semver_tags:
|
||||||
|
display_semver_tags.append(exact_semver)
|
||||||
|
elif nearest_semver and nearest_semver not in display_semver_tags:
|
||||||
|
display_semver_tags.append(nearest_semver)
|
||||||
|
|
||||||
|
versions = ['nightly']
|
||||||
|
|
||||||
|
if current_tag and not exact_semver and current_tag not in versions and current_tag not in display_semver_tags:
|
||||||
|
versions.append(current_tag)
|
||||||
|
|
||||||
|
for tag in display_semver_tags:
|
||||||
|
if tag not in versions:
|
||||||
|
versions.append(tag)
|
||||||
|
|
||||||
|
versions = versions[:6]
|
||||||
|
|
||||||
return versions, current_tag, latest_tag
|
return versions, current_tag, latest_tag
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ def check_legacy_backup(manager_files_path):
|
|||||||
|
|
||||||
# Notice board output
|
# Notice board output
|
||||||
add_startup_notice(
|
add_startup_notice(
|
||||||
"Legacy ComfyUI-Manager data backup exists. Please verify and remove when no longer needed.",
|
"Legacy ComfyUI-Manager data backup exists. Please verify and remove when no longer needed. See terminal for details.",
|
||||||
level='info'
|
level='info'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,25 @@ SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in
|
|||||||
|
|
||||||
routes = PromptServer.instance.routes
|
routes = PromptServer.instance.routes
|
||||||
|
|
||||||
|
|
||||||
|
def has_per_queue_preview():
|
||||||
|
"""
|
||||||
|
Check if ComfyUI PR #11261 (per-queue live preview override) is merged
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if ComfyUI has per-queue preview feature
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import latent_preview
|
||||||
|
return hasattr(latent_preview, 'set_preview_method')
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Detect ComfyUI per-queue preview override feature (PR #11261)
|
||||||
|
COMFYUI_HAS_PER_QUEUE_PREVIEW = has_per_queue_preview()
|
||||||
|
|
||||||
|
|
||||||
def handle_stream(stream, prefix):
|
def handle_stream(stream, prefix):
|
||||||
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
|
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
|
||||||
for msg in stream:
|
for msg in stream:
|
||||||
@ -182,10 +201,19 @@ def set_preview_method(method):
|
|||||||
core.get_config()['preview_method'] = method
|
core.get_config()['preview_method'] = method
|
||||||
|
|
||||||
|
|
||||||
if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
|
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
|
||||||
|
logging.info(
|
||||||
|
"[ComfyUI-Manager] ComfyUI per-queue preview override detected (PR #11261). "
|
||||||
|
"Manager's preview method feature is disabled. "
|
||||||
|
"Use ComfyUI's --preview-method CLI option or 'Settings > Execution > Live preview method'."
|
||||||
|
)
|
||||||
|
elif args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
|
||||||
set_preview_method(core.get_config()['preview_method'])
|
set_preview_method(core.get_config()['preview_method'])
|
||||||
else:
|
else:
|
||||||
logging.warning("[ComfyUI-Manager] Since --preview-method is set, ComfyUI-Manager's preview method feature will be ignored.")
|
logging.warning(
|
||||||
|
"[ComfyUI-Manager] Since --preview-method is set, "
|
||||||
|
"ComfyUI-Manager's preview method feature will be ignored."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_component_policy(mode):
|
def set_component_policy(mode):
|
||||||
@ -1482,13 +1510,25 @@ async def install_model(request):
|
|||||||
|
|
||||||
@routes.get("/manager/preview_method")
|
@routes.get("/manager/preview_method")
|
||||||
async def preview_method(request):
|
async def preview_method(request):
|
||||||
|
# Setting change request
|
||||||
if "value" in request.rel_url.query:
|
if "value" in request.rel_url.query:
|
||||||
|
# Reject setting change if per-queue preview feature is available
|
||||||
|
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
|
||||||
|
return web.Response(text="DISABLED", status=403)
|
||||||
|
|
||||||
|
# Process normally if not available
|
||||||
set_preview_method(request.rel_url.query['value'])
|
set_preview_method(request.rel_url.query['value'])
|
||||||
core.write_config()
|
core.write_config()
|
||||||
else:
|
return web.Response(status=200)
|
||||||
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
|
|
||||||
|
|
||||||
return web.Response(status=200)
|
# Status query request
|
||||||
|
else:
|
||||||
|
# Return DISABLED if per-queue preview feature is available
|
||||||
|
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
|
||||||
|
return web.Response(text="DISABLED", status=200)
|
||||||
|
|
||||||
|
# Return current value if not available
|
||||||
|
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/manager/db_mode")
|
@routes.get("/manager/db_mode")
|
||||||
|
|||||||
@ -55,7 +55,7 @@ def get_pip_cmd(force_uv=False):
|
|||||||
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
|
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
|
||||||
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip']
|
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip']
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.warning("[ComfyUI-Manager] python -m pip not available. Falling back to uv.")
|
logging.warning("[ComfyUI-Manager] `python -m pip` not available. Falling back to `uv`.")
|
||||||
|
|
||||||
# Try uv (either forced or pip failed)
|
# Try uv (either forced or pip failed)
|
||||||
import shutil
|
import shutil
|
||||||
@ -64,19 +64,19 @@ def get_pip_cmd(force_uv=False):
|
|||||||
try:
|
try:
|
||||||
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', '--version']
|
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', '--version']
|
||||||
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
|
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
|
||||||
logging.info("[ComfyUI-Manager] Using uv as Python module for pip operations.")
|
logging.info("[ComfyUI-Manager] Using `uv` as Python module for pip operations.")
|
||||||
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', 'pip']
|
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', 'pip']
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Try standalone uv
|
# Try standalone uv
|
||||||
if shutil.which('uv'):
|
if shutil.which('uv'):
|
||||||
logging.info("[ComfyUI-Manager] Using standalone uv for pip operations.")
|
logging.info("[ComfyUI-Manager] Using standalone `uv` for pip operations.")
|
||||||
return ['uv', 'pip']
|
return ['uv', 'pip']
|
||||||
|
|
||||||
# Nothing worked
|
# Nothing worked
|
||||||
logging.error("[ComfyUI-Manager] Neither python -m pip nor uv are available. Cannot proceed with package operations.")
|
logging.error("[ComfyUI-Manager] Neither `python -m pip` nor `uv` are available. Cannot proceed with package operations.")
|
||||||
raise Exception("Neither pip nor uv are available for package management")
|
raise Exception("Neither `pip` nor `uv` are available for package management")
|
||||||
|
|
||||||
|
|
||||||
def make_pip_cmd(cmd):
|
def make_pip_cmd(cmd):
|
||||||
|
|||||||
227
js/comfyui-gui-builder.js
Normal file
227
js/comfyui-gui-builder.js
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import { $el } from "../../scripts/ui.js";
|
||||||
|
|
||||||
|
function normalizeContent(content) {
|
||||||
|
const tmp = document.createElement('div');
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
tmp.innerHTML = content;
|
||||||
|
return Array.from(tmp.childNodes);
|
||||||
|
}
|
||||||
|
if (content instanceof Node) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSettingsCombo(label, content) {
|
||||||
|
const settingItem = $el("div.setting-item", {}, [
|
||||||
|
$el("div.flex.flex-row.items-center.gap-2",[
|
||||||
|
$el("div.form-label.flex.grow.items-center", [
|
||||||
|
$el("span.text-muted", { textContent: label },)
|
||||||
|
]),
|
||||||
|
$el("div.form-input.flex.justify-end",
|
||||||
|
[content]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
return settingItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGuiFrame(dialogId, title, iconClass, content, owner) {
|
||||||
|
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
|
||||||
|
parent: document.body,
|
||||||
|
style: {
|
||||||
|
position: "fixed",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
left: "0px",
|
||||||
|
top: "0px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
zIndex: "1000"
|
||||||
|
},
|
||||||
|
onclick: (e) => {
|
||||||
|
if (e.target === dialog_mask) {
|
||||||
|
owner.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// data-pc-section="mask"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header_actions = $el("div.p-dialog-header-actions", {
|
||||||
|
// [TODO]
|
||||||
|
// data-pc-section="headeractions"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
|
||||||
|
parent: header_actions,
|
||||||
|
type: "button",
|
||||||
|
ariaLabel: "Close",
|
||||||
|
onclick: () => owner.close(),
|
||||||
|
// "data-pc-name": "pcclosebutton",
|
||||||
|
// "data-p-disabled": "false",
|
||||||
|
// "data-p-severity": "secondary",
|
||||||
|
// "data-pc-group-section": "headericon",
|
||||||
|
// "data-pc-extend": "button",
|
||||||
|
// "data-pc-section": "root",
|
||||||
|
// [FIXME] Not sure how to do most of the SVG using $el
|
||||||
|
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label"> </span><!---->'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialog_header = $el("div.p-dialog-header",
|
||||||
|
[
|
||||||
|
$el("div", [
|
||||||
|
$el("div",
|
||||||
|
{
|
||||||
|
id: "frame-title-container",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
$el("h2.px-4", [
|
||||||
|
$el(iconClass, {
|
||||||
|
style: {
|
||||||
|
"font-size": "1.25rem",
|
||||||
|
"margin-right": ".5rem"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$el("span", { textContent: title })
|
||||||
|
])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
header_actions
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
|
||||||
|
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
|
||||||
|
id: dialogId,
|
||||||
|
parent: dialog_mask,
|
||||||
|
style: {
|
||||||
|
'display': 'flex',
|
||||||
|
'flex-direction': 'column',
|
||||||
|
'pointer-events': 'auto',
|
||||||
|
'margin': '0px',
|
||||||
|
},
|
||||||
|
role: 'dialog',
|
||||||
|
ariaModal: 'true',
|
||||||
|
// [TODO]
|
||||||
|
// ariaLabbelledby: 'cm-title',
|
||||||
|
// maximized: 'false',
|
||||||
|
// data-pc-name: 'dialog',
|
||||||
|
// data-pc-section: 'root',
|
||||||
|
// data-pd-focustrap: 'true'
|
||||||
|
},
|
||||||
|
[ dialog_header, contentFrame ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
|
||||||
|
parent: manager_dialog,
|
||||||
|
tabindex: "0",
|
||||||
|
role: "presentation",
|
||||||
|
ariaHidden: "true",
|
||||||
|
"data-p-hidden-accessible": "true",
|
||||||
|
"data-p-hidden-focusable": "true",
|
||||||
|
"data-pc-section": "firstfocusableelement"
|
||||||
|
});
|
||||||
|
|
||||||
|
return dialog_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGuiFrameCustomHeader(dialogId, customHeader, content, owner) {
|
||||||
|
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
|
||||||
|
parent: document.body,
|
||||||
|
style: {
|
||||||
|
position: "fixed",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
left: "0px",
|
||||||
|
top: "0px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
zIndex: "1000"
|
||||||
|
},
|
||||||
|
onclick: (e) => {
|
||||||
|
if (e.target === dialog_mask) {
|
||||||
|
owner.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// data-pc-section="mask"
|
||||||
|
});
|
||||||
|
|
||||||
|
const header_actions = $el("div.p-dialog-header-actions", {
|
||||||
|
// [TODO]
|
||||||
|
// data-pc-section="headeractions"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
|
||||||
|
parent: header_actions,
|
||||||
|
type: "button",
|
||||||
|
ariaLabel: "Close",
|
||||||
|
onclick: () => owner.close(),
|
||||||
|
// "data-pc-name": "pcclosebutton",
|
||||||
|
// "data-p-disabled": "false",
|
||||||
|
// "data-p-severity": "secondary",
|
||||||
|
// "data-pc-group-section": "headericon",
|
||||||
|
// "data-pc-extend": "button",
|
||||||
|
// "data-pc-section": "root",
|
||||||
|
// [FIXME] Not sure how to do most of the SVG using $el
|
||||||
|
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label"> </span><!---->'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const _customHeader = normalizeContent(customHeader);
|
||||||
|
const dialog_header = $el("div.p-dialog-header",
|
||||||
|
[
|
||||||
|
$el("div", [
|
||||||
|
$el("div",
|
||||||
|
{
|
||||||
|
id: "frame-title-container",
|
||||||
|
},
|
||||||
|
Array.isArray(_customHeader) ? _customHeader : [_customHeader]
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
header_actions
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
|
||||||
|
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
|
||||||
|
id: dialogId,
|
||||||
|
parent: dialog_mask,
|
||||||
|
style: {
|
||||||
|
'display': 'flex',
|
||||||
|
'flex-direction': 'column',
|
||||||
|
'pointer-events': 'auto',
|
||||||
|
'margin': '0px',
|
||||||
|
},
|
||||||
|
role: 'dialog',
|
||||||
|
ariaModal: 'true',
|
||||||
|
// [TODO]
|
||||||
|
// ariaLabbelledby: 'cm-title',
|
||||||
|
// maximized: 'false',
|
||||||
|
// data-pc-name: 'dialog',
|
||||||
|
// data-pc-section: 'root',
|
||||||
|
// data-pd-focustrap: 'true'
|
||||||
|
},
|
||||||
|
[ dialog_header, contentFrame ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
|
||||||
|
parent: manager_dialog,
|
||||||
|
tabindex: "0",
|
||||||
|
role: "presentation",
|
||||||
|
ariaHidden: "true",
|
||||||
|
"data-p-hidden-accessible": "true",
|
||||||
|
"data-p-hidden-focusable": "true",
|
||||||
|
"data-pc-section": "firstfocusableelement"
|
||||||
|
});
|
||||||
|
|
||||||
|
return dialog_mask;
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import { ComponentBuilderDialog, getPureName, load_components, set_component_pol
|
|||||||
import { CustomNodesManager } from "./custom-nodes-manager.js";
|
import { CustomNodesManager } from "./custom-nodes-manager.js";
|
||||||
import { ModelManager } from "./model-manager.js";
|
import { ModelManager } from "./model-manager.js";
|
||||||
import { SnapshotManager } from "./snapshot.js";
|
import { SnapshotManager } from "./snapshot.js";
|
||||||
|
import { buildGuiFrame, createSettingsCombo } from "./comfyui-gui-builder.js";
|
||||||
|
|
||||||
let manager_version = await getVersion();
|
let manager_version = await getVersion();
|
||||||
|
|
||||||
@ -44,12 +45,16 @@ docStyle.innerHTML = `
|
|||||||
|
|
||||||
#cm-manager-dialog {
|
#cm-manager-dialog {
|
||||||
width: 1000px;
|
width: 1000px;
|
||||||
height: 455px;
|
height: auto;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cm-manager-dialog br {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.cb-widget {
|
.cb-widget {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
@ -80,6 +85,7 @@ docStyle.innerHTML = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-menu-container {
|
.cm-menu-container {
|
||||||
|
padding : calc(var(--spacing)*2);
|
||||||
column-gap: 20px;
|
column-gap: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -140,8 +146,8 @@ docStyle.innerHTML = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-notice-board {
|
.cm-notice-board {
|
||||||
width: 290px;
|
width: auto;
|
||||||
height: 230px;
|
height: 280px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
border: 1px solid var(--descrip-text);
|
border: 1px solid var(--descrip-text);
|
||||||
@ -238,68 +244,50 @@ var is_updating = false;
|
|||||||
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
||||||
const style = `
|
const style = `
|
||||||
#workflowgallery-button {
|
#workflowgallery-button {
|
||||||
width: 310px;
|
height: 50px;
|
||||||
height: 27px;
|
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 17px !important;
|
|
||||||
}
|
}
|
||||||
#cm-nodeinfo-button {
|
#cm-nodeinfo-button {
|
||||||
width: 310px;
|
|
||||||
height: 27px;
|
|
||||||
padding: 0px !important;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 17px !important;
|
|
||||||
}
|
}
|
||||||
#cm-manual-button {
|
#cm-manual-button {
|
||||||
width: 310px;
|
|
||||||
height: 27px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-button {
|
.cm-button {
|
||||||
width: 310px;
|
width: auto;
|
||||||
height: 30px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 17px !important;
|
background-color: var(--comfy-menu-secondary-bg);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--input-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-button:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-button-red {
|
.cm-button-red {
|
||||||
width: 310px;
|
|
||||||
height: 30px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 17px !important;
|
|
||||||
background-color: #500000 !important;
|
background-color: #500000 !important;
|
||||||
|
border-color: #88181b !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-button-red:hover {
|
||||||
|
background-color: #88181b !important;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-button-orange {
|
.cm-button-orange {
|
||||||
width: 310px;
|
|
||||||
height: 30px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 17px !important;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: orange !important;
|
background-color: orange !important;
|
||||||
color: black !important;
|
color: black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-experimental-button {
|
.cm-experimental-button {
|
||||||
width: 290px;
|
width: 100%;
|
||||||
height: 30px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 17px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-experimental {
|
.cm-experimental {
|
||||||
width: 310px;
|
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -326,8 +314,14 @@ const style = `
|
|||||||
|
|
||||||
.cm-menu-combo {
|
.cm-menu-combo {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 310px;
|
padding: 0.5em 0.5em;
|
||||||
box-sizing: border-box;
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--comfy-menu-secondary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-menu-combo:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-small-button {
|
.cm-small-button {
|
||||||
@ -831,7 +825,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
const isElectron = 'electronAPI' in window;
|
const isElectron = 'electronAPI' in window;
|
||||||
|
|
||||||
update_comfyui_button =
|
update_comfyui_button =
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Update ComfyUI",
|
textContent: "Update ComfyUI",
|
||||||
style: {
|
style: {
|
||||||
@ -842,7 +836,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
});
|
});
|
||||||
|
|
||||||
switch_comfyui_button =
|
switch_comfyui_button =
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Switch ComfyUI",
|
textContent: "Switch ComfyUI",
|
||||||
style: {
|
style: {
|
||||||
@ -853,7 +847,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
});
|
});
|
||||||
|
|
||||||
restart_stop_button =
|
restart_stop_button =
|
||||||
$el("button.cm-button-red", {
|
$el("button.p-button.p-component.cm-button-red", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Restart",
|
textContent: "Restart",
|
||||||
onclick: () => restartOrStop()
|
onclick: () => restartOrStop()
|
||||||
@ -861,7 +855,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
|
|
||||||
if(isElectron) {
|
if(isElectron) {
|
||||||
update_all_button =
|
update_all_button =
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Update All Custom Nodes",
|
textContent: "Update All Custom Nodes",
|
||||||
onclick:
|
onclick:
|
||||||
@ -870,7 +864,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
update_all_button =
|
update_all_button =
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Update All",
|
textContent: "Update All",
|
||||||
onclick:
|
onclick:
|
||||||
@ -880,7 +874,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
|
|
||||||
const res =
|
const res =
|
||||||
[
|
[
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Custom Nodes Manager",
|
textContent: "Custom Nodes Manager",
|
||||||
onclick:
|
onclick:
|
||||||
@ -892,7 +886,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Install Missing Custom Nodes",
|
textContent: "Install Missing Custom Nodes",
|
||||||
onclick:
|
onclick:
|
||||||
@ -904,7 +898,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Custom Nodes In Workflow",
|
textContent: "Custom Nodes In Workflow",
|
||||||
onclick:
|
onclick:
|
||||||
@ -916,8 +910,8 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
$el("br", {}, []),
|
$el("div", {}, []),
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Model Manager",
|
textContent: "Model Manager",
|
||||||
onclick:
|
onclick:
|
||||||
@ -929,7 +923,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Install via Git URL",
|
textContent: "Install via Git URL",
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
@ -941,13 +935,13 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
$el("br", {}, []),
|
$el("div", {}, []),
|
||||||
update_all_button,
|
update_all_button,
|
||||||
update_comfyui_button,
|
update_comfyui_button,
|
||||||
switch_comfyui_button,
|
switch_comfyui_button,
|
||||||
// fetch_updates_button,
|
// fetch_updates_button,
|
||||||
|
|
||||||
$el("br", {}, []),
|
$el("div", {}, []),
|
||||||
restart_stop_button,
|
restart_stop_button,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -960,12 +954,13 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
// db mode
|
// db mode
|
||||||
|
|
||||||
this.datasrc_combo = document.createElement("select");
|
this.datasrc_combo = document.createElement("select");
|
||||||
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened.");
|
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened.");
|
||||||
this.datasrc_combo.className = "cm-menu-combo";
|
this.datasrc_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled ";
|
||||||
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Channel (1day cache)' }, []));
|
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'Channel (1day cache)' }, []));
|
||||||
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
|
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'Local' }, []));
|
||||||
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'DB: Channel (remote)' }, []));
|
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'Channel (remote)' }, []));
|
||||||
|
|
||||||
api.fetchApi('/manager/db_mode')
|
api.fetchApi('/manager/db_mode')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
@ -975,27 +970,110 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
|
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
|
||||||
|
|
||||||
// preview method
|
// preview method
|
||||||
let preview_combo = document.createElement("select");
|
let preview_combo = document.createElement("select");
|
||||||
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
|
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
|
||||||
preview_combo.className = "cm-menu-combo";
|
preview_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||||
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)' }, []));
|
|
||||||
|
|
||||||
|
// Loading state to prevent flash of enabled state
|
||||||
|
preview_combo.appendChild($el('option', { value: '', text: 'Loading...', disabled: true }, []));
|
||||||
|
preview_combo.appendChild($el('option', { value: 'auto', text: 'Auto' }, []));
|
||||||
|
preview_combo.appendChild($el('option', { value: 'taesd', text: 'TAESD (slow)' }, []));
|
||||||
|
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Latent2RGB (fast)' }, []));
|
||||||
|
preview_combo.appendChild($el('option', { value: 'none', text: 'None (very fast)' }, []));
|
||||||
|
|
||||||
|
// Start disabled to prevent flash
|
||||||
|
preview_combo.disabled = true;
|
||||||
|
preview_combo.value = '';
|
||||||
|
|
||||||
|
// Fetch current state
|
||||||
api.fetchApi('/manager/preview_method')
|
api.fetchApi('/manager/preview_method')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => { preview_combo.value = data; });
|
.then(data => {
|
||||||
|
// Remove loading option
|
||||||
|
preview_combo.querySelector('option[value=""]')?.remove();
|
||||||
|
|
||||||
|
if (data === "DISABLED") {
|
||||||
|
// ComfyUI per-queue preview feature is active
|
||||||
|
preview_combo.disabled = true;
|
||||||
|
preview_combo.value = 'auto';
|
||||||
|
|
||||||
|
// Accessibility attributes
|
||||||
|
preview_combo.setAttribute("aria-disabled", "true");
|
||||||
|
preview_combo.setAttribute("aria-label",
|
||||||
|
"Preview method setting (disabled - managed by ComfyUI). " +
|
||||||
|
"Use Settings > Execution > Live preview method instead."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tooltip for mouse users
|
||||||
|
preview_combo.setAttribute("title",
|
||||||
|
"This feature is now provided natively by ComfyUI. " +
|
||||||
|
"Please use 'Settings > Execution > Live preview method' instead."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
preview_combo.style.opacity = '0.6';
|
||||||
|
preview_combo.style.cursor = 'not-allowed';
|
||||||
|
} else {
|
||||||
|
// Manager feature is active
|
||||||
|
preview_combo.disabled = false;
|
||||||
|
preview_combo.value = data;
|
||||||
|
|
||||||
|
// Accessibility for enabled state
|
||||||
|
preview_combo.setAttribute("aria-label",
|
||||||
|
"Preview method setting. Select how latent variables are decoded during preview."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[ComfyUI-Manager] Failed to fetch preview method status:', error);
|
||||||
|
// Error recovery: fallback to enabled
|
||||||
|
preview_combo.querySelector('option[value=""]')?.remove();
|
||||||
|
preview_combo.disabled = false;
|
||||||
|
preview_combo.value = 'auto';
|
||||||
|
});
|
||||||
|
|
||||||
preview_combo.addEventListener('change', function (event) {
|
preview_combo.addEventListener('change', function (event) {
|
||||||
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
|
// Ignore if disabled
|
||||||
|
if (preview_combo.disabled) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal operation
|
||||||
|
api.fetchApi(`/manager/preview_method?value=${event.target.value}`)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 403) {
|
||||||
|
// Feature transitioned to native
|
||||||
|
alert(
|
||||||
|
'This feature is now provided natively by ComfyUI.\n' +
|
||||||
|
'Please use \'Settings > Execution > Live preview method\' instead.'
|
||||||
|
);
|
||||||
|
preview_combo.disabled = true;
|
||||||
|
preview_combo.style.opacity = '0.6';
|
||||||
|
preview_combo.style.cursor = 'not-allowed';
|
||||||
|
|
||||||
|
// Update aria attributes
|
||||||
|
preview_combo.setAttribute("aria-disabled", "true");
|
||||||
|
preview_combo.setAttribute("aria-label",
|
||||||
|
"Preview method setting (disabled - managed by ComfyUI). " +
|
||||||
|
"Use Settings > Execution > Live preview method instead."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[ComfyUI-Manager] Preview method update failed:', error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const previewSetttingItem = createSettingsCombo("Preview method", preview_combo);
|
||||||
|
|
||||||
// channel
|
// channel
|
||||||
let channel_combo = document.createElement("select");
|
let channel_combo = document.createElement("select");
|
||||||
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list.");
|
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list.");
|
||||||
channel_combo.className = "cm-menu-combo";
|
channel_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||||
api.fetchApi('/manager/channel_url_list')
|
api.fetchApi('/manager/channel_url_list')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(async data => {
|
.then(async data => {
|
||||||
@ -1004,7 +1082,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
for (let i in urls) {
|
for (let i in urls) {
|
||||||
if (urls[i] != '') {
|
if (urls[i] != '') {
|
||||||
let name_url = urls[i].split('::');
|
let name_url = urls[i].split('::');
|
||||||
channel_combo.appendChild($el('option', { value: name_url[0], text: `Channel: ${name_url[0]}` }, []));
|
channel_combo.appendChild($el('option', { value: name_url[0], text: `${name_url[0]}` }, []));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1019,11 +1097,13 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const channelSetttingItem = createSettingsCombo("Channel", channel_combo);
|
||||||
|
|
||||||
|
|
||||||
// share
|
// share
|
||||||
let share_combo = document.createElement("select");
|
let share_combo = document.createElement("select");
|
||||||
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
|
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
|
||||||
share_combo.className = "cm-menu-combo";
|
share_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||||
const share_options = [
|
const share_options = [
|
||||||
['none', 'None'],
|
['none', 'None'],
|
||||||
['openart', 'OpenArt AI'],
|
['openart', 'OpenArt AI'],
|
||||||
@ -1034,7 +1114,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
['all', 'All'],
|
['all', 'All'],
|
||||||
];
|
];
|
||||||
for (const option of share_options) {
|
for (const option of share_options) {
|
||||||
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
|
share_combo.appendChild($el('option', { value: option[0], text: `${option[1]}` }, []));
|
||||||
}
|
}
|
||||||
|
|
||||||
api.fetchApi('/manager/share_option')
|
api.fetchApi('/manager/share_option')
|
||||||
@ -1056,12 +1136,14 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shareSetttingItem = createSettingsCombo("Share", share_combo);
|
||||||
|
|
||||||
let component_policy_combo = document.createElement("select");
|
let component_policy_combo = document.createElement("select");
|
||||||
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
|
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
|
||||||
component_policy_combo.className = "cm-menu-combo";
|
component_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||||
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
|
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Use workflow version' }, []));
|
||||||
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
|
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Use higher version' }, []));
|
||||||
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
|
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Use my version' }, []));
|
||||||
api.fetchApi('/manager/policy/component')
|
api.fetchApi('/manager/policy/component')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@ -1074,15 +1156,14 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
set_component_policy(event.target.value);
|
set_component_policy(event.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
update_policy_combo = document.createElement("select");
|
const componentSetttingItem = createSettingsCombo("Component", component_policy_combo);
|
||||||
|
|
||||||
if(isElectron)
|
update_policy_combo = document.createElement("select");
|
||||||
update_policy_combo.style.display = 'none';
|
|
||||||
|
|
||||||
update_policy_combo.setAttribute("title", "Sets the policy to be applied when performing an update.");
|
update_policy_combo.setAttribute("title", "Sets the policy to be applied when performing an update.");
|
||||||
update_policy_combo.className = "cm-menu-combo";
|
update_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||||
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'Update: ComfyUI Stable Version' }, []));
|
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'ComfyUI Stable Version' }, []));
|
||||||
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'Update: ComfyUI Nightly Version' }, []));
|
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'ComfyUI Nightly Version' }, []));
|
||||||
api.fetchApi('/manager/policy/update')
|
api.fetchApi('/manager/policy/update')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@ -1093,20 +1174,22 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
|
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
|
||||||
$el("br", {}, []),
|
|
||||||
this.datasrc_combo,
|
if(isElectron)
|
||||||
channel_combo,
|
updateSetttingItem.style.display = 'none';
|
||||||
preview_combo,
|
|
||||||
share_combo,
|
|
||||||
component_policy_combo,
|
|
||||||
update_policy_combo,
|
|
||||||
$el("br", {}, []),
|
|
||||||
|
|
||||||
$el("br", {}, []),
|
return [
|
||||||
$el("filedset.cm-experimental", {}, [
|
dbRetrievalSetttingItem,
|
||||||
|
channelSetttingItem,
|
||||||
|
previewSetttingItem,
|
||||||
|
shareSetttingItem,
|
||||||
|
componentSetttingItem,
|
||||||
|
updateSetttingItem,
|
||||||
|
//[TODO] replace mt-2 with wrapper div with flex column gap
|
||||||
|
$el("filedset.cm-experimental.mt-auto", {}, [
|
||||||
$el("legend.cm-experimental-legend", {}, ["EXPERIMENTAL"]),
|
$el("legend.cm-experimental-legend", {}, ["EXPERIMENTAL"]),
|
||||||
$el("button.cm-experimental-button", {
|
$el("button.p-button.p-component.cm-button.cm-experimental-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Snapshot Manager",
|
textContent: "Snapshot Manager",
|
||||||
onclick:
|
onclick:
|
||||||
@ -1116,7 +1199,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
SnapshotManager.instance.show();
|
SnapshotManager.instance.show();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
$el("button.cm-experimental-button", {
|
$el("button.p-button.p-component.cm-button.cm-experimental-button.mt-2", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Install PIP packages",
|
textContent: "Install PIP packages",
|
||||||
onclick:
|
onclick:
|
||||||
@ -1134,7 +1217,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
|
|
||||||
createControlsRight() {
|
createControlsRight() {
|
||||||
const elts = [
|
const elts = [
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
id: 'cm-manual-button',
|
id: 'cm-manual-button',
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Community Manual",
|
textContent: "Community Manual",
|
||||||
@ -1185,11 +1268,11 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
|
|
||||||
$el("button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
id: 'workflowgallery-button',
|
id: 'workflowgallery-button',
|
||||||
type: "button",
|
type: "button",
|
||||||
style: {
|
style: {
|
||||||
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
|
// ...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
|
||||||
},
|
},
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
const last_visited_site = localStorage.getItem("wg_last_visited")
|
const last_visited_site = localStorage.getItem("wg_last_visited")
|
||||||
@ -1212,7 +1295,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}, [
|
}, [
|
||||||
$el("p", {
|
$el("p", {
|
||||||
id: 'workflowgallery-button-last-visited-label',
|
id: 'workflowgallery-button-last-visited-label',
|
||||||
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : ''})`,
|
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : 'none selected'})`,
|
||||||
style: {
|
style: {
|
||||||
'text-align': 'center',
|
'text-align': 'center',
|
||||||
'color': 'var(--input-text)',
|
'color': 'var(--input-text)',
|
||||||
@ -1228,13 +1311,12 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
|
|
||||||
$el("button.cm-button", {
|
$el("button.p-button.p-component.cm-button", {
|
||||||
id: 'cm-nodeinfo-button',
|
id: 'cm-nodeinfo-button',
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Nodes Info",
|
textContent: "Nodes Info",
|
||||||
onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); }
|
onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); }
|
||||||
}),
|
}),
|
||||||
$el("br", {}, []),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
var textarea = document.createElement("div");
|
var textarea = document.createElement("div");
|
||||||
@ -1249,31 +1331,23 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => this.close() });
|
const content = $el("div.cm-menu-container",
|
||||||
|
[
|
||||||
|
$el("div.cm-menu-column.gap-2", [...this.createControlsLeft()]),
|
||||||
|
$el("div.cm-menu-column.gap-2", [...this.createControlsMid()]),
|
||||||
|
$el("div.cm-menu-column.gap-2", [...this.createControlsRight()])
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const content =
|
const frame = buildGuiFrame(
|
||||||
$el("div.comfy-modal-content",
|
'cm-manager-dialog', // dialog id
|
||||||
[
|
`ComfyUI Manager ${manager_version}`, // dialog title
|
||||||
$el("tr.cm-title", {}, [
|
"i.mdi.mdi-puzzle", // dialog icon class to show before title
|
||||||
$el("font", {size:6, color:"white"}, [`ComfyUI Manager ${manager_version}`])]
|
content, // dialog content element
|
||||||
),
|
this
|
||||||
$el("br", {}, []),
|
); // send this so we can attach close functions
|
||||||
$el("div.cm-menu-container",
|
|
||||||
[
|
|
||||||
$el("div.cm-menu-column", [...this.createControlsLeft()]),
|
|
||||||
$el("div.cm-menu-column", [...this.createControlsMid()]),
|
|
||||||
$el("div.cm-menu-column", [...this.createControlsRight()])
|
|
||||||
]),
|
|
||||||
|
|
||||||
$el("br", {}, []),
|
this.element = frame;
|
||||||
close_button,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
content.style.width = '100%';
|
|
||||||
content.style.height = '100%';
|
|
||||||
|
|
||||||
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
@ -1281,7 +1355,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.element.style.display = "block";
|
this.element.style.display = "flex";
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleVisibility() {
|
toggleVisibility() {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
.cn-manager {
|
.cn-manager {
|
||||||
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
z-index: 1099;
|
z-index: 1099;
|
||||||
width: 80%;
|
width: 80vw;
|
||||||
height: 80%;
|
height: 75vh;
|
||||||
|
min-height: 30em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -10,6 +11,7 @@
|
|||||||
font-family: arial, sans-serif;
|
font-family: arial, sans-serif;
|
||||||
text-underline-offset: 3px;
|
text-underline-offset: 3px;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
margin: calc(var(--spacing)*2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cn-manager .cn-flex-auto {
|
.cn-manager .cn-flex-auto {
|
||||||
@ -17,17 +19,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cn-manager button {
|
.cn-manager button {
|
||||||
|
width: auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
background-color: var(--comfy-input-bg);
|
background-color: var(--comfy-input-bg);
|
||||||
border-radius: 8px;
|
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
border-style: solid;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 8px;
|
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cn-manager button:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
|
}
|
||||||
|
|
||||||
.cn-manager button:disabled,
|
.cn-manager button:disabled,
|
||||||
.cn-manager input:disabled,
|
.cn-manager input:disabled,
|
||||||
.cn-manager select:disabled {
|
.cn-manager select:disabled {
|
||||||
@ -40,8 +46,13 @@
|
|||||||
|
|
||||||
.cn-manager .cn-manager-restart {
|
.cn-manager .cn-manager-restart {
|
||||||
display: none;
|
display: none;
|
||||||
background-color: #500000;
|
background-color: #500000 !important;
|
||||||
color: white;
|
border-color: #88181b !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-manager-restart:hover {
|
||||||
|
background-color: #88181b !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cn-manager .cn-manager-stop {
|
.cn-manager .cn-manager-stop {
|
||||||
@ -79,7 +90,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cn-manager-header label {
|
.cn-manager-header label {
|
||||||
@ -91,16 +101,32 @@
|
|||||||
.cn-manager-filter {
|
.cn-manager-filter {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5em 0.5em;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-filter:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cn-manager-keywords {
|
.cn-manager-keywords {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
padding: 0 5px 0 26px;
|
padding: 0 5px 0 26px;
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
background-size: 16px;
|
background-size: 16px;
|
||||||
background-position: 5px center;
|
background-position: 5px center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
|
||||||
|
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
outline-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cn-manager-status {
|
.cn-manager-status {
|
||||||
@ -588,6 +614,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cn-install-buttons button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.cn-selected-buttons {
|
.cn-selected-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
|
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
manager_instance, rebootAPI, install_via_git_url,
|
manager_instance, rebootAPI, install_via_git_url,
|
||||||
@ -18,32 +19,19 @@ loadCss("./custom-nodes-manager.css");
|
|||||||
const gridId = "node";
|
const gridId = "node";
|
||||||
|
|
||||||
const pageHtml = `
|
const pageHtml = `
|
||||||
<div class="cn-manager-header">
|
<div class="cn-manager cn-manager-dark">
|
||||||
<label>Filter
|
<div class="cn-manager-grid"></div>
|
||||||
<select class="cn-manager-filter"></select>
|
<div class="cn-manager-selection"></div>
|
||||||
</label>
|
<div class="cn-manager-message"></div>
|
||||||
<input class="cn-manager-keywords" type="search" placeholder="Search" />
|
<div class="cn-manager-footer">
|
||||||
<div class="cn-manager-status"></div>
|
<button class="cn-manager-restart p-button p-component">Restart</button>
|
||||||
<div class="cn-flex-auto"></div>
|
<button class="cn-manager-stop p-button p-component">Stop</button>
|
||||||
<div class="cn-manager-channel"></div>
|
<div class="cn-flex-auto"></div>
|
||||||
</div>
|
<button class="cn-manager-used-in-workflow p-button p-component">Used In Workflow</button>
|
||||||
<div class="cn-manager-grid"></div>
|
<button class="cn-manager-check-update p-button p-component">Check Update</button>
|
||||||
<div class="cn-manager-selection"></div>
|
<button class="cn-manager-check-missing p-button p-component">Check Missing</button>
|
||||||
<div class="cn-manager-message"></div>
|
<button class="cn-manager-install-url p-button p-component">Install via Git URL</button>
|
||||||
<div class="cn-manager-footer">
|
</div>
|
||||||
<button class="cn-manager-back">
|
|
||||||
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button class="cn-manager-restart">Restart</button>
|
|
||||||
<button class="cn-manager-stop">Stop</button>
|
|
||||||
<div class="cn-flex-auto"></div>
|
|
||||||
<button class="cn-manager-used-in-workflow">Used In Workflow</button>
|
|
||||||
<button class="cn-manager-check-update">Check Update</button>
|
|
||||||
<button class="cn-manager-check-missing">Check Missing</button>
|
|
||||||
<button class="cn-manager-install-url">Install via Git URL</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -89,11 +77,26 @@ export class CustomNodesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.element = $el("div", {
|
const header = $el("div.cn-manager-header.px-2", {}, [
|
||||||
parent: document.body,
|
// $el("label", {}, [
|
||||||
className: "comfy-modal cn-manager"
|
// $el("span", { textContent: "Filter" }),
|
||||||
});
|
// $el("select.cn-manager-filter")
|
||||||
this.element.innerHTML = pageHtml;
|
// ]),
|
||||||
|
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
|
||||||
|
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
|
||||||
|
$el("div.cn-manager-status"),
|
||||||
|
$el("div.cn-flex-auto"),
|
||||||
|
$el("div.cn-manager-channel")
|
||||||
|
]);
|
||||||
|
|
||||||
|
const frame = buildGuiFrameCustomHeader(
|
||||||
|
'cn-manager-dialog', // dialog id
|
||||||
|
header, // custom header element
|
||||||
|
pageHtml, // dialog content element
|
||||||
|
this
|
||||||
|
); // send this so we can attach close functions
|
||||||
|
|
||||||
|
this.element = frame;
|
||||||
this.element.setAttribute("tabindex", 0);
|
this.element.setAttribute("tabindex", 0);
|
||||||
this.element.focus();
|
this.element.focus();
|
||||||
|
|
||||||
@ -372,7 +375,7 @@ export class CustomNodesManager {
|
|||||||
|
|
||||||
return list.map(id => {
|
return list.map(id => {
|
||||||
const bt = buttons[id];
|
const bt = buttons[id];
|
||||||
return `<button class="cn-btn-${id}" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
|
return `<button class="cn-btn-${id} p-button p-component" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,7 +658,6 @@ export class CustomNodesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderGrid() {
|
renderGrid() {
|
||||||
|
|
||||||
// update theme
|
// update theme
|
||||||
const globalStyle = window.getComputedStyle(document.body);
|
const globalStyle = window.getComputedStyle(document.body);
|
||||||
this.colorVars = {
|
this.colorVars = {
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
.cmm-manager {
|
.cmm-manager {
|
||||||
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
z-index: 1099;
|
z-index: 1099;
|
||||||
width: 80%;
|
width: 80vw;
|
||||||
height: 80%;
|
height: 75vh;
|
||||||
|
min-height: 30em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
font-family: arial, sans-serif;
|
font-family: arial, sans-serif;
|
||||||
|
margin: calc(var(--spacing)*2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cmm-manager .cmm-flex-auto {
|
.cmm-manager .cmm-flex-auto {
|
||||||
@ -18,14 +20,15 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
background-color: var(--comfy-input-bg);
|
background-color: var(--comfy-input-bg);
|
||||||
border-radius: 8px;
|
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
border-style: solid;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 8px;
|
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cmm-manager button:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
|
}
|
||||||
|
|
||||||
.cmm-manager button:disabled,
|
.cmm-manager button:disabled,
|
||||||
.cmm-manager input:disabled,
|
.cmm-manager input:disabled,
|
||||||
.cmm-manager select:disabled {
|
.cmm-manager select:disabled {
|
||||||
@ -38,7 +41,7 @@
|
|||||||
|
|
||||||
.cmm-manager .cmm-manager-refresh {
|
.cmm-manager .cmm-manager-refresh {
|
||||||
display: none;
|
display: none;
|
||||||
background-color: #000080;
|
background-color: #000080 !important;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +56,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cmm-manager-header label {
|
.cmm-manager-header label {
|
||||||
@ -67,16 +69,34 @@
|
|||||||
.cmm-manager-filter {
|
.cmm-manager-filter {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5em 0.5em;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-type:hover,
|
||||||
|
.cmm-manager-base:hover,
|
||||||
|
.cmm-manager-filter:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cmm-manager-keywords {
|
.cmm-manager-keywords {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
padding: 0 5px 0 26px;
|
padding: 0 5px 0 26px;
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
background-size: 16px;
|
background-size: 16px;
|
||||||
background-position: 5px center;
|
background-position: 5px center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
|
||||||
|
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
outline-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cmm-manager-status {
|
.cmm-manager-status {
|
||||||
@ -148,6 +168,10 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cmm-btn-install {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.cmm-btn-download {
|
.cmm-btn-download {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|||||||
@ -9,39 +9,22 @@ import { api } from "../../scripts/api.js";
|
|||||||
|
|
||||||
// https://cenfun.github.io/turbogrid/api.html
|
// https://cenfun.github.io/turbogrid/api.html
|
||||||
import TG from "./turbogrid.esm.js";
|
import TG from "./turbogrid.esm.js";
|
||||||
|
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
|
||||||
|
|
||||||
loadCss("./model-manager.css");
|
loadCss("./model-manager.css");
|
||||||
|
|
||||||
const gridId = "model";
|
const gridId = "model";
|
||||||
|
|
||||||
const pageHtml = `
|
const pageHtml = `
|
||||||
<div class="cmm-manager-header">
|
<div class="cmm-manager cmm-manager-dark">
|
||||||
<label>Filter
|
<div class="cmm-manager-grid"></div>
|
||||||
<select class="cmm-manager-filter"></select>
|
<div class="cmm-manager-selection"></div>
|
||||||
</label>
|
<div class="cmm-manager-message"></div>
|
||||||
<label>Type
|
<div class="cmm-manager-footer">
|
||||||
<select class="cmm-manager-type"></select>
|
<button class="cmm-manager-refresh p-button p-component">Refresh</button>
|
||||||
</label>
|
<button class="cmm-manager-stop p-button p-component">Stop</button>
|
||||||
<label>Base
|
<div class="cmm-flex-auto"></div>
|
||||||
<select class="cmm-manager-base"></select>
|
</div>
|
||||||
</label>
|
|
||||||
<input class="cmm-manager-keywords" type="search" placeholder="Search" />
|
|
||||||
<div class="cmm-manager-status"></div>
|
|
||||||
<div class="cmm-flex-auto"></div>
|
|
||||||
</div>
|
|
||||||
<div class="cmm-manager-grid"></div>
|
|
||||||
<div class="cmm-manager-selection"></div>
|
|
||||||
<div class="cmm-manager-message"></div>
|
|
||||||
<div class="cmm-manager-footer">
|
|
||||||
<button class="cmm-manager-back">
|
|
||||||
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button class="cmm-manager-refresh">Refresh</button>
|
|
||||||
<button class="cmm-manager-stop">Stop</button>
|
|
||||||
<div class="cmm-flex-auto"></div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -64,11 +47,23 @@ export class ModelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.element = $el("div", {
|
const header = $el("div.cmm-manager-header", {}, [
|
||||||
parent: document.body,
|
createSettingsCombo("Filter", $el("select.cmm-manager-filter")),
|
||||||
className: "comfy-modal cmm-manager"
|
createSettingsCombo("Type", $el("select.cmm-manager-type")),
|
||||||
});
|
createSettingsCombo("Base", $el("select.cmm-manager-base")),
|
||||||
this.element.innerHTML = pageHtml;
|
$el("input.cmm-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
|
||||||
|
$el("div.cmm-manager-status"),
|
||||||
|
$el("div.cmm-flex-auto")
|
||||||
|
]);
|
||||||
|
|
||||||
|
const frame = buildGuiFrameCustomHeader(
|
||||||
|
'cmm-manager-dialog', // dialog id
|
||||||
|
header, // custom header element
|
||||||
|
pageHtml, // dialog content element
|
||||||
|
this
|
||||||
|
); // send this so we can attach close functions
|
||||||
|
|
||||||
|
this.element = frame;
|
||||||
this.initFilter();
|
this.initFilter();
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.initGrid();
|
this.initGrid();
|
||||||
@ -347,7 +342,7 @@ export class ModelManager {
|
|||||||
if (installed === "True") {
|
if (installed === "True") {
|
||||||
return `<div class="cmm-icon-passed">${icons.passed}</div>`;
|
return `<div class="cmm-icon-passed">${icons.passed}</div>`;
|
||||||
}
|
}
|
||||||
return `<button class="cmm-btn-install" mode="install">Install</button>`;
|
return `<button class="cmm-btn-install p-button p-component" mode="install">Install</button>`;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
id: 'url',
|
id: 'url',
|
||||||
@ -420,7 +415,7 @@ export class ModelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.selectedModels = selectedList;
|
this.selectedModels = selectedList;
|
||||||
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install" mode="install">Install</button>`);
|
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install p-button p-component" mode="install">Install</button>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
focusInstall(item) {
|
focusInstall(item) {
|
||||||
|
|||||||
65
js/snapshot.css
Normal file
65
js/snapshot.css
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
.snapshot-manager {
|
||||||
|
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
|
z-index: 1099;
|
||||||
|
width: 80vw;
|
||||||
|
height: 75vh;
|
||||||
|
min-height: 30em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--fg-color);
|
||||||
|
font-family: arial, sans-serif;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
outline: none;
|
||||||
|
margin: calc(var(--spacing)*2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-manager button {
|
||||||
|
width: auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--input-text);
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
margin: 0;
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-manager .snapshot-restore-btn {
|
||||||
|
background-color: #00158f !important;
|
||||||
|
border-color: #2025b9 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-manager .snapshot-remove-btn {
|
||||||
|
background-color: #970000 !important;
|
||||||
|
border-color: #be2127 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-manager button:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-manager .data-btns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(var(--spacing)*2);
|
||||||
|
padding: calc(var(--spacing)*2);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-manager .cn-flex-auto {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js"
|
import { api } from "../../scripts/api.js"
|
||||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||||
import { manager_instance, rebootAPI, show_message, handle403Response } from "./common.js";
|
import { manager_instance, rebootAPI, show_message, handle403Response, loadCss } from "./common.js";
|
||||||
|
import { buildGuiFrame } from "./comfyui-gui-builder.js";
|
||||||
|
|
||||||
|
loadCss("./snapshot.css");
|
||||||
|
|
||||||
async function restore_snapshot(target) {
|
async function restore_snapshot(target) {
|
||||||
if(SnapshotManager.instance) {
|
if(SnapshotManager.instance) {
|
||||||
@ -27,7 +29,7 @@ async function restore_snapshot(target) {
|
|||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
await SnapshotManager.instance.invalidateControl();
|
await SnapshotManager.instance.invalidateControl();
|
||||||
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='cm-small-button'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
|
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='p-button p-component'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,6 +90,8 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
message_box = null;
|
message_box = null;
|
||||||
data = null;
|
data = null;
|
||||||
|
|
||||||
|
content = $el("div.snapshot-manager");
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.restore_buttons = [];
|
this.restore_buttons = [];
|
||||||
this.message_box = null;
|
this.message_box = null;
|
||||||
@ -96,9 +100,18 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
|
|
||||||
constructor(app, manager_dialog) {
|
constructor(app, manager_dialog) {
|
||||||
super();
|
super();
|
||||||
this.manager_dialog = manager_dialog;
|
// this.manager_dialog = manager_dialog;
|
||||||
this.search_keyword = '';
|
this.search_keyword = '';
|
||||||
this.element = $el("div.comfy-modal", { parent: document.body }, []);
|
|
||||||
|
const frame = buildGuiFrame(
|
||||||
|
'snapshot-manager-dialog', // dialog id
|
||||||
|
'Snapshot Manager', // title
|
||||||
|
'i.mdi.mdi-puzzle', // icon class
|
||||||
|
this.content, // dialog content element
|
||||||
|
this
|
||||||
|
); // send this so we can attach close functions
|
||||||
|
|
||||||
|
this.element = frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove_item() {
|
async remove_item() {
|
||||||
@ -109,7 +122,7 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
|
|
||||||
createControls() {
|
createControls() {
|
||||||
return [
|
return [
|
||||||
$el("button.cm-small-button", {
|
$el("button.p-button.p-component", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Close",
|
textContent: "Close",
|
||||||
onclick: () => { this.close(); }
|
onclick: () => { this.close(); }
|
||||||
@ -132,8 +145,8 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
this.clear();
|
this.clear();
|
||||||
this.data = (await getSnapshotList()).items;
|
this.data = (await getSnapshotList()).items;
|
||||||
|
|
||||||
while (this.element.children.length) {
|
while (this.content.children.length) {
|
||||||
this.element.removeChild(this.element.children[0]);
|
this.content.removeChild(this.content.children[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.createGrid();
|
await this.createGrid();
|
||||||
@ -204,20 +217,21 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
data2.innerHTML = ` ${data}`;
|
data2.innerHTML = ` ${data}`;
|
||||||
var data_button = document.createElement('td');
|
var data_button = document.createElement('td');
|
||||||
data_button.style.textAlign = "center";
|
data_button.style.textAlign = "center";
|
||||||
|
data_button.className = "data-btns";
|
||||||
|
|
||||||
var restoreBtn = document.createElement('button');
|
var restoreBtn = document.createElement('button');
|
||||||
|
restoreBtn.className = "snapshot-restore-btn p-button p-component";
|
||||||
restoreBtn.innerHTML = 'Restore';
|
restoreBtn.innerHTML = 'Restore';
|
||||||
restoreBtn.style.width = "100px";
|
restoreBtn.style.width = "100px";
|
||||||
restoreBtn.style.backgroundColor = 'blue';
|
|
||||||
|
|
||||||
restoreBtn.addEventListener('click', function() {
|
restoreBtn.addEventListener('click', function() {
|
||||||
restore_snapshot(data);
|
restore_snapshot(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
var removeBtn = document.createElement('button');
|
var removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className = "snapshot-remove-btn p-button p-component";
|
||||||
removeBtn.innerHTML = 'Remove';
|
removeBtn.innerHTML = 'Remove';
|
||||||
removeBtn.style.width = "100px";
|
removeBtn.style.width = "100px";
|
||||||
removeBtn.style.backgroundColor = 'red';
|
|
||||||
|
|
||||||
removeBtn.addEventListener('click', function() {
|
removeBtn.addEventListener('click', function() {
|
||||||
remove_snapshot(data);
|
remove_snapshot(data);
|
||||||
@ -241,13 +255,14 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
let self = this;
|
let self = this;
|
||||||
const panel = document.createElement('div');
|
const panel = document.createElement('div');
|
||||||
panel.style.width = "100%";
|
panel.style.width = "100%";
|
||||||
|
panel.style.height = "100%";
|
||||||
panel.appendChild(grid);
|
panel.appendChild(grid);
|
||||||
|
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
const parentHeight = self.element.clientHeight;
|
const parentHeight = self.element.clientHeight;
|
||||||
const gridHeight = parentHeight - 200;
|
const gridHeight = parentHeight - 200;
|
||||||
|
|
||||||
grid.style.height = gridHeight + "px";
|
// grid.style.height = gridHeight + "px";
|
||||||
}
|
}
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
@ -256,25 +271,17 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
grid.style.width = "100%";
|
grid.style.width = "100%";
|
||||||
grid.style.height = "100%";
|
grid.style.height = "100%";
|
||||||
grid.style.overflowY = "scroll";
|
grid.style.overflowY = "scroll";
|
||||||
this.element.style.height = "85%";
|
|
||||||
this.element.style.width = "80%";
|
this.content.appendChild(panel);
|
||||||
this.element.appendChild(panel);
|
|
||||||
|
|
||||||
handleResize();
|
handleResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBottomControls() {
|
async createBottomControls() {
|
||||||
var close_button = document.createElement("button");
|
|
||||||
close_button.className = "cm-small-button";
|
|
||||||
close_button.innerHTML = "Close";
|
|
||||||
close_button.onclick = () => { this.close(); }
|
|
||||||
close_button.style.display = "inline-block";
|
|
||||||
|
|
||||||
var save_button = document.createElement("button");
|
var save_button = document.createElement("button");
|
||||||
save_button.className = "cm-small-button";
|
save_button.className = "p-button p-component";
|
||||||
save_button.innerHTML = "Save snapshot";
|
save_button.innerHTML = "Save snapshot";
|
||||||
save_button.onclick = () => { save_current_snapshot(); }
|
save_button.onclick = () => { save_current_snapshot(); }
|
||||||
save_button.style.display = "inline-block";
|
|
||||||
save_button.style.horizontalAlign = "right";
|
save_button.style.horizontalAlign = "right";
|
||||||
save_button.style.width = "170px";
|
save_button.style.width = "170px";
|
||||||
|
|
||||||
@ -282,15 +289,19 @@ export class SnapshotManager extends ComfyDialog {
|
|||||||
this.message_box.style.height = '60px';
|
this.message_box.style.height = '60px';
|
||||||
this.message_box.style.verticalAlign = 'middle';
|
this.message_box.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
this.element.appendChild(this.message_box);
|
const footer = $el("div.snapshot-footer");
|
||||||
this.element.appendChild(close_button);
|
const spacer = $el("div.cn-flex-auto");
|
||||||
this.element.appendChild(save_button);
|
footer.appendChild(spacer);
|
||||||
|
footer.appendChild(save_button);
|
||||||
|
|
||||||
|
this.content.appendChild(this.message_box);
|
||||||
|
this.content.appendChild(footer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async show() {
|
async show() {
|
||||||
try {
|
try {
|
||||||
this.invalidateControl();
|
this.invalidateControl();
|
||||||
this.element.style.display = "block";
|
this.element.style.display = "flex";
|
||||||
this.element.style.zIndex = 1099;
|
this.element.style.zIndex = 1099;
|
||||||
}
|
}
|
||||||
catch(exception) {
|
catch(exception) {
|
||||||
|
|||||||
273
json-checker.py
273
json-checker.py
@ -1,25 +1,264 @@
|
|||||||
import json
|
#!/usr/bin/env python3
|
||||||
import argparse
|
"""JSON Entry Validator
|
||||||
|
|
||||||
def check_json_syntax(file_path):
|
Validates JSON entries based on content structure.
|
||||||
|
|
||||||
|
Validation rules based on JSON content:
|
||||||
|
- {"custom_nodes": [...]}: Validates required fields (author, title, reference, files, install_type, description)
|
||||||
|
- {"models": [...]}: Validates JSON syntax only (no required fields)
|
||||||
|
- Other JSON structures: Validates JSON syntax only
|
||||||
|
|
||||||
|
Git repository URL validation (for custom_nodes):
|
||||||
|
1. URLs must NOT end with .git
|
||||||
|
2. URLs must follow format: https://github.com/{author}/{reponame}
|
||||||
|
3. .py and .js files are exempt from this check
|
||||||
|
|
||||||
|
Supported formats:
|
||||||
|
- Array format: [{...}, {...}]
|
||||||
|
- Object format: {"custom_nodes": [...]} or {"models": [...]}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
# Required fields for each entry type
|
||||||
|
REQUIRED_FIELDS_CUSTOM_NODE = ['author', 'title', 'reference', 'files', 'install_type', 'description']
|
||||||
|
REQUIRED_FIELDS_MODEL = [] # model-list.json doesn't require field validation
|
||||||
|
|
||||||
|
# Pattern for valid GitHub repository URL (without .git suffix)
|
||||||
|
GITHUB_REPO_PATTERN = re.compile(r'^https://github\.com/[^/]+/[^/]+$')
|
||||||
|
|
||||||
|
|
||||||
|
def get_entry_context(entry: Dict) -> str:
|
||||||
|
"""Get identifying information from entry for error messages
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry: JSON entry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
String with author and reference info
|
||||||
|
"""
|
||||||
|
parts = []
|
||||||
|
if 'author' in entry:
|
||||||
|
parts.append(f"author={entry['author']}")
|
||||||
|
if 'reference' in entry:
|
||||||
|
parts.append(f"ref={entry['reference']}")
|
||||||
|
if 'title' in entry:
|
||||||
|
parts.append(f"title={entry['title']}")
|
||||||
|
|
||||||
|
if parts:
|
||||||
|
return " | ".join(parts)
|
||||||
|
else:
|
||||||
|
# No identifying info - show actual entry content (truncated)
|
||||||
|
import json
|
||||||
|
entry_str = json.dumps(entry, ensure_ascii=False)
|
||||||
|
if len(entry_str) > 100:
|
||||||
|
entry_str = entry_str[:100] + "..."
|
||||||
|
return f"content={entry_str}"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_required_fields(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
|
||||||
|
"""Validate that all required fields are present
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry: JSON entry to validate
|
||||||
|
entry_index: Index of entry in array (for error reporting)
|
||||||
|
required_fields: List of required field names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of error descriptions (without entry prefix/context)
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in entry:
|
||||||
|
errors.append(f"Missing required field '{field}'")
|
||||||
|
elif entry[field] is None:
|
||||||
|
errors.append(f"Field '{field}' is null")
|
||||||
|
elif isinstance(entry[field], str) and not entry[field].strip():
|
||||||
|
errors.append(f"Field '{field}' is empty")
|
||||||
|
elif field == 'files' and not entry[field]: # Empty array
|
||||||
|
errors.append("Field 'files' is empty array")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def validate_git_repo_urls(entry: Dict, entry_index: int) -> List[str]:
|
||||||
|
"""Validate git repository URLs in 'files' array
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Git repo URLs must NOT end with .git
|
||||||
|
- Must follow format: https://github.com/{author}/{reponame}
|
||||||
|
- .py and .js files are exempt
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry: JSON entry to validate
|
||||||
|
entry_index: Index of entry in array (for error reporting)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of error descriptions (without entry prefix/context)
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
if 'files' not in entry or not isinstance(entry['files'], list):
|
||||||
|
return errors
|
||||||
|
|
||||||
|
for file_url in entry['files']:
|
||||||
|
if not isinstance(file_url, str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip .py and .js files - they're exempt from git repo validation
|
||||||
|
if file_url.endswith('.py') or file_url.endswith('.js'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if it's a GitHub URL (likely a git repo)
|
||||||
|
if 'github.com' in file_url:
|
||||||
|
# Error if URL ends with .git
|
||||||
|
if file_url.endswith('.git'):
|
||||||
|
errors.append(f"Git repo URL must NOT end with .git: {file_url}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Validate format: https://github.com/{author}/{reponame}
|
||||||
|
if not GITHUB_REPO_PATTERN.match(file_url):
|
||||||
|
errors.append(f"Invalid git repo URL format (expected https://github.com/author/reponame): {file_url}")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def validate_entry(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
|
||||||
|
"""Validate a single JSON entry
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry: JSON entry to validate
|
||||||
|
entry_index: Index of entry in array (for error reporting)
|
||||||
|
required_fields: List of required field names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of error messages (empty if valid)
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
errors.extend(validate_required_fields(entry, entry_index, required_fields))
|
||||||
|
|
||||||
|
# Check git repository URLs
|
||||||
|
errors.extend(validate_git_repo_urls(entry, entry_index))
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def validate_json_file(file_path: str) -> Tuple[bool, List[str]]:
|
||||||
|
"""Validate JSON file containing entries
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to JSON file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, error_messages)
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Check file exists
|
||||||
|
path = Path(file_path)
|
||||||
|
if not path.exists():
|
||||||
|
return False, [f"File not found: {file_path}"]
|
||||||
|
|
||||||
|
# Load JSON
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='utf-8') as file:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
json_str = file.read()
|
data = json.load(f)
|
||||||
json.loads(json_str)
|
|
||||||
print(f"[ OK ] {file_path}")
|
|
||||||
except UnicodeDecodeError as e:
|
|
||||||
print(f"Unicode decode error: {e}")
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"[FAIL] {file_path}\n\n {e}\n")
|
return False, [f"Invalid JSON: {e}"]
|
||||||
except FileNotFoundError:
|
except Exception as e:
|
||||||
print(f"[FAIL] {file_path}\n\n File not found\n")
|
return False, [f"Error reading file: {e}"]
|
||||||
|
|
||||||
|
# Determine required fields based on JSON content
|
||||||
|
required_fields = []
|
||||||
|
|
||||||
|
# Validate structure - support both array and object formats
|
||||||
|
entries_to_validate = []
|
||||||
|
|
||||||
|
if isinstance(data, list):
|
||||||
|
# Direct array format: [{...}, {...}]
|
||||||
|
entries_to_validate = data
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
# Object format: {"custom_nodes": [...]} or {"models": [...]}
|
||||||
|
# Determine validation based on keys
|
||||||
|
if 'custom_nodes' in data and isinstance(data['custom_nodes'], list):
|
||||||
|
required_fields = REQUIRED_FIELDS_CUSTOM_NODE
|
||||||
|
entries_to_validate = data['custom_nodes']
|
||||||
|
elif 'models' in data and isinstance(data['models'], list):
|
||||||
|
required_fields = REQUIRED_FIELDS_MODEL
|
||||||
|
entries_to_validate = data['models']
|
||||||
|
else:
|
||||||
|
# Other JSON structures (extension-node-map.json, etc.) - just validate JSON syntax
|
||||||
|
return True, []
|
||||||
|
else:
|
||||||
|
return False, ["JSON root must be either an array or an object containing arrays"]
|
||||||
|
|
||||||
|
# Validate each entry
|
||||||
|
for idx, entry in enumerate(entries_to_validate, start=1):
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
# Show actual value for type errors
|
||||||
|
entry_str = json.dumps(entry, ensure_ascii=False) if not isinstance(entry, str) else repr(entry)
|
||||||
|
if len(entry_str) > 150:
|
||||||
|
entry_str = entry_str[:150] + "..."
|
||||||
|
errors.append(f"\n❌ Entry #{idx}: Must be an object, got {type(entry).__name__}")
|
||||||
|
errors.append(f" Actual value: {entry_str}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry_errors = validate_entry(entry, idx, required_fields)
|
||||||
|
if entry_errors:
|
||||||
|
# Group errors by entry with context
|
||||||
|
context = get_entry_context(entry)
|
||||||
|
errors.append(f"\n❌ Entry #{idx} ({context}):")
|
||||||
|
for error in entry_errors:
|
||||||
|
errors.append(f" - {error}")
|
||||||
|
|
||||||
|
is_valid = len(errors) == 0
|
||||||
|
return is_valid, errors
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="JSON File Syntax Checker")
|
"""Main entry point"""
|
||||||
parser.add_argument("file_path", type=str, help="Path to the JSON file for syntax checking")
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python json-checker.py <json-file>")
|
||||||
|
print("\nValidates JSON entries based on content:")
|
||||||
|
print(" - {\"custom_nodes\": [...]}: Validates required fields (author, title, reference, files, install_type, description)")
|
||||||
|
print(" - {\"models\": [...]}: Validates JSON syntax only (no required fields)")
|
||||||
|
print(" - Other JSON structures: Validates JSON syntax only")
|
||||||
|
print("\nGit repo URL validation (for custom_nodes):")
|
||||||
|
print(" - URLs must NOT end with .git")
|
||||||
|
print(" - URLs must follow: https://github.com/{author}/{reponame}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
args = parser.parse_args()
|
file_path = sys.argv[1]
|
||||||
check_json_syntax(args.file_path)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
is_valid, errors = validate_json_file(file_path)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
print(f"✅ {file_path}: Validation passed")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print(f"Validating: {file_path}")
|
||||||
|
print("=" * 60)
|
||||||
|
print("❌ Validation failed!\n")
|
||||||
|
print("Errors:")
|
||||||
|
# Count actual errors (lines starting with " -")
|
||||||
|
error_count = sum(1 for e in errors if e.strip().startswith('-'))
|
||||||
|
for error in errors:
|
||||||
|
# Don't add ❌ prefix to grouped entries (they already have it)
|
||||||
|
if error.strip().startswith('❌'):
|
||||||
|
print(error)
|
||||||
|
else:
|
||||||
|
print(error)
|
||||||
|
print(f"\nTotal errors: {error_count}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"custom_nodes": [
|
"custom_nodes": [
|
||||||
|
{
|
||||||
|
"author": "Fossiel",
|
||||||
|
"title": "ComfyUI-MultiGPU-Patched",
|
||||||
|
"reference": "https://github.com/Fossiel/ComfyUI-MultiGPU-Patched",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Fossiel/ComfyUI-MultiGPU-Patched"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Patched fork of ComfyUI-MultiGPU providing universal .safetensors and GGUF multi-GPU distribution with DisTorch 2.0 engine, model-driven allocation options (bytes/ratio modes), WanVideoWrapper integration, and up to 10% faster GGUF inference. (Description by CC)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"author": "synchronicity-labs",
|
"author": "synchronicity-labs",
|
||||||
"title": "ComfyUI Sync Lipsync Node",
|
"title": "ComfyUI Sync Lipsync Node",
|
||||||
|
|||||||
@ -1,5 +1,368 @@
|
|||||||
{
|
{
|
||||||
"custom_nodes": [
|
"custom_nodes": [
|
||||||
|
{
|
||||||
|
"author": "amamisonlyuser",
|
||||||
|
"title": "MixvtonComfyui [REMOVED]",
|
||||||
|
"reference": "https://github.com/amamisonlyuser/MixvtonComfyui",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/amamisonlyuser/MixvtonComfyui"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: CXH_Leffa_Viton_Load, CXH_Leffa_Viton_Run\nNOTE: The files in the repo are not organized."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "AhBumm",
|
||||||
|
"title": "ComfyUI_MangaLineExtraction [REMOVED]",
|
||||||
|
"reference": "https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf"
|
||||||
|
],
|
||||||
|
"description": "p1atdev/MangaLineExtraction-hf as a node in comfyui",
|
||||||
|
"install_type": "git-clone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "danieljanata",
|
||||||
|
"title": "ComfyUI-QwenVL-Override [REMOVED]",
|
||||||
|
"reference": "https://github.com/danieljanata/ComfyUI-QwenVL-Override",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/danieljanata/ComfyUI-QwenVL-Override"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Adds two nodes that reuse upstream ComfyUI-QwenVL presets but add a runtime override that can be wired/unwired without getting stuck."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Futureversecom",
|
||||||
|
"title": "ComfyUI-JEN [REMOVED]",
|
||||||
|
"reference": "https://github.com/futureversecom/ComfyUI-JEN",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/futureversecom/ComfyUI-JEN"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Comfy UI custom nodes for JEN music generation powered by Futureverse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "TheBill2001",
|
||||||
|
"title": "comfyui-upscale-by-model [REMOVED]",
|
||||||
|
"reference": "https://github.com/TheBill2001/comfyui-upscale-by-model",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/TheBill2001/comfyui-upscale-by-model"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This custom node allow upscaling an image by a factor using a model."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "XYMikky12138",
|
||||||
|
"title": "ComfyUI-NanoBanana-inpaint [REMOVED]",
|
||||||
|
"reference": "https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI nodes for API-based inpainting (Gemini, Imagen) with aspect ratio constraints, smart cropping, resize fitting, intelligent paste-back with transparency support. (Description by CC)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Blonicx",
|
||||||
|
"title": "ComfyUI-Rework-X [REMOVED]",
|
||||||
|
"id": "rework-x",
|
||||||
|
"reference": "https://github.com/Blonicx/ComfyUI-X-Rework",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Blonicx/ComfyUI-X-Rework"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This is a plugin for ComfyUI that adds new Util Nodes and Nodes for easier image creation and sharing."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "scott-createplay",
|
||||||
|
"title": "ComfyUI_video_essentials [REMOVED]",
|
||||||
|
"reference": "https://github.com/scott-createplay/ComfyUI_video_essentials",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/scott-createplay/ComfyUI_video_essentials"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Essential video processing nodes for ComfyUI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "thnikk",
|
||||||
|
"title": "comfyui-thnikk-utils [REMOVED]",
|
||||||
|
"reference": "https://github.com/thnikk/comfyui-thnikk-utils",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/thnikk/comfyui-thnikk-utils"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Nodes to clean up your workflow."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Tr1dae",
|
||||||
|
"title": "LoRA Matcher Nodes for ComfyUI [REMOVED]",
|
||||||
|
"reference": "https://github.com/Tr1dae/ComfyUI-LoraPromptMatcher",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Tr1dae/ComfyUI-LoraPromptMatcher"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This custom node provides two different approaches to automatically match text prompts with LoRA models using their descriptions."
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"author": "jkhayiying",
|
||||||
|
"title": "ImageLoadFromLocalOrUrl Node for ComfyUI [REMOVED]",
|
||||||
|
"id": "JkhaImageLoaderPathOrUrl",
|
||||||
|
"reference": "https://gitee.com/yyh915/jkha-load-img",
|
||||||
|
"files": [
|
||||||
|
"https://gitee.com/yyh915/jkha-load-img"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This is a node to load an image from local path or url."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "pizurny",
|
||||||
|
"title": "ComfyUI-Just-DWPose [REMOVED]",
|
||||||
|
"reference": "https://github.com/pizurny/ComfyUI-Just-DWPose",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/pizurny/ComfyUI-Just-DWPose"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "An advanced DWPose annotator for ComfyUI with TorchScript and ONNX backends, featuring comprehensive pose detection, bone validation, temporal smoothing, and custom visualization tools."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "lfelipegg",
|
||||||
|
"title": "lfgg_custom_nodes [REMOVED]",
|
||||||
|
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/lfelipegg/lfgg_custom_nodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: ModelMergeCombos\nNOTE: The files in the repo are not organized."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "AndSni",
|
||||||
|
"title": "Comfy-FL-Nodes [REMOVED]",
|
||||||
|
"reference": "https://github.com/AndSni/Comfy-FL-Nodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/AndSni/Comfy-FL-Nodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Generates human characters for commerce applications in ComfyUI. (Description by CC)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "neeltheninja",
|
||||||
|
"title": "ComfyUI-ControlNeXt [REMOVED]",
|
||||||
|
"reference": "https://github.com/neverbiasu/ComfyUI-ControlNeXt",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/neverbiasu/ComfyUI-ControlNeXt"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "In progress🚧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "TheArtOfficial",
|
||||||
|
"title": "ComfyUI-Nitra [REMOVED]",
|
||||||
|
"reference": "https://github.com/TheArtOfficial/ComfyUI-Nitra",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/TheArtOfficial/ComfyUI-Nitra"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Nitra custom node for ComfyUI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "AngelCookies",
|
||||||
|
"title": "ComfyUI-Seed-Tracker [REMOVED]",
|
||||||
|
"reference": "https://github.com/AngelCookies/ComfyUI-Seed-Tracker",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/AngelCookies/ComfyUI-Seed-Tracker"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A ComfyUI extension that tracks random seeds throughout your image generation workflows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-nothing-happened [REMOVED]",
|
||||||
|
"reference": "httphttps://github.com/shinich39/comfyui-nothing-happened",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-nothing-happened"
|
||||||
|
],
|
||||||
|
"description": "Save image and keep metadata.",
|
||||||
|
"install_type": "git-clone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "ashtar1984",
|
||||||
|
"title": "comfyui-switch-bypass-mute-by-group [REMOVED]",
|
||||||
|
"reference": "https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI custom node for group-based node switching, bypassing, and muting control. (Description by CC)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "wallen0322",
|
||||||
|
"title": "ComfyUI-TTM-WAN22 [REMOVED]",
|
||||||
|
"reference": "https://github.com/wallen0322/ComfyUI-TTM-WAN22",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/wallen0322/ComfyUI-TTM-WAN22"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "TTM (Time-to-Move) node for ComfyUI enabling motion-controlled video generation with Wan2.2 models using dual-clock denoising for independent background and object animation control."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "cdanielp",
|
||||||
|
"title": "COMFYUI_PROMPTMODELS [REMOVED]",
|
||||||
|
"reference": "https://github.com/cdanielp/COMFYUI_PROMPTMODELS",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/cdanielp/COMFYUI_PROMPTMODELS"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Custom nodes for ComfyUI by PROMPTMODELS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "mcrataobrabo",
|
||||||
|
"title": "comfyui-smart-lora-downloader - Automatically Fetch Missing LoRAs [REMOVED]",
|
||||||
|
"reference": "https://github.com/mcrataobrabo/comfyui-smart-lora-downloader",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/mcrataobrabo/comfyui-smart-lora-downloader"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Automatically detect and download missing LoRAs for ComfyUI workflows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "KANAsho34636",
|
||||||
|
"title": "ComfyUI-NaturalSort-ImageLoader [REMOVED]",
|
||||||
|
"reference": "https://github.com/KANAsho34636/ComfyUI-NaturalSort-ImageLoader",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/KANAsho34636/ComfyUI-NaturalSort-ImageLoader"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Custom image loader node supporting natural number sorting with multiple sort modes (natural, lexicographic, modification time, creation time, reverse natural). (Description by CC)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "johninthewinter",
|
||||||
|
"title": "comfyui-fal-flux-2-John [REMOVED]",
|
||||||
|
"reference": "https://github.com/johninthewinter/comfyui-fal-flux-2-John",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/johninthewinter/comfyui-fal-flux-2-John"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Custom nodes for ComfyUI that integrate with fal.ai's FLUX 2 and FLUX 1 LoRA APIs for text-to-image generation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "LargeModGames",
|
||||||
|
"title": "ComfyUI LoRA Auto Downloader [REMOVED]",
|
||||||
|
"reference": "https://github.com/LargeModGames/comfyui-smart-lora-downloader",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/LargeModGames/comfyui-smart-lora-downloader"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Automatically download missing LoRAs from CivitAI and detect missing LoRAs in workflows. Features smart directory detection and easy installation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "DiffusionWave",
|
||||||
|
"title": "PickResolution_DiffusionWave [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/DiffusionWave/PickResolution_DiffusionWave",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/DiffusionWave/PickResolution_DiffusionWave"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A custom node for ComfyUI that allows selecting a base resolution, applying a custom scaling value based on FLOAT (up to 10 decimal places), and adding an extra integer value. Outputs include both INT and FLOAT resolutions, making it perfect for you to play around with."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "geltz",
|
||||||
|
"title": "ComfyUI-geltz [REMOVED]",
|
||||||
|
"reference": "https://github.com/geltz/ComfyUI-geltz",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/geltz/ComfyUI-geltz"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Various custom nodes; guidance, latents, sampling, tokenization, etc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "anilsathyan7",
|
||||||
|
"title": "ComfyUI-Crystal-Upscaler [REMOVED]",
|
||||||
|
"reference": "https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/anilsathyan7/ComfyUI-Crystal-Upscaler"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI custom node for image upscaling using crystal upscaling technology. (Description by CC)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "nohikomiso",
|
||||||
|
"title": "ComfyUI-ImageFolderPicker [REMOVED/UNSAFE]",
|
||||||
|
"reference": "https://github.com/nohikomiso/ComfyUI-ImageFolderPicker",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/nohikomiso/ComfyUI-ImageFolderPicker"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Custom ComfyUI node for browsing local server folders and selecting images via thumbnail display in a grid interface. (Description by CC)[w/This nodepack has a vulnerability that allows it to retrieve a list of files from arbitrary paths.]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "rzasharp79",
|
||||||
|
"title": "ComfyUI--SolarFlare [REMOVED]",
|
||||||
|
"reference": "https://github.com/rzasharp79/ComfyUI--SolarFlare",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/rzasharp79/ComfyUI--SolarFlare"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Qwen Image, ..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-no-one-above-me [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-no-one-above-me",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-no-one-above-me"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Fix node to top."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "octapus8085",
|
||||||
|
"title": "OpenAI-comfyui-O [REMOVED]",
|
||||||
|
"reference": "https://github.com/Spicely/Comfyui-File-Utils",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Spicely/Comfyui-File-Utils"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This plugin provides multiple file-handling and utility nodes for ComfyUI, including: image saving, audio saving, video saving, video composition, audio-to-subtitle conversion, and random number generation nodes. These nodes not only process files but also return their absolute file paths.\nNOTE: The files in the repo are not organized.[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "yemanou",
|
||||||
|
"title": "NABA Image (Gemini REST) Node [REMOVED]",
|
||||||
|
"reference": "https://github.com/yemanou/ComfyUI-NABA",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/yemanou/ComfyUI-NABA"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Simplified Gemini 2.5 Flash Image Preview node for ComfyUI. REST-only for stability, two optional reference images, padded aspect ratio resizing (no stretching), and basic sampling controls. All extra debug layers, SDK path, multi-seed, and legacy compatibility code removed to avoid crashes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "comrender",
|
||||||
|
"title": "ComfyUI-Nano-Banana-Resizer [REMOVED]",
|
||||||
|
"reference": "https://github.com/comrender/ComfyUI-Nano-Banana-Resizer",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/comrender/ComfyUI-Nano-Banana-Resizer"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A ComfyUI custom node that automatically calculates optimal output dimensions for Google's Nano Banana image editing model, supporting 22 aspect ratio buckets and ensuring pixel-perfect outputs without shifting or cropping."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "comrender",
|
||||||
|
"title": "ComfyUI-edge-match-checker [REMOVED]",
|
||||||
|
"reference": "https://github.com/comrender/ComfyUI-edge-match-checker",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/comrender/ComfyUI-edge-match-checker"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Node comparing two image masks or images with adjustable overlap threshold (default 95%) for detecting minor shifts and mismatches in proportions, suitable for automated post-processing validation. (Description by CC)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "comrender",
|
||||||
|
"title": "ComfyUI-gpt5_image_text [REMOVED]",
|
||||||
|
"reference": "https://github.com/comrender/ComfyUI-gpt5_image_text",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/comrender/ComfyUI-gpt5_image_text"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A ComfyUI custom node for vision + text analysis using GPT-5 and GPT-4o with direct API key input, system prompt, temperature, max tokens, and multi-image support."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"author": "PozzettiAndrea",
|
"author": "PozzettiAndrea",
|
||||||
"title": "ComfyUI-CameraAnalysis [REMOVED]",
|
"title": "ComfyUI-CameraAnalysis [REMOVED]",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-manager"
|
name = "comfyui-manager"
|
||||||
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
|
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
|
||||||
version = "3.38"
|
version = "3.39"
|
||||||
license = { file = "LICENSE.txt" }
|
license = { file = "LICENSE.txt" }
|
||||||
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]
|
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]
|
||||||
|
|
||||||
|
|||||||
765
scanner.py
765
scanner.py
@ -16,6 +16,108 @@ import sys
|
|||||||
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from github import Github, Auth
|
from github import Github, Auth
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Set, Dict, Optional
|
||||||
|
|
||||||
|
# Scanner version for cache invalidation
|
||||||
|
SCANNER_VERSION = "2.0.12" # Add dict comprehension + export list detection
|
||||||
|
|
||||||
|
# Cache for extract_nodes and extract_nodes_enhanced results
|
||||||
|
_extract_nodes_cache: Dict[str, Set[str]] = {}
|
||||||
|
_extract_nodes_enhanced_cache: Dict[str, Set[str]] = {}
|
||||||
|
_file_mtime_cache: Dict[Path, float] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_repo_root(file_path: Path) -> Optional[Path]:
|
||||||
|
"""Find the repository root directory containing .git"""
|
||||||
|
current = file_path if file_path.is_dir() else file_path.parent
|
||||||
|
while current != current.parent:
|
||||||
|
if (current / ".git").exists():
|
||||||
|
return current
|
||||||
|
current = current.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_repo_hash(repo_path: Path) -> str:
|
||||||
|
"""Get git commit hash or fallback identifier"""
|
||||||
|
git_dir = repo_path / ".git"
|
||||||
|
if not git_dir.exists():
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read HEAD to get current commit
|
||||||
|
head_file = git_dir / "HEAD"
|
||||||
|
if head_file.exists():
|
||||||
|
head_content = head_file.read_text().strip()
|
||||||
|
if head_content.startswith("ref:"):
|
||||||
|
# HEAD points to a ref
|
||||||
|
ref_path = git_dir / head_content[5:].strip()
|
||||||
|
if ref_path.exists():
|
||||||
|
commit_hash = ref_path.read_text().strip()
|
||||||
|
return commit_hash[:16] # First 16 chars
|
||||||
|
else:
|
||||||
|
# Detached HEAD
|
||||||
|
return head_content[:16]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _load_per_repo_cache(repo_path: Path) -> Optional[tuple]:
|
||||||
|
"""Load nodes and metadata from per-repo cache
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (nodes_set, metadata_dict) or None if cache invalid
|
||||||
|
"""
|
||||||
|
cache_file = repo_path / ".git" / "nodecache.json"
|
||||||
|
|
||||||
|
if not cache_file.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
cache_data = json.load(f)
|
||||||
|
|
||||||
|
# Verify scanner version
|
||||||
|
if cache_data.get('scanner_version') != SCANNER_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Verify git hash
|
||||||
|
current_hash = _get_repo_hash(repo_path)
|
||||||
|
if cache_data.get('git_hash') != current_hash:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return nodes and metadata
|
||||||
|
nodes = cache_data.get('nodes', [])
|
||||||
|
metadata = cache_data.get('metadata', {})
|
||||||
|
return (set(nodes) if nodes else set(), metadata)
|
||||||
|
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _save_per_repo_cache(repo_path: Path, all_nodes: Set[str], metadata: dict = None):
|
||||||
|
"""Save nodes and metadata to per-repo cache"""
|
||||||
|
cache_file = repo_path / ".git" / "nodecache.json"
|
||||||
|
|
||||||
|
if not cache_file.parent.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
git_hash = _get_repo_hash(repo_path)
|
||||||
|
cache_data = {
|
||||||
|
"scanner_version": SCANNER_VERSION,
|
||||||
|
"git_hash": git_hash,
|
||||||
|
"scanned_at": datetime.datetime.now().isoformat(),
|
||||||
|
"nodes": sorted(list(all_nodes)),
|
||||||
|
"metadata": metadata if metadata else {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
json.dump(cache_data, f, indent=2)
|
||||||
|
except:
|
||||||
|
pass # Silently fail - cache is optional
|
||||||
|
|
||||||
|
|
||||||
def download_url(url, dest_folder, filename=None):
|
def download_url(url, dest_folder, filename=None):
|
||||||
@ -51,11 +153,12 @@ Examples:
|
|||||||
# Standard mode
|
# Standard mode
|
||||||
python3 scanner.py
|
python3 scanner.py
|
||||||
python3 scanner.py --skip-update
|
python3 scanner.py --skip-update
|
||||||
|
python3 scanner.py --skip-all --force-rescan
|
||||||
|
|
||||||
# Scan-only mode
|
# Scan-only mode
|
||||||
python3 scanner.py --scan-only temp-urls-clean.list
|
python3 scanner.py --scan-only temp-urls-clean.list
|
||||||
python3 scanner.py --scan-only urls.list --temp-dir /custom/temp
|
python3 scanner.py --scan-only urls.list --temp-dir /custom/temp
|
||||||
python3 scanner.py --scan-only urls.list --skip-update
|
python3 scanner.py --scan-only urls.list --skip-update --force-rescan
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,6 +172,8 @@ Examples:
|
|||||||
help='Skip GitHub stats collection')
|
help='Skip GitHub stats collection')
|
||||||
parser.add_argument('--skip-all', action='store_true',
|
parser.add_argument('--skip-all', action='store_true',
|
||||||
help='Skip all update operations')
|
help='Skip all update operations')
|
||||||
|
parser.add_argument('--force-rescan', action='store_true',
|
||||||
|
help='Force rescan all nodes (ignore cache)')
|
||||||
|
|
||||||
# Backward compatibility: positional argument for temp_dir
|
# Backward compatibility: positional argument for temp_dir
|
||||||
parser.add_argument('temp_dir_positional', nargs='?', metavar='TEMP_DIR',
|
parser.add_argument('temp_dir_positional', nargs='?', metavar='TEMP_DIR',
|
||||||
@ -94,6 +199,11 @@ parse_cnt = 0
|
|||||||
def extract_nodes(code_text):
|
def extract_nodes(code_text):
|
||||||
global parse_cnt
|
global parse_cnt
|
||||||
|
|
||||||
|
# Check cache first
|
||||||
|
cache_key = hash(code_text)
|
||||||
|
if cache_key in _extract_nodes_cache:
|
||||||
|
return _extract_nodes_cache[cache_key].copy()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if parse_cnt % 100 == 0:
|
if parse_cnt % 100 == 0:
|
||||||
print(".", end="", flush=True)
|
print(".", end="", flush=True)
|
||||||
@ -128,12 +238,614 @@ def extract_nodes(code_text):
|
|||||||
if key is not None and isinstance(key.value, str):
|
if key is not None and isinstance(key.value, str):
|
||||||
s.add(key.value.strip())
|
s.add(key.value.strip())
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
_extract_nodes_cache[cache_key] = s
|
||||||
return s
|
return s
|
||||||
else:
|
else:
|
||||||
|
# Cache empty result
|
||||||
|
_extract_nodes_cache[cache_key] = set()
|
||||||
return set()
|
return set()
|
||||||
|
except:
|
||||||
|
# Cache empty result on error
|
||||||
|
_extract_nodes_cache[cache_key] = set()
|
||||||
|
return set()
|
||||||
|
|
||||||
|
def extract_nodes_from_repo(repo_path: Path, verbose: bool = False, force_rescan: bool = False) -> tuple:
|
||||||
|
"""
|
||||||
|
Extract all nodes and metadata from a repository with per-repo caching.
|
||||||
|
|
||||||
|
Automatically caches results in .git/nodecache.json.
|
||||||
|
Cache is invalidated when:
|
||||||
|
- Git commit hash changes
|
||||||
|
- Scanner version changes
|
||||||
|
- force_rescan flag is True
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_path: Path to repository root
|
||||||
|
verbose: If True, print UI-only extension detection messages
|
||||||
|
force_rescan: If True, ignore cache and force fresh scan
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (nodes_set, metadata_dict)
|
||||||
|
"""
|
||||||
|
# Ensure path is absolute
|
||||||
|
repo_path = repo_path.resolve()
|
||||||
|
|
||||||
|
# Check per-repo cache first (unless force_rescan is True)
|
||||||
|
if not force_rescan:
|
||||||
|
cached_result = _load_per_repo_cache(repo_path)
|
||||||
|
if cached_result is not None:
|
||||||
|
return cached_result
|
||||||
|
|
||||||
|
# Cache miss - scan all .py files
|
||||||
|
all_nodes = set()
|
||||||
|
all_metadata = {}
|
||||||
|
py_files = list(repo_path.rglob("*.py"))
|
||||||
|
|
||||||
|
# Filter out __pycache__, .git, and other hidden directories
|
||||||
|
filtered_files = []
|
||||||
|
for f in py_files:
|
||||||
|
try:
|
||||||
|
rel_path = f.relative_to(repo_path)
|
||||||
|
# Skip __pycache__, .git, and any directory starting with .
|
||||||
|
if '__pycache__' not in str(rel_path) and not any(part.startswith('.') for part in rel_path.parts):
|
||||||
|
filtered_files.append(f)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
py_files = filtered_files
|
||||||
|
|
||||||
|
for py_file in py_files:
|
||||||
|
try:
|
||||||
|
# Read file with proper encoding
|
||||||
|
with open(py_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
code = f.read()
|
||||||
|
|
||||||
|
if code:
|
||||||
|
# Extract nodes using SAME logic as scan_in_file
|
||||||
|
# V1 nodes (enhanced with fallback patterns)
|
||||||
|
nodes = extract_nodes_enhanced(code, py_file, visited=set(), verbose=verbose)
|
||||||
|
all_nodes.update(nodes)
|
||||||
|
|
||||||
|
# V3 nodes detection
|
||||||
|
v3_nodes = extract_v3_nodes(code)
|
||||||
|
all_nodes.update(v3_nodes)
|
||||||
|
|
||||||
|
# Dict parsing - exclude commented NODE_CLASS_MAPPINGS lines
|
||||||
|
pattern = r"_CLASS_MAPPINGS\s*(?::\s*\w+\s*)?=\s*(?:\\\s*)?{([^}]*)}"
|
||||||
|
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
|
||||||
|
|
||||||
|
for match_obj in regex.finditer(code):
|
||||||
|
# Get the line where NODE_CLASS_MAPPINGS is defined
|
||||||
|
match_start = match_obj.start()
|
||||||
|
line_start = code.rfind('\n', 0, match_start) + 1
|
||||||
|
line_end = code.find('\n', match_start)
|
||||||
|
if line_end == -1:
|
||||||
|
line_end = len(code)
|
||||||
|
line = code[line_start:line_end]
|
||||||
|
|
||||||
|
# Skip if line starts with # (commented)
|
||||||
|
if re.match(r'^\s*#', line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = match_obj.group(1)
|
||||||
|
|
||||||
|
# Filter out commented lines from dict content
|
||||||
|
match_lines = match.split('\n')
|
||||||
|
match_filtered = '\n'.join(
|
||||||
|
line for line in match_lines
|
||||||
|
if not re.match(r'^\s*#', line)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract key-value pairs with double quotes
|
||||||
|
key_value_pairs = re.findall(r"\"([^\"]*)\"\s*:\s*([^,\n]*)", match_filtered)
|
||||||
|
for key, value in key_value_pairs:
|
||||||
|
all_nodes.add(key.strip())
|
||||||
|
|
||||||
|
# Extract key-value pairs with single quotes
|
||||||
|
key_value_pairs = re.findall(r"'([^']*)'\s*:\s*([^,\n]*)", match_filtered)
|
||||||
|
for key, value in key_value_pairs:
|
||||||
|
all_nodes.add(key.strip())
|
||||||
|
|
||||||
|
# Handle .update() pattern (AFTER comment removal)
|
||||||
|
code_cleaned = re.sub(r'^#.*?$', '', code, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
update_pattern = r"_CLASS_MAPPINGS\.update\s*\(\s*{([^}]*)}\s*\)"
|
||||||
|
update_match = re.search(update_pattern, code_cleaned, re.DOTALL)
|
||||||
|
if update_match:
|
||||||
|
update_dict_text = update_match.group(1)
|
||||||
|
# Extract key-value pairs (double quotes)
|
||||||
|
update_pairs = re.findall(r'"([^"]*)"\s*:\s*([^,\n]*)', update_dict_text)
|
||||||
|
for key, value in update_pairs:
|
||||||
|
all_nodes.add(key.strip())
|
||||||
|
# Extract key-value pairs (single quotes)
|
||||||
|
update_pairs_single = re.findall(r"'([^']*)'\s*:\s*([^,\n]*)", update_dict_text)
|
||||||
|
for key, value in update_pairs_single:
|
||||||
|
all_nodes.add(key.strip())
|
||||||
|
|
||||||
|
# Additional regex patterns (AFTER comment removal)
|
||||||
|
patterns = [
|
||||||
|
r'^[^=]*_CLASS_MAPPINGS\["(.*?)"\]',
|
||||||
|
r'^[^=]*_CLASS_MAPPINGS\[\'(.*?)\'\]',
|
||||||
|
r'@register_node\("(.+)",\s*\".+"\)',
|
||||||
|
r'"(\w+)"\s*:\s*{"class":\s*\w+\s*'
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
keys = re.findall(pattern, code_cleaned)
|
||||||
|
all_nodes.update(key.strip() for key in keys)
|
||||||
|
|
||||||
|
# Extract metadata from this file
|
||||||
|
metadata = extract_metadata_only(str(py_file))
|
||||||
|
all_metadata.update(metadata)
|
||||||
|
except Exception:
|
||||||
|
# Silently skip files that can't be read
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Save to per-repo cache
|
||||||
|
_save_per_repo_cache(repo_path, all_nodes, all_metadata)
|
||||||
|
|
||||||
|
return (all_nodes, all_metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_class_exists(node_name: str, code_text: str, file_path: Optional[Path] = None) -> tuple[bool, Optional[str], Optional[int]]:
|
||||||
|
"""
|
||||||
|
Verify that a node class exists and has ComfyUI node structure.
|
||||||
|
|
||||||
|
Returns: (exists: bool, file_path: str, line_number: int)
|
||||||
|
|
||||||
|
A valid ComfyUI node must have:
|
||||||
|
- Class definition (not commented)
|
||||||
|
- At least one of: INPUT_TYPES, RETURN_TYPES, FUNCTION method/attribute
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||||
|
tree = ast.parse(code_text)
|
||||||
|
except:
|
||||||
|
return (False, None, None)
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ClassDef):
|
||||||
|
if node.name == node_name or node.name.replace('_', '') == node_name.replace('_', ''):
|
||||||
|
# Found class definition - check if it has ComfyUI interface
|
||||||
|
has_input_types = False
|
||||||
|
has_return_types = False
|
||||||
|
has_function = False
|
||||||
|
|
||||||
|
for item in node.body:
|
||||||
|
# Check for INPUT_TYPES method
|
||||||
|
if isinstance(item, ast.FunctionDef) and item.name == 'INPUT_TYPES':
|
||||||
|
has_input_types = True
|
||||||
|
# Check for RETURN_TYPES attribute
|
||||||
|
elif isinstance(item, ast.Assign):
|
||||||
|
for target in item.targets:
|
||||||
|
if isinstance(target, ast.Name):
|
||||||
|
if target.id == 'RETURN_TYPES':
|
||||||
|
has_return_types = True
|
||||||
|
elif target.id == 'FUNCTION':
|
||||||
|
has_function = True
|
||||||
|
# Check for FUNCTION method
|
||||||
|
elif isinstance(item, ast.FunctionDef):
|
||||||
|
has_function = True
|
||||||
|
|
||||||
|
# Valid if has any ComfyUI signature
|
||||||
|
if has_input_types or has_return_types or has_function:
|
||||||
|
file_str = str(file_path) if file_path else None
|
||||||
|
return (True, file_str, node.lineno)
|
||||||
|
|
||||||
|
return (False, None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_display_name_mappings(code_text: str) -> Set[str]:
|
||||||
|
"""
|
||||||
|
Extract node names from NODE_DISPLAY_NAME_MAPPINGS.
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"node_key": "Display Name",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of node keys from NODE_DISPLAY_NAME_MAPPINGS
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||||
|
tree = ast.parse(code_text)
|
||||||
except:
|
except:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
nodes = set()
|
||||||
|
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name) and target.id == 'NODE_DISPLAY_NAME_MAPPINGS':
|
||||||
|
if isinstance(node.value, ast.Dict):
|
||||||
|
for key in node.value.keys:
|
||||||
|
if isinstance(key, ast.Constant) and isinstance(key.value, str):
|
||||||
|
nodes.add(key.value.strip())
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def extract_nodes_enhanced(
|
||||||
|
code_text: str,
|
||||||
|
file_path: Optional[Path] = None,
|
||||||
|
visited: Optional[Set[Path]] = None,
|
||||||
|
verbose: bool = False
|
||||||
|
) -> Set[str]:
|
||||||
|
"""
|
||||||
|
Enhanced node extraction with multi-layer detection system.
|
||||||
|
|
||||||
|
Scanner 2.0.11 - Comprehensive detection strategy:
|
||||||
|
- Phase 1: NODE_CLASS_MAPPINGS dict literal
|
||||||
|
- Phase 2: Class.NAME attribute access (e.g., FreeChat.NAME)
|
||||||
|
- Phase 3: Item assignment (NODE_CLASS_MAPPINGS["key"] = value)
|
||||||
|
- Phase 4: Class existence verification (detects active classes even if registration commented)
|
||||||
|
- Phase 5: NODE_DISPLAY_NAME_MAPPINGS cross-reference
|
||||||
|
- Phase 6: Empty dict detection (UI-only extensions, logging only)
|
||||||
|
|
||||||
|
Fixed Bugs:
|
||||||
|
- Scanner 2.0.9: Fallback cascade prevented Phase 3 execution
|
||||||
|
- Scanner 2.0.10: Missed active classes with commented registrations (15 false negatives)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code_text: Python source code
|
||||||
|
file_path: Path to file (for logging and caching)
|
||||||
|
visited: Visited paths (for circular import prevention)
|
||||||
|
verbose: If True, print UI-only extension detection messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of node names (union of all detected patterns)
|
||||||
|
"""
|
||||||
|
# Check file-based cache if file_path provided
|
||||||
|
if file_path is not None:
|
||||||
|
try:
|
||||||
|
file_path_obj = Path(file_path) if not isinstance(file_path, Path) else file_path
|
||||||
|
if file_path_obj.exists():
|
||||||
|
current_mtime = file_path_obj.stat().st_mtime
|
||||||
|
|
||||||
|
# Check if we have cached result with matching mtime and scanner version
|
||||||
|
if file_path_obj in _file_mtime_cache:
|
||||||
|
cached_mtime = _file_mtime_cache[file_path_obj]
|
||||||
|
cache_key = (str(file_path_obj), cached_mtime, SCANNER_VERSION)
|
||||||
|
|
||||||
|
if current_mtime == cached_mtime and cache_key in _extract_nodes_enhanced_cache:
|
||||||
|
return _extract_nodes_enhanced_cache[cache_key].copy()
|
||||||
|
except:
|
||||||
|
pass # Ignore cache errors, proceed with normal execution
|
||||||
|
|
||||||
|
# Suppress warnings from AST parsing
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||||
|
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
||||||
|
|
||||||
|
# Phase 1: Original extract_nodes() - dict literal
|
||||||
|
phase1_nodes = extract_nodes(code_text)
|
||||||
|
|
||||||
|
# Phase 2: Class.NAME pattern
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
phase2_nodes = _fallback_classname_resolver(code_text, file_path)
|
||||||
|
|
||||||
|
# Phase 3: Item assignment pattern
|
||||||
|
phase3_nodes = _fallback_item_assignment(code_text)
|
||||||
|
|
||||||
|
# Phase 4: NODE_DISPLAY_NAME_MAPPINGS cross-reference (NEW in 2.0.11)
|
||||||
|
# This catches nodes that are in display names but not in NODE_CLASS_MAPPINGS
|
||||||
|
phase4_nodes = _extract_display_name_mappings(code_text)
|
||||||
|
|
||||||
|
# Phase 5: Class existence verification ONLY for display name candidates (NEW in 2.0.11)
|
||||||
|
# This phase is CONSERVATIVE - only verify classes that appear in display names
|
||||||
|
# This catches the specific Scanner 2.0.10 bug pattern:
|
||||||
|
# - NODE_CLASS_MAPPINGS registration is commented
|
||||||
|
# - NODE_DISPLAY_NAME_MAPPINGS still has the entry
|
||||||
|
# - Class implementation exists
|
||||||
|
# Example: Bjornulf_ollamaLoader in Bjornulf_custom_nodes
|
||||||
|
phase5_nodes = set()
|
||||||
|
for node_name in phase4_nodes:
|
||||||
|
# Only check classes that appear in display names but not in registrations
|
||||||
|
if node_name not in (phase1_nodes | phase2_nodes | phase3_nodes):
|
||||||
|
exists, _, _ = _verify_class_exists(node_name, code_text, file_path)
|
||||||
|
if exists:
|
||||||
|
phase5_nodes.add(node_name)
|
||||||
|
|
||||||
|
# Phase 6: Dict comprehension pattern (NEW in 2.0.12)
|
||||||
|
# Detects: NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
|
||||||
|
# Example: TobiasGlaubach/ComfyUI-TG_PyCode
|
||||||
|
phase6_nodes = _fallback_dict_comprehension(code_text, file_path)
|
||||||
|
|
||||||
|
# Phase 7: Import-based class names for dict comprehension (NEW in 2.0.12)
|
||||||
|
# Detects imported classes that are added to export lists
|
||||||
|
phase7_nodes = _fallback_import_class_names(code_text, file_path)
|
||||||
|
|
||||||
|
# Union all results (FIX: Scanner 2.0.9 bug + Scanner 2.0.10 bug + Scanner 2.0.12 dict comp)
|
||||||
|
# 2.0.9: Used early return which missed Phase 3 nodes
|
||||||
|
# 2.0.10: Only checked registrations, missed classes referenced in display names
|
||||||
|
# 2.0.12: Added dict comprehension and import-based class detection
|
||||||
|
all_nodes = phase1_nodes | phase2_nodes | phase3_nodes | phase4_nodes | phase5_nodes | phase6_nodes | phase7_nodes
|
||||||
|
|
||||||
|
# Phase 8: Empty dict detector (logging only, doesn't add nodes)
|
||||||
|
if not all_nodes:
|
||||||
|
_fallback_empty_dict_detector(code_text, file_path, verbose)
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
if file_path is not None:
|
||||||
|
try:
|
||||||
|
file_path_obj = Path(file_path) if not isinstance(file_path, Path) else file_path
|
||||||
|
if file_path_obj.exists():
|
||||||
|
current_mtime = file_path_obj.stat().st_mtime
|
||||||
|
cache_key = (str(file_path_obj), current_mtime, SCANNER_VERSION)
|
||||||
|
_extract_nodes_enhanced_cache[cache_key] = all_nodes
|
||||||
|
_file_mtime_cache[file_path_obj] = current_mtime
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return all_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_classname_resolver(code_text: str, file_path: Optional[Path]) -> Set[str]:
|
||||||
|
"""
|
||||||
|
Detect Class.NAME pattern in NODE_CLASS_MAPPINGS.
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
FreeChat.NAME: FreeChat,
|
||||||
|
PaidChat.NAME: PaidChat
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||||
|
parsed = ast.parse(code_text)
|
||||||
|
except:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
nodes = set()
|
||||||
|
|
||||||
|
for node in parsed.body:
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name) and target.id == 'NODE_CLASS_MAPPINGS':
|
||||||
|
if isinstance(node.value, ast.Dict):
|
||||||
|
for key in node.value.keys:
|
||||||
|
# Detect Class.NAME pattern
|
||||||
|
if isinstance(key, ast.Attribute):
|
||||||
|
if isinstance(key.value, ast.Name):
|
||||||
|
# Use class name as node name
|
||||||
|
nodes.add(key.value.id)
|
||||||
|
# Also handle literal strings
|
||||||
|
elif isinstance(key, ast.Constant) and isinstance(key.value, str):
|
||||||
|
nodes.add(key.value.strip())
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_item_assignment(code_text: str) -> Set[str]:
|
||||||
|
"""
|
||||||
|
Detect item assignment pattern.
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
NODE_CLASS_MAPPINGS = {}
|
||||||
|
NODE_CLASS_MAPPINGS["MyNode"] = MyNode
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||||
|
parsed = ast.parse(code_text)
|
||||||
|
except:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
nodes = set()
|
||||||
|
|
||||||
|
for node in ast.walk(parsed):
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Subscript):
|
||||||
|
if (isinstance(target.value, ast.Name) and
|
||||||
|
target.value.id in ['NODE_CLASS_MAPPINGS', 'NODE_CONFIG']):
|
||||||
|
# Extract key
|
||||||
|
if isinstance(target.slice, ast.Constant):
|
||||||
|
if isinstance(target.slice.value, str):
|
||||||
|
nodes.add(target.slice.value)
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_dict_comprehension(code_text: str, file_path: Optional[Path] = None) -> Set[str]:
|
||||||
|
"""
|
||||||
|
Detect dict comprehension pattern with __name__ attribute access.
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
|
||||||
|
NODE_CLASS_MAPPINGS = {c.__name__: c for c in [ClassA, ClassB]}
|
||||||
|
|
||||||
|
This function detects dict comprehension assignments to NODE_CLASS_MAPPINGS
|
||||||
|
and extracts class names from the iterable (list literal or variable reference).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of class names extracted from the dict comprehension
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||||
|
parsed = ast.parse(code_text)
|
||||||
|
except:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
nodes = set()
|
||||||
|
export_lists = {} # Track list variables and their contents
|
||||||
|
|
||||||
|
# First pass: collect list assignments (to_export = [...], exports = [...])
|
||||||
|
for node in ast.walk(parsed):
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name):
|
||||||
|
var_name = target.id
|
||||||
|
# Check for list literal
|
||||||
|
if isinstance(node.value, ast.List):
|
||||||
|
class_names = set()
|
||||||
|
for elt in node.value.elts:
|
||||||
|
if isinstance(elt, ast.Name):
|
||||||
|
class_names.add(elt.id)
|
||||||
|
export_lists[var_name] = class_names
|
||||||
|
|
||||||
|
# Handle augmented assignment: to_export += [...]
|
||||||
|
elif isinstance(node, ast.AugAssign):
|
||||||
|
if isinstance(node.target, ast.Name) and isinstance(node.op, ast.Add):
|
||||||
|
var_name = node.target.id
|
||||||
|
if isinstance(node.value, ast.List):
|
||||||
|
class_names = set()
|
||||||
|
for elt in node.value.elts:
|
||||||
|
if isinstance(elt, ast.Name):
|
||||||
|
class_names.add(elt.id)
|
||||||
|
if var_name in export_lists:
|
||||||
|
export_lists[var_name].update(class_names)
|
||||||
|
else:
|
||||||
|
export_lists[var_name] = class_names
|
||||||
|
|
||||||
|
# Second pass: find NODE_CLASS_MAPPINGS dict comprehension
|
||||||
|
for node in ast.walk(parsed):
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name) and target.id in ['NODE_CLASS_MAPPINGS', 'NODE_CONFIG']:
|
||||||
|
# Check for dict comprehension
|
||||||
|
if isinstance(node.value, ast.DictComp):
|
||||||
|
dictcomp = node.value
|
||||||
|
|
||||||
|
# Check if key is cls.__name__ pattern
|
||||||
|
key = dictcomp.key
|
||||||
|
if isinstance(key, ast.Attribute) and key.attr == '__name__':
|
||||||
|
# Get the iterable from the first generator
|
||||||
|
for generator in dictcomp.generators:
|
||||||
|
iter_node = generator.iter
|
||||||
|
|
||||||
|
# Case 1: Inline list [ClassA, ClassB, ...]
|
||||||
|
if isinstance(iter_node, ast.List):
|
||||||
|
for elt in iter_node.elts:
|
||||||
|
if isinstance(elt, ast.Name):
|
||||||
|
nodes.add(elt.id)
|
||||||
|
|
||||||
|
# Case 2: Variable reference (to_export, exports, etc.)
|
||||||
|
elif isinstance(iter_node, ast.Name):
|
||||||
|
var_name = iter_node.id
|
||||||
|
if var_name in export_lists:
|
||||||
|
nodes.update(export_lists[var_name])
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_import_class_names(code_text: str, file_path: Optional[Path] = None) -> Set[str]:
|
||||||
|
"""
|
||||||
|
Extract class names from imports that are added to export lists.
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
from .module import ClassA, ClassB
|
||||||
|
to_export = [ClassA, ClassB]
|
||||||
|
NODE_CLASS_MAPPINGS = {cls.__name__: cls for cls in to_export}
|
||||||
|
|
||||||
|
This is a complementary fallback that works with _fallback_dict_comprehension
|
||||||
|
to resolve import-based node registrations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of imported class names that appear in export-like contexts
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||||
|
parsed = ast.parse(code_text)
|
||||||
|
except:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
# Collect imported names
|
||||||
|
imported_names = set()
|
||||||
|
for node in ast.walk(parsed):
|
||||||
|
if isinstance(node, ast.ImportFrom):
|
||||||
|
for alias in node.names:
|
||||||
|
name = alias.asname if alias.asname else alias.name
|
||||||
|
imported_names.add(name)
|
||||||
|
|
||||||
|
# Check if these names appear in list assignments that feed into NODE_CLASS_MAPPINGS
|
||||||
|
export_candidates = set()
|
||||||
|
has_dict_comp_mapping = False
|
||||||
|
|
||||||
|
for node in ast.walk(parsed):
|
||||||
|
# Check for dict comprehension NODE_CLASS_MAPPINGS
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name) and target.id == 'NODE_CLASS_MAPPINGS':
|
||||||
|
if isinstance(node.value, ast.DictComp):
|
||||||
|
has_dict_comp_mapping = True
|
||||||
|
|
||||||
|
# Collect list contents
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
if isinstance(node.value, ast.List):
|
||||||
|
for elt in node.value.elts:
|
||||||
|
if isinstance(elt, ast.Name) and elt.id in imported_names:
|
||||||
|
export_candidates.add(elt.id)
|
||||||
|
|
||||||
|
# Handle augmented assignment
|
||||||
|
elif isinstance(node, ast.AugAssign):
|
||||||
|
if isinstance(node.value, ast.List):
|
||||||
|
for elt in node.value.elts:
|
||||||
|
if isinstance(elt, ast.Name) and elt.id in imported_names:
|
||||||
|
export_candidates.add(elt.id)
|
||||||
|
|
||||||
|
# Only return if there's a dict comprehension mapping
|
||||||
|
if has_dict_comp_mapping:
|
||||||
|
return export_candidates
|
||||||
|
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_repo_name(file_path: Path) -> str:
|
||||||
|
"""
|
||||||
|
Extract repository name from file path.
|
||||||
|
|
||||||
|
Path structure: /home/rho/.tmp/analysis/temp/{author}_{reponame}/{path/to/file.py}
|
||||||
|
Returns: {author}_{reponame} or filename if extraction fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parts = file_path.parts
|
||||||
|
# Find 'temp' directory in path
|
||||||
|
if 'temp' in parts:
|
||||||
|
temp_idx = parts.index('temp')
|
||||||
|
if temp_idx + 1 < len(parts):
|
||||||
|
# Next part after 'temp' is the repo directory
|
||||||
|
return parts[temp_idx + 1]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to filename if extraction fails
|
||||||
|
return file_path.name if hasattr(file_path, 'name') else str(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_empty_dict_detector(code_text: str, file_path: Optional[Path], verbose: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Detect empty NODE_CLASS_MAPPINGS (UI-only extensions).
|
||||||
|
Logs for documentation purposes only (when verbose=True).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code_text: Python source code to analyze
|
||||||
|
file_path: Path to the file being analyzed
|
||||||
|
verbose: If True, print detection messages
|
||||||
|
"""
|
||||||
|
empty_patterns = [
|
||||||
|
'NODE_CLASS_MAPPINGS = {}',
|
||||||
|
'NODE_CLASS_MAPPINGS={}',
|
||||||
|
]
|
||||||
|
|
||||||
|
code_normalized = code_text.replace(' ', '').replace('\n', '')
|
||||||
|
|
||||||
|
for pattern in empty_patterns:
|
||||||
|
pattern_normalized = pattern.replace(' ', '')
|
||||||
|
if pattern_normalized in code_normalized:
|
||||||
|
if file_path and verbose:
|
||||||
|
repo_name = _extract_repo_name(file_path)
|
||||||
|
print(f"Info: UI-only extension (empty NODE_CLASS_MAPPINGS): {repo_name}")
|
||||||
|
return
|
||||||
|
|
||||||
def has_comfy_node_base(class_node):
|
def has_comfy_node_base(class_node):
|
||||||
"""Check if class inherits from io.ComfyNode or ComfyNode"""
|
"""Check if class inherits from io.ComfyNode or ComfyNode"""
|
||||||
@ -229,6 +941,25 @@ def extract_v3_nodes(code_text):
|
|||||||
|
|
||||||
|
|
||||||
# scan
|
# scan
|
||||||
|
def extract_metadata_only(filename):
|
||||||
|
"""Extract only metadata (@author, @title, etc) without node scanning"""
|
||||||
|
try:
|
||||||
|
with open(filename, encoding='utf-8', errors='ignore') as file:
|
||||||
|
code = file.read()
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
lines = code.strip().split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('@'):
|
||||||
|
if line.startswith("@author:") or line.startswith("@title:") or line.startswith("@nickname:") or line.startswith("@description:"):
|
||||||
|
key, value = line[1:].strip().split(':', 1)
|
||||||
|
metadata[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def scan_in_file(filename, is_builtin=False):
|
def scan_in_file(filename, is_builtin=False):
|
||||||
global builtin_nodes
|
global builtin_nodes
|
||||||
|
|
||||||
@ -242,8 +973,8 @@ def scan_in_file(filename, is_builtin=False):
|
|||||||
nodes = set()
|
nodes = set()
|
||||||
class_dict = {}
|
class_dict = {}
|
||||||
|
|
||||||
# V1 nodes detection
|
# V1 nodes detection (enhanced with fallback patterns)
|
||||||
nodes |= extract_nodes(code)
|
nodes |= extract_nodes_enhanced(code, file_path=Path(filename), visited=set())
|
||||||
|
|
||||||
# V3 nodes detection
|
# V3 nodes detection
|
||||||
nodes |= extract_v3_nodes(code)
|
nodes |= extract_v3_nodes(code)
|
||||||
@ -620,13 +1351,14 @@ def update_custom_nodes(scan_only_mode=False, url_list_file=None):
|
|||||||
return node_info
|
return node_info
|
||||||
|
|
||||||
|
|
||||||
def gen_json(node_info, scan_only_mode=False):
|
def gen_json(node_info, scan_only_mode=False, force_rescan=False):
|
||||||
"""
|
"""
|
||||||
Generate extension-node-map.json from scanned node information
|
Generate extension-node-map.json from scanned node information
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
node_info (dict): Repository metadata mapping
|
node_info (dict): Repository metadata mapping
|
||||||
scan_only_mode (bool): If True, exclude metadata from output
|
scan_only_mode (bool): If True, exclude metadata from output
|
||||||
|
force_rescan (bool): If True, ignore cache and force rescan all nodes
|
||||||
"""
|
"""
|
||||||
# scan from .py file
|
# scan from .py file
|
||||||
node_files, node_dirs = get_nodes(temp_dir)
|
node_files, node_dirs = get_nodes(temp_dir)
|
||||||
@ -642,13 +1374,17 @@ def gen_json(node_info, scan_only_mode=False):
|
|||||||
py_files = get_py_file_paths(dirname)
|
py_files = get_py_file_paths(dirname)
|
||||||
metadata = {}
|
metadata = {}
|
||||||
|
|
||||||
nodes = set()
|
# Use per-repo cache for node AND metadata extraction
|
||||||
for py in py_files:
|
try:
|
||||||
nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI")
|
nodes, metadata = extract_nodes_from_repo(Path(dirname), verbose=False, force_rescan=force_rescan)
|
||||||
nodes.update(nodes_in_file)
|
except:
|
||||||
# Include metadata from .py files in both modes
|
# Fallback to file-by-file scanning if extract_nodes_from_repo fails
|
||||||
metadata.update(metadata_in_file)
|
nodes = set()
|
||||||
|
for py in py_files:
|
||||||
|
nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI")
|
||||||
|
nodes.update(nodes_in_file)
|
||||||
|
metadata.update(metadata_in_file)
|
||||||
|
|
||||||
dirname = os.path.basename(dirname)
|
dirname = os.path.basename(dirname)
|
||||||
|
|
||||||
if 'Jovimetrix' in dirname:
|
if 'Jovimetrix' in dirname:
|
||||||
@ -810,11 +1546,14 @@ if __name__ == "__main__":
|
|||||||
print("\n# Generating 'extension-node-map.json'...\n")
|
print("\n# Generating 'extension-node-map.json'...\n")
|
||||||
|
|
||||||
# Generate extension-node-map.json
|
# Generate extension-node-map.json
|
||||||
gen_json(updated_node_info, scan_only_mode)
|
force_rescan = args.force_rescan if hasattr(args, 'force_rescan') else False
|
||||||
|
if force_rescan:
|
||||||
|
print("⚠️ Force rescan enabled - ignoring all cached results\n")
|
||||||
|
gen_json(updated_node_info, scan_only_mode, force_rescan)
|
||||||
|
|
||||||
print("\n✅ DONE.\n")
|
print("\n✅ DONE.\n")
|
||||||
|
|
||||||
if scan_only_mode:
|
if scan_only_mode:
|
||||||
print("Output: extension-node-map.json (node mappings only)")
|
print("Output: extension-node-map.json (node mappings only)")
|
||||||
else:
|
else:
|
||||||
print("Output: extension-node-map.json (full metadata)")
|
print("Output: extension-node-map.json (full metadata)")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user