Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec13f830f2 | ||
|
|
cf137b84f5 | ||
|
|
36ab9e1946 | ||
|
|
2db8592267 | ||
|
|
9cb8f7faba | ||
|
|
a95d2ca1b7 | ||
|
|
f5f5016346 | ||
|
|
e9c7205e25 | ||
|
|
6b06b18bca | ||
|
|
1c6886712a | ||
|
|
2286083b9b | ||
|
|
a23ba4fae2 | ||
|
|
09677772c6 | ||
|
|
37522e484a | ||
|
|
270616a744 | ||
|
|
2941e48879 | ||
|
|
f7436011c4 | ||
|
|
e783422e7c | ||
|
|
41d2e38f2f | ||
|
|
56247cfa8b | ||
|
|
328915a7ee | ||
|
|
543e9e999a | ||
|
|
421361edb8 | ||
|
|
647c95a5df | ||
|
|
7421dfa756 | ||
|
|
60883fdf63 | ||
|
|
b6143a3abf | ||
|
|
828d510891 | ||
|
|
024309c050 | ||
|
|
2e09cf3963 | ||
|
|
ef424be4b1 | ||
|
|
a1fa872e2e | ||
|
|
d31852e5ab | ||
|
|
bb98171982 | ||
|
|
004744e8e2 | ||
|
|
94933a0246 | ||
|
|
c942976c02 | ||
|
|
0d19394c8f | ||
|
|
4bca1435a6 | ||
|
|
4eb67f25f0 | ||
|
|
40a9cf3db9 | ||
|
|
ff0466957e | ||
|
|
294e610eb1 | ||
|
|
a33052141a | ||
|
|
e2a3c0c596 | ||
|
|
557d910ab4 | ||
|
|
be5c8c41c9 | ||
|
|
408517273a | ||
|
|
e8b6622919 | ||
|
|
4001848e37 | ||
|
|
0733d001f3 | ||
|
|
33878502af | ||
|
|
2c9ded2cbf |
@@ -6,23 +6,29 @@ A compact and powerful layout overhaul for ComfyUI that makes generation results
|
|||||||
|
|
||||||
* **Compact, information-dense layout** with visual previews that let you see what is happening at a glance. Optional UI elements and sidebar tabs can be hidden when you don't need them.
|
* **Compact, information-dense layout** with visual previews that let you see what is happening at a glance. Optional UI elements and sidebar tabs can be hidden when you don't need them.
|
||||||
|
|
||||||
<img src="assets/layout.png" alt="ComfySidebar layout" width="100%">
|
<img src="assets/layout.png" alt="ComfySidebar layout">
|
||||||
|
|
||||||
* **Left or right side:** Choose which side the sidebar appears on in ComfyUI settings.
|
* **Left or right side:** Choose which side the sidebar appears on in ComfyUI settings.
|
||||||
|
|
||||||
* **Simplified mouse-friendly workflow** with drag-and-drop support and convenient actions for moving results back into the workflow, opening files, and managing outputs. Hold `Ctrl` for extra actions such as opening an image's location or deleting it directly from your device.
|
* **Simplified mouse-friendly workflow** with drag-and-drop support and convenient actions for moving results back into the workflow, opening files, and managing outputs. Hold `Ctrl` for extra actions such as opening an image's location or deleting it directly from your device.
|
||||||
|
|
||||||
<img src="assets/controls.png" alt="ComfySidebar controls overview" width="100%">
|
<img src="assets/controls.png" alt="ComfySidebar controls overview">
|
||||||
|
|
||||||
* **Built-in comparison tool** for quickly comparing two generated results side by side.
|
* **Built-in comparison tool** for quickly comparing two generated results side by side.
|
||||||
|
|
||||||
<img src="assets/compare.png" alt="ComfySidebar compare" width="100%">
|
<img src="assets/compare.png" alt="ComfySidebar compare">
|
||||||
|
|
||||||
* **[NEW] Output Gallery sidebar:** Browse files and subfolders directly from your ComfyUI `output` directory without leaving the interface. Features dedicated cards for images, videos, audio, and 3D assets (`.glb`/`.gltf`), breadcrumb navigation, prompt metadata search across subfolders, and persistent sorting.
|
* **Output Gallery sidebar:** Browse files and subfolders directly from your ComfyUI `output` directory without leaving the interface. Features dedicated cards for images, videos, audio, and 3D assets (`.glb`/`.gltf`), breadcrumb navigation, filename and prompt metadata search across subfolders, and persistent sorting.
|
||||||
|
|
||||||
|
* **Hotkeys on any keyboard layout:** Shortcuts such as `Q`, `Ctrl+Q` and `Alt+R` keep working while a non-Latin layout (for example Russian) is active. Non-Latin keys are treated as the Latin key in the same physical position, and typing in text fields is not affected. Can be turned off with **Fix non-ASCII keybinds** in the settings.
|
||||||
|
|
||||||
* **Runtime preview mode** combined with workflow overview makes ComfyUI practical for running and monitoring workflows in a more streamlined, queue-oriented way.
|
* **Runtime preview mode** combined with workflow overview makes ComfyUI practical for running and monitoring workflows in a more streamlined, queue-oriented way.
|
||||||
|
|
||||||
<img src="assets/runtime.png" alt="ComfySidebar runtime mode" width="100%">
|
<img src="assets/runtime.png" alt="ComfySidebar runtime mode">
|
||||||
|
|
||||||
|
* **[NEW] Quick Workflow Picker** Allows you to quickly find and load an existing workflow from your collection by pressing `+` button on the workflow tab.
|
||||||
|
|
||||||
|
<img src="assets/picker.png" alt="ComfySidebar quick picker">
|
||||||
|
|
||||||
## 🛠️ Installation
|
## 🛠️ Installation
|
||||||
|
|
||||||
@@ -61,24 +67,27 @@ Open **Settings → Comfy Sidebar** to configure:
|
|||||||
* Unified top bar layout
|
* Unified top bar layout
|
||||||
* Permanent deletion behavior
|
* Permanent deletion behavior
|
||||||
* Output Gallery sidebar toggle
|
* Output Gallery sidebar toggle
|
||||||
|
* Fix non-ASCII keybinds (hotkeys on any keyboard layout)
|
||||||
|
|
||||||
## ⌨️ Controls
|
## ⌨️ Controls
|
||||||
|
|
||||||
| Shortcut | Action |
|
| Shortcut | Action |
|
||||||
| ------------- | ---------------------------------------------- |
|
| ------------- | ---------------------------------------------- |
|
||||||
| `Q` | Toggle ComfySidebar |
|
| `Q` | Toggle ComfySidebar* |
|
||||||
| `Ctrl+Q` | Exclude the selected node from sidebar results |
|
| `Ctrl+Q` | Exclude the selected node from sidebar results* |
|
||||||
| `Alt+R` | Renumber nodes in execution order |
|
| `Alt+R` | Renumber nodes in execution order* |
|
||||||
| `Click` | Open result preview |
|
| `Click` | Open result preview |
|
||||||
| `Shift+Click` | Compare another result |
|
| `Shift+Click` | Compare another result |
|
||||||
| `Ctrl+Click` | Open the file location / alternative action |
|
| `Ctrl+Click` | Open the file location / alternative action |
|
||||||
|
|
||||||
You can also use the available actions from result cards, the sidebar header, and the node/canvas context menus.
|
*You can see and change keybindings in **Settings → Keybinding**.
|
||||||
|
|
||||||
## 📝 Notes
|
## 📝 Notes
|
||||||
|
|
||||||
ComfySidebar integrates with the ComfyUI frontend and depends on parts of its UI structure. Frontend changes in ComfyUI may require updates to this extension.
|
ComfySidebar integrates with the ComfyUI frontend and depends on parts of its UI structure. Frontend changes in ComfyUI may require updates to this extension.
|
||||||
|
|
||||||
|
**Keyboard layouts:** the *Fix non-ASCII keybinds* option only re-maps keys that produce a non-ASCII character, so QWERTY, AZERTY and Dvorak users are not affected. It cannot help with layouts that type a *different ASCII* symbol on a key (for example, `+` where a US layout has `=`). ComfyUI's "edit keybinding" dialog still records the typed character, so switch to an English layout while rebinding a key.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
See [LICENSE](LICENSE).
|
See [LICENSE](LICENSE).
|
||||||
|
|||||||
+182
-124
@@ -1,11 +1,14 @@
|
|||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import threading
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from server import PromptServer
|
from server import PromptServer
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
|
||||||
|
from . import prompt_search
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {}
|
NODE_CLASS_MAPPINGS = {}
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||||
WEB_DIRECTORY = "./js"
|
WEB_DIRECTORY = "./js"
|
||||||
@@ -61,10 +64,35 @@ def get_all_candidate_roots(subfolder="", folder_type="output"):
|
|||||||
return unique
|
return unique
|
||||||
|
|
||||||
|
|
||||||
def resolve_existing_path(filename="", subfolder="", folder_type="output", candidate_roots=None):
|
def resolve_existing_path(filename="", subfolder="", folder_type="output", candidate_roots=None, strict=False):
|
||||||
"""Finds the actual location of a file or folder across all candidate root directories."""
|
"""Finds the actual location of a file or folder across all candidate root directories.
|
||||||
|
|
||||||
|
strict=True is for destructive operations: the file must exist at exactly
|
||||||
|
<root>/<subfolder>/<filename> (no fallback to a same-named file in the root),
|
||||||
|
and only the root matching `folder_type` (output/input/temp) is considered.
|
||||||
|
"""
|
||||||
candidates = candidate_roots if candidate_roots is not None else get_all_candidate_roots(subfolder, folder_type)
|
candidates = candidate_roots if candidate_roots is not None else get_all_candidate_roots(subfolder, folder_type)
|
||||||
|
|
||||||
|
if strict:
|
||||||
|
type_dirs = {
|
||||||
|
"output": folder_paths.get_output_directory(),
|
||||||
|
"input": folder_paths.get_input_directory(),
|
||||||
|
"temp": folder_paths.get_temp_directory(),
|
||||||
|
}
|
||||||
|
wanted = type_dirs.get(folder_type)
|
||||||
|
if wanted is None:
|
||||||
|
return None, None
|
||||||
|
wanted = os.path.realpath(wanted)
|
||||||
|
if not filename:
|
||||||
|
return None, None
|
||||||
|
for root in candidates:
|
||||||
|
if os.path.realpath(root) != wanted:
|
||||||
|
continue
|
||||||
|
p = os.path.join(root, subfolder, filename) if subfolder else os.path.join(root, filename)
|
||||||
|
if os.path.isfile(p) and is_path_safe(root, p):
|
||||||
|
return p, root
|
||||||
|
return None, None
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
for root in candidates:
|
for root in candidates:
|
||||||
if subfolder:
|
if subfolder:
|
||||||
@@ -118,41 +146,122 @@ def open_file_in_os(target_path):
|
|||||||
raise RuntimeError("Headless Linux server detected (no desktop GUI available).")
|
raise RuntimeError("Headless Linux server detected (no desktop GUI available).")
|
||||||
|
|
||||||
|
|
||||||
def check_png_prompt_match(filepath, search_term):
|
_GALLERY_EXTS = {
|
||||||
"""Checks PNG prompt metadata strictly for text inputs matching search."""
|
".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif",
|
||||||
try:
|
".mp4", ".webm", ".mov", ".mkv",
|
||||||
with open(filepath, "rb") as f:
|
".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac",
|
||||||
if f.read(8) != b"\x89PNG\r\n\x1a\n":
|
".glb", ".gltf", ".obj", ".fbx", ".stl",
|
||||||
return False
|
".json"
|
||||||
while True:
|
}
|
||||||
len_b = f.read(4)
|
|
||||||
if len(len_b) < 4:
|
|
||||||
break
|
def _relative_subfolder(directory, output_dir):
|
||||||
length = int.from_bytes(len_b, "big")
|
rel_sub = os.path.relpath(directory, output_dir).replace("\\", "/")
|
||||||
ctype = f.read(4)
|
return "" if rel_sub == "." else rel_sub
|
||||||
if ctype in (b"tEXt", b"iTXt"):
|
|
||||||
data = f.read(length)
|
|
||||||
if data.startswith(b"prompt\x00") or b'"inputs"' in data:
|
def _list_gallery(subfolder, search, cancel):
|
||||||
text = data.decode("utf-8", errors="ignore")
|
"""Blocking directory listing / prompt search. Runs in a worker thread.
|
||||||
idx = text.find("{")
|
|
||||||
if idx != -1:
|
Returns (http_status, json_payload).
|
||||||
try:
|
"""
|
||||||
prompt_dict = json.loads(text[idx:])
|
output_dir = folder_paths.get_output_directory()
|
||||||
for node in prompt_dict.values():
|
target_dir = os.path.realpath(os.path.join(output_dir, subfolder)) if subfolder else os.path.realpath(output_dir)
|
||||||
if isinstance(node, dict) and "inputs" in node:
|
|
||||||
for val in node["inputs"].values():
|
if not is_path_safe(output_dir, target_dir):
|
||||||
if isinstance(val, str) and search_term in val.lower():
|
return 403, {"error": "Unsafe path"}
|
||||||
return True
|
|
||||||
except Exception:
|
if not os.path.exists(target_dir) or not os.path.isdir(target_dir):
|
||||||
pass
|
return 200, {"subfolder": subfolder, "folders": [], "files": []}
|
||||||
elif ctype in (b"IDAT", b"IEND"):
|
|
||||||
break
|
folders = []
|
||||||
else:
|
files = []
|
||||||
f.seek(length, 1)
|
|
||||||
f.seek(4, 1)
|
if search:
|
||||||
return False
|
# Recursive filename + prompt-text search. Prompt metadata is only read for PNGs.
|
||||||
except Exception:
|
for root, dirs, fnames in os.walk(target_dir):
|
||||||
return False
|
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||||
|
for fname in fnames:
|
||||||
|
if cancel.is_set():
|
||||||
|
return 200, {
|
||||||
|
"subfolder": subfolder.replace("\\", "/"),
|
||||||
|
"folders": folders,
|
||||||
|
"files": files
|
||||||
|
}
|
||||||
|
if fname.startswith("."):
|
||||||
|
continue
|
||||||
|
ext = os.path.splitext(fname)[1].lower()
|
||||||
|
if ext not in _GALLERY_EXTS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
fpath = os.path.join(root, fname)
|
||||||
|
matched = search in fname.lower()
|
||||||
|
if not matched and ext == ".png":
|
||||||
|
matched = search in prompt_search.get_prompt_search_text(fpath)
|
||||||
|
if matched:
|
||||||
|
try:
|
||||||
|
stat = os.stat(fpath)
|
||||||
|
except (PermissionError, FileNotFoundError):
|
||||||
|
continue
|
||||||
|
files.append({
|
||||||
|
"filename": fname,
|
||||||
|
"subfolder": _relative_subfolder(root, output_dir),
|
||||||
|
"type": "output",
|
||||||
|
"mtime": stat.st_mtime,
|
||||||
|
"size": stat.st_size
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Instant single-directory listing without reading files from disk
|
||||||
|
with os.scandir(target_dir) as entries:
|
||||||
|
for entry in entries:
|
||||||
|
if entry.name.startswith("."):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if entry.is_dir(follow_symlinks=False):
|
||||||
|
folders.append({
|
||||||
|
"name": entry.name,
|
||||||
|
"subfolder": os.path.relpath(entry.path, output_dir).replace("\\", "/"),
|
||||||
|
"mtime": entry.stat().st_mtime
|
||||||
|
})
|
||||||
|
elif entry.is_file(follow_symlinks=False):
|
||||||
|
if os.path.splitext(entry.name)[1].lower() in _GALLERY_EXTS:
|
||||||
|
stat = entry.stat()
|
||||||
|
files.append({
|
||||||
|
"filename": entry.name,
|
||||||
|
"subfolder": _relative_subfolder(os.path.dirname(entry.path), output_dir),
|
||||||
|
"type": "output",
|
||||||
|
"mtime": stat.st_mtime,
|
||||||
|
"size": stat.st_size
|
||||||
|
})
|
||||||
|
except (PermissionError, FileNotFoundError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
folders.sort(key=lambda x: x["name"].lower())
|
||||||
|
files.sort(key=lambda x: x["mtime"], reverse=True)
|
||||||
|
|
||||||
|
return 200, {
|
||||||
|
"subfolder": subfolder.replace("\\", "/"),
|
||||||
|
"folders": folders,
|
||||||
|
"files": files
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_LOCALES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "locales")
|
||||||
|
_ALLOWED_LOCALE_FILES = {"main.json", "settings.json"}
|
||||||
|
|
||||||
|
|
||||||
|
@PromptServer.instance.routes.get("/comfy-sidebar/locales/{lang}/{filename}")
|
||||||
|
async def locale_file_handler(request):
|
||||||
|
lang = request.match_info.get("lang", "").strip()
|
||||||
|
filename = request.match_info.get("filename", "").strip()
|
||||||
|
if not lang or filename not in _ALLOWED_LOCALE_FILES:
|
||||||
|
return web.json_response({"error": "Not found"}, status=404)
|
||||||
|
|
||||||
|
target_file = os.path.join(_LOCALES_DIR, lang, filename)
|
||||||
|
if not is_path_safe(_LOCALES_DIR, target_file) or not os.path.isfile(target_file):
|
||||||
|
return web.json_response({"error": "Not found"}, status=404)
|
||||||
|
|
||||||
|
return web.FileResponse(target_file)
|
||||||
|
|
||||||
|
|
||||||
@PromptServer.instance.routes.get("/comfy-sidebar/gallery/list")
|
@PromptServer.instance.routes.get("/comfy-sidebar/gallery/list")
|
||||||
@@ -160,96 +269,45 @@ async def gallery_list_handler(request):
|
|||||||
try:
|
try:
|
||||||
subfolder = request.query.get("subfolder", "").strip().strip("/\\")
|
subfolder = request.query.get("subfolder", "").strip().strip("/\\")
|
||||||
search = request.query.get("search", "").strip().lower()
|
search = request.query.get("search", "").strip().lower()
|
||||||
output_dir = folder_paths.get_output_directory()
|
|
||||||
|
|
||||||
target_dir = os.path.realpath(os.path.join(output_dir, subfolder)) if subfolder else os.path.realpath(output_dir)
|
# Scanning can touch thousands of files: keep it off the event loop so
|
||||||
|
# ComfyUI's server (websocket, queue updates) stays responsive.
|
||||||
if not is_path_safe(output_dir, target_dir):
|
cancel = threading.Event()
|
||||||
return web.json_response({"error": "Unsafe path"}, status=403)
|
loop = asyncio.get_running_loop()
|
||||||
|
try:
|
||||||
if not os.path.exists(target_dir) or not os.path.isdir(target_dir):
|
status, payload = await loop.run_in_executor(None, _list_gallery, subfolder, search, cancel)
|
||||||
return web.json_response({"subfolder": subfolder, "folders": [], "files": []})
|
except asyncio.CancelledError:
|
||||||
|
cancel.set() # client went away / sent a newer search: stop scanning
|
||||||
folders = []
|
raise
|
||||||
files = []
|
return web.json_response(payload, status=status)
|
||||||
|
|
||||||
supported_exts = {
|
|
||||||
".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif",
|
|
||||||
".mp4", ".webm", ".mov", ".mkv",
|
|
||||||
".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac",
|
|
||||||
".glb", ".gltf", ".obj", ".fbx", ".stl",
|
|
||||||
".json"
|
|
||||||
}
|
|
||||||
|
|
||||||
if search:
|
|
||||||
# Recursive search in current directory and all subfolders
|
|
||||||
for root, dirs, fnames in os.walk(target_dir):
|
|
||||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
||||||
for fname in fnames:
|
|
||||||
if fname.startswith("."):
|
|
||||||
continue
|
|
||||||
ext = os.path.splitext(fname)[1].lower()
|
|
||||||
if ext in supported_exts:
|
|
||||||
fpath = os.path.join(root, fname)
|
|
||||||
matched = search in fname.lower()
|
|
||||||
if not matched and ext == ".png":
|
|
||||||
matched = check_png_prompt_match(fpath, search)
|
|
||||||
|
|
||||||
if matched:
|
|
||||||
rel_sub = os.path.relpath(root, output_dir).replace("\\", "/")
|
|
||||||
if rel_sub == ".":
|
|
||||||
rel_sub = ""
|
|
||||||
stat = os.stat(fpath)
|
|
||||||
files.append({
|
|
||||||
"filename": fname,
|
|
||||||
"subfolder": rel_sub,
|
|
||||||
"type": "output",
|
|
||||||
"mtime": stat.st_mtime,
|
|
||||||
"size": stat.st_size
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
# Instant single-directory listing without reading files from disk
|
|
||||||
with os.scandir(target_dir) as entries:
|
|
||||||
for entry in entries:
|
|
||||||
if entry.name.startswith("."):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if entry.is_dir(follow_symlinks=False):
|
|
||||||
rel_sub = os.path.relpath(entry.path, output_dir).replace("\\", "/")
|
|
||||||
folders.append({
|
|
||||||
"name": entry.name,
|
|
||||||
"subfolder": rel_sub,
|
|
||||||
"mtime": entry.stat().st_mtime
|
|
||||||
})
|
|
||||||
elif entry.is_file(follow_symlinks=False):
|
|
||||||
ext = os.path.splitext(entry.name)[1].lower()
|
|
||||||
if ext in supported_exts:
|
|
||||||
rel_sub = os.path.relpath(os.path.dirname(entry.path), output_dir).replace("\\", "/")
|
|
||||||
if rel_sub == ".":
|
|
||||||
rel_sub = ""
|
|
||||||
stat = entry.stat()
|
|
||||||
files.append({
|
|
||||||
"filename": entry.name,
|
|
||||||
"subfolder": rel_sub,
|
|
||||||
"type": "output",
|
|
||||||
"mtime": stat.st_mtime,
|
|
||||||
"size": stat.st_size
|
|
||||||
})
|
|
||||||
except (PermissionError, FileNotFoundError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
folders.sort(key=lambda x: x["name"].lower())
|
|
||||||
files.sort(key=lambda x: x["mtime"], reverse=True)
|
|
||||||
|
|
||||||
return web.json_response({
|
|
||||||
"subfolder": subfolder.replace("\\", "/"),
|
|
||||||
"folders": folders,
|
|
||||||
"files": files
|
|
||||||
})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
@PromptServer.instance.routes.post("/comfy-sidebar/queue/prompt-search")
|
||||||
|
async def queue_prompt_search_handler(request):
|
||||||
|
"""Extract searchable prompt text from Queue API-format prompt graphs."""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
prompts = data.get("prompts", [])
|
||||||
|
if not isinstance(prompts, list) or len(prompts) > 32:
|
||||||
|
return web.json_response({"error": "Expected up to 32 prompt graphs"}, status=400)
|
||||||
|
|
||||||
|
texts = {}
|
||||||
|
for item in prompts:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
pid = item.get("pid")
|
||||||
|
graph = item.get("prompt")
|
||||||
|
if pid is None or not isinstance(graph, dict):
|
||||||
|
continue
|
||||||
|
texts[str(pid)] = prompt_search.get_prompt_search_text_from_graph(graph)
|
||||||
|
|
||||||
|
return web.json_response({"texts": texts})
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
@PromptServer.instance.routes.post("/comfy-sidebar/open-folder")
|
@PromptServer.instance.routes.post("/comfy-sidebar/open-folder")
|
||||||
async def open_folder_handler(request):
|
async def open_folder_handler(request):
|
||||||
try:
|
try:
|
||||||
@@ -286,7 +344,7 @@ async def delete_file_handler(request):
|
|||||||
]
|
]
|
||||||
delete_roots = [os.path.realpath(r) for r in delete_roots if r and os.path.exists(r)]
|
delete_roots = [os.path.realpath(r) for r in delete_roots if r and os.path.exists(r)]
|
||||||
|
|
||||||
target_file, root = resolve_existing_path(filename, subfolder, folder_type, candidate_roots=delete_roots)
|
target_file, root = resolve_existing_path(filename, subfolder, folder_type, candidate_roots=delete_roots, strict=True)
|
||||||
|
|
||||||
if not target_file or not os.path.isfile(target_file) or not is_path_safe(root, target_file):
|
if not target_file or not os.path.isfile(target_file) or not is_path_safe(root, target_file):
|
||||||
return web.json_response({"error": "File not found or outside output directory"}, status=404)
|
return web.json_response({"error": "File not found or outside output directory"}, status=404)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+2
-1
@@ -364,6 +364,7 @@ function setupCanvasMenuPopoverRelocator() {
|
|||||||
if (!sidebar || !popovers.length) return;
|
if (!sidebar || !popovers.length) return;
|
||||||
|
|
||||||
const btnRect = btn.getBoundingClientRect();
|
const btnRect = btn.getBoundingClientRect();
|
||||||
|
const sidebarRect = sidebar.getBoundingClientRect();
|
||||||
const onLeft = isSidebarOnLeft();
|
const onLeft = isSidebarOnLeft();
|
||||||
|
|
||||||
popovers.forEach((pop) => {
|
popovers.forEach((pop) => {
|
||||||
@@ -1030,7 +1031,7 @@ export function setupPropertiesPanelToggleFix() {
|
|||||||
|
|
||||||
const isInternalSidebarMutation = (node) => {
|
const isInternalSidebarMutation = (node) => {
|
||||||
if (!node || node.nodeType !== 1) return false;
|
if (!node || node.nodeType !== 1) return false;
|
||||||
return node.closest?.('.comfyui-sidebar, .comfy-sidebar, .comfy-sidebar-comparison-overlay') ||
|
return node.closest?.('.comfyui-sidebar, .comfy-sidebar, .comfy-sidebar-comparison-overlay, .comfy-sidebar-wf-picker') ||
|
||||||
node.classList?.contains('comfy-sidebar-card');
|
node.classList?.contains('comfy-sidebar-card');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const SettingIds = Object.freeze({
|
|||||||
OVERRIDE_STOCK_HISTORY: "Comfy Sidebar.Hide Junk.Override Stock Job History Tab",
|
OVERRIDE_STOCK_HISTORY: "Comfy Sidebar.Hide Junk.Override Stock Job History Tab",
|
||||||
HIDE_GRAPH_BUTTON: "Comfy Sidebar.Hide Junk.Graph Button",
|
HIDE_GRAPH_BUTTON: "Comfy Sidebar.Hide Junk.Graph Button",
|
||||||
CLASSIC_LAYOUT: "Comfy Sidebar.Comfy Layout",
|
CLASSIC_LAYOUT: "Comfy Sidebar.Comfy Layout",
|
||||||
|
LATIN_HOTKEYS: "Comfy Sidebar.Latin Hotkeys On Any Layout",
|
||||||
|
QUICK_WORKFLOW_PICKER: "Comfy Sidebar.Quick Workflow Picker",
|
||||||
MAX_HISTORY_ITEMS: "Comfy.Queue.MaxHistoryItems",
|
MAX_HISTORY_ITEMS: "Comfy.Queue.MaxHistoryItems",
|
||||||
DOCK_QUEUE: "Comfy.Queue.QPOV2",
|
DOCK_QUEUE: "Comfy.Queue.QPOV2",
|
||||||
SHOW_PROGRESS_BAR: "Comfy.Queue.ShowRunProgressBar",
|
SHOW_PROGRESS_BAR: "Comfy.Queue.ShowRunProgressBar",
|
||||||
|
|||||||
+102
-22
@@ -1,14 +1,39 @@
|
|||||||
import { app } from "/scripts/app.js";
|
import { app } from "/scripts/app.js";
|
||||||
|
|
||||||
let currentLocale = "en";
|
let currentLocale = "en";
|
||||||
|
let currentSettingRaw = null;
|
||||||
|
let loadSeq = 0;
|
||||||
let translations = {};
|
let translations = {};
|
||||||
let fallbackTranslations = {};
|
let fallbackTranslations = {};
|
||||||
|
let fallbackLoaded = false;
|
||||||
|
const localeCache = new Map();
|
||||||
|
const localeListeners = new Set();
|
||||||
|
|
||||||
|
function detectRawLocale() {
|
||||||
|
let settingVal = app?.extensionManager?.setting?.get?.("Comfy.Locale")
|
||||||
|
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale")
|
||||||
|
|| app?.extensionManager?.setting?.get?.("Comfy.Locale.Language")
|
||||||
|
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale.Language");
|
||||||
|
|
||||||
|
if (!settingVal) {
|
||||||
|
try {
|
||||||
|
settingVal = localStorage.getItem("Comfy.Settings.Comfy.Locale")
|
||||||
|
|| localStorage.getItem("Comfy.Locale");
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (settingVal || "en").toLowerCase().replace("_", "-");
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchLocaleFiles(lang) {
|
async function fetchLocaleFiles(lang) {
|
||||||
|
if (localeCache.has(lang)) {
|
||||||
|
return localeCache.get(lang);
|
||||||
|
}
|
||||||
|
|
||||||
const data = {};
|
const data = {};
|
||||||
for (const file of ["main.json", "settings.json"]) {
|
for (const file of ["main.json"]) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(`../locales/${lang}/${file}`, import.meta.url).href;
|
const url = `/comfy-sidebar/locales/${encodeURIComponent(lang)}/${file}`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
@@ -16,53 +41,108 @@ async function fetchLocaleFiles(lang) {
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localeCache.set(lang, data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initI18n() {
|
export async function initI18n() {
|
||||||
|
const seq = ++loadSeq;
|
||||||
try {
|
try {
|
||||||
let settingVal = app?.extensionManager?.setting?.get?.("Comfy.Locale")
|
const raw = detectRawLocale();
|
||||||
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale")
|
|
||||||
|| app?.extensionManager?.setting?.get?.("Comfy.Locale.Language")
|
|
||||||
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale.Language");
|
|
||||||
|
|
||||||
if (!settingVal) {
|
|
||||||
try {
|
|
||||||
settingVal = localStorage.getItem("Comfy.Settings.Comfy.Locale")
|
|
||||||
|| localStorage.getItem("Comfy.Locale");
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = (settingVal || "en").toLowerCase().replace("_", "-");
|
|
||||||
const candidates = [raw];
|
const candidates = [raw];
|
||||||
const base = raw.split("-")[0];
|
const base = raw.split("-")[0];
|
||||||
if (base !== raw) candidates.push(base);
|
if (base !== raw) candidates.push(base);
|
||||||
|
|
||||||
|
currentSettingRaw = raw;
|
||||||
currentLocale = candidates[0];
|
currentLocale = candidates[0];
|
||||||
console.log("[ComfySidebar] Detected locale:", currentLocale);
|
|
||||||
|
|
||||||
// 1. Load English fallback
|
// 1. Load English fallback once
|
||||||
fallbackTranslations = await fetchLocaleFiles("en");
|
if (!fallbackLoaded) {
|
||||||
|
const fb = await fetchLocaleFiles("en");
|
||||||
|
if (seq !== loadSeq) return;
|
||||||
|
fallbackTranslations = fb;
|
||||||
|
fallbackLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Load active locale if not English
|
// 2. Load active locale if not English (and clear previous locale if switching back to English)
|
||||||
|
let nextTranslations = {};
|
||||||
if (base !== "en") {
|
if (base !== "en") {
|
||||||
for (const cand of candidates) {
|
for (const cand of candidates) {
|
||||||
const locData = await fetchLocaleFiles(cand);
|
const locData = await fetchLocaleFiles(cand);
|
||||||
|
if (seq !== loadSeq) return;
|
||||||
if (Object.keys(locData).length > 0) {
|
if (Object.keys(locData).length > 0) {
|
||||||
translations = locData;
|
nextTranslations = locData;
|
||||||
console.log(`[ComfySidebar] Loaded ${cand} with ${Object.keys(translations).length} translations`);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (seq !== loadSeq) return;
|
||||||
|
translations = nextTranslations;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[ComfySidebar] i18n initialization failed", e);
|
console.warn("[ComfySidebar] i18n initialization failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onLocaleChange(callback) {
|
||||||
|
if (typeof callback !== "function") return () => {};
|
||||||
|
localeListeners.add(callback);
|
||||||
|
return () => localeListeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupLocaleWatcher() {
|
||||||
|
const cleanups = [];
|
||||||
|
|
||||||
|
const checkAndReload = () => {
|
||||||
|
const nextRaw = detectRawLocale();
|
||||||
|
if (nextRaw !== currentSettingRaw) {
|
||||||
|
initI18n().then(() => {
|
||||||
|
for (const fn of localeListeners) {
|
||||||
|
try { fn(currentLocale); } catch (e) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingStore = app?.extensionManager?.setting?.settingStore;
|
||||||
|
if (settingStore?.$subscribe) {
|
||||||
|
const unsub = settingStore.$subscribe(() => {
|
||||||
|
checkAndReload();
|
||||||
|
});
|
||||||
|
cleanups.push(unsub);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of ["Comfy.Locale", "Comfy.Locale.Language"]) {
|
||||||
|
const settingObj =
|
||||||
|
app?.ui?.settings?.settings?.[key] ||
|
||||||
|
app?.extensionManager?.setting?.settings?.[key];
|
||||||
|
if (settingObj && !settingObj._comfySidebarLocaleWatched) {
|
||||||
|
settingObj._comfySidebarLocaleWatched = true;
|
||||||
|
const origOnChange = settingObj.onChange;
|
||||||
|
settingObj.onChange = (val) => {
|
||||||
|
if (origOnChange) origOnChange(val);
|
||||||
|
checkAndReload();
|
||||||
|
};
|
||||||
|
cleanups.push(() => {
|
||||||
|
settingObj.onChange = origOnChange;
|
||||||
|
delete settingObj._comfySidebarLocaleWatched;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanups.forEach(fn => {
|
||||||
|
try { if (typeof fn === "function") fn(); } catch (e) {}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function t(key, vars = {}) {
|
export function t(key, vars = {}) {
|
||||||
if (!key) return "";
|
if (!key) return "";
|
||||||
let str = translations[key] || fallbackTranslations[key] || key;
|
const locVal = typeof translations[key] === "string" ? translations[key] : null;
|
||||||
|
const fbVal = typeof fallbackTranslations[key] === "string" ? fallbackTranslations[key] : null;
|
||||||
|
let str = locVal || fbVal || key;
|
||||||
for (const [k, v] of Object.entries(vars)) {
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
str = str.replaceAll(`{${k}}`, v);
|
str = str.replaceAll(`{${k}}`, v);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const PersistenceAdapter = {
|
|||||||
status: state.status,
|
status: state.status,
|
||||||
images: cleanedImages,
|
images: cleanedImages,
|
||||||
texts: state.texts || [],
|
texts: state.texts || [],
|
||||||
|
promptSearchText: state.promptSearchText,
|
||||||
nodeOutputs: state.nodeOutputs || {},
|
nodeOutputs: state.nodeOutputs || {},
|
||||||
nodeTitles: state.nodeTitles || {}, // Always preserved across restarts
|
nodeTitles: state.nodeTitles || {}, // Always preserved across restarts
|
||||||
workflow: includeWorkflow ? state.workflow : null,
|
workflow: includeWorkflow ? state.workflow : null,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export function createPromptState(pid, initial = {}) {
|
|||||||
status: initial.status || PromptStatus.PENDING,
|
status: initial.status || PromptStatus.PENDING,
|
||||||
images: Array.isArray(initial.images) ? [...initial.images] : [],
|
images: Array.isArray(initial.images) ? [...initial.images] : [],
|
||||||
texts: Array.isArray(initial.texts) ? [...initial.texts] : [],
|
texts: Array.isArray(initial.texts) ? [...initial.texts] : [],
|
||||||
|
promptSearchText: typeof initial.promptSearchText === "string" ? initial.promptSearchText : null,
|
||||||
nodeOutputs: initial.nodeOutputs ? { ...initial.nodeOutputs } : {},
|
nodeOutputs: initial.nodeOutputs ? { ...initial.nodeOutputs } : {},
|
||||||
nodeTitles: initial.nodeTitles ? { ...initial.nodeTitles } : {},
|
nodeTitles: initial.nodeTitles ? { ...initial.nodeTitles } : {},
|
||||||
progress: typeof initial.progress === "number" ? initial.progress : 0,
|
progress: typeof initial.progress === "number" ? initial.progress : 0,
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "حد أعمدة قائمة الانتظار",
|
|
||||||
"Width Threshold for Queue Columns (px)": "العرض بالبكسل لتقسيم القائمة إلى عدة أعمدة",
|
|
||||||
"Show Pending Count Only": "إظهار عدد المهام المعلقة فقط",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "في حال التعطيل، ستظهر كل مهمة في القائمة ببطاقة مستقلة",
|
|
||||||
"Show Working Node Name": "إظهار اسم العقدة النشطة",
|
|
||||||
"Shows the name of the node which is currently in the process": "يعرض اسم العقدة التي تتم معالجتها حالياً",
|
|
||||||
"Auto Clear Interrupted": "مسح تلقائي للمهام المقاطعة",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "مسح المهام الملغاة والفاشلة تلقائياً عند بدء توليد جديد",
|
|
||||||
"Permanent Delete on Disk Deletion": "حذف دائم من القرص",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "حذف الملفات نهائياً عند المسح من القرص (دون نقلها لسلة المحذوفات)",
|
|
||||||
"Hide Tab: {tab}": "إخفاء التبويب: {tab}",
|
|
||||||
"Assets": "الأصول",
|
|
||||||
"Nodes": "العقد",
|
|
||||||
"Models": "النماذج",
|
|
||||||
"Workflows": "مخططات العمل",
|
|
||||||
"Apps": "التطبيقات",
|
|
||||||
"Templates": "القوالب",
|
|
||||||
"Override Stock History": "استبدال السجل الافتراضي",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "استبدال الشريط الجانبي لسجل المهام الافتراضي بـ Comfy Queue",
|
|
||||||
"Hide Graph Button": "إخفاء زر 'المخطط'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "إخفاء الزر العائم 'المخطط' (خريطة العقد)",
|
|
||||||
"Hide Avatar": "إخفاء الصورة الرمزية",
|
|
||||||
"Floating Canvas Controls": "أدوات التحكم العائمة في اللوحة",
|
|
||||||
"Classic Layout": "المظهر الكلاسيكي",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "وضع عناصر التحكم وتبويبات العمل المفتوحة في شريط علوي موحد",
|
|
||||||
"Comfy Layout": "مظهر مدمج",
|
|
||||||
"Hide Junk": "إخفاء العناصر غير المرغوبة",
|
|
||||||
"Default": "افتراضي",
|
|
||||||
"Dock": "إرساء",
|
|
||||||
"Hide": "إخفاء",
|
|
||||||
"Enable Output Gallery Sidebar": "تمكين الشريط الجانبي لمعرض المخرجات"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "Grid Columns Threshold",
|
|
||||||
"Width Threshold for Queue Columns (px)": "Width Threshold for Queue Columns (px)",
|
|
||||||
"Show Pending Count Only": "Show Pending Count Only",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "If disabled, each queued job will have a separate individual card",
|
|
||||||
"Show Working Node Name": "Show Working Node Name",
|
|
||||||
"Shows the name of the node which is currently in the process": "Shows the name of the node which is currently in the process",
|
|
||||||
"Auto Clear Interrupted": "Auto Clear Interrupted",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "Auto-clear cancelled & failed jobs on new generation",
|
|
||||||
"Permanent Delete on Disk Deletion": "Permanent Delete on Disk Deletion",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)",
|
|
||||||
"Hide Tab: {tab}": "Hide Tab: {tab}",
|
|
||||||
"Assets": "Assets",
|
|
||||||
"Nodes": "Nodes",
|
|
||||||
"Models": "Models",
|
|
||||||
"Workflows": "Workflows",
|
|
||||||
"Apps": "Apps",
|
|
||||||
"Templates": "Templates",
|
|
||||||
"Override Stock History": "Override Stock History",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "Replace the stock Job History sidebar with Comfy Queue",
|
|
||||||
"Hide Graph Button": "Hide Graph Button",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Hide floating 'Graph' (Workflow/Node Map) button",
|
|
||||||
"Hide Avatar": "Hide Avatar",
|
|
||||||
"Floating Canvas Controls": "Floating Canvas Controls",
|
|
||||||
"Classic Layout": "Classic Layout",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "Places the controls and the open workflow tabs on a single unified top bar",
|
|
||||||
"Comfy Layout": "Comfy Layout",
|
|
||||||
"Hide Junk": "Hide Junk",
|
|
||||||
"Default": "Default",
|
|
||||||
"Dock": "Dock",
|
|
||||||
"Hide": "Hide",
|
|
||||||
"Enable Output Gallery Sidebar": "Enable Output Gallery Sidebar"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "Umbral de columnas de la cola",
|
|
||||||
"Width Threshold for Queue Columns (px)": "Ancho en píxeles para dividir la cola en varias columnas",
|
|
||||||
"Show Pending Count Only": "Mostrar solo total en espera",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "Si se desactiva, cada tarea en cola tendrá una tarjeta individual",
|
|
||||||
"Show Working Node Name": "Mostrar nombre del nodo en proceso",
|
|
||||||
"Shows the name of the node which is currently in the process": "Muestra el nombre del nodo que se está procesando actualmente",
|
|
||||||
"Auto Clear Interrupted": "Limpieza auto de interrumpidos",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "Borrar automáticamente tareas canceladas y fallidas al iniciar una nueva generación",
|
|
||||||
"Permanent Delete on Disk Deletion": "Eliminación permanente del disco",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Eliminar permanentemente archivos al borrarlos (sin pasar por la papelera)",
|
|
||||||
"Hide Tab: {tab}": "Ocultar pestaña: {tab}",
|
|
||||||
"Assets": "Recursos",
|
|
||||||
"Nodes": "Nodos",
|
|
||||||
"Models": "Modelos",
|
|
||||||
"Workflows": "Flujos",
|
|
||||||
"Apps": "Apps",
|
|
||||||
"Templates": "Plantillas",
|
|
||||||
"Override Stock History": "Reemplazar historial estándar",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "Reemplaza la barra de historial estándar con Comfy Queue",
|
|
||||||
"Hide Graph Button": "Ocultar botón 'Grafo'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Oculta el botón flotante 'Grafo' (mapa de nodos)",
|
|
||||||
"Hide Avatar": "Ocultar avatar",
|
|
||||||
"Floating Canvas Controls": "Controles flotantes del lienzo",
|
|
||||||
"Classic Layout": "Diseño clásico",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "Coloca los controles y las pestañas abiertas en una barra superior unificada",
|
|
||||||
"Comfy Layout": "Diseño compacto",
|
|
||||||
"Hide Junk": "Ocultar elementos innecesarios",
|
|
||||||
"Default": "Por defecto",
|
|
||||||
"Dock": "Acoplar",
|
|
||||||
"Hide": "Ocultar",
|
|
||||||
"Enable Output Gallery Sidebar": "Habilitar barra lateral de galería de salidas"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "آستانه ستونهای صف",
|
|
||||||
"Width Threshold for Queue Columns (px)": "عرض بر حسب پیکسل برای تقسیم صف به چند ستون",
|
|
||||||
"Show Pending Count Only": "فقط نمایش تعداد در انتظار",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "در صورت غیرفعال بودن، هر کار در صف یک کارت جداگانه خواهد داشت",
|
|
||||||
"Show Working Node Name": "نمایش نام گره در حال پردازش",
|
|
||||||
"Shows the name of the node which is currently in the process": "نام گرهی را که در حال حاضر پردازش میشود نشان میدهد",
|
|
||||||
"Auto Clear Interrupted": "پاکسازی خودکار متوقفشدهها",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "پاک کردن خودکار کارهای لغو شده و ناموفق در تولید جدید",
|
|
||||||
"Permanent Delete on Disk Deletion": "حذف دائمی از دیسک",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "حذف دائمی فایلها هنگام پاک کردن از دیسک (بدون انتقال به سطل بازیافت)",
|
|
||||||
"Hide Tab: {tab}": "پنهان کردن برگه: {tab}",
|
|
||||||
"Assets": "داراییها",
|
|
||||||
"Nodes": "گرهها",
|
|
||||||
"Models": "مدلها",
|
|
||||||
"Workflows": "جریانهای کاری",
|
|
||||||
"Apps": "برنامهها",
|
|
||||||
"Templates": "الگوها",
|
|
||||||
"Override Stock History": "جایگزینی تاریخچه پیشفرض",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "نوار کناری تاریخچه کارهای پیشفرض را با Comfy Queue جایگزین میکند",
|
|
||||||
"Hide Graph Button": "پنهان کردن دکمه 'نمودار'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "دکمه شناور 'نمودار' (نقشه گرهها) را پنهان میکند",
|
|
||||||
"Hide Avatar": "پنهان کردن آواتار",
|
|
||||||
"Floating Canvas Controls": "کنترلهای شناور بوم",
|
|
||||||
"Classic Layout": "طرحبندی کلاسیک",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "کنترلها و برگههای باز جریان کار را در یک نوار بالایی یکپارچه قرار میدهد",
|
|
||||||
"Comfy Layout": "طرحبندی فشرده",
|
|
||||||
"Hide Junk": "پنهان کردن موارد غیرضروری",
|
|
||||||
"Default": "پیشفرض",
|
|
||||||
"Dock": "اتصال",
|
|
||||||
"Hide": "پنهان",
|
|
||||||
"Enable Output Gallery Sidebar": "فعالسازی نوار کناری گالری خروجیها"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "Seuil de colonnes",
|
|
||||||
"Width Threshold for Queue Columns (px)": "Largeur en pixels pour diviser la file en plusieurs colonnes",
|
|
||||||
"Show Pending Count Only": "Afficher uniquement le total en attente",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "Si désactivé, chaque tâche en file aura une carte individuelle",
|
|
||||||
"Show Working Node Name": "Afficher le nom du nœud actif",
|
|
||||||
"Shows the name of the node which is currently in the process": "Affiche le nom du nœud en cours de traitement",
|
|
||||||
"Auto Clear Interrupted": "Nettoyage auto des interruptions",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "Effacer automatiquement les tâches annulées et échouées lors d'une nouvelle génération",
|
|
||||||
"Permanent Delete on Disk Deletion": "Suppression définitive du disque",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Supprimer définitivement les fichiers lors de la suppression (sans passer par la corbeille)",
|
|
||||||
"Hide Tab: {tab}": "Masquer l'onglet : {tab}",
|
|
||||||
"Assets": "Ressources",
|
|
||||||
"Nodes": "Nœuds",
|
|
||||||
"Models": "Modèles",
|
|
||||||
"Workflows": "Workflows",
|
|
||||||
"Apps": "Applications",
|
|
||||||
"Templates": "Modèles",
|
|
||||||
"Override Stock History": "Remplacer l'historique par défaut",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "Remplace la barre latérale d'historique par défaut par Comfy Queue",
|
|
||||||
"Hide Graph Button": "Masquer le bouton 'Graphe'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Masque le bouton flottant 'Graphe' (carte des nœuds)",
|
|
||||||
"Hide Avatar": "Masquer l'avatar",
|
|
||||||
"Floating Canvas Controls": "Contrôles flottants du canevas",
|
|
||||||
"Classic Layout": "Disposition classique",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "Regroupe les contrôles et les onglets ouverts dans une barre supérieure unique",
|
|
||||||
"Comfy Layout": "Disposition compacte",
|
|
||||||
"Hide Junk": "Masquer les éléments inutiles",
|
|
||||||
"Default": "Par défaut",
|
|
||||||
"Dock": "Ancrer",
|
|
||||||
"Hide": "Masquer",
|
|
||||||
"Enable Output Gallery Sidebar": "Activer la barre latérale de la galerie de sorties"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "סף עמודות תור",
|
|
||||||
"Width Threshold for Queue Columns (px)": "רוחב בפיקסלים לחלוקת התור לעמודות מרובות",
|
|
||||||
"Show Pending Count Only": "הצג רק ספירת ממתינים",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "אם מושבת, לכל משימה בתור יהיה כרטיס נפרד",
|
|
||||||
"Show Working Node Name": "הצג שם צומת פעיל",
|
|
||||||
"Shows the name of the node which is currently in the process": "מציג את שם הצומת המעובד כעת",
|
|
||||||
"Auto Clear Interrupted": "ניקוי אוטומטי של משימות שהופסקו",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "נקה אוטומטית משימות שבוטלו או נכשלו בעת יצירה חדשה",
|
|
||||||
"Permanent Delete on Disk Deletion": "מחיקה לצמיתות מהדיסק",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "מחק קבצים לצמיתות בעת מחיקה (עוקף את סל המיחזור)",
|
|
||||||
"Hide Tab: {tab}": "הסתר לשונית: {tab}",
|
|
||||||
"Assets": "נכסים",
|
|
||||||
"Nodes": "צמתים",
|
|
||||||
"Models": "מודלים",
|
|
||||||
"Workflows": "תהליכי עבודה",
|
|
||||||
"Apps": "יישומים",
|
|
||||||
"Templates": "תבניות",
|
|
||||||
"Override Stock History": "החלף היסטוריית ברירת מחדל",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "מחליף את סרגל הצד של היסטוריית העבודות הרגילה ב-Comfy Queue",
|
|
||||||
"Hide Graph Button": "הסתר כפתור 'גרף'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "מסתיר את הכפתור הצף 'גרף' (מפת הצמתים)",
|
|
||||||
"Hide Avatar": "הסתר אווטאר",
|
|
||||||
"Floating Canvas Controls": "פקדי לוח צפים",
|
|
||||||
"Classic Layout": "פריסה קלאסית",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "ממקם את הפקדים ואת לשוניות תהליך העבודה הפתוחות בסרגל עליון מאוחד",
|
|
||||||
"Comfy Layout": "פריסה קומפקטית",
|
|
||||||
"Hide Junk": "הסתר פריטים מיותרים",
|
|
||||||
"Default": "ברירת מחדל",
|
|
||||||
"Dock": "הצמד",
|
|
||||||
"Hide": "הסתר",
|
|
||||||
"Enable Output Gallery Sidebar": "הפעל סרגל צד של גלריית תוצרים"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "Soglia colonne coda",
|
|
||||||
"Width Threshold for Queue Columns (px)": "Larghezza in pixel per dividere la coda in più colonne",
|
|
||||||
"Show Pending Count Only": "Mostra solo conteggio in attesa",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "Se disabilitato, ogni task in coda avrà una scheda separata",
|
|
||||||
"Show Working Node Name": "Mostra nome nodo in elaborazione",
|
|
||||||
"Shows the name of the node which is currently in the process": "Mostra il nome del nodo attualmente in elaborazione",
|
|
||||||
"Auto Clear Interrupted": "Pulizia automatica interrotti",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "Cancella automaticamente i task annullati e falliti a ogni nuova generazione",
|
|
||||||
"Permanent Delete on Disk Deletion": "Eliminazione definitiva dal disco",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Elimina definitivamente i file al momento della cancellazione (senza passare dal cestino)",
|
|
||||||
"Hide Tab: {tab}": "Nascondi scheda: {tab}",
|
|
||||||
"Assets": "Risorse",
|
|
||||||
"Nodes": "Nodi",
|
|
||||||
"Models": "Modelli",
|
|
||||||
"Workflows": "Flussi di lavoro",
|
|
||||||
"Apps": "Applicazioni",
|
|
||||||
"Templates": "Modelli predefiniti",
|
|
||||||
"Override Stock History": "Sostituisci cronologia standard",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "Sostituisce la barra laterale della cronologia standard con Comfy Queue",
|
|
||||||
"Hide Graph Button": "Nascondi pulsante 'Grafo'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Nasconde il pulsante mobile 'Grafo' (mappa dei nodi)",
|
|
||||||
"Hide Avatar": "Nascondi avatar",
|
|
||||||
"Floating Canvas Controls": "Controlli mobili dell'area di lavoro",
|
|
||||||
"Classic Layout": "Layout classico",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "Posiziona i controlli e le schede aperte su una barra superiore unificata",
|
|
||||||
"Comfy Layout": "Layout compatto",
|
|
||||||
"Hide Junk": "Nascondi elementi inutili",
|
|
||||||
"Default": "Predefinito",
|
|
||||||
"Dock": "Aggancia",
|
|
||||||
"Hide": "Nascondi",
|
|
||||||
"Enable Output Gallery Sidebar": "Abilita barra laterale galleria di output"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "列分割の幅しきい値",
|
|
||||||
"Width Threshold for Queue Columns (px)": "キューを複数列に分割する幅のしきい値 (px)",
|
|
||||||
"Show Pending Count Only": "待機数のみ表示",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "無効にすると、キュー内の各タスクが個別のカードとして表示されます",
|
|
||||||
"Show Working Node Name": "処理中のノード名を表示",
|
|
||||||
"Shows the name of the node which is currently in the process": "現在処理されているノードの名前を表示します",
|
|
||||||
"Auto Clear Interrupted": "中断タスクの自動クリア",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "新規生成時にキャンセルおよび失敗したタスクを自動的に消去します",
|
|
||||||
"Permanent Delete on Disk Deletion": "ディスクから完全消去",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "ディスクからの削除時にゴミ箱を経由せず完全に消去します",
|
|
||||||
"Hide Tab: {tab}": "タブを非表示: {tab}",
|
|
||||||
"Assets": "アセット",
|
|
||||||
"Nodes": "ノード",
|
|
||||||
"Models": "モデル",
|
|
||||||
"Workflows": "ワークフロー",
|
|
||||||
"Apps": "アプリ",
|
|
||||||
"Templates": "テンプレート",
|
|
||||||
"Override Stock History": "標準の履歴を置き換える",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "標準の履歴サイドバーを Comfy Queue に置き換えます",
|
|
||||||
"Hide Graph Button": "「グラフ」ボタンを非表示",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "フローティングの「グラフ」(ノードマップ) ボタンを非表示にします",
|
|
||||||
"Hide Avatar": "アバターを非表示",
|
|
||||||
"Floating Canvas Controls": "フローティングキャンバス操作",
|
|
||||||
"Classic Layout": "クラシックレイアウト",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "操作ボタンと開いているワークフロータブを統合された単一の上部バーに配置します",
|
|
||||||
"Comfy Layout": "コンパクトレイアウト",
|
|
||||||
"Hide Junk": "不要な項目を非表示",
|
|
||||||
"Default": "デフォルト",
|
|
||||||
"Dock": "ドック",
|
|
||||||
"Hide": "非表示",
|
|
||||||
"Enable Output Gallery Sidebar": "出力ギャラリーのサイドバーを有効化"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "열 분할 너비 기준값",
|
|
||||||
"Width Threshold for Queue Columns (px)": "대기열을 여러 열로 나눌 너비 기준값 (px)",
|
|
||||||
"Show Pending Count Only": "대기 수량만 표시",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "비활성화 시 대기열의 각 작업이 개별 카드로 표시됩니다",
|
|
||||||
"Show Working Node Name": "진행 중인 노드 이름 표시",
|
|
||||||
"Shows the name of the node which is currently in the process": "현재 처리 중인 노드의 이름을 표시합니다",
|
|
||||||
"Auto Clear Interrupted": "중단된 작업 자동 삭제",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "새 생성 시 취소되거나 실패한 작업을 자동으로 삭제합니다",
|
|
||||||
"Permanent Delete on Disk Deletion": "디스크에서 완전 삭제",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "디스크에서 삭제 시 휴지통을 거치지 않고 영구 삭제합니다",
|
|
||||||
"Hide Tab: {tab}": "탭 숨기기: {tab}",
|
|
||||||
"Assets": "에셋",
|
|
||||||
"Nodes": "노드",
|
|
||||||
"Models": "모델",
|
|
||||||
"Workflows": "워크플로",
|
|
||||||
"Apps": "앱",
|
|
||||||
"Templates": "템플릿",
|
|
||||||
"Override Stock History": "기본 작업 기록 대체",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "기본 작업 기록 사이드바를 Comfy Queue로 대체합니다",
|
|
||||||
"Hide Graph Button": "'그래프' 버튼 숨기기",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "플로팅 '그래프'(워크플로 맵) 버튼을 숨깁니다",
|
|
||||||
"Hide Avatar": "아바타 숨기기",
|
|
||||||
"Floating Canvas Controls": "플로팅 캔버스 제어 도구",
|
|
||||||
"Classic Layout": "클래식 레이아웃",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "제어 도구와 열린 워크플로 탭을 하나의 상단 바로 통합합니다",
|
|
||||||
"Comfy Layout": "컴팩트 레이아웃",
|
|
||||||
"Hide Junk": "불필요한 항목 숨기기",
|
|
||||||
"Default": "기본값",
|
|
||||||
"Dock": "도킹",
|
|
||||||
"Hide": "숨기기",
|
|
||||||
"Enable Output Gallery Sidebar": "출력 갤러리 사이드바 활성화"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "Limite de colunas da fila",
|
|
||||||
"Width Threshold for Queue Columns (px)": "Largura em pixels para dividir a fila em várias colunas",
|
|
||||||
"Show Pending Count Only": "Mostrar apenas contagem pendente",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "Se desativado, cada tarefa na fila terá um cartão individual",
|
|
||||||
"Show Working Node Name": "Mostrar nome do nó em execução",
|
|
||||||
"Shows the name of the node which is currently in the process": "Mostra o nome do nó que está sendo processado no momento",
|
|
||||||
"Auto Clear Interrupted": "Limpeza automática de interrompidos",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "Limpar automaticamente tarefas canceladas e com falha ao iniciar nova geração",
|
|
||||||
"Permanent Delete on Disk Deletion": "Exclusão permanente do disco",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Excluir arquivos permanentemente (sem passar pela Lixeira)",
|
|
||||||
"Hide Tab: {tab}": "Ocultar aba: {tab}",
|
|
||||||
"Assets": "Recursos",
|
|
||||||
"Nodes": "Nós",
|
|
||||||
"Models": "Modelos",
|
|
||||||
"Workflows": "Fluxos de trabalho",
|
|
||||||
"Apps": "Aplicativos",
|
|
||||||
"Templates": "Modelos prévios",
|
|
||||||
"Override Stock History": "Substituir histórico padrão",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "Substitui a barra lateral de histórico padrão pelo Comfy Queue",
|
|
||||||
"Hide Graph Button": "Ocultar botão 'Grafo'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Oculta o botão flutuante 'Grafo' (mapa de nós)",
|
|
||||||
"Hide Avatar": "Ocultar avatar",
|
|
||||||
"Floating Canvas Controls": "Controles flutuantes da tela",
|
|
||||||
"Classic Layout": "Layout clássico",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "Posiciona os controles e as abas abertas em uma barra superior unificada",
|
|
||||||
"Comfy Layout": "Layout compacto",
|
|
||||||
"Hide Junk": "Ocultar itens desnecessários",
|
|
||||||
"Default": "Padrão",
|
|
||||||
"Dock": "Fixar",
|
|
||||||
"Hide": "Ocultar",
|
|
||||||
"Enable Output Gallery Sidebar": "Ativar barra lateral da galeria de saídas"
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "Порог колонок очереди",
|
|
||||||
"Width Threshold for Queue Columns (px)": "Ширина в пикселях для разделения очереди на несколько колонок",
|
|
||||||
"Show Pending Count Only": "Только количество в очереди",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "Если выключено, каждая задача в очереди будет отдельной карточкой",
|
|
||||||
"Show Working Node Name": "Имя выполняющейся ноды",
|
|
||||||
"Shows the name of the node which is currently in the process": "Показывать имя ноды, выполняющейся в данный момент",
|
|
||||||
"Auto Clear Interrupted": "Автоочистка отменённых",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "Автоматически очищать отменённые и неудачные задачи при новой генерации",
|
|
||||||
"Permanent Delete on Disk Deletion": "Безвозвратное удаление с диска",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Удалять файлы навсегда при удалении с диска (минуя корзину)",
|
|
||||||
"Hide Tab: {tab}": "Скрыть вкладку: {tab}",
|
|
||||||
"Assets": "Ресурсы",
|
|
||||||
"Nodes": "Ноды",
|
|
||||||
"Models": "Модели",
|
|
||||||
"Workflows": "Рабочие процессы",
|
|
||||||
"Apps": "Приложения",
|
|
||||||
"Templates": "Шаблоны",
|
|
||||||
"Override Stock History": "Замена стандартной истории",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "Заменить стандартную панель истории на Comfy Queue",
|
|
||||||
"Hide Graph Button": "Скрыть кнопку 'Граф'",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Скрыть плавающую кнопку 'Граф' (карта нод)",
|
|
||||||
"Hide Avatar": "Скрыть аватар",
|
|
||||||
"Floating Canvas Controls": "Плавающие элементы управления",
|
|
||||||
"Classic Layout": "Классический интерфейс",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "Разместить элементы управления и открытые вкладки на единой верхней панели",
|
|
||||||
"Comfy Layout": "Компактный интерфейс",
|
|
||||||
"Classic Layout": "Компактный интерфейс",
|
|
||||||
"Hide Junk": "Скрыть лишнее",
|
|
||||||
"Default": "По умолчанию",
|
|
||||||
"Dock": "Закрепить",
|
|
||||||
"Hide": "Скрыть",
|
|
||||||
"Enable Output Gallery Sidebar": "Включить боковую панель галереи результатов"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "Kuyruk sütun genişliği eşiği",
|
|
||||||
"Width Threshold for Queue Columns (px)": "Kuyruğu birden fazla sütuna bölmek için piksel genişliği",
|
|
||||||
"Show Pending Count Only": "Yalnızca bekleyen sayısını göster",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "Devre dışı bırakılırsa kuyruktaki her görev ayrı bir karta sahip olur",
|
|
||||||
"Show Working Node Name": "Çalışan düğüm adını göster",
|
|
||||||
"Shows the name of the node which is currently in the process": "Şu anda işlenmekte olan düğümün adını gösterir",
|
|
||||||
"Auto Clear Interrupted": "Kesilenleri otomatik temizle",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "Yeni üretimde iptal edilen ve başarısız olan görevleri otomatik olarak temizle",
|
|
||||||
"Permanent Delete on Disk Deletion": "Diskten kalıcı silme",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Diskten silerken dosyaları kalıcı olarak sil (Geri Dönüşüm Kutusu'nu atlar)",
|
|
||||||
"Hide Tab: {tab}": "Sekmeyi gizle: {tab}",
|
|
||||||
"Assets": "Varlıklar",
|
|
||||||
"Nodes": "Düğümler",
|
|
||||||
"Models": "Modeller",
|
|
||||||
"Workflows": "İş Akışları",
|
|
||||||
"Apps": "Uygulamalar",
|
|
||||||
"Templates": "Şablonlar",
|
|
||||||
"Override Stock History": "Varsayılan geçmişi değiştir",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "Varsayılan Görev Geçmişi kenar çubuğunu Comfy Queue ile değiştirir",
|
|
||||||
"Hide Graph Button": "'Grafik' düğmesini gizle",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Kayan 'Grafik' (İş akışı haritası) düğmesini gizler",
|
|
||||||
"Hide Avatar": "Avatarı gizle",
|
|
||||||
"Floating Canvas Controls": "Kayan tuval kontrolleri",
|
|
||||||
"Classic Layout": "Klasik düzen",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "Kontrolleri ve açık iş akışı sekmelerini tek bir birleşik üst çubukta toplar",
|
|
||||||
"Comfy Layout": "Kompakt Düzen",
|
|
||||||
"Hide Junk": "Gereksiz Öğeleri Gizle",
|
|
||||||
"Default": "Varsayılan",
|
|
||||||
"Dock": "Sabitle",
|
|
||||||
"Hide": "Gizle",
|
|
||||||
"Enable Output Gallery Sidebar": "Çıktı Galerisi Kenar Çubuğunu Etkinleştir"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "隊列欄寬閾值",
|
|
||||||
"Width Threshold for Queue Columns (px)": "隊列顯示多欄的寬度閾值 (像素)",
|
|
||||||
"Show Pending Count Only": "僅顯示等待數量",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "若停用,每個排隊任務將顯示為獨立卡片",
|
|
||||||
"Show Working Node Name": "顯示執行中節點名稱",
|
|
||||||
"Shows the name of the node which is currently in the process": "顯示目前正在處理的節點名稱",
|
|
||||||
"Auto Clear Interrupted": "自動清除中斷任務",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "新生成時自動清除已取消與失敗的任務",
|
|
||||||
"Permanent Delete on Disk Deletion": "從磁碟永久刪除",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "從磁碟刪除時永久刪除檔案 (略過資源回收筒)",
|
|
||||||
"Hide Tab: {tab}": "隱藏標籤頁: {tab}",
|
|
||||||
"Assets": "資產",
|
|
||||||
"Nodes": "節點",
|
|
||||||
"Models": "模型",
|
|
||||||
"Workflows": "工作流",
|
|
||||||
"Apps": "應用",
|
|
||||||
"Templates": "範本",
|
|
||||||
"Override Stock History": "取代原生任務歷史",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "使用 Comfy Queue 取代原生任務歷史側邊欄",
|
|
||||||
"Hide Graph Button": "隱藏「圖」按鈕",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "隱藏懸浮的「圖」(工作流/節點圖) 按鈕",
|
|
||||||
"Hide Avatar": "隱藏頭像",
|
|
||||||
"Floating Canvas Controls": "懸浮畫布控制項",
|
|
||||||
"Classic Layout": "經典版面配置",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "將控制項與已開啟的工作流標籤頁整合於統一的頂端列",
|
|
||||||
"Comfy Layout": "緊湊版面配置",
|
|
||||||
"Hide Junk": "隱藏雜項",
|
|
||||||
"Default": "預設",
|
|
||||||
"Dock": "停靠",
|
|
||||||
"Hide": "隱藏",
|
|
||||||
"Enable Output Gallery Sidebar": "啟用輸出圖庫側邊欄"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"Grid Columns Threshold": "队列列宽阈值",
|
|
||||||
"Width Threshold for Queue Columns (px)": "队列显示多列的宽度阈值 (像素)",
|
|
||||||
"Show Pending Count Only": "仅显示等待数量",
|
|
||||||
"If disabled, each queued job will have a separate individual card": "如果禁用,每个排队的任务将拥有单独的卡片",
|
|
||||||
"Show Working Node Name": "显示正在执行的节点名称",
|
|
||||||
"Shows the name of the node which is currently in the process": "显示当前正在处理的节点名称",
|
|
||||||
"Auto Clear Interrupted": "自动清除中断任务",
|
|
||||||
"Auto-clear cancelled & failed jobs on new generation": "新生成时自动清除已取消和失败的任务",
|
|
||||||
"Permanent Delete on Disk Deletion": "从磁盘永久删除",
|
|
||||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "从磁盘删除时永久删除文件 (绕过回收站)",
|
|
||||||
"Hide Tab: {tab}": "隐藏标签页: {tab}",
|
|
||||||
"Assets": "资产",
|
|
||||||
"Nodes": "节点",
|
|
||||||
"Models": "模型",
|
|
||||||
"Workflows": "工作流",
|
|
||||||
"Apps": "应用",
|
|
||||||
"Templates": "模板",
|
|
||||||
"Override Stock History": "替换原生任务历史",
|
|
||||||
"Replace the stock Job History sidebar with Comfy Queue": "使用 Comfy Queue 替换原生任务历史侧边栏",
|
|
||||||
"Hide Graph Button": "隐藏'图'按钮",
|
|
||||||
"Hide floating 'Graph' (Workflow/Node Map) button": "隐藏悬浮的'图' (工作流/节点图) 按钮",
|
|
||||||
"Hide Avatar": "隐藏头像",
|
|
||||||
"Floating Canvas Controls": "悬浮画布控件",
|
|
||||||
"Classic Layout": "经典布局",
|
|
||||||
"Places the controls and the open workflow tabs on a single unified top bar": "将控件与已打开的工作流标签页整合在统一的顶部栏",
|
|
||||||
"Comfy Layout": "紧凑布局",
|
|
||||||
"Hide Junk": "隐藏杂项",
|
|
||||||
"Default": "默认",
|
|
||||||
"Dock": "停靠",
|
|
||||||
"Hide": "隐藏",
|
|
||||||
"Enable Output Gallery Sidebar": "启用输出图库侧边栏"
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,8 @@ export function normalizeQueueItem(p, defaultIndex = 0) {
|
|||||||
pid,
|
pid,
|
||||||
seq,
|
seq,
|
||||||
workflow: parseWorkflow(rawWorkflow),
|
workflow: parseWorkflow(rawWorkflow),
|
||||||
outputs
|
outputs,
|
||||||
|
promptGraph: Array.isArray(p) ? (p[2] || null) : (p.prompt || null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,46 @@ export function extractPromptOutputs(outputs, workflow, promptGraph = null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updatePromptSearchTexts(entries) {
|
||||||
|
const pending = entries.filter(({ pid, promptGraph }) => {
|
||||||
|
const state = store.getPrompt(pid);
|
||||||
|
return promptGraph && typeof promptGraph === "object" && state && state.promptSearchText === null;
|
||||||
|
});
|
||||||
|
if (pending.length === 0) return;
|
||||||
|
|
||||||
|
// Keep each request comfortably below aiohttp's default 1 MB body limit.
|
||||||
|
const batches = [];
|
||||||
|
let batch = [];
|
||||||
|
let batchSize = 0;
|
||||||
|
for (const entry of pending) {
|
||||||
|
const itemSize = JSON.stringify(entry).length * 3; // Conservative UTF-8 byte estimate
|
||||||
|
if (batch.length > 0 && (batch.length >= 32 || batchSize + itemSize > 512 * 1024)) {
|
||||||
|
batches.push(batch);
|
||||||
|
batch = [];
|
||||||
|
batchSize = 0;
|
||||||
|
}
|
||||||
|
batch.push(entry);
|
||||||
|
batchSize += itemSize;
|
||||||
|
}
|
||||||
|
if (batch.length > 0) batches.push(batch);
|
||||||
|
|
||||||
|
for (const prompts of batches) {
|
||||||
|
try {
|
||||||
|
const response = await api.fetchApi("/comfy-sidebar/queue/prompt-search", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ prompts: prompts.map(({ pid, promptGraph }) => ({ pid, prompt: promptGraph })) })
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
for (const [pid, text] of Object.entries(data.texts || {})) {
|
||||||
|
if (store.hasPrompt(pid)) store.updatePrompt(pid, { promptSearchText: String(text || "") });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Comfy Sidebar: Failed to extract Queue prompt search text", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function syncQueue() {
|
export async function syncQueue() {
|
||||||
try {
|
try {
|
||||||
const q = await api.getQueue();
|
const q = await api.getQueue();
|
||||||
@@ -35,12 +75,14 @@ export async function syncQueue() {
|
|||||||
|
|
||||||
const runningIds = new Set();
|
const runningIds = new Set();
|
||||||
const pendingIds = new Set();
|
const pendingIds = new Set();
|
||||||
|
const promptGraphs = [];
|
||||||
|
|
||||||
runningList.forEach((raw, idx) => {
|
runningList.forEach((raw, idx) => {
|
||||||
const item = normalizeQueueItem(raw, idx);
|
const item = normalizeQueueItem(raw, idx);
|
||||||
if (!item.pid) return;
|
if (!item.pid) return;
|
||||||
|
|
||||||
runningIds.add(item.pid);
|
runningIds.add(item.pid);
|
||||||
|
if (item.promptGraph) promptGraphs.push({ pid: item.pid, promptGraph: item.promptGraph });
|
||||||
store.setActivePromptId(item.pid);
|
store.setActivePromptId(item.pid);
|
||||||
|
|
||||||
const existing = store.getPrompt(item.pid);
|
const existing = store.getPrompt(item.pid);
|
||||||
@@ -84,6 +126,7 @@ export async function syncQueue() {
|
|||||||
normalizedPending.forEach((item, index) => {
|
normalizedPending.forEach((item, index) => {
|
||||||
const number = index + 1;
|
const number = index + 1;
|
||||||
pendingIds.add(item.pid);
|
pendingIds.add(item.pid);
|
||||||
|
if (item.promptGraph) promptGraphs.push({ pid: item.pid, promptGraph: item.promptGraph });
|
||||||
|
|
||||||
const existing = store.getPrompt(item.pid);
|
const existing = store.getPrompt(item.pid);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -108,6 +151,8 @@ export async function syncQueue() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await updatePromptSearchTexts(promptGraphs);
|
||||||
|
|
||||||
for (const prompt of store.getAllPrompts()) {
|
for (const prompt of store.getAllPrompts()) {
|
||||||
if (prompt.status === PromptStatus.PENDING && !pendingIds.has(prompt.pid)) {
|
if (prompt.status === PromptStatus.PENDING && !pendingIds.has(prompt.pid)) {
|
||||||
store.deletePrompt(prompt.pid);
|
store.deletePrompt(prompt.pid);
|
||||||
@@ -153,6 +198,7 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
|
|||||||
state.status = norm.status;
|
state.status = norm.status;
|
||||||
|
|
||||||
const promptGraph = hItem[key].prompt?.[2] || null;
|
const promptGraph = hItem[key].prompt?.[2] || null;
|
||||||
|
await updatePromptSearchTexts([{ pid: key, promptGraph }]);
|
||||||
const { images: primaryImgs, texts: fetchedTexts, nodeTitles: titles } = extractPromptOutputs(norm.outputs, state.workflow, promptGraph);
|
const { images: primaryImgs, texts: fetchedTexts, nodeTitles: titles } = extractPromptOutputs(norm.outputs, state.workflow, promptGraph);
|
||||||
Object.assign(state.nodeTitles, titles);
|
Object.assign(state.nodeTitles, titles);
|
||||||
|
|
||||||
@@ -257,6 +303,8 @@ export async function initSessionAndHistory() {
|
|||||||
store.setPrompt(item.pid, newState);
|
store.setPrompt(item.pid, newState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await updatePromptSearchTexts(normalized.map(({ norm, promptGraph }) => ({ pid: norm.pid, promptGraph })));
|
||||||
|
|
||||||
const maxItems = getSetting(SettingIds.MAX_HISTORY_ITEMS, 64);
|
const maxItems = getSetting(SettingIds.MAX_HISTORY_ITEMS, 64);
|
||||||
store.pruneHistory(maxItems);
|
store.pruneHistory(maxItems);
|
||||||
await syncQueue();
|
await syncQueue();
|
||||||
|
|||||||
+34
-14
@@ -2,15 +2,17 @@ import { app } from "/scripts/app.js";
|
|||||||
import { SettingIds } from "./core/constants.js";
|
import { SettingIds } from "./core/constants.js";
|
||||||
import { injectStyles } from "./utils/styles.js";
|
import { injectStyles } from "./utils/styles.js";
|
||||||
import { setupDragAndDrop } from "./utils/dragdrop.js";
|
import { setupDragAndDrop } from "./utils/dragdrop.js";
|
||||||
|
import { setupLatinHotkeys } from "./utils/latinHotkeys.js";
|
||||||
|
import { setupNewWorkflowQuickPicker } from "./utils/newWorkflowPicker.js";
|
||||||
import { renumberNodesTopologically, setManualNodeId } from "./utils/nodeRenumber.js";
|
import { renumberNodesTopologically, setManualNodeId } from "./utils/nodeRenumber.js";
|
||||||
import { findOurSidebarButton, applySidebarOverride } from "./comfy/adapter.js";
|
import { applySidebarOverride } from "./comfy/adapter.js";
|
||||||
import { syncAllNodeBadges, setupVueNodeObserver, toggleIgnoreActiveNode } from "./comfy/nodes.js";
|
import { syncAllNodeBadges, setupVueNodeObserver, toggleIgnoreActiveNode } from "./comfy/nodes.js";
|
||||||
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings, destroyLayoutFix } from "./comfy/layout.js";
|
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings, destroyLayoutFix } from "./comfy/layout.js";
|
||||||
import { initSessionAndHistory, syncQueue } from "./queue/queueService.js";
|
import { initSessionAndHistory, syncQueue } from "./queue/queueService.js";
|
||||||
import { setupExecutionTracker } from "./queue/executionTracker.js";
|
import { setupExecutionTracker } from "./queue/executionTracker.js";
|
||||||
import { setupSidebarView, renderSidebar, teardownSidebarView } from "./ui/sidebarView.js";
|
import { setupSidebarView, renderSidebar, teardownSidebarView } from "./ui/sidebarView.js";
|
||||||
import { setupGalleryView, teardownGalleryView } from "./ui/galleryView.js";
|
import { setupGalleryView, teardownGalleryView } from "./ui/galleryView.js";
|
||||||
import { initI18n, t } from "./core/i18n.js";
|
import { initI18n, t, setupLocaleWatcher, onLocaleChange } from "./core/i18n.js";
|
||||||
import { updateCardProgressTargeted, cardPool } from "./ui/cardRenderer.js";
|
import { updateCardProgressTargeted, cardPool } from "./ui/cardRenderer.js";
|
||||||
import { getScrollContainer } from "./utils/utils.js";
|
import { getScrollContainer } from "./utils/utils.js";
|
||||||
import { store } from "./core/store.js";
|
import { store } from "./core/store.js";
|
||||||
@@ -30,18 +32,6 @@ function setupKeydownShortcuts() {
|
|||||||
activeEl.tagName === "SELECT"
|
activeEl.tagName === "SELECT"
|
||||||
)) return;
|
)) return;
|
||||||
|
|
||||||
// Q: Toggle Queue Sidebar
|
|
||||||
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (app.extensionManager?.command?.hasCommand?.("Workspace.ToggleSidebarTab.classic-comfy-sidebar")) {
|
|
||||||
app.extensionManager.command.execute("Workspace.ToggleSidebarTab.classic-comfy-sidebar");
|
|
||||||
} else {
|
|
||||||
const ourBtn = findOurSidebarButton();
|
|
||||||
if (ourBtn) ourBtn.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+Q / Cmd+Q: Toggle Ignore Selected Node
|
// Ctrl+Q / Cmd+Q: Toggle Ignore Selected Node
|
||||||
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -183,6 +173,14 @@ function registerSettings() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.ui.settings.addSetting({
|
||||||
|
id: SettingIds.LATIN_HOTKEYS,
|
||||||
|
name: t("Fix non-ASCII keybinds (hotkeys on any keyboard layout)"),
|
||||||
|
tooltip: t("Treats non-Latin letters (e.g. Cyrillic) as the Latin key in the same physical position, so hotkeys like Q or Ctrl+Z work while another layout is active. Typing in text fields is not affected."),
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true
|
||||||
|
});
|
||||||
|
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: SettingIds.CLASSIC_LAYOUT,
|
id: SettingIds.CLASSIC_LAYOUT,
|
||||||
name: t("Places the controls and the open workflow tabs on a single unified top bar"),
|
name: t("Places the controls and the open workflow tabs on a single unified top bar"),
|
||||||
@@ -194,6 +192,14 @@ function registerSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.ui.settings.addSetting({
|
||||||
|
id: SettingIds.QUICK_WORKFLOW_PICKER,
|
||||||
|
name: t("Quick Workflow Picker"),
|
||||||
|
tooltip: t("Show quick workflow picker when creating a new blank workflow"),
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupWorkflowTabsPositionWatcher() {
|
function setupWorkflowTabsPositionWatcher() {
|
||||||
@@ -293,6 +299,14 @@ app.registerExtension({
|
|||||||
registerSettings();
|
registerSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Plain Q toggles the Queue tab through the frontend keybinding system (rebindable in Settings > Keybinding).
|
||||||
|
keybindings: [
|
||||||
|
{
|
||||||
|
combo: { key: "q" },
|
||||||
|
commandId: "Workspace.ToggleSidebarTab.classic-comfy-sidebar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
id: "ComfySidebar.ToggleIgnoreNode",
|
id: "ComfySidebar.ToggleIgnoreNode",
|
||||||
@@ -371,6 +385,12 @@ app.registerExtension({
|
|||||||
|
|
||||||
cleanupFns.push(setupWorkflowTabsPositionWatcher());
|
cleanupFns.push(setupWorkflowTabsPositionWatcher());
|
||||||
cleanupFns.push(setupDragAndDrop());
|
cleanupFns.push(setupDragAndDrop());
|
||||||
|
cleanupFns.push(setupLatinHotkeys());
|
||||||
|
cleanupFns.push(setupNewWorkflowQuickPicker());
|
||||||
|
cleanupFns.push(setupLocaleWatcher());
|
||||||
|
cleanupFns.push(onLocaleChange(() => {
|
||||||
|
renderSidebar();
|
||||||
|
}));
|
||||||
cleanupFns.push(setupVueNodeObserver());
|
cleanupFns.push(setupVueNodeObserver());
|
||||||
|
|
||||||
cleanupFns.push(setupExecutionTracker((pid, progress, nodeName) => {
|
cleanupFns.push(setupExecutionTracker((pid, progress, nodeName) => {
|
||||||
|
|||||||
+70
-23
@@ -17,6 +17,16 @@ let stickyHeaderGroupEl = null;
|
|||||||
let lastScrollTop = 0;
|
let lastScrollTop = 0;
|
||||||
let scrollDeltaAccumulator = 0;
|
let scrollDeltaAccumulator = 0;
|
||||||
let isHeaderHidden = false;
|
let isHeaderHidden = false;
|
||||||
|
let galleryResizeObserver = null;
|
||||||
|
|
||||||
|
// Module-level so teardownGalleryView() can remove the exact same references
|
||||||
|
const handleCtrlDown = (e) => {
|
||||||
|
if (e.key === "Control" || e.key === "Meta") document.body.classList.add("comfy-sidebar-ctrl-active");
|
||||||
|
};
|
||||||
|
const handleCtrlUp = (e) => {
|
||||||
|
if (e.key === "Control" || e.key === "Meta") document.body.classList.remove("comfy-sidebar-ctrl-active");
|
||||||
|
};
|
||||||
|
const handleCtrlBlur = () => document.body.classList.remove("comfy-sidebar-ctrl-active");
|
||||||
|
|
||||||
// Persistent sort order
|
// Persistent sort order
|
||||||
let sortOrder = "newest";
|
let sortOrder = "newest";
|
||||||
@@ -45,6 +55,14 @@ function updateScrollTopVisibility() {
|
|||||||
}
|
}
|
||||||
const scrollEl = getScrollContainer(cardStackEl);
|
const scrollEl = getScrollContainer(cardStackEl);
|
||||||
if (scrollEl) {
|
if (scrollEl) {
|
||||||
|
const viewportEl = scrollEl.parentElement || galleryContainerEl;
|
||||||
|
if (viewportEl && scrollToTopBtnEl && scrollToTopBtnEl.parentNode !== viewportEl) {
|
||||||
|
if (window.getComputedStyle(viewportEl).position === "static") {
|
||||||
|
viewportEl.style.position = "relative";
|
||||||
|
}
|
||||||
|
viewportEl.appendChild(scrollToTopBtnEl);
|
||||||
|
}
|
||||||
|
|
||||||
if (scrollToTopBtnEl) {
|
if (scrollToTopBtnEl) {
|
||||||
scrollToTopBtnEl.style.display = scrollEl.scrollTop > 200 ? "flex" : "none";
|
scrollToTopBtnEl.style.display = scrollEl.scrollTop > 200 ? "flex" : "none";
|
||||||
}
|
}
|
||||||
@@ -61,15 +79,21 @@ function updateScrollTopVisibility() {
|
|||||||
function handleHeaderScroll(scrollEl) {
|
function handleHeaderScroll(scrollEl) {
|
||||||
if (!stickyHeaderGroupEl) return;
|
if (!stickyHeaderGroupEl) return;
|
||||||
|
|
||||||
// Never hide header if search bar is currently active
|
const currentScroll = scrollEl.scrollTop;
|
||||||
const searchContainer = stickyHeaderGroupEl.querySelector(".comfy-sidebar-search-container");
|
const searchContainer = stickyHeaderGroupEl.querySelector(".comfy-sidebar-search-container");
|
||||||
if (currentSearchQuery || (searchContainer && searchContainer.style.display !== "none")) {
|
const searchIsOpen = currentSearchQuery || (searchContainer && searchContainer.style.display !== "none");
|
||||||
|
|
||||||
|
// Keep the frosted surface in sync even while search forces the header to stay visible.
|
||||||
|
stickyHeaderGroupEl.classList.toggle("is-scrolled", currentScroll > 35);
|
||||||
|
|
||||||
|
if (searchIsOpen) {
|
||||||
|
lastScrollTop = currentScroll;
|
||||||
|
scrollDeltaAccumulator = 0;
|
||||||
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
||||||
isHeaderHidden = false;
|
isHeaderHidden = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentScroll = scrollEl.scrollTop;
|
|
||||||
const delta = currentScroll - lastScrollTop;
|
const delta = currentScroll - lastScrollTop;
|
||||||
lastScrollTop = currentScroll;
|
lastScrollTop = currentScroll;
|
||||||
|
|
||||||
@@ -107,14 +131,23 @@ function handleHeaderScroll(scrollEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let galleryFetchController = null;
|
||||||
|
|
||||||
|
// Returns false only when the request was superseded by a newer one (aborted),
|
||||||
|
// so callers can skip rendering stale results.
|
||||||
async function fetchGalleryData(subfolder = "", searchQuery = "") {
|
async function fetchGalleryData(subfolder = "", searchQuery = "") {
|
||||||
|
// A newer request (e.g. the next keystroke in the search box) cancels the in-flight one;
|
||||||
|
// the server stops scanning as soon as the client disconnects.
|
||||||
|
if (galleryFetchController) galleryFetchController.abort();
|
||||||
|
const controller = new AbortController();
|
||||||
|
galleryFetchController = controller;
|
||||||
try {
|
try {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
const normalized = subfolder.replace(/\\/g, "/");
|
const normalized = subfolder.replace(/\\/g, "/");
|
||||||
const queryParams = new URLSearchParams({ subfolder: normalized });
|
const queryParams = new URLSearchParams({ subfolder: normalized });
|
||||||
if (searchQuery) queryParams.set("search", searchQuery);
|
if (searchQuery) queryParams.set("search", searchQuery);
|
||||||
|
|
||||||
const res = await fetch(`/comfy-sidebar/gallery/list?${queryParams.toString()}`);
|
const res = await fetch(`/comfy-sidebar/gallery/list?${queryParams.toString()}`, { signal: controller.signal });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
cachedFolders = data.folders || [];
|
cachedFolders = data.folders || [];
|
||||||
@@ -123,11 +156,16 @@ async function fetchGalleryData(subfolder = "", searchQuery = "") {
|
|||||||
applySorting();
|
applySorting();
|
||||||
displayedFilesCount = 0;
|
displayedFilesCount = 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e?.name === "AbortError") return false;
|
||||||
console.error("[ComfySidebar] Failed to load gallery", e);
|
console.error("[ComfySidebar] Failed to load gallery", e);
|
||||||
showNotification(t("Failed to load gallery from disk."), "error");
|
showNotification(t("Failed to load gallery from disk."), "error");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
if (galleryFetchController === controller) {
|
||||||
|
galleryFetchController = null;
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySorting() {
|
function applySorting() {
|
||||||
@@ -588,6 +626,7 @@ export function setupGalleryView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let searchDebounce = null;
|
let searchDebounce = null;
|
||||||
|
let searchSeq = 0;
|
||||||
const closeSearch = () => {
|
const closeSearch = () => {
|
||||||
const hadFilter = Boolean(currentSearchQuery || sInput.value.trim());
|
const hadFilter = Boolean(currentSearchQuery || sInput.value.trim());
|
||||||
sInput.value = "";
|
sInput.value = "";
|
||||||
@@ -618,12 +657,15 @@ export function setupGalleryView() {
|
|||||||
|
|
||||||
sIcon.className = "pi pi-spin pi-spinner";
|
sIcon.className = "pi pi-spin pi-spinner";
|
||||||
|
|
||||||
|
const seq = ++searchSeq;
|
||||||
searchDebounce = setTimeout(async () => {
|
searchDebounce = setTimeout(async () => {
|
||||||
|
let applied = false;
|
||||||
try {
|
try {
|
||||||
await fetchGalleryData(currentSubfolder, currentSearchQuery);
|
applied = (await fetchGalleryData(currentSubfolder, currentSearchQuery)) !== false;
|
||||||
renderGallery();
|
if (applied) renderGallery();
|
||||||
} finally {
|
} finally {
|
||||||
sIcon.className = "pi pi-search";
|
// Only the latest search may stop the spinner
|
||||||
|
if (seq === searchSeq) sIcon.className = "pi pi-search";
|
||||||
}
|
}
|
||||||
}, 350);
|
}, 350);
|
||||||
};
|
};
|
||||||
@@ -636,19 +678,13 @@ export function setupGalleryView() {
|
|||||||
const cardStack = document.createElement("div");
|
const cardStack = document.createElement("div");
|
||||||
cardStackEl = cardStack;
|
cardStackEl = cardStack;
|
||||||
Object.assign(cardStack.style, {
|
Object.assign(cardStack.style, {
|
||||||
flex: "1", overflowY: "visible", display: "flex", flexDirection: "column", paddingBottom: "28px"
|
flex: "1", overflowY: "visible", display: "flex", flexDirection: "column", paddingBottom: "46px"
|
||||||
});
|
});
|
||||||
container.appendChild(cardStack);
|
container.appendChild(cardStack);
|
||||||
|
|
||||||
scrollToTopBtnEl = document.createElement("button");
|
scrollToTopBtnEl = document.createElement("button");
|
||||||
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
|
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
|
||||||
scrollToTopBtnEl.title = t("Scroll to Top");
|
scrollToTopBtnEl.title = t("Scroll to Top");
|
||||||
Object.assign(scrollToTopBtnEl.style, {
|
|
||||||
position: "sticky",
|
|
||||||
bottom: "14px",
|
|
||||||
marginLeft: "auto",
|
|
||||||
zIndex: "30"
|
|
||||||
});
|
|
||||||
scrollToTopBtnEl.onclick = (e) => {
|
scrollToTopBtnEl.onclick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const scrollEl = getScrollContainer(cardStackEl);
|
const scrollEl = getScrollContainer(cardStackEl);
|
||||||
@@ -656,28 +692,24 @@ export function setupGalleryView() {
|
|||||||
};
|
};
|
||||||
container.appendChild(scrollToTopBtnEl);
|
container.appendChild(scrollToTopBtnEl);
|
||||||
|
|
||||||
const handleCtrlDown = (e) => {
|
|
||||||
if (e.key === "Control" || e.key === "Meta") document.body.classList.add("comfy-sidebar-ctrl-active");
|
|
||||||
};
|
|
||||||
const handleCtrlUp = (e) => {
|
|
||||||
if (e.key === "Control" || e.key === "Meta") document.body.classList.remove("comfy-sidebar-ctrl-active");
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", handleCtrlDown);
|
window.addEventListener("keydown", handleCtrlDown);
|
||||||
window.addEventListener("keyup", handleCtrlUp);
|
window.addEventListener("keyup", handleCtrlUp);
|
||||||
|
window.addEventListener("blur", handleCtrlBlur);
|
||||||
|
|
||||||
// Reveal Spotlight: track cursor position over Gallery card and folder borders
|
// Reveal Spotlight: track cursor position over Gallery card and folder borders
|
||||||
setupSpotlightTracker(container, ".comfy-sidebar-card, .comfy-sidebar-folder-card");
|
setupSpotlightTracker(container, ".comfy-sidebar-card, .comfy-sidebar-folder-card");
|
||||||
|
|
||||||
document.addEventListener("scroll", updateScrollTopVisibility, { capture: true, passive: true });
|
document.addEventListener("scroll", updateScrollTopVisibility, { capture: true, passive: true });
|
||||||
|
|
||||||
new ResizeObserver((entries) => {
|
galleryResizeObserver = new ResizeObserver((entries) => {
|
||||||
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
|
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
|
||||||
const cols = calculateGridColumns(entries[0].contentRect.width, threshold);
|
const cols = calculateGridColumns(entries[0].contentRect.width, threshold);
|
||||||
if (cols !== currentColumnCount) {
|
if (cols !== currentColumnCount) {
|
||||||
currentColumnCount = cols;
|
currentColumnCount = cols;
|
||||||
renderGallery();
|
renderGallery();
|
||||||
}
|
}
|
||||||
}).observe(container);
|
});
|
||||||
|
galleryResizeObserver.observe(container);
|
||||||
|
|
||||||
fetchGalleryData("").then(() => renderGallery());
|
fetchGalleryData("").then(() => renderGallery());
|
||||||
|
|
||||||
@@ -686,10 +718,25 @@ export function setupGalleryView() {
|
|||||||
|
|
||||||
export function teardownGalleryView() {
|
export function teardownGalleryView() {
|
||||||
document.removeEventListener("scroll", updateScrollTopVisibility, { capture: true, passive: true });
|
document.removeEventListener("scroll", updateScrollTopVisibility, { capture: true, passive: true });
|
||||||
|
window.removeEventListener("keydown", handleCtrlDown);
|
||||||
|
window.removeEventListener("keyup", handleCtrlUp);
|
||||||
|
window.removeEventListener("blur", handleCtrlBlur);
|
||||||
|
document.body.classList.remove("comfy-sidebar-ctrl-active");
|
||||||
|
if (galleryResizeObserver) {
|
||||||
|
galleryResizeObserver.disconnect();
|
||||||
|
galleryResizeObserver = null;
|
||||||
|
}
|
||||||
|
// Reset header-reveal state so a re-opened gallery starts with a visible header
|
||||||
|
lastScrollTop = 0;
|
||||||
|
scrollDeltaAccumulator = 0;
|
||||||
|
isHeaderHidden = false;
|
||||||
cachedFolders = [];
|
cachedFolders = [];
|
||||||
cachedFiles = [];
|
cachedFiles = [];
|
||||||
stickyHeaderGroupEl = null;
|
stickyHeaderGroupEl = null;
|
||||||
galleryContainerEl = null;
|
galleryContainerEl = null;
|
||||||
cardStackEl = null;
|
cardStackEl = null;
|
||||||
|
if (scrollToTopBtnEl && scrollToTopBtnEl.parentNode) {
|
||||||
|
scrollToTopBtnEl.parentNode.removeChild(scrollToTopBtnEl);
|
||||||
|
}
|
||||||
scrollToTopBtnEl = null;
|
scrollToTopBtnEl = null;
|
||||||
}
|
}
|
||||||
+69
-2
@@ -17,6 +17,10 @@ let globalClickRegistered = false;
|
|||||||
let renderAnimationFrameId = null;
|
let renderAnimationFrameId = null;
|
||||||
let activeTimerInterval = null;
|
let activeTimerInterval = null;
|
||||||
let currentColumnCount = 1;
|
let currentColumnCount = 1;
|
||||||
|
let stickyHeaderGroupEl = null;
|
||||||
|
let lastScrollTop = 0;
|
||||||
|
let scrollDeltaAccumulator = 0;
|
||||||
|
let isHeaderHidden = false;
|
||||||
|
|
||||||
function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
|
function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
|
||||||
if (!cardStack) return;
|
if (!cardStack) return;
|
||||||
@@ -91,6 +95,58 @@ function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleQueueHeaderScroll(scrollEl) {
|
||||||
|
if (!stickyHeaderGroupEl) return;
|
||||||
|
|
||||||
|
const currentScroll = scrollEl.scrollTop;
|
||||||
|
const searchContainer = stickyHeaderGroupEl.querySelector(".comfy-sidebar-search-container");
|
||||||
|
const searchIsOpen = searchContainer && searchContainer.style.display !== "none";
|
||||||
|
|
||||||
|
// Keep the frosted surface visible while search is open, matching Gallery.
|
||||||
|
stickyHeaderGroupEl.classList.toggle("is-scrolled", currentScroll > 35);
|
||||||
|
|
||||||
|
if (searchIsOpen) {
|
||||||
|
lastScrollTop = currentScroll;
|
||||||
|
scrollDeltaAccumulator = 0;
|
||||||
|
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
||||||
|
isHeaderHidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = currentScroll - lastScrollTop;
|
||||||
|
lastScrollTop = currentScroll;
|
||||||
|
|
||||||
|
if (currentScroll <= 35) {
|
||||||
|
stickyHeaderGroupEl.classList.remove("is-scrolled");
|
||||||
|
if (isHeaderHidden) {
|
||||||
|
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
||||||
|
isHeaderHidden = false;
|
||||||
|
}
|
||||||
|
scrollDeltaAccumulator = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stickyHeaderGroupEl.classList.add("is-scrolled");
|
||||||
|
|
||||||
|
if (delta > 0) {
|
||||||
|
if (scrollDeltaAccumulator < 0) scrollDeltaAccumulator = 0;
|
||||||
|
scrollDeltaAccumulator += delta;
|
||||||
|
|
||||||
|
if (scrollDeltaAccumulator > 25 && !isHeaderHidden) {
|
||||||
|
stickyHeaderGroupEl.style.transform = "translateY(-100%)";
|
||||||
|
isHeaderHidden = true;
|
||||||
|
}
|
||||||
|
} else if (delta < 0) {
|
||||||
|
if (scrollDeltaAccumulator > 0) scrollDeltaAccumulator = 0;
|
||||||
|
scrollDeltaAccumulator += delta;
|
||||||
|
|
||||||
|
if (scrollDeltaAccumulator < -15 && isHeaderHidden) {
|
||||||
|
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
||||||
|
isHeaderHidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateScrollTopBtnVisibility() {
|
function updateScrollTopBtnVisibility() {
|
||||||
if (!scrollToTopBtnEl) return;
|
if (!scrollToTopBtnEl) return;
|
||||||
if (!store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) {
|
if (!store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) {
|
||||||
@@ -112,6 +168,8 @@ function updateScrollTopBtnVisibility() {
|
|||||||
} else {
|
} else {
|
||||||
scrollToTopBtnEl.style.display = "none";
|
scrollToTopBtnEl.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleQueueHeaderScroll(scrollEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +363,12 @@ export function setupSidebarView() {
|
|||||||
|
|
||||||
header.appendChild(standardHeader);
|
header.appendChild(standardHeader);
|
||||||
header.appendChild(searchContainer);
|
header.appendChild(searchContainer);
|
||||||
container.appendChild(header);
|
|
||||||
|
const stickyHeaderGroup = document.createElement("div");
|
||||||
|
stickyHeaderGroup.className = "comfy-sidebar-sticky-header-group";
|
||||||
|
stickyHeaderGroupEl = stickyHeaderGroup;
|
||||||
|
stickyHeaderGroup.appendChild(header);
|
||||||
|
container.appendChild(stickyHeaderGroup);
|
||||||
|
|
||||||
searchIcon.addEventListener("click", (e) => {
|
searchIcon.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -336,7 +399,7 @@ export function setupSidebarView() {
|
|||||||
overflowY: "visible",
|
overflowY: "visible",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
paddingBottom: "28px"
|
paddingBottom: "46px"
|
||||||
});
|
});
|
||||||
container.appendChild(cardStack);
|
container.appendChild(cardStack);
|
||||||
|
|
||||||
@@ -997,4 +1060,8 @@ export function teardownSidebarView() {
|
|||||||
renderAnimationFrameId = null;
|
renderAnimationFrameId = null;
|
||||||
}
|
}
|
||||||
cardPool.clear();
|
cardPool.clear();
|
||||||
|
if (scrollToTopBtnEl && scrollToTopBtnEl.parentNode) {
|
||||||
|
scrollToTopBtnEl.parentNode.removeChild(scrollToTopBtnEl);
|
||||||
|
}
|
||||||
|
scrollToTopBtnEl = null;
|
||||||
}
|
}
|
||||||
+19
-6
@@ -328,6 +328,7 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
|
|||||||
const createMediaElement = (src, muted = false) => {
|
const createMediaElement = (src, muted = false) => {
|
||||||
const isVideo = isVideoFormat(src);
|
const isVideo = isVideoFormat(src);
|
||||||
const el = isVideo ? document.createElement("video") : document.createElement("img");
|
const el = isVideo ? document.createElement("video") : document.createElement("img");
|
||||||
|
el.className = "comfy-sidebar-comparison-media";
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
el.muted = muted;
|
el.muted = muted;
|
||||||
el.playsInline = true;
|
el.playsInline = true;
|
||||||
@@ -345,7 +346,10 @@ const createMediaElement = (src, muted = false) => {
|
|||||||
pointerEvents: isVideo ? "none" : "auto",
|
pointerEvents: isVideo ? "none" : "auto",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
webkitUserSelect: "none",
|
webkitUserSelect: "none",
|
||||||
display: "block"
|
display: "block",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
boxShadow: "none"
|
||||||
});
|
});
|
||||||
el.draggable = false;
|
el.draggable = false;
|
||||||
el.ondragstart = (e) => e.preventDefault();
|
el.ondragstart = (e) => e.preventDefault();
|
||||||
@@ -576,21 +580,30 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slider = document.createElement("div");
|
const slider = document.createElement("div");
|
||||||
|
slider.className = "comfy-sidebar-comparison-slider";
|
||||||
Object.assign(slider.style, {
|
Object.assign(slider.style, {
|
||||||
position: "absolute", top: "0", bottom: "0", left: "50%",
|
position: "absolute", top: "0", bottom: "0", left: "50%",
|
||||||
width: "2px", background: "#fff", cursor: "ew-resize",
|
width: "2px",
|
||||||
|
background: "var(--foreground, #ffffff)",
|
||||||
|
cursor: "ew-resize",
|
||||||
zIndex: "25", display: "none", pointerEvents: "auto",
|
zIndex: "25", display: "none", pointerEvents: "auto",
|
||||||
boxShadow: "0 0 8px rgba(0,0,0,0.5)"
|
boxShadow: "0 0 6px rgba(0,0,0,0.6)"
|
||||||
});
|
});
|
||||||
const sliderButton = document.createElement("div");
|
const sliderButton = document.createElement("div");
|
||||||
|
sliderButton.className = "comfy-sidebar-comparison-handle";
|
||||||
Object.assign(sliderButton.style, {
|
Object.assign(sliderButton.style, {
|
||||||
position: "absolute", top: "50%", left: "50%",
|
position: "absolute", top: "50%", left: "50%",
|
||||||
transform: "translate(-50%, -50%)", width: "32px", height: "32px",
|
transform: "translate(-50%, -50%)", width: "32px", height: "32px",
|
||||||
borderRadius: "50%", background: "#fff", color: "#333",
|
borderRadius: "50%",
|
||||||
|
background: "var(--card, var(--secondary, rgba(30, 30, 30, 0.9)))",
|
||||||
|
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
|
||||||
|
color: "var(--foreground, #eee)",
|
||||||
display: "flex", alignItems: "center", justifyContent: "center",
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
boxShadow: "0 2px 6px rgba(0,0,0,0.4)", pointerEvents: "auto"
|
boxShadow: "0 4px 12px rgba(0,0,0,0.5)", pointerEvents: "auto",
|
||||||
|
cursor: "ew-resize",
|
||||||
|
transition: "background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, color 0.15s ease-in-out"
|
||||||
});
|
});
|
||||||
sliderButton.innerHTML = `<span class="pi pi-arrows-h" style="font-size: 12px;"></span>`;
|
sliderButton.innerHTML = `<span class="pi pi-arrows-h" style="font-size: 13px; pointer-events: none;"></span>`;
|
||||||
slider.appendChild(sliderButton);
|
slider.appendChild(sliderButton);
|
||||||
wrapper.appendChild(slider);
|
wrapper.appendChild(slider);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { SettingIds } from "../core/constants.js";
|
||||||
|
import { getSetting } from "../core/settings.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes hotkeys work regardless of the active keyboard layout.
|
||||||
|
*
|
||||||
|
* ComfyUI's keybinding system matches on `KeyboardEvent.key`, which follows the active layout:
|
||||||
|
* with a Russian layout, physical Q produces "й", so a binding on "q" never fires.
|
||||||
|
* `KeyboardEvent.code` is the physical key ("KeyQ") and does not change with the layout.
|
||||||
|
*
|
||||||
|
* Strategy: in the capture phase on `window` (which runs before ComfyUI's own `keydown` listener),
|
||||||
|
* if `key` is a single non-ASCII character, swallow the event and re-dispatch an identical one
|
||||||
|
* whose `key` is the US-QWERTY character for that physical key. Everything downstream (keybindings,
|
||||||
|
* LiteGraph shortcuts, other extensions) then sees plain Latin keys.
|
||||||
|
*
|
||||||
|
* Deliberately NOT touched:
|
||||||
|
* - layouts that already report ASCII keys (QWERTY, AZERTY, Dvorak...), so their users keep
|
||||||
|
* their own mapping;
|
||||||
|
* - text fields (input / textarea / select / contenteditable / LiteGraph property values),
|
||||||
|
* so typing in any language keeps working;
|
||||||
|
* - IME composition;
|
||||||
|
* - named keys such as "Enter", "Escape", "Dead".
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Unshifted / shifted characters of the US-QWERTY layout for non-letter keys.
|
||||||
|
const SYMBOL_KEYS = {
|
||||||
|
Backquote: ["`", "~"],
|
||||||
|
Minus: ["-", "_"],
|
||||||
|
Equal: ["=", "+"],
|
||||||
|
BracketLeft: ["[", "{"],
|
||||||
|
BracketRight: ["]", "}"],
|
||||||
|
Backslash: ["\\", "|"],
|
||||||
|
Semicolon: [";", ":"],
|
||||||
|
Quote: ["'", "\""],
|
||||||
|
Comma: [",", "<"],
|
||||||
|
Period: [".", ">"],
|
||||||
|
Slash: ["/", "?"],
|
||||||
|
Digit1: ["1", "!"],
|
||||||
|
Digit2: ["2", "@"],
|
||||||
|
Digit3: ["3", "#"],
|
||||||
|
Digit4: ["4", "$"],
|
||||||
|
Digit5: ["5", "%"],
|
||||||
|
Digit6: ["6", "^"],
|
||||||
|
Digit7: ["7", "&"],
|
||||||
|
Digit8: ["8", "*"],
|
||||||
|
Digit9: ["9", "("],
|
||||||
|
Digit0: ["0", ")"]
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns the US-QWERTY `key` for the physical key of this event, or null if unknown. */
|
||||||
|
export function latinKeyFromEvent(e) {
|
||||||
|
const letter = /^Key([A-Z])$/.exec(e.code || "");
|
||||||
|
if (letter) return e.shiftKey ? letter[1] : letter[1].toLowerCase();
|
||||||
|
|
||||||
|
const pair = SYMBOL_KEYS[e.code];
|
||||||
|
if (pair) return e.shiftKey ? pair[1] : pair[0];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSingleNonAsciiChar(key) {
|
||||||
|
if (typeof key !== "string") return false;
|
||||||
|
const chars = Array.from(key);
|
||||||
|
return chars.length === 1 && chars[0].codePointAt(0) > 127;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTextEntryTarget(el) {
|
||||||
|
if (!el || el.nodeType !== 1) return false;
|
||||||
|
const tag = el.tagName;
|
||||||
|
return (
|
||||||
|
tag === "INPUT" ||
|
||||||
|
tag === "TEXTAREA" ||
|
||||||
|
tag === "SELECT" ||
|
||||||
|
el.isContentEditable ||
|
||||||
|
// LiteGraph's inline value editors are spans
|
||||||
|
(tag === "SPAN" && el.classList.contains("property_value"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupLatinHotkeys() {
|
||||||
|
// Events we dispatched ourselves; used to avoid handling them again.
|
||||||
|
const synthetic = new WeakSet();
|
||||||
|
|
||||||
|
const handler = (e) => {
|
||||||
|
if (synthetic.has(e)) return;
|
||||||
|
if (e.isComposing || e.keyCode === 229) return;
|
||||||
|
if (!isSingleNonAsciiChar(e.key)) return;
|
||||||
|
if (!getSetting(SettingIds.LATIN_HOTKEYS, true)) return;
|
||||||
|
|
||||||
|
const latinKey = latinKeyFromEvent(e);
|
||||||
|
if (!latinKey) return;
|
||||||
|
|
||||||
|
const target = e.composedPath?.()[0] ?? e.target;
|
||||||
|
if (isTextEntryTarget(target)) return;
|
||||||
|
|
||||||
|
const clone = new KeyboardEvent(e.type, {
|
||||||
|
key: latinKey,
|
||||||
|
code: e.code,
|
||||||
|
location: e.location,
|
||||||
|
repeat: e.repeat,
|
||||||
|
ctrlKey: e.ctrlKey,
|
||||||
|
shiftKey: e.shiftKey,
|
||||||
|
altKey: e.altKey,
|
||||||
|
metaKey: e.metaKey,
|
||||||
|
keyCode: e.keyCode,
|
||||||
|
which: e.which,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
view: window
|
||||||
|
});
|
||||||
|
synthetic.add(clone);
|
||||||
|
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
const notPrevented = (target || document).dispatchEvent(clone);
|
||||||
|
if (!notPrevented) e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handler, true);
|
||||||
|
window.addEventListener("keyup", handler, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handler, true);
|
||||||
|
window.removeEventListener("keyup", handler, true);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
import { app } from "/scripts/app.js";
|
||||||
|
import { api } from "/scripts/api.js";
|
||||||
|
import { SettingIds } from "../core/constants.js";
|
||||||
|
import { getSetting } from "../core/settings.js";
|
||||||
|
import { t } from "../core/i18n.js";
|
||||||
|
import { setupSpotlightTracker } from "./utils.js";
|
||||||
|
|
||||||
|
function isTemporaryWorkflow(wf) {
|
||||||
|
if (!wf) return false;
|
||||||
|
return wf.isTemporary === true || wf.isPersisted === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNewWorkflowButton(target) {
|
||||||
|
if (!target || typeof target.closest !== "function") return null;
|
||||||
|
|
||||||
|
const direct = target.closest(".new-blank-workflow-button");
|
||||||
|
if (direct) return direct;
|
||||||
|
|
||||||
|
const btn = target.closest('button, [role="button"]');
|
||||||
|
if (!btn) return null;
|
||||||
|
|
||||||
|
const tabsContainer = btn.closest(".workflow-tabs-container, .workflow-tabs");
|
||||||
|
if (!tabsContainer) return null;
|
||||||
|
if (btn.closest('.workflow-tab, [role="tab"], .p-togglebutton')) return null;
|
||||||
|
|
||||||
|
const label = (btn.getAttribute("aria-label") || btn.getAttribute("title") || "").toLowerCase();
|
||||||
|
if (label.includes("blank workflow") || label.includes("new workflow")) {
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btn.querySelector('.pi-plus, [class*="lucide--plus"], svg.lucide-plus')) {
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFallbackAnchorRect() {
|
||||||
|
const activeTab = document.querySelector('.workflow-tabs-container [role="tab"][data-state="active"], [role="tab"][data-state="active"]');
|
||||||
|
if (activeTab) {
|
||||||
|
const r = activeTab.getBoundingClientRect();
|
||||||
|
if (r.width > 0) return { left: r.left, bottom: r.bottom };
|
||||||
|
}
|
||||||
|
const plusBtn = document.querySelector(".new-blank-workflow-button");
|
||||||
|
if (plusBtn) {
|
||||||
|
const r = plusBtn.getBoundingClientRect();
|
||||||
|
if (r.width > 0) return { left: r.left, bottom: r.bottom };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSavedWorkflows() {
|
||||||
|
const seen = new Set();
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
const addEntry = (rawPath) => {
|
||||||
|
if (!rawPath || typeof rawPath !== "string") return;
|
||||||
|
let rel = rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
||||||
|
if (rel.startsWith("workflows/")) {
|
||||||
|
rel = rel.slice("workflows/".length);
|
||||||
|
}
|
||||||
|
if (!rel.toLowerCase().endsWith(".json")) return;
|
||||||
|
|
||||||
|
const parts = rel.split("/");
|
||||||
|
if (parts.some(p => !p || p.startsWith("."))) return;
|
||||||
|
|
||||||
|
if (seen.has(rel)) return;
|
||||||
|
seen.add(rel);
|
||||||
|
|
||||||
|
const fileNameWithExt = parts[parts.length - 1];
|
||||||
|
const displayName = fileNameWithExt.replace(/\.json$/i, "");
|
||||||
|
const folder = parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
relPath: rel,
|
||||||
|
fullPath: `workflows/${rel}`,
|
||||||
|
displayName,
|
||||||
|
folder,
|
||||||
|
searchStr: rel.replace(/\.json$/i, "").toLowerCase()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.fetchApi("/userdata?dir=workflows&recurse=true");
|
||||||
|
if (res.ok) {
|
||||||
|
const list = await res.json();
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
for (const entry of list) {
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
addEntry(entry);
|
||||||
|
} else if (entry && typeof entry.path === "string") {
|
||||||
|
addEntry(entry.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[ComfySidebar] Failed to list saved workflows via API:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wfMgr = app.extensionManager?.workflow;
|
||||||
|
const storeList = wfMgr?.persistedWorkflows || wfMgr?.workflows || [];
|
||||||
|
for (const wf of storeList) {
|
||||||
|
if (wf && wf.isPersisted !== false && !wf.isTemporary && wf.path) {
|
||||||
|
addEntry(wf.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.sort((a, b) => a.displayName.localeCompare(b.displayName, undefined, { sensitivity: "base" }));
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterWorkflows(allItems, rawQuery) {
|
||||||
|
const q = rawQuery.trim().toLowerCase();
|
||||||
|
if (!q) return [];
|
||||||
|
|
||||||
|
const tokens = q.split(/\s+/).filter(Boolean);
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
for (const item of allItems) {
|
||||||
|
const nameLower = item.displayName.toLowerCase();
|
||||||
|
if (!tokens.every(tok => item.searchStr.includes(tok))) continue;
|
||||||
|
|
||||||
|
let rank = 3;
|
||||||
|
if (nameLower === q) rank = 0;
|
||||||
|
else if (nameLower.startsWith(q)) rank = 1;
|
||||||
|
else if (nameLower.includes(q)) rank = 2;
|
||||||
|
|
||||||
|
matches.push({ item, rank });
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.sort((a, b) => {
|
||||||
|
if (a.rank !== b.rank) return a.rank - b.rank;
|
||||||
|
return a.item.displayName.localeCompare(b.item.displayName, undefined, { sensitivity: "base" });
|
||||||
|
});
|
||||||
|
|
||||||
|
return matches.slice(0, 30).map(m => m.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupNewWorkflowQuickPicker() {
|
||||||
|
let activeTooltip = null;
|
||||||
|
let activeCleanup = null;
|
||||||
|
let activeSession = null;
|
||||||
|
let isReplacing = false;
|
||||||
|
let activeCloseCount = 0;
|
||||||
|
let userInitiatedBlankUntil = 0;
|
||||||
|
const promptedWorkflows = new WeakSet();
|
||||||
|
|
||||||
|
const closePicker = () => {
|
||||||
|
if (activeCleanup) {
|
||||||
|
activeCleanup();
|
||||||
|
activeCleanup = null;
|
||||||
|
}
|
||||||
|
if (activeTooltip) {
|
||||||
|
activeTooltip.remove();
|
||||||
|
activeTooltip = null;
|
||||||
|
}
|
||||||
|
activeSession = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceUnsavedWithWorkflow = async (item, tempWf) => {
|
||||||
|
const wfMgr = app.extensionManager?.workflow;
|
||||||
|
const targetTemp = tempWf || (isTemporaryWorkflow(wfMgr?.activeWorkflow) ? wfMgr.activeWorkflow : null);
|
||||||
|
|
||||||
|
isReplacing = true;
|
||||||
|
try {
|
||||||
|
if (wfMgr) {
|
||||||
|
const targetWf =
|
||||||
|
wfMgr.getWorkflowByPath?.(item.fullPath) ||
|
||||||
|
wfMgr.workflows?.find(w => (w.path || "").replace(/\\/g, "/") === item.fullPath);
|
||||||
|
|
||||||
|
if (targetWf && typeof wfMgr.openWorkflow === "function") {
|
||||||
|
if (typeof targetWf.load === "function" && !targetWf.isLoaded) {
|
||||||
|
try {
|
||||||
|
await targetWf.load();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let graphData = targetWf.changeTracker?.activeState || targetWf.activeState;
|
||||||
|
if (!graphData) {
|
||||||
|
const res = await api.fetchApi(`/userdata/${encodeURIComponent(item.fullPath)}`);
|
||||||
|
if (res.ok) {
|
||||||
|
graphData = await res.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanGraphData = graphData ? JSON.parse(JSON.stringify(graphData)) : null;
|
||||||
|
|
||||||
|
const canCloseTemp =
|
||||||
|
targetTemp &&
|
||||||
|
targetTemp !== targetWf &&
|
||||||
|
isTemporaryWorkflow(targetTemp) &&
|
||||||
|
typeof wfMgr.closeWorkflow === "function";
|
||||||
|
|
||||||
|
await wfMgr.openWorkflow(targetWf);
|
||||||
|
|
||||||
|
if (canCloseTemp) {
|
||||||
|
try {
|
||||||
|
await wfMgr.closeWorkflow(targetTemp, { warnIfUnsaved: false });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[ComfySidebar] Could not close temporary workflow tab:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanGraphData) {
|
||||||
|
await app.loadGraphData(cleanGraphData, true, true, targetWf);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.fetchApi(`/userdata/${encodeURIComponent(item.fullPath)}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const graphData = await res.json();
|
||||||
|
await app.loadGraphData(graphData, true, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[ComfySidebar] Failed to replace unsaved workflow:", e);
|
||||||
|
} finally {
|
||||||
|
isReplacing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showPicker = (anchorRect, initialTempWf = null, prevWorkflow = null) => {
|
||||||
|
if (initialTempWf) {
|
||||||
|
if (promptedWorkflows.has(initialTempWf)) return;
|
||||||
|
promptedWorkflows.add(initialTempWf);
|
||||||
|
}
|
||||||
|
|
||||||
|
closePicker();
|
||||||
|
|
||||||
|
let resolvedTempWf = initialTempWf;
|
||||||
|
|
||||||
|
const tooltip = document.createElement("div");
|
||||||
|
tooltip.className = "comfy-sidebar-wf-picker";
|
||||||
|
|
||||||
|
const inputWrap = document.createElement("div");
|
||||||
|
inputWrap.className = "comfy-sidebar-wf-picker-input-wrap";
|
||||||
|
|
||||||
|
const icon = document.createElement("i");
|
||||||
|
icon.className = "pi pi-search comfy-sidebar-wf-picker-icon";
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.className = "comfy-sidebar-wf-picker-input";
|
||||||
|
input.placeholder = t("Type to load saved workflow...");
|
||||||
|
input.autocomplete = "off";
|
||||||
|
input.spellcheck = false;
|
||||||
|
|
||||||
|
inputWrap.append(icon, input);
|
||||||
|
tooltip.appendChild(inputWrap);
|
||||||
|
|
||||||
|
const listEl = document.createElement("div");
|
||||||
|
listEl.className = "comfy-sidebar-wf-picker-list";
|
||||||
|
listEl.style.display = "none";
|
||||||
|
setupSpotlightTracker(listEl, ".comfy-sidebar-wf-picker-item");
|
||||||
|
tooltip.appendChild(listEl);
|
||||||
|
|
||||||
|
// Edge-to-edge alignment using pre-click '+' button coords (exact left edge of the new tab)
|
||||||
|
const applyCoords = (coords) => {
|
||||||
|
const width = tooltip.offsetWidth || 300;
|
||||||
|
if (!coords) {
|
||||||
|
tooltip.style.left = `${Math.round((window.innerWidth - width) / 2)}px`;
|
||||||
|
tooltip.style.top = "52px";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const left = Math.max(12, Math.min(coords.left, window.innerWidth - width - 12));
|
||||||
|
const top = Math.min(coords.bottom + 6, window.innerHeight - 60);
|
||||||
|
tooltip.style.left = `${Math.round(left)}px`;
|
||||||
|
tooltip.style.top = `${Math.round(top)}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.appendChild(tooltip);
|
||||||
|
activeTooltip = tooltip;
|
||||||
|
applyCoords(anchorRect || findFallbackAnchorRect());
|
||||||
|
|
||||||
|
let cachedWorkflows = null;
|
||||||
|
let loadingPromise = null;
|
||||||
|
let currentMatches = [];
|
||||||
|
let selectedIdx = -1;
|
||||||
|
|
||||||
|
const ensureWorkflowsLoaded = async () => {
|
||||||
|
if (cachedWorkflows !== null) return cachedWorkflows;
|
||||||
|
if (!loadingPromise) {
|
||||||
|
loadingPromise = fetchSavedWorkflows().then(list => {
|
||||||
|
cachedWorkflows = list;
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return loadingPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-warm cache in the background immediately so typing 1st letter is instant
|
||||||
|
ensureWorkflowsLoaded();
|
||||||
|
|
||||||
|
const updateSelectionHighlight = () => {
|
||||||
|
const rows = listEl.querySelectorAll(".comfy-sidebar-wf-picker-item");
|
||||||
|
rows.forEach((row, idx) => {
|
||||||
|
row.classList.toggle("selected", idx === selectedIdx);
|
||||||
|
});
|
||||||
|
if (selectedIdx >= 0 && rows[selectedIdx]) {
|
||||||
|
rows[selectedIdx].scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveTargetTempWf = () => {
|
||||||
|
if (resolvedTempWf) return resolvedTempWf;
|
||||||
|
const active = app.extensionManager?.workflow?.activeWorkflow;
|
||||||
|
if (active && active !== prevWorkflow && isTemporaryWorkflow(active)) {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
return isTemporaryWorkflow(active) ? active : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickItem = async (item) => {
|
||||||
|
const tempWf = resolveTargetTempWf();
|
||||||
|
closePicker();
|
||||||
|
await replaceUnsavedWithWorkflow(item, tempWf);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderList = async () => {
|
||||||
|
const query = input.value.trim();
|
||||||
|
if (query.length < 1) {
|
||||||
|
currentMatches = [];
|
||||||
|
selectedIdx = -1;
|
||||||
|
listEl.style.display = "none";
|
||||||
|
listEl.replaceChildren();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = await ensureWorkflowsLoaded();
|
||||||
|
if (activeTooltip !== tooltip) return;
|
||||||
|
|
||||||
|
const latestQuery = input.value.trim();
|
||||||
|
if (latestQuery.length < 1) {
|
||||||
|
listEl.style.display = "none";
|
||||||
|
listEl.replaceChildren();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMatches = filterWorkflows(all, latestQuery);
|
||||||
|
listEl.replaceChildren();
|
||||||
|
listEl.style.display = "block";
|
||||||
|
|
||||||
|
if (currentMatches.length === 0) {
|
||||||
|
selectedIdx = -1;
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.className = "comfy-sidebar-wf-picker-empty";
|
||||||
|
empty.textContent = t("No matching workflows");
|
||||||
|
listEl.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIdx = 0;
|
||||||
|
currentMatches.forEach((item, idx) => {
|
||||||
|
const row = document.createElement("button");
|
||||||
|
row.type = "button";
|
||||||
|
row.className = "comfy-sidebar-wf-picker-item" + (idx === 0 ? " selected" : "");
|
||||||
|
|
||||||
|
const nameSpan = document.createElement("span");
|
||||||
|
nameSpan.className = "comfy-sidebar-wf-picker-name";
|
||||||
|
nameSpan.textContent = item.displayName;
|
||||||
|
row.appendChild(nameSpan);
|
||||||
|
|
||||||
|
if (item.folder) {
|
||||||
|
const folderSpan = document.createElement("span");
|
||||||
|
folderSpan.className = "comfy-sidebar-wf-picker-folder";
|
||||||
|
folderSpan.textContent = item.folder;
|
||||||
|
row.appendChild(folderSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.addEventListener("mouseenter", () => {
|
||||||
|
selectedIdx = idx;
|
||||||
|
updateSelectionHighlight();
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener("click", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
pickItem(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
listEl.appendChild(row);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
renderList();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closePicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowDown" && currentMatches.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedIdx = (selectedIdx + 1) % currentMatches.length;
|
||||||
|
updateSelectionHighlight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowUp" && currentMatches.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedIdx = (selectedIdx - 1 + currentMatches.length) % currentMatches.length;
|
||||||
|
updateSelectionHighlight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Enter" && currentMatches.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const chosen = currentMatches[selectedIdx >= 0 ? selectedIdx : 0];
|
||||||
|
if (chosen) pickItem(chosen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPointerDownOutside = (e) => {
|
||||||
|
if (!tooltip.contains(e.target)) {
|
||||||
|
closePicker();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWindowResize = () => {
|
||||||
|
applyCoords(findFallbackAnchorRect() || anchorRect);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("pointerdown", onPointerDownOutside, true);
|
||||||
|
window.addEventListener("resize", onWindowResize);
|
||||||
|
|
||||||
|
activeCleanup = () => {
|
||||||
|
document.removeEventListener("pointerdown", onPointerDownOutside, true);
|
||||||
|
window.removeEventListener("resize", onWindowResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
activeSession = {
|
||||||
|
openedAt: Date.now(),
|
||||||
|
attachTempWorkflow(wf) {
|
||||||
|
if (wf && isTemporaryWorkflow(wf)) {
|
||||||
|
resolvedTempWf = wf;
|
||||||
|
promptedWorkflows.add(wf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Focus immediately, and re-verify after Vue activates the new tab so focus isn't stolen
|
||||||
|
input.focus({ preventScroll: true });
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (activeTooltip === tooltip && !tooltip.contains(document.activeElement)) {
|
||||||
|
input.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Pinia workflow store hook (attaches temp workflow to click-opened picker, or opens picker for shortcut/menu)
|
||||||
|
let unsubWorkflowAction = null;
|
||||||
|
const wfMgr = app.extensionManager?.workflow;
|
||||||
|
if (wfMgr && typeof wfMgr.$onAction === "function") {
|
||||||
|
unsubWorkflowAction = wfMgr.$onAction(({ name, after, onError }) => {
|
||||||
|
if (name === "closeWorkflow") {
|
||||||
|
activeCloseCount++;
|
||||||
|
after(() => { activeCloseCount = Math.max(0, activeCloseCount - 1); });
|
||||||
|
onError(() => { activeCloseCount = Math.max(0, activeCloseCount - 1); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name !== "createNewTemporary" && name !== "createTemporary") return;
|
||||||
|
if (!getSetting(SettingIds.QUICK_WORKFLOW_PICKER, true)) return;
|
||||||
|
if (isReplacing || activeCloseCount > 0 || wfMgr.tabsRestored === false) return;
|
||||||
|
|
||||||
|
const isExplicitNew = name === "createNewTemporary" || Date.now() <= userInitiatedBlankUntil;
|
||||||
|
if (!isExplicitNew) return;
|
||||||
|
|
||||||
|
after((createdWf) => {
|
||||||
|
const wf = createdWf || wfMgr.activeWorkflow;
|
||||||
|
if (!isTemporaryWorkflow(wf)) return;
|
||||||
|
|
||||||
|
// If click handler already opened the picker instantaneously, just attach the created workflow
|
||||||
|
if (activeTooltip && activeSession && Date.now() - activeSession.openedAt < 1000) {
|
||||||
|
activeSession.attachTempWorkflow(wf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triggered via hotkey or menu: wait a frame for tab DOM to render, then align edge-to-edge
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!promptedWorkflows.has(wf)) {
|
||||||
|
showPicker(findFallbackAnchorRect(), wf);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Command hook: catches New Blank Workflow from menus or keybindings
|
||||||
|
let restoreCmdExecute = null;
|
||||||
|
const cmd = app.extensionManager?.command;
|
||||||
|
if (cmd && typeof cmd.execute === "function") {
|
||||||
|
const origExecute = cmd.execute.bind(cmd);
|
||||||
|
const wrappedExecute = function (commandId, ...rest) {
|
||||||
|
if (typeof commandId === "string" && /new.*blank.*workflow|new.*workflow/i.test(commandId)) {
|
||||||
|
userInitiatedBlankUntil = Date.now() + 1200;
|
||||||
|
}
|
||||||
|
return origExecute(commandId, ...rest);
|
||||||
|
};
|
||||||
|
cmd.execute = wrappedExecute;
|
||||||
|
restoreCmdExecute = () => {
|
||||||
|
if (cmd.execute === wrappedExecute) {
|
||||||
|
cmd.execute = origExecute;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. UI click hook: draws popup immediately at pre-click '+' left edge (0ms delay)
|
||||||
|
const onDocumentClick = (e) => {
|
||||||
|
if (!getSetting(SettingIds.QUICK_WORKFLOW_PICKER, true)) return;
|
||||||
|
|
||||||
|
const btn = findNewWorkflowButton(e.target);
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
userInitiatedBlankUntil = Date.now() + 1200;
|
||||||
|
const rect = btn.getBoundingClientRect();
|
||||||
|
const prevWorkflow = app.extensionManager?.workflow?.activeWorkflow || null;
|
||||||
|
|
||||||
|
showPicker({ left: rect.left, bottom: rect.bottom }, null, prevWorkflow);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", onDocumentClick, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
closePicker();
|
||||||
|
document.removeEventListener("click", onDocumentClick, true);
|
||||||
|
if (typeof unsubWorkflowAction === "function") unsubWorkflowAction();
|
||||||
|
if (typeof restoreCmdExecute === "function") restoreCmdExecute();
|
||||||
|
};
|
||||||
|
}
|
||||||
+13
-1
@@ -83,7 +83,13 @@ export class SidebarOverlay {
|
|||||||
// Live observer for opening/closing panels or layout changes
|
// Live observer for opening/closing panels or layout changes
|
||||||
if (window.MutationObserver) {
|
if (window.MutationObserver) {
|
||||||
let updateScheduled = false;
|
let updateScheduled = false;
|
||||||
const mo = new MutationObserver(() => {
|
const mo = new MutationObserver((mutations) => {
|
||||||
|
// Skip mutations caused purely by the sticky header's scroll animation
|
||||||
|
const relevant = mutations.some((m) => {
|
||||||
|
const el = m.target.nodeType === 1 ? m.target : m.target.parentElement;
|
||||||
|
return !el?.closest?.(".comfy-sidebar-sticky-header-group");
|
||||||
|
});
|
||||||
|
if (!relevant) return;
|
||||||
if (!updateScheduled) {
|
if (!updateScheduled) {
|
||||||
updateScheduled = true;
|
updateScheduled = true;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -118,6 +124,8 @@ export class SidebarOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateOverlayBounds() {
|
updateOverlayBounds() {
|
||||||
|
// A pending rAF/ResizeObserver callback can fire after destroy(); don't re-attach observers
|
||||||
|
if (this.destroyed) return;
|
||||||
let leftOffset = 0;
|
let leftOffset = 0;
|
||||||
let rightOffset = 0;
|
let rightOffset = 0;
|
||||||
let topOffset = 0;
|
let topOffset = 0;
|
||||||
@@ -129,6 +137,8 @@ export class SidebarOverlay {
|
|||||||
// Dynamically detect any top header/tab bar docked to the top of the screen
|
// Dynamically detect any top header/tab bar docked to the top of the screen
|
||||||
const potentialTopBars = document.querySelectorAll("header, .comfyui-header, .comfyui-menu, .comfy-menu, [class*='tab-bar'], [class*='workflow-tab'], [class*='workspace-tab'], [class*='header']");
|
const potentialTopBars = document.querySelectorAll("header, .comfyui-header, .comfyui-menu, .comfy-menu, [class*='tab-bar'], [class*='workflow-tab'], [class*='workspace-tab'], [class*='header']");
|
||||||
for (const bar of potentialTopBars) {
|
for (const bar of potentialTopBars) {
|
||||||
|
// Ignore our own sliding gallery/queue header (it animates via translateY)
|
||||||
|
if (bar.closest(".comfy-sidebar-sticky-header-group, .comfy-sidebar-header-root")) continue;
|
||||||
// Must be an element sitting at the top of the viewport
|
// Must be an element sitting at the top of the viewport
|
||||||
const r = bar.getBoundingClientRect();
|
const r = bar.getBoundingClientRect();
|
||||||
if (r.top <= 2 && r.bottom > 20 && r.bottom < 120 && r.width > 200) {
|
if (r.top <= 2 && r.bottom > 20 && r.bottom < 120 && r.width > 200) {
|
||||||
@@ -178,6 +188,8 @@ export class SidebarOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
if (this.destroyed) return;
|
||||||
|
this.destroyed = true;
|
||||||
document.body.classList.remove("comfy-sidebar-overlay-active");
|
document.body.classList.remove("comfy-sidebar-overlay-active");
|
||||||
|
|
||||||
for (const fn of this.cleanupFns.splice(0)) {
|
for (const fn of this.cleanupFns.splice(0)) {
|
||||||
|
|||||||
+195
-15
@@ -614,26 +614,58 @@ export function injectStyles() {
|
|||||||
-webkit-backdrop-filter: blur(10px) !important;
|
-webkit-backdrop-filter: blur(10px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Round (X) Close Button across all preview overlays (matches button, div, or span) */
|
/* Comparison Media: Ensures no borders or outlines appear on compared images */
|
||||||
body.comfy-sidebar-overlay-active [style*="50%"],
|
.comfy-sidebar-comparison-media {
|
||||||
.comfy-sidebar-comparison-overlay [style*="50%"],
|
border: none !important;
|
||||||
.comfy-sidebar-3d-overlay-root [style*="50%"],
|
outline: none !important;
|
||||||
div[class*="sidebar-overlay"] [style*="50%"] {
|
box-shadow: none !important;
|
||||||
background: var(--card, var(--secondary, rgba(28, 28, 35, 0.9))) !important;
|
}
|
||||||
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.2))) !important;
|
|
||||||
color: var(--muted-foreground, #cbd5e1) !important;
|
/* Comparison Slider Line & Handle */
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5) !important;
|
.comfy-sidebar-comparison-slider {
|
||||||
|
background: var(--foreground, #ffffff) !important;
|
||||||
|
box-shadow: 0 0 6px rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-comparison-handle {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
background: var(--card, var(--secondary, rgba(30, 30, 30, 0.9))) !important;
|
||||||
|
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15))) !important;
|
||||||
|
color: var(--foreground, #eee) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5) !important;
|
||||||
|
cursor: ew-resize !important;
|
||||||
|
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, color 0.15s ease-in-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-comparison-handle:hover {
|
||||||
|
background: var(--primary, var(--p-primary-color, #ea580c)) !important;
|
||||||
|
border-color: var(--primary, var(--p-primary-color, #ea580c)) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Round (X) Close Button across all preview overlays (matches buttons, ignores images/sliders) */
|
||||||
|
body.comfy-sidebar-overlay-active button[style*="50%"],
|
||||||
|
body.comfy-sidebar-overlay-active [style*="border-radius: 50%"]:not(img):not(video):not(.comfy-sidebar-comparison-handle):not(.comfy-sidebar-comparison-slider):not(.comfy-sidebar-comparison-media),
|
||||||
|
body.comfy-sidebar-overlay-active [style*="border-radius:50%"]:not(img):not(video):not(.comfy-sidebar-comparison-handle):not(.comfy-sidebar-comparison-slider):not(.comfy-sidebar-comparison-media),
|
||||||
|
.comfy-sidebar-comparison-overlay button[style*="50%"],
|
||||||
|
div[class*="sidebar-overlay"] button[style*="50%"] {
|
||||||
|
background: var(--card, var(--secondary, rgba(30, 30, 30, 0.9))) !important;
|
||||||
|
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15))) !important;
|
||||||
|
color: var(--foreground, #eee) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5) !important;
|
||||||
transition: all 0.15s ease-in-out !important;
|
transition: all 0.15s ease-in-out !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.comfy-sidebar-overlay-active [style*="50%"]:hover,
|
body.comfy-sidebar-overlay-active button[style*="50%"]:hover,
|
||||||
.comfy-sidebar-comparison-overlay [style*="50%"]:hover,
|
body.comfy-sidebar-overlay-active [style*="border-radius: 50%"]:not(img):not(video):not(.comfy-sidebar-comparison-handle):not(.comfy-sidebar-comparison-slider):not(.comfy-sidebar-comparison-media):hover,
|
||||||
.comfy-sidebar-3d-overlay-root [style*="50%"]:hover,
|
body.comfy-sidebar-overlay-active [style*="border-radius:50%"]:not(img):not(video):not(.comfy-sidebar-comparison-handle):not(.comfy-sidebar-comparison-slider):not(.comfy-sidebar-comparison-media):hover,
|
||||||
div[class*="sidebar-overlay"] [style*="50%"]:hover {
|
.comfy-sidebar-comparison-overlay button[style*="50%"]:hover,
|
||||||
|
div[class*="sidebar-overlay"] button[style*="50%"]:hover {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
border-color: var(--primary, var(--p-primary-color, #ea580c)) !important;
|
border-color: var(--primary, var(--p-primary-color, #ea580c)) !important;
|
||||||
background: color-mix(in srgb, var(--primary, var(--p-primary-color, #ea580c)) 25%, var(--card, var(--secondary, #222)) 75%) !important;
|
background: var(--primary, var(--p-primary-color, #ea580c)) !important;
|
||||||
transform: scale(1.08) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.comfy-sidebar-overlay-active [style*="50%"] svg,
|
body.comfy-sidebar-overlay-active [style*="50%"] svg,
|
||||||
@@ -918,6 +950,154 @@ export function injectStyles() {
|
|||||||
.comfy-sidebar-runtime-preview-btn.active:hover {
|
.comfy-sidebar-runtime-preview-btn.active:hover {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* New Workflow Quick Picker Tooltip */
|
||||||
|
.comfy-sidebar-wf-picker {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
width: 300px;
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
background: color-mix(in srgb, var(--card, var(--secondary, var(--comfy-menu-bg, #181818))) 94%, transparent);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.16)));
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
|
||||||
|
padding: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--foreground, var(--fg-color, #eee));
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--background, rgba(0, 0, 0, 0.25));
|
||||||
|
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.12)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-input-wrap:focus-within {
|
||||||
|
border-color: var(--primary, var(--p-primary-color, #ea580c));
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.65;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-input {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--foreground, var(--fg-color, #eee));
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-list {
|
||||||
|
margin-top: 6px;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-item {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--card, var(--secondary, var(--p-surface-800, color-mix(in srgb, var(--bg-color, var(--comfy-menu-bg, #181818)) 85%, var(--fg-color, #fff) 15%))));
|
||||||
|
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.12)));
|
||||||
|
color: var(--foreground, var(--comfy-input-color, var(--fg-color, #eee)));
|
||||||
|
text-align: left;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-item::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: radial-gradient(
|
||||||
|
160px circle at var(--mouse-x, -999px) var(--mouse-y, -999px),
|
||||||
|
color-mix(in srgb, var(--primary, var(--p-primary-color, var(--primary-color, var(--accent, currentColor)))) 60%, transparent),
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-item:hover::before {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-item:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--border, var(--border-color, #555)) 50%, var(--foreground, var(--fg-color, #fff)) 50%);
|
||||||
|
background: color-mix(in srgb, var(--card, var(--secondary, var(--p-surface-800, var(--comfy-menu-bg, #262626)))) 85%, var(--foreground, var(--fg-color, #fff)) 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-item.selected {
|
||||||
|
border-color: color-mix(in srgb, var(--primary, var(--p-primary-color, #ea580c)) 70%, var(--border, #666) 30%);
|
||||||
|
background: color-mix(in srgb, var(--primary, var(--p-primary-color, #ea580c)) 14%, var(--card, var(--secondary, #181818)) 86%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-name,
|
||||||
|
.comfy-sidebar-wf-picker-folder {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-folder {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.65;
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-sidebar-wf-picker-empty {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.65;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide stock hover tooltips while the Quick Workflow Picker is open */
|
||||||
|
body:has(.comfy-sidebar-wf-picker) [role="tooltip"],
|
||||||
|
body:has(.comfy-sidebar-wf-picker) [data-reka-popper-content-wrapper]:has([role="tooltip"]),
|
||||||
|
body:has(.comfy-sidebar-wf-picker) .p-tooltip {
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
@@ -426,6 +426,7 @@ export function matchesFilter(state, query) {
|
|||||||
|
|
||||||
if (state.pid && String(state.pid).toLowerCase().includes(q)) return true;
|
if (state.pid && String(state.pid).toLowerCase().includes(q)) return true;
|
||||||
if (state.texts && state.texts.some(t => String(t).toLowerCase().includes(q))) return true;
|
if (state.texts && state.texts.some(t => String(t).toLowerCase().includes(q))) return true;
|
||||||
|
if (state.promptSearchText && state.promptSearchText.includes(q)) return true;
|
||||||
if (state.images && state.images.some(img => (img.filename || "").toLowerCase().includes(q))) return true;
|
if (state.images && state.images.some(img => (img.filename || "").toLowerCase().includes(q))) return true;
|
||||||
if (state.activeNodeName && state.activeNodeName.toLowerCase().includes(q)) return true;
|
if (state.activeNodeName && state.activeNodeName.toLowerCase().includes(q)) return true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "مسح المهام المتوقفة تلقائياً",
|
||||||
|
"Comfy Layout": "التخطيط الكلاسيكي",
|
||||||
|
"Enable Gallery": "تفعيل المعرض",
|
||||||
|
"Grid Columns Threshold": "حد أعمدة الشبكة",
|
||||||
|
"Hide Junk": "إخفاء العناصر الزائدة",
|
||||||
|
"Latin Hotkeys On Any Layout": "اختصارات لاتينية على أي تخطيط لوحة مفاتيح",
|
||||||
|
"Permanent Delete on Disk Deletion": "حذف نهائي من القرص",
|
||||||
|
"Quick Workflow Picker": "منتقي سير العمل السريع",
|
||||||
|
"Show Pending Count Only": "عرض عدد المهام المعلقة فقط",
|
||||||
|
"Show Working Node Name": "إظهار اسم العقدة النشطة"
|
||||||
|
},
|
||||||
"Queue": "قائمة الانتظار",
|
"Queue": "قائمة الانتظار",
|
||||||
"Search History": "البحث في السجل",
|
"Search History": "البحث في السجل",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "تجاهل/تضمين العقد المحددة (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "تجاهل/تضمين العقد المحددة (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "الترتيب: الأحدث أولاً",
|
"Sort: Newest First": "الترتيب: الأحدث أولاً",
|
||||||
"Sort: Oldest First": "الترتيب: الأقدم أولاً",
|
"Sort: Oldest First": "الترتيب: الأقدم أولاً",
|
||||||
"Search Gallery (Filename or Prompt)": "البحث في المعرض (اسم الملف أو الموجه)",
|
"Search Gallery (Filename or Prompt)": "البحث في المعرض (اسم الملف أو الموجه)",
|
||||||
"Search filenames or prompt metadata...": "البحث في أسماء الملفات أو بيانات الموجه..."
|
"Search filenames or prompt metadata...": "البحث في أسماء الملفات أو بيانات الموجه...",
|
||||||
|
"Type to load saved workflow...": "اكتب لتحميل سير عمل محفوظ...",
|
||||||
|
"No matching workflows": "لا توجد مسارات عمل مطابقة"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "حد العرض لأعمدة قائمة الانتظار (بكسل)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "عند التعطيل، ستظهر بطاقة منفصلة لكل مهمة في قائمة الانتظار"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "إظهار اسم العقدة قيد التشغيل حالياً"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "مسح المهام الملغاة والفاشلة تلقائياً عند بدء توليد جديد"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "تفعيل الشريط الجانبي لمعرض المخرجات"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "حذف الملفات نهائياً من القرص (تجاوز سلة المحذوفات)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "إخفاء التبويب: الأصول"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "إخفاء التبويب: العقد"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "إخفاء التبويب: النماذج"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "إخفاء التبويب: مسارات العمل"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "إخفاء التبويب: التطبيقات"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "إخفاء التبويب: القوالب"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "استبدال شريط سجل المهام الافتراضي بـ Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "إخفاء زر 'Graph' العائم (خريطة العقد/سير العمل)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "إخفاء الصورة الشخصية"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "عناصر التحكم العائمة للوحة",
|
||||||
|
"options": {
|
||||||
|
"Default": "افتراضي",
|
||||||
|
"Dock": "تثبيت",
|
||||||
|
"Hide": "إخفاء"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "إصلاح اختصارات المفاتيح غير اللاتينية على أي تخطيط لوحة مفاتيح",
|
||||||
|
"tooltip": "يعامل الأحرف غير اللاتينية كمفاتيح لاتينية في نفس الموضع الفيزيائي، لتعمل اختصارات مثل Q و Ctrl+Z أثناء تفعيل أي لغة أخرى. لا يؤثر على الكتابة في حقول النصوص."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "وضع عناصر التحكم وتبويبات سير العمل المفتوحة في شريط علوي موحد"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "منتقي سير العمل السريع",
|
||||||
|
"tooltip": "إظهار منتقي سير العمل السريع عند إنشاء سير عمل فارغ جديد"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Auto Clear Interrupted",
|
||||||
|
"Comfy Layout": "Comfy Layout",
|
||||||
|
"Enable Gallery": "Enable Gallery",
|
||||||
|
"Grid Columns Threshold": "Grid Columns Threshold",
|
||||||
|
"Hide Junk": "Hide Junk",
|
||||||
|
"Latin Hotkeys On Any Layout": "Latin Hotkeys On Any Layout",
|
||||||
|
"Permanent Delete on Disk Deletion": "Permanent Delete on Disk Deletion",
|
||||||
|
"Quick Workflow Picker": "Quick Workflow Picker",
|
||||||
|
"Show Pending Count Only": "Show Pending Count Only",
|
||||||
|
"Show Working Node Name": "Show Working Node Name"
|
||||||
|
},
|
||||||
"Queue": "Queue",
|
"Queue": "Queue",
|
||||||
"Search History": "Search History",
|
"Search History": "Search History",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "Sort: Newest First",
|
"Sort: Newest First": "Sort: Newest First",
|
||||||
"Sort: Oldest First": "Sort: Oldest First",
|
"Sort: Oldest First": "Sort: Oldest First",
|
||||||
"Search Gallery (Filename or Prompt)": "Search Gallery (Filename or Prompt)",
|
"Search Gallery (Filename or Prompt)": "Search Gallery (Filename or Prompt)",
|
||||||
"Search filenames or prompt metadata...": "Search filenames or prompt metadata..."
|
"Search filenames or prompt metadata...": "Search filenames or prompt metadata...",
|
||||||
|
"Type to load saved workflow...": "Type to load saved workflow...",
|
||||||
|
"No matching workflows": "No matching workflows"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "Width Threshold for Queue Columns (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "If disabled, each queued job will have a separate individual card"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "Shows the name of the node which is currently in the process"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "Auto-clear cancelled & failed jobs on new generation"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "Enable Output Gallery Sidebar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "Hide Tab: Assets"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "Hide Tab: Nodes"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "Hide Tab: Models"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "Hide Tab: Workflows"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "Hide Tab: Apps"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "Hide Tab: Templates"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "Replace the stock Job History sidebar with Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "Hide floating 'Graph' (Workflow/Node Map) button"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "Hide Avatar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "Floating Canvas Controls",
|
||||||
|
"options": {
|
||||||
|
"Default": "Default",
|
||||||
|
"Dock": "Dock",
|
||||||
|
"Hide": "Hide"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "Fix non-ASCII keybinds (hotkeys on any keyboard layout)",
|
||||||
|
"tooltip": "Treats non-Latin letters (e.g. Cyrillic) as the Latin key in the same physical position, so hotkeys like Q or Ctrl+Z work while another layout is active. Typing in text fields is not affected."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "Places the controls and the open workflow tabs on a single unified top bar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "Quick Workflow Picker",
|
||||||
|
"tooltip": "Show quick workflow picker when creating a new blank workflow"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Limpiar interrumpidos automáticamente",
|
||||||
|
"Comfy Layout": "Diseño clásico",
|
||||||
|
"Enable Gallery": "Habilitar galería",
|
||||||
|
"Grid Columns Threshold": "Umbral de columnas de cuadrícula",
|
||||||
|
"Hide Junk": "Ocultar elementos innecesarios",
|
||||||
|
"Latin Hotkeys On Any Layout": "Atajos latinos en cualquier distribución",
|
||||||
|
"Permanent Delete on Disk Deletion": "Eliminación permanente del disco",
|
||||||
|
"Quick Workflow Picker": "Selector rápido de flujos de trabajo",
|
||||||
|
"Show Pending Count Only": "Mostrar solo cantidad pendiente",
|
||||||
|
"Show Working Node Name": "Mostrar nombre del nodo activo"
|
||||||
|
},
|
||||||
"Queue": "Cola",
|
"Queue": "Cola",
|
||||||
"Search History": "Buscar en el historial",
|
"Search History": "Buscar en el historial",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nodo(s) seleccionado(s) (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nodo(s) seleccionado(s) (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "Ordenar: más recientes primero",
|
"Sort: Newest First": "Ordenar: más recientes primero",
|
||||||
"Sort: Oldest First": "Ordenar: más antiguos primero",
|
"Sort: Oldest First": "Ordenar: más antiguos primero",
|
||||||
"Search Gallery (Filename or Prompt)": "Buscar en galería (nombre de archivo o prompt)",
|
"Search Gallery (Filename or Prompt)": "Buscar en galería (nombre de archivo o prompt)",
|
||||||
"Search filenames or prompt metadata...": "Buscar nombres de archivo o metadatos de prompt..."
|
"Search filenames or prompt metadata...": "Buscar nombres de archivo o metadatos de prompt...",
|
||||||
|
"Type to load saved workflow...": "Escribe para cargar un flujo guardado...",
|
||||||
|
"No matching workflows": "No hay flujos coincidentes"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "Umbral de ancho para columnas de la cola (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "Si se desactiva, cada tarea en cola tendrá su propia tarjeta individual"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "Muestra el nombre del nodo que se está ejecutando actualmente"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "Limpiar automáticamente tareas canceladas y fallidas al iniciar una nueva generación"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "Habilitar barra lateral de galería de resultados"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "Eliminar archivos permanentemente del disco (omite la Papelera)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "Ocultar pestaña: Recursos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "Ocultar pestaña: Nodos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "Ocultar pestaña: Modelos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "Ocultar pestaña: Flujos de trabajo"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "Ocultar pestaña: Aplicaciones"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "Ocultar pestaña: Plantillas"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "Reemplazar la barra de Historial de trabajos por Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "Ocultar botón flotante 'Graph' (Mapa de nodos/flujo)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "Ocultar avatar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "Controles flotantes del lienzo",
|
||||||
|
"options": {
|
||||||
|
"Default": "Predeterminado",
|
||||||
|
"Dock": "Acoplar",
|
||||||
|
"Hide": "Ocultar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "Corregir atajos no ASCII (atajos en cualquier distribución de teclado)",
|
||||||
|
"tooltip": "Trata las letras no latinas como la tecla latina en la misma posición física para que atajos como Q o Ctrl+Z funcionen en cualquier idioma. No afecta a la escritura en campos de texto."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "Coloca los controles y las pestañas de flujos abiertos en una única barra superior"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "Selector rápido de flujos de trabajo",
|
||||||
|
"tooltip": "Mostrar el selector rápido de flujos de trabajo al crear un nuevo flujo en blanco"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "پاکسازی خودکار کارهای متوقفشده",
|
||||||
|
"Comfy Layout": "چیدمان کلاسیک",
|
||||||
|
"Enable Gallery": "فعالسازی گالری",
|
||||||
|
"Grid Columns Threshold": "آستانه ستونهای شبکه",
|
||||||
|
"Hide Junk": "پنهان کردن موارد اضافی",
|
||||||
|
"Latin Hotkeys On Any Layout": "کلیدهای میانبر لاتین در هر چیدمان",
|
||||||
|
"Permanent Delete on Disk Deletion": "حذف دائمی از دیسک",
|
||||||
|
"Quick Workflow Picker": "انتخابگر سریع گردش کار",
|
||||||
|
"Show Pending Count Only": "نمایش فقط تعداد در انتظار",
|
||||||
|
"Show Working Node Name": "نمایش نام گره در حال اجرا"
|
||||||
|
},
|
||||||
"Queue": "صف",
|
"Queue": "صف",
|
||||||
"Search History": "جستجو در تاریخچه",
|
"Search History": "جستجو در تاریخچه",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "نادیده گرفتن گرههای انتخابی در صف (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "نادیده گرفتن گرههای انتخابی در صف (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "مرتبسازی: جدیدترینها در ابتدا",
|
"Sort: Newest First": "مرتبسازی: جدیدترینها در ابتدا",
|
||||||
"Sort: Oldest First": "مرتبسازی: قدیمیترینها در ابتدا",
|
"Sort: Oldest First": "مرتبسازی: قدیمیترینها در ابتدا",
|
||||||
"Search Gallery (Filename or Prompt)": "جستجو در گالری (نام فایل یا پرامپت)",
|
"Search Gallery (Filename or Prompt)": "جستجو در گالری (نام فایل یا پرامپت)",
|
||||||
"Search filenames or prompt metadata...": "جستجوی نام فایلها یا متادیتای پرامپت..."
|
"Search filenames or prompt metadata...": "جستجوی نام فایلها یا متادیتای پرامپت...",
|
||||||
|
"Type to load saved workflow...": "برای بارگذاری گردش کار ذخیرهشده تایپ کنید...",
|
||||||
|
"No matching workflows": "گردش کار مطابقی یافت نشد"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "آستانه عرض برای ستونهای صف (پیکسل)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "در صورت غیرفعال شدن، هر کار در صف یک کارت جداگانه خواهد داشت"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "نمایش نام گرهای که در حال حاضر در حال اجرا است"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "پاکسازی خودکار کارهای لغوشده و ناموفق هنگام تولید جدید"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "فعالسازی نوار کناری گالری خروجیها"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "حذف دائمی فایلها از دیسک (بدون انتقال به سطل زباله)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "پنهان کردن زبانه: داراییها"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "پنهان کردن زبانه: گرهها"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "پنهان کردن زبانه: مدلها"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "پنهان کردن زبانه: گردشهای کار"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "پنهان کردن زبانه: برنامهها"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "پنهان کردن زبانه: قالبها"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "جایگزینی نوار کناری تاریخچه پیشفرض با Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "پنهان کردن دکمه شناور 'Graph' (نقشه گرهها)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "پنهان کردن آواتار"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "کنترلهای شناور بوم",
|
||||||
|
"options": {
|
||||||
|
"Default": "پیشفرض",
|
||||||
|
"Dock": "چسباندن",
|
||||||
|
"Hide": "پنهان"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "اصلاح کلیدهای میانبر غیرلاتین در هر چیدمان صفحهکلید",
|
||||||
|
"tooltip": "حروف غیرلاتین را معادل کلید لاتین در همان موقعیت فیزیکی در نظر میگیرد تا میانبرهایی مانند Q یا Ctrl+Z در هر زبانی کار کنند. تایپ در فیلدهای متنی تحت تأثیر قرار نمیگیرد."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "قرار دادن کنترلها و زبانههای گردش کار باز در یک نوار بالایی یکپارچه"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "انتخابگر سریع گردش کار",
|
||||||
|
"tooltip": "نمایش انتخابگر سریع گردش کار هنگام ایجاد یک گردش کار خالی جدید"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Nettoyage auto des tâches interrompues",
|
||||||
|
"Comfy Layout": "Disposition classique",
|
||||||
|
"Enable Gallery": "Activer la galerie",
|
||||||
|
"Grid Columns Threshold": "Seuil des colonnes de la grille",
|
||||||
|
"Hide Junk": "Masquer les éléments superflus",
|
||||||
|
"Latin Hotkeys On Any Layout": "Raccourcis latins sur toute disposition",
|
||||||
|
"Permanent Delete on Disk Deletion": "Suppression définitive du disque",
|
||||||
|
"Quick Workflow Picker": "Sélecteur rapide de workflow",
|
||||||
|
"Show Pending Count Only": "Afficher uniquement le nombre en attente",
|
||||||
|
"Show Working Node Name": "Afficher le nom du nœud actif"
|
||||||
|
},
|
||||||
"Queue": "File d'attente",
|
"Queue": "File d'attente",
|
||||||
"Search History": "Rechercher dans l'historique",
|
"Search History": "Rechercher dans l'historique",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorer/Activer les nœuds sélectionnés (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorer/Activer les nœuds sélectionnés (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "Trier : Plus récents d'abord",
|
"Sort: Newest First": "Trier : Plus récents d'abord",
|
||||||
"Sort: Oldest First": "Trier : Plus anciens d'abord",
|
"Sort: Oldest First": "Trier : Plus anciens d'abord",
|
||||||
"Search Gallery (Filename or Prompt)": "Rechercher dans la galerie (nom de fichier ou prompt)",
|
"Search Gallery (Filename or Prompt)": "Rechercher dans la galerie (nom de fichier ou prompt)",
|
||||||
"Search filenames or prompt metadata...": "Rechercher des noms de fichiers ou métadonnées de prompt..."
|
"Search filenames or prompt metadata...": "Rechercher des noms de fichiers ou métadonnées de prompt...",
|
||||||
|
"Type to load saved workflow...": "Tapez pour charger un workflow enregistré...",
|
||||||
|
"No matching workflows": "Aucun workflow correspondant"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "Seuil de largeur pour les colonnes de la file (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "Si désactivé, chaque tâche en attente aura sa propre carte"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "Affiche le nom du nœud en cours d'exécution"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "Nettoyer automatiquement les tâches annulées et échouées à chaque nouvelle génération"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "Activer le panneau latéral Galerie des sorties"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "Supprimer définitivement les fichiers du disque (contourne la Corbeille)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "Masquer l'onglet : Ressources"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "Masquer l'onglet : Nœuds"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "Masquer l'onglet : Modèles"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "Masquer l'onglet : Workflows"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "Masquer l'onglet : Applications"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "Masquer l'onglet : Modèles de workflow"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "Remplacer l'historique des tâches par défaut par Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "Masquer le bouton flottant 'Graph' (carte du workflow)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "Masquer l'avatar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "Contrôles flottants du canevas",
|
||||||
|
"options": {
|
||||||
|
"Default": "Par défaut",
|
||||||
|
"Dock": "Ancrer",
|
||||||
|
"Hide": "Masquer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "Corriger les raccourcis non-ASCII (quelle que soit la disposition du clavier)",
|
||||||
|
"tooltip": "Interprète les touches non latines comme la touche latine à la même position physique afin que Q ou Ctrl+Z fonctionnent dans toute disposition. La saisie dans les champs texte n'est pas affectée."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "Place les contrôles et les onglets de workflows ouverts sur une seule barre supérieure"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "Sélecteur rapide de workflow",
|
||||||
|
"tooltip": "Afficher le sélecteur rapide de workflow lors de la création d'un nouveau workflow vide"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "ניקוי אוטומטי של משימות שהופסקו",
|
||||||
|
"Comfy Layout": "פריסה קלאסית",
|
||||||
|
"Enable Gallery": "הפעל גלריה",
|
||||||
|
"Grid Columns Threshold": "סף עמודות רשת",
|
||||||
|
"Hide Junk": "הסתר פריטים מיותרים",
|
||||||
|
"Latin Hotkeys On Any Layout": "קיצורי מקשים לטיניים בכל פריסה",
|
||||||
|
"Permanent Delete on Disk Deletion": "מחיקה לצמיתות מהדיסק",
|
||||||
|
"Quick Workflow Picker": "בוחר תהליכי עבודה מהיר",
|
||||||
|
"Show Pending Count Only": "הצג רק מספר משימות בהמתנה",
|
||||||
|
"Show Working Node Name": "הצג שם צומת פעיל"
|
||||||
|
},
|
||||||
"Queue": "תור",
|
"Queue": "תור",
|
||||||
"Search History": "חיפוש בהיסטוריה",
|
"Search History": "חיפוש בהיסטוריה",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "התעלם/כלול צמתים נבחרים בתור (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "התעלם/כלול צמתים נבחרים בתור (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "מיון: החדשים ביותר קודם",
|
"Sort: Newest First": "מיון: החדשים ביותר קודם",
|
||||||
"Sort: Oldest First": "מיון: הישנים ביותר קודם",
|
"Sort: Oldest First": "מיון: הישנים ביותר קודם",
|
||||||
"Search Gallery (Filename or Prompt)": "חיפוש בגלריה (שם קובץ או פרומפט)",
|
"Search Gallery (Filename or Prompt)": "חיפוש בגלריה (שם קובץ או פרומפט)",
|
||||||
"Search filenames or prompt metadata...": "חיפוש שמות קבצים או מטא-דאטה של פרומפט..."
|
"Search filenames or prompt metadata...": "חיפוש שמות קבצים או מטא-דאטה של פרומפט...",
|
||||||
|
"Type to load saved workflow...": "הקלד כדי לטעון תהליך עבודה שמור...",
|
||||||
|
"No matching workflows": "לא נמצאו תהליכי עבודה תואמים"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "סף רוחב לעמודות התור (פיקסלים)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "אם כבוי, לכל משימה בתור יהיה כרטיס נפרד"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "מציג את שם הצומת שנמצא כעת בעיבוד"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "נקה אוטומטית משימות שבוטלו או נכשלו ביצירה חדשה"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "הפעל סרגל צד של גלריית תוצאות"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "מחק קבצים לצמיתות מהדיסק (עוקף את סל המיחזור)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "הסתר לשונית: נכסים"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "הסתר לשונית: צמתים"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "הסתר לשונית: מודלים"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "הסתר לשונית: תהליכי עבודה"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "הסתר לשונית: יישומים"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "הסתר לשונית: תבניות"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "החלף את סרגל היסטוריית המשימות הרגיל ב-Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "הסתר את הכפתור הצף 'Graph' (מפת צמתים)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "הסתר תמונת פרופיל"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "פקדי קנבס צפים",
|
||||||
|
"options": {
|
||||||
|
"Default": "ברירת מחדל",
|
||||||
|
"Dock": "עגון",
|
||||||
|
"Hide": "הסתר"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "תקן קיצורי מקשים לא-לטיניים בכל פריסת מקלדת",
|
||||||
|
"tooltip": "מתייחס לאותיות לא-לטיניות כמקש הלטיני באותו מיקום פיזי, כך שקיצורים כמו Q או Ctrl+Z יעבדו בכל שפה. הקלדה בשדות טקסט אינה מושפעת."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "מציב את הפקדים ואת לשוניות תהליכי העבודה הפתוחים בסרגל עליון אחד"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "בוחר תהליכי עבודה מהיר",
|
||||||
|
"tooltip": "הצג בוחר תהליכי עבודה מהיר בעת יצירת תהליך עבודה ריק חדש"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Pulizia automatica lavori interrotti",
|
||||||
|
"Comfy Layout": "Layout classico",
|
||||||
|
"Enable Gallery": "Abilita galleria",
|
||||||
|
"Grid Columns Threshold": "Soglia colonne griglia",
|
||||||
|
"Hide Junk": "Nascondi elementi superflui",
|
||||||
|
"Latin Hotkeys On Any Layout": "Scorciatoie latine su qualsiasi layout",
|
||||||
|
"Permanent Delete on Disk Deletion": "Eliminazione permanente dal disco",
|
||||||
|
"Quick Workflow Picker": "Selettore rapido dei workflow",
|
||||||
|
"Show Pending Count Only": "Mostra solo conteggio in attesa",
|
||||||
|
"Show Working Node Name": "Mostra nome del nodo attivo"
|
||||||
|
},
|
||||||
"Queue": "Coda",
|
"Queue": "Coda",
|
||||||
"Search History": "Cerca nella cronologia",
|
"Search History": "Cerca nella cronologia",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignora/Attiva nodo/i selezionato/i (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignora/Attiva nodo/i selezionato/i (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "Ordina: Più recenti prima",
|
"Sort: Newest First": "Ordina: Più recenti prima",
|
||||||
"Sort: Oldest First": "Ordina: Più vecchi prima",
|
"Sort: Oldest First": "Ordina: Più vecchi prima",
|
||||||
"Search Gallery (Filename or Prompt)": "Cerca nella galleria (nome file o prompt)",
|
"Search Gallery (Filename or Prompt)": "Cerca nella galleria (nome file o prompt)",
|
||||||
"Search filenames or prompt metadata...": "Cerca nomi di file o metadati del prompt..."
|
"Search filenames or prompt metadata...": "Cerca nomi di file o metadati del prompt...",
|
||||||
|
"Type to load saved workflow...": "Digita per caricare un workflow salvato...",
|
||||||
|
"No matching workflows": "Nessun workflow corrispondente"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "Soglia larghezza per le colonne della coda (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "Se disattivato, ogni attività in coda avrà una scheda separata"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "Mostra il nome del nodo attualmente in esecuzione"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "Pulisci automaticamente i lavori annullati e falliti a ogni nuova generazione"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "Abilita barra laterale Galleria risultati"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "Elimina definitivamente i file dal disco (bypassa il Cestino)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "Nascondi scheda: Risorse"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "Nascondi scheda: Nodi"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "Nascondi scheda: Modelli"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "Nascondi scheda: Workflow"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "Nascondi scheda: App"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "Nascondi scheda: Template"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "Sostituisci la barra Cronologia lavori predefinita con Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "Nascondi pulsante flottante 'Graph' (Mappa nodi/workflow)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "Nascondi avatar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "Controlli flottanti dell'area di lavoro",
|
||||||
|
"options": {
|
||||||
|
"Default": "Predefinito",
|
||||||
|
"Dock": "Aggancia",
|
||||||
|
"Hide": "Nascondi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "Correggi scorciatoie non-ASCII su qualsiasi layout di tastiera",
|
||||||
|
"tooltip": "Tratta le lettere non latine come il tasto latino nella stessa posizione fisica, così scorciatoie come Q o Ctrl+Z funzionano con qualsiasi layout. La digitazione nei campi di testo non viene modificata."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "Posiziona i controlli e le schede dei workflow aperti in un'unica barra superiore"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "Selettore rapido dei workflow",
|
||||||
|
"tooltip": "Mostra il selettore rapido dei workflow quando crei un nuovo workflow vuoto"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "中断・失敗したジョブを自動消去",
|
||||||
|
"Comfy Layout": "クラシックレイアウト",
|
||||||
|
"Enable Gallery": "ギャラリーを有効化",
|
||||||
|
"Grid Columns Threshold": "グリッド列の幅しきい値",
|
||||||
|
"Hide Junk": "不要な要素を非表示",
|
||||||
|
"Latin Hotkeys On Any Layout": "すべてのキーボード配列でラテン文字ホットキーを有効化",
|
||||||
|
"Permanent Delete on Disk Deletion": "ディスクから完全削除",
|
||||||
|
"Quick Workflow Picker": "クイックワークフローピッカー",
|
||||||
|
"Show Pending Count Only": "待機中の件数のみ表示",
|
||||||
|
"Show Working Node Name": "実行中のノード名を表示"
|
||||||
|
},
|
||||||
"Queue": "キュー",
|
"Queue": "キュー",
|
||||||
"Search History": "履歴を検索",
|
"Search History": "履歴を検索",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "選択したノードをキューで無視/解除 (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "選択したノードをキューで無視/解除 (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "並び替え:新しい順",
|
"Sort: Newest First": "並び替え:新しい順",
|
||||||
"Sort: Oldest First": "並び替え:古い順",
|
"Sort: Oldest First": "並び替え:古い順",
|
||||||
"Search Gallery (Filename or Prompt)": "ギャラリーを検索 (ファイル名またはプロンプト)",
|
"Search Gallery (Filename or Prompt)": "ギャラリーを検索 (ファイル名またはプロンプト)",
|
||||||
"Search filenames or prompt metadata...": "ファイル名またはプロンプトを検索..."
|
"Search filenames or prompt metadata...": "ファイル名またはプロンプトを検索...",
|
||||||
|
"Type to load saved workflow...": "保存済みワークフローを読み込むには入力...",
|
||||||
|
"No matching workflows": "一致するワークフローがありません"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "キュー列を分割する幅のしきい値 (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "無効にすると、待機中のジョブごとに個別のカードが表示されます"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "現在処理中のノード名を表示します"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "新規生成時にキャンセル・失敗したジョブを自動消去する"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "出力ギャラリーサイドバーを有効にする"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "ディスクから削除する際にゴミ箱を経由せず完全に削除する"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "タブを非表示: アセット"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "タブを非表示: ノード"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "タブを非表示: モデル"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "タブを非表示: ワークフロー"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "タブを非表示: アプリ"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "タブを非表示: テンプレート"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "標準のジョブ履歴サイドバーを Comfy Queue に置き換える"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "フローティングの 'Graph'(ワークフローマップ)ボタンを非表示"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "アバターを非表示"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "フローティングキャンバスコントロール",
|
||||||
|
"options": {
|
||||||
|
"Default": "デフォルト",
|
||||||
|
"Dock": "ドックに固定",
|
||||||
|
"Hide": "非表示"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "非ASCIIキーバインドの修正(どのキーボード配列でもホットキーを有効化)",
|
||||||
|
"tooltip": "非ラテン文字を同じ物理位置のラテン文字キーとして扱うため、他の入力配列でも Q や Ctrl+Z などが動作します。テキスト入力欄には影響しません。"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "操作ボタンと開いているワークフロータブを単一の上部バーにまとめる"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "クイックワークフローピッカー",
|
||||||
|
"tooltip": "新しい空のワークフローを作成するときにクイックワークフローピッカーを表示する"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "중단된 작업 자동 지우기",
|
||||||
|
"Comfy Layout": "클래식 레이아웃",
|
||||||
|
"Enable Gallery": "갤러리 활성화",
|
||||||
|
"Grid Columns Threshold": "그리드 열 너비 임계값",
|
||||||
|
"Hide Junk": "불필요한 항목 숨기기",
|
||||||
|
"Latin Hotkeys On Any Layout": "모든 키보드 배열에서 라틴 단축키 지원",
|
||||||
|
"Permanent Delete on Disk Deletion": "디스크에서 영구 삭제",
|
||||||
|
"Quick Workflow Picker": "빠른 워크플로우 선택기",
|
||||||
|
"Show Pending Count Only": "대기 중인 작업 수만 표시",
|
||||||
|
"Show Working Node Name": "실행 중인 노드 이름 표시"
|
||||||
|
},
|
||||||
"Queue": "대기열",
|
"Queue": "대기열",
|
||||||
"Search History": "기록 검색",
|
"Search History": "기록 검색",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "대기열에서 선택한 노드 무시/해제 (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "대기열에서 선택한 노드 무시/해제 (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "정렬: 최신순",
|
"Sort: Newest First": "정렬: 최신순",
|
||||||
"Sort: Oldest First": "정렬: 오래된순",
|
"Sort: Oldest First": "정렬: 오래된순",
|
||||||
"Search Gallery (Filename or Prompt)": "갤러리 검색 (파일명 또는 프롬프트)",
|
"Search Gallery (Filename or Prompt)": "갤러리 검색 (파일명 또는 프롬프트)",
|
||||||
"Search filenames or prompt metadata...": "파일명 또는 프롬프트 메타데이터 검색..."
|
"Search filenames or prompt metadata...": "파일명 또는 프롬프트 메타데이터 검색...",
|
||||||
|
"Type to load saved workflow...": "저장된 워크플로우를 불러오려면 입력하세요...",
|
||||||
|
"No matching workflows": "일치하는 워크플로우가 없습니다"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "대기열 열 분할 너비 임계값 (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "비활성화하면 대기 중인 각 작업마다 개별 카드가 표시됩니다"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "현재 실행 중인 노드의 이름을 표시합니다"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "새 생성 시작 시 취소 및 실패한 작업 자동 지우기"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "출력 갤러리 사이드바 활성화"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "디스크에서 삭제 시 휴지통을 거치지 않고 영구 삭제"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "탭 숨기기: 에셋"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "탭 숨기기: 노드"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "탭 숨기기: 모델"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "탭 숨기기: 워크플로우"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "탭 숨기기: 앱"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "탭 숨기기: 템플릿"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "기본 작업 기록 사이드바를 Comfy Queue로 대체"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "플로팅 'Graph'(워크플로우 맵) 버튼 숨기기"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "아바타 숨기기"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "플로팅 캔버스 컨트롤",
|
||||||
|
"options": {
|
||||||
|
"Default": "기본값",
|
||||||
|
"Dock": "고정",
|
||||||
|
"Hide": "숨기기"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "비ASCII 단축키 수정 (모든 키보드 배열에서 단축키 사용)",
|
||||||
|
"tooltip": "비라틴 문자를 같은 물리적 위치의 라틴 키로 처리하여 다른 입력기에서도 Q나 Ctrl+Z 같은 단축키가 동작합니다. 텍스트 입력란 타이핑에는 영향을 주지 않습니다."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "컨트롤과 열린 워크플로우 탭을 하나의 상단 바에 배치합니다"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "빠른 워크플로우 선택기",
|
||||||
|
"tooltip": "새 빈 워크플로우를 만들 때 빠른 워크플로우 선택기 표시"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Limpar interrompidos automaticamente",
|
||||||
|
"Comfy Layout": "Layout clássico",
|
||||||
|
"Enable Gallery": "Ativar galeria",
|
||||||
|
"Grid Columns Threshold": "Limite de colunas da grade",
|
||||||
|
"Hide Junk": "Ocultar itens desnecessários",
|
||||||
|
"Latin Hotkeys On Any Layout": "Atalhos latinos em qualquer layout",
|
||||||
|
"Permanent Delete on Disk Deletion": "Exclusão permanente no disco",
|
||||||
|
"Quick Workflow Picker": "Seletor Rápido de Fluxos",
|
||||||
|
"Show Pending Count Only": "Mostrar apenas contagem pendente",
|
||||||
|
"Show Working Node Name": "Mostrar nome do nó ativo"
|
||||||
|
},
|
||||||
"Queue": "Fila",
|
"Queue": "Fila",
|
||||||
"Search History": "Pesquisar histórico",
|
"Search History": "Pesquisar histórico",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nó(s) selecionado(s) na fila (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nó(s) selecionado(s) na fila (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "Ordenar: Mais recentes primeiro",
|
"Sort: Newest First": "Ordenar: Mais recentes primeiro",
|
||||||
"Sort: Oldest First": "Ordenar: Mais antigos primeiro",
|
"Sort: Oldest First": "Ordenar: Mais antigos primeiro",
|
||||||
"Search Gallery (Filename or Prompt)": "Pesquisar na galeria (nome do arquivo ou prompt)",
|
"Search Gallery (Filename or Prompt)": "Pesquisar na galeria (nome do arquivo ou prompt)",
|
||||||
"Search filenames or prompt metadata...": "Pesquisar nomes de arquivos ou metadados de prompt..."
|
"Search filenames or prompt metadata...": "Pesquisar nomes de arquivos ou metadados de prompt...",
|
||||||
|
"Type to load saved workflow...": "Digite para carregar um fluxo salvo...",
|
||||||
|
"No matching workflows": "Nenhum fluxo correspondente"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "Limite de largura para colunas da fila (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "Se desativado, cada tarefa na fila terá um card individual"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "Mostra o nome do nó que está em execução no momento"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "Limpar automaticamente tarefas canceladas e com erro ao iniciar nova geração"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "Ativar barra lateral da Galeria de saídas"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "Excluir arquivos permanentemente do disco (ignora a Lixeira)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "Ocultar aba: Recursos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "Ocultar aba: Nós"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "Ocultar aba: Modelos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "Ocultar aba: Fluxos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "Ocultar aba: Aplicativos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "Ocultar aba: Modelos prontos"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "Substituir o Histórico de tarefas padrão pelo Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "Ocultar botão flutuante 'Graph' (Mapa de nós)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "Ocultar avatar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "Controles flutuantes do canvas",
|
||||||
|
"options": {
|
||||||
|
"Default": "Padrão",
|
||||||
|
"Dock": "Acoplar",
|
||||||
|
"Hide": "Ocultar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "Corrigir atalhos não-ASCII em qualquer layout de teclado",
|
||||||
|
"tooltip": "Trata letras não latinas como a tecla latina na mesma posição física para que atalhos como Q ou Ctrl+Z funcionem em qualquer idioma. Não afeta a digitação em campos de texto."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "Coloca os controles e as abas de fluxos abertos em uma única barra superior"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "Seletor Rápido de Fluxos",
|
||||||
|
"tooltip": "Mostrar seletor rápido de fluxos ao criar um novo fluxo em branco"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Автоочистка прерванных",
|
||||||
|
"Comfy Layout": "Классическая компоновка",
|
||||||
|
"Enable Gallery": "Включить галерею",
|
||||||
|
"Grid Columns Threshold": "Порог колонок сетки",
|
||||||
|
"Hide Junk": "Скрыть лишнее",
|
||||||
|
"Latin Hotkeys On Any Layout": "Латинские горячие клавиши на любой раскладке",
|
||||||
|
"Permanent Delete on Disk Deletion": "Безвозвратное удаление с диска",
|
||||||
|
"Quick Workflow Picker": "Быстрый выбор рабочего процесса",
|
||||||
|
"Show Pending Count Only": "Показывать только счётчик очереди",
|
||||||
|
"Show Working Node Name": "Показывать имя активного узла"
|
||||||
|
},
|
||||||
"Queue": "Очередь",
|
"Queue": "Очередь",
|
||||||
"Search History": "Поиск по истории",
|
"Search History": "Поиск по истории",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Игнорировать выбранные ноды в очереди (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Игнорировать выбранные ноды в очереди (Ctrl+Q)",
|
||||||
@@ -114,5 +127,8 @@
|
|||||||
"Sort: Newest First": "Сортировка: сначала новые",
|
"Sort: Newest First": "Сортировка: сначала новые",
|
||||||
"Sort: Oldest First": "Сортировка: сначала старые",
|
"Sort: Oldest First": "Сортировка: сначала старые",
|
||||||
"Search Gallery (Filename or Prompt)": "Поиск по галерее (имя файла или промпт)",
|
"Search Gallery (Filename or Prompt)": "Поиск по галерее (имя файла или промпт)",
|
||||||
"Search filenames or prompt metadata...": "Поиск по именам файлов или промптам..."
|
"Search filenames or prompt metadata...": "Поиск по именам файлов или промптам...",
|
||||||
}
|
"Type to load saved workflow...": "Найти сохранённый рабочий процесс...",
|
||||||
|
"No matching workflows": "Подходящие рабочие процессы не найдены"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "Ширина в пикселях для разделения очереди на несколько колонок"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "Если отключено, для каждой задачи в очереди будет отдельная карточка"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "Показывать имя узла, который сейчас выполняется"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "Автоматически очищать отменённые и неудачные задачи при новой генерации"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "Включить боковую панель галереи результатов"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "Удалять файлы с диска безвозвратно (минуя Корзину)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "Скрыть вкладку: Ассеты"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "Скрыть вкладку: Узлы"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "Скрыть вкладку: Модели"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "Скрыть вкладку: Рабочие процессы"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "Скрыть вкладку: Приложения"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "Скрыть вкладку: Шаблоны"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "Заменить стандартную историю заданий на Comfy Queue"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "Скрыть плавающую кнопку 'Graph' (карта рабочего процесса)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "Скрыть аватар"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "Плавающие элементы управления холстом",
|
||||||
|
"options": {
|
||||||
|
"Default": "По умолчанию",
|
||||||
|
"Dock": "Закрепить",
|
||||||
|
"Hide": "Скрыть"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "Исправить нелатинские горячие клавиши (работа на любой раскладке)",
|
||||||
|
"tooltip": "Обрабатывает нелатинские буквы (например, кириллицу) как соответствующие латинские клавиши, чтобы горячие клавиши вроде Q или Ctrl+Z работали на любой раскладке. На ввод текста в полях не влияет."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "Разместить элементы управления и открытые вкладки на единой верхней панели"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "Быстрый выбор рабочего процесса",
|
||||||
|
"tooltip": "Показывать быстрый выбор при создании нового пустого рабочего процесса"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,30 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Kesilenleri Otomatik Temizle",
|
||||||
|
"Comfy Layout": "Klasik Düzen",
|
||||||
|
"Enable Gallery": "Galeriyi Etkinleştir",
|
||||||
|
"Grid Columns Threshold": "Izgara Sütun Eşiği",
|
||||||
|
"Hide Junk": "Gereksiz Öğeleri Gizle",
|
||||||
|
"Latin Hotkeys On Any Layout": "Her Düzende Latin Kısayol Tuşları",
|
||||||
|
"Permanent Delete on Disk Deletion": "Diskten Kalıcı Olarak Sil",
|
||||||
|
"Quick Workflow Picker": "Hızlı İş Akışı Seçici",
|
||||||
|
"Show Pending Count Only": "Yalnızca Bekleyen Sayısını Göster",
|
||||||
|
"Show Working Node Name": "Çalışan Düğüm Adını Göster"
|
||||||
|
},
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "Kesilenleri Otomatik Temizle",
|
||||||
|
"Comfy Layout": "Klasik Düzen",
|
||||||
|
"Enable Gallery": "Galeriyi Etkinleştir",
|
||||||
|
"Grid Columns Threshold": "Izgara Sütun Eşiği",
|
||||||
|
"Hide Junk": "Gereksiz Öğeleri Gizle",
|
||||||
|
"Latin Hotkeys On Any Layout": "Her Düzende Latin Kısayol Tuşları",
|
||||||
|
"Permanent Delete on Disk Deletion": "Diskten Kalıcı Olarak Sil",
|
||||||
|
"Quick Workflow Picker": "Hızlı İş Akışı Seçici",
|
||||||
|
"Show Pending Count Only": "Yalnızca Bekleyen Sayısını Göster",
|
||||||
|
"Show Working Node Name": "Çalışan Düğüm Adını Göster"
|
||||||
|
},
|
||||||
"Queue": "Kuyruk",
|
"Queue": "Kuyruk",
|
||||||
"Search History": "Geçmişte ara",
|
"Search History": "Geçmişte ara",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Seçili düğümleri kuyrukta yoksay/etkinleştir (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Seçili düğümleri kuyrukta yoksay/etkinleştir (Ctrl+Q)",
|
||||||
@@ -114,5 +140,7 @@
|
|||||||
"Sort: Newest First": "Sıralama: Önce En Yeniler",
|
"Sort: Newest First": "Sıralama: Önce En Yeniler",
|
||||||
"Sort: Oldest First": "Sıralama: Önce En Eskiler",
|
"Sort: Oldest First": "Sıralama: Önce En Eskiler",
|
||||||
"Search Gallery (Filename or Prompt)": "Galeride Ara (Dosya Adı veya Prompt)",
|
"Search Gallery (Filename or Prompt)": "Galeride Ara (Dosya Adı veya Prompt)",
|
||||||
"Search filenames or prompt metadata...": "Dosya adlarını veya prompt meta verilerini ara..."
|
"Search filenames or prompt metadata...": "Dosya adlarını veya prompt meta verilerini ara...",
|
||||||
|
"Type to load saved workflow...": "Kayıtlı iş akışını yüklemek için yazın...",
|
||||||
|
"No matching workflows": "Eşleşen iş akışı yok"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "Kuyruk sütunları için genişlik eşiği (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "Devre dışı bırakılırsa, kuyruktaki her iş için ayrı kart gösterilir"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "Şu anda işlenmekte olan düğümün adını gösterir"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "Yeni üretimde iptal edilen ve başarısız işleri otomatik temizle"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "Çıktı Galerisi Kenar Çubuğunu Etkinleştir"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "Diskten silerken dosyaları kalıcı olarak sil (Çöp Kutusunu atlar)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "Sekmeyi Gizle: Varlıklar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "Sekmeyi Gizle: Düğümler"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "Sekmeyi Gizle: Modeller"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "Sekmeyi Gizle: İş Akışları"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "Sekmeyi Gizle: Uygulamalar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "Sekmeyi Gizle: Şablonlar"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "Varsayılan İş Geçmişi kenar çubuğunu Comfy Queue ile değiştir"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "Yüzen 'Graph' (İş Akışı/Düğüm Haritası) düğmesini gizle"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "Avatarı Gizle"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "Yüzen Tuval Kontrolleri",
|
||||||
|
"options": {
|
||||||
|
"Default": "Varsayılan",
|
||||||
|
"Dock": "Sabitle",
|
||||||
|
"Hide": "Gizle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "ASCII dışı kısayolları düzelt (her klavye düzeninde kısayollar)",
|
||||||
|
"tooltip": "Latin olmayan harfleri aynı fiziksel konumdaki Latin tuşu gibi işleyerek Q veya Ctrl+Z gibi kısayolların her klavye düzeninde çalışmasını sağlar. Metin alanlarına yazmayı etkilemez."
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "Kontrolleri ve açık iş akışı sekmelerini tek bir üst çubuğa yerleştirir"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "Hızlı İş Akışı Seçici",
|
||||||
|
"tooltip": "Yeni bir boş iş akışı oluştururken hızlı iş akışı seçiciyi göster"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "自動清除已中斷任務",
|
||||||
|
"Comfy Layout": "經典版面配置",
|
||||||
|
"Enable Gallery": "啟用圖庫",
|
||||||
|
"Grid Columns Threshold": "網格欄寬閾值",
|
||||||
|
"Hide Junk": "隱藏多餘元素",
|
||||||
|
"Latin Hotkeys On Any Layout": "任意鍵盤配置下使用拉丁快速鍵",
|
||||||
|
"Permanent Delete on Disk Deletion": "從磁碟永久刪除",
|
||||||
|
"Quick Workflow Picker": "快速工作流程選擇器",
|
||||||
|
"Show Pending Count Only": "僅顯示等待數量",
|
||||||
|
"Show Working Node Name": "顯示目前執行節點名稱"
|
||||||
|
},
|
||||||
"Queue": "隊列",
|
"Queue": "隊列",
|
||||||
"Search History": "搜尋歷史",
|
"Search History": "搜尋歷史",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在隊列中忽略選取節點 (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在隊列中忽略選取節點 (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "排序:最新優先",
|
"Sort: Newest First": "排序:最新優先",
|
||||||
"Sort: Oldest First": "排序:最舊優先",
|
"Sort: Oldest First": "排序:最舊優先",
|
||||||
"Search Gallery (Filename or Prompt)": "搜尋圖庫 (檔案名稱或提示詞)",
|
"Search Gallery (Filename or Prompt)": "搜尋圖庫 (檔案名稱或提示詞)",
|
||||||
"Search filenames or prompt metadata...": "搜尋檔案名稱或提示詞元資料..."
|
"Search filenames or prompt metadata...": "搜尋檔案名稱或提示詞元資料...",
|
||||||
|
"Type to load saved workflow...": "輸入以載入已儲存的工作流程...",
|
||||||
|
"No matching workflows": "沒有符合的工作流程"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "佇列分欄的寬度閾值 (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "關閉後,每個排隊任務將顯示為獨立的卡片"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "顯示目前正在執行的節點名稱"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "開始新生成時自動清除已取消和失敗的任務"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "啟用輸出圖庫側邊欄"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "從磁碟刪除時永久刪除檔案(略過資源回收筒)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "隱藏分頁:資產"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "隱藏分頁:節點"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "隱藏分頁:模型"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "隱藏分頁:工作流程"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "隱藏分頁:應用程式"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "隱藏分頁:範本"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "使用 Comfy Queue 取代預設任務歷程側邊欄"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "隱藏浮動 'Graph'(工作流程/節點地圖)按鈕"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "隱藏頭像"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "浮動畫布控制列",
|
||||||
|
"options": {
|
||||||
|
"Default": "預設",
|
||||||
|
"Dock": "停靠",
|
||||||
|
"Hide": "隱藏"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "修正非 ASCII 快速鍵(任意鍵盤配置下使用快速鍵)",
|
||||||
|
"tooltip": "將非拉丁字元按鍵識別為相同實體位置的拉丁鍵位,使 Q 或 Ctrl+Z 等快速鍵在任意輸入法配置下都能正常運作。不影響文字框輸入。"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "將控制按鈕與開啟的工作流程分頁整合至單一頂部列"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "快速工作流程選擇器",
|
||||||
|
"tooltip": "建立新的空白工作流程時顯示快速工作流程選擇器"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"settingsCategories": {
|
||||||
|
"Comfy Sidebar": "Comfy Sidebar",
|
||||||
|
"Auto Clear Interrupted": "自动清除已中断任务",
|
||||||
|
"Comfy Layout": "经典布局",
|
||||||
|
"Enable Gallery": "启用图库",
|
||||||
|
"Grid Columns Threshold": "网格列宽阈值",
|
||||||
|
"Hide Junk": "隐藏多余元素",
|
||||||
|
"Latin Hotkeys On Any Layout": "任意输入法布局下使用拉丁快捷键",
|
||||||
|
"Permanent Delete on Disk Deletion": "从磁盘永久删除",
|
||||||
|
"Quick Workflow Picker": "快速工作流选择器",
|
||||||
|
"Show Pending Count Only": "仅显示等待数量",
|
||||||
|
"Show Working Node Name": "显示当前运行节点名称"
|
||||||
|
},
|
||||||
"Queue": "队列",
|
"Queue": "队列",
|
||||||
"Search History": "搜索历史",
|
"Search History": "搜索历史",
|
||||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在队列中忽略所选节点 (Ctrl+Q)",
|
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在队列中忽略所选节点 (Ctrl+Q)",
|
||||||
@@ -114,5 +127,7 @@
|
|||||||
"Sort: Newest First": "排序:最新优先",
|
"Sort: Newest First": "排序:最新优先",
|
||||||
"Sort: Oldest First": "排序:最早优先",
|
"Sort: Oldest First": "排序:最早优先",
|
||||||
"Search Gallery (Filename or Prompt)": "搜索图库 (文件名或提示词)",
|
"Search Gallery (Filename or Prompt)": "搜索图库 (文件名或提示词)",
|
||||||
"Search filenames or prompt metadata...": "搜索文件名或提示词元数据..."
|
"Search filenames or prompt metadata...": "搜索文件名或提示词元数据...",
|
||||||
|
"Type to load saved workflow...": "输入以加载已保存的工作流...",
|
||||||
|
"No matching workflows": "没有匹配的工作流"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"Comfy Sidebar_Grid Columns Threshold": {
|
||||||
|
"name": "队列分栏的宽度阈值 (px)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Pending Count Only": {
|
||||||
|
"name": "关闭后,每个排队任务将显示为单独的卡片"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Show Working Node Name": {
|
||||||
|
"name": "显示当前正在运行的节点名称"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Auto Clear Interrupted": {
|
||||||
|
"name": "开始新生成时自动清除已取消和失败的任务"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Enable Gallery": {
|
||||||
|
"name": "启用输出图库侧边栏"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Permanent Delete on Disk Deletion": {
|
||||||
|
"name": "从磁盘删除时永久删除文件(跳过回收站)"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Assets": {
|
||||||
|
"name": "隐藏标签页:资产"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Nodes": {
|
||||||
|
"name": "隐藏标签页:节点"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Models": {
|
||||||
|
"name": "隐藏标签页:模型"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Workflows": {
|
||||||
|
"name": "隐藏标签页:工作流"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Apps": {
|
||||||
|
"name": "隐藏标签页:应用"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Templates": {
|
||||||
|
"name": "隐藏标签页:模板"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Override Stock Job History Tab": {
|
||||||
|
"name": "用 Comfy Queue 替换默认任务历史侧边栏"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Graph Button": {
|
||||||
|
"name": "隐藏浮动 'Graph'(工作流/节点地图)按钮"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Avatar": {
|
||||||
|
"name": "隐藏头像"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Hide Junk_Floating Canvas Controls": {
|
||||||
|
"name": "浮动画布控制栏",
|
||||||
|
"options": {
|
||||||
|
"Default": "默认",
|
||||||
|
"Dock": "停靠",
|
||||||
|
"Hide": "隐藏"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Latin Hotkeys On Any Layout": {
|
||||||
|
"name": "修复非 ASCII 快捷键(任意输入法下使用快捷键)",
|
||||||
|
"tooltip": "将非拉丁字符按键识别为同物理位置的拉丁键位,使 Q 或 Ctrl+Z 等快捷键在任意布局下都能生效。不影响文本框输入。"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Comfy Layout": {
|
||||||
|
"name": "将控制按钮和打开的工作流标签整合到单一顶部栏"
|
||||||
|
},
|
||||||
|
"Comfy Sidebar_Quick Workflow Picker": {
|
||||||
|
"name": "快速工作流选择器",
|
||||||
|
"tooltip": "创建新的空白工作流时显示快速工作流选择器"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"""Extract searchable prompt text from ComfyUI prompt graphs and PNG outputs.
|
||||||
|
|
||||||
|
This module has no ComfyUI imports, so it can be used and tested on its own.
|
||||||
|
|
||||||
|
How it works
|
||||||
|
------------
|
||||||
|
* Only the PNG ``prompt`` text chunk is read. Other chunks (notably the often
|
||||||
|
multi-megabyte ``workflow`` chunk) are skipped with a seek, never parsed.
|
||||||
|
* From API-format prompt graphs (whether supplied by Queue or read from PNGs),
|
||||||
|
collect only the strings that actually
|
||||||
|
feed a text-encoder node (``CLIPTextEncode`` and friends), following links to
|
||||||
|
upstream string/primitive/concat nodes. Model names, sampler names, file
|
||||||
|
names and other string widgets are ignored.
|
||||||
|
* Extracted text is cached per file (keyed by mtime + size), so repeated
|
||||||
|
searches only pay for files that are new or changed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import zlib
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||||
|
|
||||||
|
# Input names that can carry prompt text (substring match, lower-case).
|
||||||
|
# Deliberately does not match plain "clip", "model", "vae", "image", etc.
|
||||||
|
_PROMPT_KEY_HINTS = (
|
||||||
|
"text", "prompt", "positive", "negative", "caption",
|
||||||
|
"string", "value", "t5xxl", "clip_l", "clip_g",
|
||||||
|
)
|
||||||
|
_MAX_LINK_DEPTH = 6
|
||||||
|
_MAX_PROMPT_CHUNK_BYTES = 16 * 1024 * 1024
|
||||||
|
_MAX_TEXT_CHARS_PER_FILE = 50_000
|
||||||
|
|
||||||
|
_CACHE_MAX_ENTRIES = 20_000
|
||||||
|
_cache = OrderedDict() # path -> ((mtime_ns, size), lowercased prompt text)
|
||||||
|
_cache_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Prompt graph -> text
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def _is_prompt_key(name):
|
||||||
|
lowered = str(name).lower()
|
||||||
|
return any(hint in lowered for hint in _PROMPT_KEY_HINTS)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_encoder(class_type):
|
||||||
|
lowered = str(class_type or "").lower()
|
||||||
|
return "textencode" in lowered or "prompt" in lowered
|
||||||
|
|
||||||
|
|
||||||
|
def _is_link(value):
|
||||||
|
# API-format link: [source_node_id, output_slot]
|
||||||
|
return isinstance(value, list) and len(value) == 2 and isinstance(value[1], int)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_prompt_texts(prompt):
|
||||||
|
"""Return the prompt strings found in an API-format ComfyUI prompt dict."""
|
||||||
|
if not isinstance(prompt, dict):
|
||||||
|
return []
|
||||||
|
|
||||||
|
texts = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def visit(node_id, depth):
|
||||||
|
key = str(node_id)
|
||||||
|
if key in seen or depth > _MAX_LINK_DEPTH:
|
||||||
|
return
|
||||||
|
seen.add(key)
|
||||||
|
node = prompt.get(key)
|
||||||
|
inputs = node.get("inputs") if isinstance(node, dict) else None
|
||||||
|
if not isinstance(inputs, dict):
|
||||||
|
return
|
||||||
|
for name, value in inputs.items():
|
||||||
|
if not _is_prompt_key(name):
|
||||||
|
continue
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value.strip():
|
||||||
|
texts.append(value)
|
||||||
|
elif _is_link(value):
|
||||||
|
visit(value[0], depth + 1)
|
||||||
|
|
||||||
|
starts = [
|
||||||
|
node_id for node_id, node in prompt.items()
|
||||||
|
if isinstance(node, dict) and _is_encoder(node.get("class_type"))
|
||||||
|
]
|
||||||
|
if not starts:
|
||||||
|
# Unusual graph with no recognisable text-encoder node: fall back to
|
||||||
|
# every node, still restricted to prompt-like input names.
|
||||||
|
starts = list(prompt)
|
||||||
|
for node_id in starts:
|
||||||
|
visit(node_id, 0)
|
||||||
|
return texts
|
||||||
|
|
||||||
|
|
||||||
|
def get_prompt_search_text_from_graph(prompt):
|
||||||
|
"""Return lower-cased text-encoder prompt text from an API-format graph."""
|
||||||
|
return "\n".join(extract_prompt_texts(prompt)).lower()[:_MAX_TEXT_CHARS_PER_FILE]
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# PNG chunk reading
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def _inflate(data, limit=_MAX_PROMPT_CHUNK_BYTES):
|
||||||
|
# Bounded decompress: refuses to expand beyond `limit` bytes.
|
||||||
|
return zlib.decompressobj().decompress(data, limit)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_text_chunk(chunk_type, data, nul):
|
||||||
|
"""Decode tEXt / zTXt / iTXt chunk data (keyword already located at `nul`)."""
|
||||||
|
if chunk_type == b"tEXt":
|
||||||
|
return data[nul + 1:].decode("latin-1")
|
||||||
|
|
||||||
|
if chunk_type == b"zTXt":
|
||||||
|
if data[nul + 1] != 0: # only compression method 0 (zlib) exists
|
||||||
|
return None
|
||||||
|
return _inflate(data[nul + 2:]).decode("latin-1")
|
||||||
|
|
||||||
|
# iTXt: keyword\0 compression_flag compression_method language\0 translated\0 text
|
||||||
|
flag = data[nul + 1]
|
||||||
|
rest = data[nul + 3:]
|
||||||
|
lang_end = rest.index(b"\x00")
|
||||||
|
translated_end = rest.index(b"\x00", lang_end + 1)
|
||||||
|
body = rest[translated_end + 1:]
|
||||||
|
if flag == 1:
|
||||||
|
body = _inflate(body)
|
||||||
|
return body.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def read_png_prompt(fileobj):
|
||||||
|
"""Return the parsed ``prompt`` JSON from an open PNG file, or None."""
|
||||||
|
if fileobj.read(8) != _PNG_SIGNATURE:
|
||||||
|
return None
|
||||||
|
while True:
|
||||||
|
header = fileobj.read(8)
|
||||||
|
if len(header) < 8:
|
||||||
|
return None
|
||||||
|
length = int.from_bytes(header[:4], "big")
|
||||||
|
chunk_type = header[4:]
|
||||||
|
if chunk_type in (b"IDAT", b"IEND"):
|
||||||
|
return None # ComfyUI writes its text chunks before the image data
|
||||||
|
|
||||||
|
if chunk_type in (b"tEXt", b"zTXt", b"iTXt"):
|
||||||
|
head = fileobj.read(min(length, 80)) # keyword is at most 79 bytes
|
||||||
|
nul = head.find(b"\x00")
|
||||||
|
if nul != -1 and head[:nul] == b"prompt":
|
||||||
|
if length > _MAX_PROMPT_CHUNK_BYTES:
|
||||||
|
return None
|
||||||
|
data = head + fileobj.read(length - len(head))
|
||||||
|
text = _decode_text_chunk(chunk_type, data, nul)
|
||||||
|
return json.loads(text) if text else None
|
||||||
|
fileobj.seek(length - len(head) + 4, 1) # rest of data + CRC
|
||||||
|
else:
|
||||||
|
fileobj.seek(length + 4, 1)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Cached per-file text
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def get_prompt_search_text(path):
|
||||||
|
"""Lower-cased prompt text of a PNG ('' if none). Cached by mtime + size."""
|
||||||
|
try:
|
||||||
|
st = os.stat(path)
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
signature = (st.st_mtime_ns, st.st_size)
|
||||||
|
|
||||||
|
with _cache_lock:
|
||||||
|
hit = _cache.get(path)
|
||||||
|
if hit and hit[0] == signature:
|
||||||
|
_cache.move_to_end(path)
|
||||||
|
return hit[1]
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
prompt = read_png_prompt(f)
|
||||||
|
if prompt is not None:
|
||||||
|
text = get_prompt_search_text_from_graph(prompt)
|
||||||
|
except Exception:
|
||||||
|
# Corrupt / truncated / non-ComfyUI PNG: treat as "no prompt".
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
with _cache_lock:
|
||||||
|
_cache[path] = (signature, text)
|
||||||
|
_cache.move_to_end(path)
|
||||||
|
while len(_cache) > _CACHE_MAX_ENTRIES:
|
||||||
|
_cache.popitem(last=False)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def clear_cache():
|
||||||
|
with _cache_lock:
|
||||||
|
_cache.clear()
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfy-sidebar"
|
name = "comfy-sidebar"
|
||||||
description = "A compact and powerful layout overhaul for ComfyUI that makes generation results easier to browse, preview, compare, and manage."
|
description = "A compact and powerful layout overhaul for ComfyUI that makes generation results easier to browse, preview, compare, and manage."
|
||||||
version = "1.8.2"
|
version = "1.8.5"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"send2trash>=1.8.0",
|
"send2trash>=1.8.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user