Compare commits

...
60 Commits
Author SHA1 Message Date
m0rtus59 cf137b84f5 NEW Workflow Quick Pick. Update locales 2026-11-14 02:31:29 +05:00
m0rtus59 36ab9e1946 Merge pull request #6 from m0rtus59/chore/cleanup-prompt-search
Clean up leftovers from prompt search change
2026-11-11 07:03:00 +05:00
Claude 2db8592267 Remove unused prompt scan helper and stale test reference 2026-11-10 20:06:51 +00:00
M0rtus59 9cb8f7faba version bump 2026-11-10 16:52:03 +05:00
m0rtus59 a95d2ca1b7 Merge pull request #5 from m0rtus59/fix/queue-prompt-search-sticky-header
Use shared prompt parser for Queue search and fix sticky headers
2026-11-10 16:46:40 +05:00
m0rtus59 f5f5016346 Remove leftover prompt search test file 2026-11-10 16:40:28 +05:00
m0rtus59 e9c7205e25 Match Queue sticky header reveal behavior to Gallery 2026-11-10 16:40:24 +05:00
m0rtus59 6b06b18bca Tidy shared prompt search documentation 2026-11-10 16:17:52 +05:00
m0rtus59 1c6886712a Correct newline expectation in prompt search test 2026-11-10 16:17:27 +05:00
m0rtus59 2286083b9b Bound Queue prompt extraction request sizes 2026-11-10 16:16:56 +05:00
m0rtus59 a23ba4fae2 Test shared Queue prompt extraction helper 2026-11-10 16:16:54 +05:00
m0rtus59 09677772c6 Share graph extraction between Queue and Gallery 2026-11-10 16:16:51 +05:00
m0rtus59 37522e484a Make Queue search header sticky and frosted on scroll 2026-11-10 16:16:06 +05:00
m0rtus59 270616a744 Keep gallery frosted header state while searching 2026-11-10 16:16:03 +05:00
m0rtus59 2941e48879 Populate Queue prompt search text with shared Python parser 2026-11-10 16:15:51 +05:00
m0rtus59 f7436011c4 Search extracted generation prompts in Queue cards 2026-11-10 16:15:33 +05:00
m0rtus59 e783422e7c Persist extracted prompt search text 2026-11-10 16:15:30 +05:00
m0rtus59 41d2e38f2f Add cached prompt search text to card state 2026-11-10 16:15:28 +05:00
m0rtus59 56247cfa8b Retain Queue API prompt graph 2026-11-10 16:15:25 +05:00
m0rtus59 328915a7ee Add Queue prompt extraction endpoint 2026-11-10 16:15:22 +05:00
m0rtus59 543e9e999a Reuse graph parser for Queue search 2026-11-10 16:15:11 +05:00
m0rtus59 421361edb8 Merge pull request #4 from m0rtus59/feat/prompt-only-search
Improve gallery prompt search while retaining filename search
2026-11-10 15:54:54 +05:00
m0rtus59 647c95a5df Revert locale-only search label changes 2026-11-10 15:42:06 +05:00
m0rtus59 7421dfa756 Revert locale-only search label changes 2026-11-10 15:42:03 +05:00
m0rtus59 60883fdf63 Revert locale-only search label changes 2026-11-10 15:41:48 +05:00
m0rtus59 b6143a3abf Revert locale-only search label changes 2026-11-10 15:41:45 +05:00
m0rtus59 828d510891 Revert locale-only search label changes 2026-11-10 15:41:43 +05:00
m0rtus59 024309c050 Revert locale-only search label changes 2026-11-10 15:41:40 +05:00
m0rtus59 2e09cf3963 Revert locale-only search label changes 2026-11-10 15:41:37 +05:00
m0rtus59 ef424be4b1 Revert locale-only search label changes 2026-11-10 15:41:35 +05:00
m0rtus59 a1fa872e2e Revert locale-only search label changes 2026-11-10 15:41:32 +05:00
m0rtus59 d31852e5ab Revert locale-only search label changes 2026-11-10 15:41:26 +05:00
m0rtus59 bb98171982 Revert locale-only search label changes 2026-11-10 15:41:23 +05:00
m0rtus59 004744e8e2 Revert locale-only search label changes 2026-11-10 15:41:21 +05:00
m0rtus59 94933a0246 Revert locale-only search label changes 2026-11-10 15:41:19 +05:00
m0rtus59 c942976c02 Revert locale-only search label changes 2026-11-10 15:41:17 +05:00
m0rtus59 0d19394c8f Document filename and prompt metadata search 2026-11-10 15:41:00 +05:00
m0rtus59 4bca1435a6 Restore filename and prompt search labels 2026-11-10 15:40:57 +05:00
m0rtus59 4eb67f25f0 Keep filename matching alongside prompt search 2026-11-10 15:40:55 +05:00
m0rtus59 40a9cf3db9 Merge pull request #3 from m0rtus59/fix/gallery-teardown-leaks
fix: gallery teardown listener cleanup + idempotent overlay.destroy()
2026-11-10 15:03:12 +05:00
Claude ff0466957e feat(gallery): search only generation prompts; scan off the event loop; cache results
- New prompt_search.py (no ComfyUI imports): reads only the PNG 'prompt' chunk (tEXt/zTXt/iTXt), skips the large 'workflow' chunk, and extracts only text that feeds text-encoder nodes (following links to string/primitive/concat nodes). Model names, sampler names, filenames and other widgets are no longer matched, and filenames are no longer searched.
- Per-file results are cached by (mtime, size); repeat searches only touch new/changed files.
- gallery/list runs in a worker thread so large searches don't block ComfyUI's server; the scan stops when the client disconnects.
- Frontend aborts superseded search requests and ignores stale results.
- UI strings/locales and README updated; unit tests in tests/.
2026-11-10 09:49:31 +00:00
Claude 294e610eb1 fix: clean up gallery key/resize listeners on teardown; guard overlay.destroy()
- galleryView: hoist Ctrl/Meta key handlers to module scope and remove them (plus a new blur reset) in teardownGalleryView(); disconnect the ResizeObserver; clear the ctrl-active body class; reset header-reveal state so a re-initialised gallery starts clean.
- overlay: make destroy() idempotent and make updateOverlayBounds() a no-op afterwards, so a pending rAF/ResizeObserver callback can't re-observe elements after disconnect().
2026-11-10 09:32:57 +00:00
M0rtus59 a33052141a overlay jitter fix 2026-11-10 14:25:34 +05:00
M0rtus59 e2a3c0c596 fixes 2026-11-10 14:18:59 +05:00
m0rtus59 557d910ab4 Merge pull request #2 from m0rtus59/feat/layout-independent-hotkeys
Feat/layout independent hotkeys
2026-11-09 18:27:58 +05:00
Claude be5c8c41c9 i18n: add 'Fix non-ASCII keybinds' setting strings to the remaining locales 2026-11-09 13:20:43 +00:00
Claude 408517273a docs/settings: rename toggle to 'Fix non-ASCII keybinds' and document layout-independent hotkeys in the README 2026-11-09 09:06:39 +00:00
Claude e8b6622919 feat: make hotkeys work on any keyboard layout
ComfyUI matches keybindings on KeyboardEvent.key, which follows the active layout (physical Q is 'й' on a Russian layout). Re-dispatch non-ASCII keys as the US-QWERTY key of the same physical position (event.code) in the window capture phase, before the frontend handler runs.

Skips text fields, IME composition, named keys and layouts that already report ASCII. Toggle: 'Hotkeys work on any keyboard layout' (on by default). en/ru strings added.
2026-11-09 08:51:43 +00:00
Claude 4001848e37 refactor: bind Q through extension keybindings instead of a global keydown handler
Makes the shortcut visible and rebindable in the Keybinding panel. Ctrl+Q and Alt+R keep using the keydown handler for now.
2026-11-09 08:51:43 +00:00
Claude 0733d001f3 fix(i18n): remove duplicate 'Classic Layout' key in ru settings (the later entry already won) 2026-11-09 08:51:43 +00:00
m0rtus59 33878502af Merge pull request #1 from m0rtus59/fix-delete-stale-subfolder
Fix delete-file removing the wrong file on stale subfolder
2026-11-09 02:38:44 +05:00
Claude 2c9ded2cbf Fix delete-file removing the wrong file on stale subfolder
resolve_existing_path fell back to <root>/<filename> when
<root>/<subfolder>/<filename> did not exist, so deleting a file that was
already gone could trash a same-named file in the output root. It also
ignored the requested `type`, so type=temp could resolve into output.

Add a strict mode used only by delete-file: exact subfolder match, no root
fallback, and only the root matching the requested type is considered.
Other callers (open-folder, promote-output) keep the old behavior.
2026-11-08 21:18:42 +00:00
m0rtus59 1a848e2b00 theme consistency fix 2026-11-08 20:34:49 +05:00
m0rtus59 700b85408c DRYing 2026-11-08 20:24:09 +05:00
m0rtus59 6a0b0cd71a wtf, single button keybindings don't work in comfy frontend, did nothing to fix it, just removed some junk 2026-11-08 19:16:44 +05:00
m0rtus59 56452eb855 API optimizations 2026-11-08 18:18:50 +05:00
m0rtus59 81d98f908b Improved themes 2026-11-07 21:56:15 +05:00
M0rtus59 cad4098496 UI/UX improvements 2026-11-06 16:35:46 +05:00
m0rtus59 6c3422e5c0 gallery improve nav panel usability 2026-11-05 23:57:51 +05:00
m0rtus59 2554e6dc45 New Gallery sidebar (optional) 2026-11-05 23:20:44 +05:00
71 changed files with 4663 additions and 1088 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
__pycache__/
*.pyc
Thumbs.db
.DS_Store
.DS_Store
desktop.ini
+20 -8
View File
@@ -6,21 +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.
<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.
* **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.
<img src="assets/compare.png" alt="ComfySidebar compare" width="100%">
<img src="assets/compare.png" alt="ComfySidebar compare">
* **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.
<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
@@ -58,24 +66,28 @@ Open **Settings → Comfy Sidebar** to configure:
* Graph button visibility
* Unified top bar layout
* Permanent deletion behavior
* Output Gallery sidebar toggle
* Fix non-ASCII keybinds (hotkeys on any keyboard layout)
## ⌨️ Controls
| Shortcut | Action |
| ------------- | ---------------------------------------------- |
| `Q` | Toggle ComfySidebar |
| `Ctrl+Q` | Exclude the selected node from sidebar results |
| `Alt+R` | Renumber nodes in execution order |
| `Q` | Toggle ComfySidebar* |
| `Ctrl+Q` | Exclude the selected node from sidebar results* |
| `Alt+R` | Renumber nodes in execution order* |
| `Click` | Open result preview |
| `Shift+Click` | Compare another result |
| `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
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
See [LICENSE](LICENSE).
+194 -3
View File
@@ -1,10 +1,14 @@
import asyncio
import os
import platform
import subprocess
import threading
from aiohttp import web
from server import PromptServer
import folder_paths
from . import prompt_search
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
WEB_DIRECTORY = "./js"
@@ -60,10 +64,35 @@ def get_all_candidate_roots(subfolder="", folder_type="output"):
return unique
def resolve_existing_path(filename="", subfolder="", folder_type="output", candidate_roots=None):
"""Finds the actual location of a file or folder across all candidate root directories."""
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.
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)
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:
for root in candidates:
if subfolder:
@@ -117,6 +146,168 @@ def open_file_in_os(target_path):
raise RuntimeError("Headless Linux server detected (no desktop GUI available).")
_GALLERY_EXTS = {
".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif",
".mp4", ".webm", ".mov", ".mkv",
".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac",
".glb", ".gltf", ".obj", ".fbx", ".stl",
".json"
}
def _relative_subfolder(directory, output_dir):
rel_sub = os.path.relpath(directory, output_dir).replace("\\", "/")
return "" if rel_sub == "." else rel_sub
def _list_gallery(subfolder, search, cancel):
"""Blocking directory listing / prompt search. Runs in a worker thread.
Returns (http_status, json_payload).
"""
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)
if not is_path_safe(output_dir, target_dir):
return 403, {"error": "Unsafe path"}
if not os.path.exists(target_dir) or not os.path.isdir(target_dir):
return 200, {"subfolder": subfolder, "folders": [], "files": []}
folders = []
files = []
if search:
# Recursive filename + prompt-text search. Prompt metadata is only read for PNGs.
for root, dirs, fnames in os.walk(target_dir):
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")
async def gallery_list_handler(request):
try:
subfolder = request.query.get("subfolder", "").strip().strip("/\\")
search = request.query.get("search", "").strip().lower()
# Scanning can touch thousands of files: keep it off the event loop so
# ComfyUI's server (websocket, queue updates) stays responsive.
cancel = threading.Event()
loop = asyncio.get_running_loop()
try:
status, payload = await loop.run_in_executor(None, _list_gallery, subfolder, search, cancel)
except asyncio.CancelledError:
cancel.set() # client went away / sent a newer search: stop scanning
raise
return web.json_response(payload, status=status)
except Exception as e:
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")
async def open_folder_handler(request):
try:
@@ -153,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)]
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):
return web.json_response({"error": "File not found or outside output directory"}, status=404)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+9 -5
View File
@@ -1,5 +1,6 @@
import { app } from "/scripts/app.js";
import { SettingIds } from "../core/constants.js";
import { getSetting } from "../core/settings.js";
export function findOriginalPropertiesButton() {
const byTestId = document.querySelector('[data-testid="properties-panel-toggle"]');
@@ -71,6 +72,11 @@ export function findPropertiesPanel() {
}
export function isPropertiesPanelOpen() {
const modernOpen = getSetting("Comfy.RightSidePanel.IsOpen");
if (typeof modernOpen === "boolean") {
return modernOpen;
}
const panel = findPropertiesPanel();
if (panel) return true;
@@ -278,11 +284,9 @@ export function applySidebarOverride() {
try {
const overrideStock = (
app.extensionManager?.setting?.get?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Override Stock History") ??
false
getSetting(SettingIds.OVERRIDE_STOCK_HISTORY) ??
getSetting("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ??
getSetting("Comfy Sidebar.Override Stock History", false)
);
const stdBtn = findStandardQueueButton();
const ourBtn = findOurSidebarButton();
+42 -82
View File
@@ -11,6 +11,7 @@ import {
} from "./adapter.js";
import { SettingIds } from "../core/constants.js";
import { isRuntimePreviewEnabled, setRuntimePreviewEnabled, setRuntimePreviewStateListener } from "../utils/comparison.js";
import { getSetting, setSetting } from "../core/settings.js";
// Listen for runtime preview state changes to keep button UI in sync
setRuntimePreviewStateListener(() => {
@@ -88,20 +89,11 @@ div:has(> [class*="actionbar"]):not(header):not(.topbar):not(.actionbar-containe
export function syncStockHistoryAndProgressSettings(enable) {
const dockedVal = !!enable;
const progressVal = !enable;
const QPOV2_ID = "Comfy.Queue.QPOV2";
const progressKey = "Comfy.Queue.ShowRunProgressBar";
const QPOV2_ID = SettingIds.DOCK_QUEUE || "Comfy.Queue.QPOV2";
const progressKey = SettingIds.SHOW_PROGRESS_BAR || "Comfy.Queue.ShowRunProgressBar";
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set(QPOV2_ID, dockedVal); } catch (e) {}
try { app.extensionManager.setting.set(progressKey, progressVal); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue(QPOV2_ID, dockedVal); } catch (e) {}
try { app.ui.settings.setSettingValue(progressKey, progressVal); } catch (e) {}
}
try { localStorage.setItem(QPOV2_ID, JSON.stringify(dockedVal)); } catch (e) {}
try { localStorage.setItem(progressKey, JSON.stringify(progressVal)); } catch (e) {}
try { window.dispatchEvent(new Event("storage")); } catch (e) {}
setSetting(QPOV2_ID, dockedVal);
setSetting(progressKey, progressVal);
}
let savedButtonData = null;
@@ -128,6 +120,11 @@ function setupSidebarTopObserver() {
}
function isSidebarOnLeft() {
const location = getSetting("Comfy.Sidebar.Location");
if (location === "left" || location === "right") {
return location === "left";
}
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
if (!sidebar) return false;
return sidebar.getBoundingClientRect().left < window.innerWidth / 2;
@@ -366,9 +363,9 @@ function setupCanvasMenuPopoverRelocator() {
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
if (!sidebar || !popovers.length) return;
const sidebarRect = sidebar.getBoundingClientRect();
const btnRect = btn.getBoundingClientRect();
const onLeft = sidebarRect.left < window.innerWidth / 2;
const sidebarRect = sidebar.getBoundingClientRect();
const onLeft = isSidebarOnLeft();
popovers.forEach((pop) => {
if (pop.style.display !== "none" && !pop.classList.contains("p-overlay-hidden")) {
@@ -425,11 +422,7 @@ export function applyClassicLayout(enable, updateSetting = false) {
if (enable) {
if (updateSetting) {
if (app.extensionManager?.setting) {
app.extensionManager.setting.set("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
} else if (app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
}
setSetting("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
}
if (!styleEl) {
styleEl = document.createElement("style");
@@ -453,7 +446,7 @@ export function applyClassicLayout(enable, updateSetting = false) {
}
function syncAvatarVisibility() {
const hideAvatar = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Avatar") ?? false;
const hideAvatar = getSetting("Comfy Sidebar.Hide Junk.Avatar", false);
const STYLE_AVATAR_ID = "comfy-sidebar-hide-avatar-override";
let styleEl = document.getElementById(STYLE_AVATAR_ID);
@@ -501,7 +494,7 @@ function updateSidebarTabsVisibility() {
};
Object.entries(tabSelectors).forEach(([tab, selector]) => {
const shouldHide = app.ui?.settings?.getSettingValue(`Comfy Sidebar.Hide Junk.${tab}`) ?? false;
const shouldHide = getSetting(`Comfy Sidebar.Hide Junk.${tab}`, false);
const target = sidebar.querySelector(selector);
if (target) {
const tabBtn = target.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || target;
@@ -514,11 +507,9 @@ function updateSidebarTabsVisibility() {
});
const hideStockHistory = (
app.extensionManager?.setting?.get?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Override Stock History") ??
false
getSetting(SettingIds.OVERRIDE_STOCK_HISTORY) ??
getSetting("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ??
getSetting("Comfy Sidebar.Override Stock History", false)
);
const historyTarget = sidebar.querySelector('[data-testid="queue-tab-button"], [data-testid="job-history-tab-button"], [class*="lucide--history"]');
if (historyTarget) {
@@ -558,7 +549,7 @@ function cleanHTML(html) {
}
function syncGraphButton() {
const hideGraphBtn = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Graph Button") ?? false;
const hideGraphBtn = getSetting("Comfy Sidebar.Hide Junk.Graph Button", false);
const graphBtn = findGraphButton();
if (graphBtn) {
if (hideGraphBtn) {
@@ -653,11 +644,17 @@ function syncPropertiesButton(isClassicLayoutEnabled) {
customBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const nativeBtn = findOriginalPropertiesButton();
if (nativeBtn) {
nativeBtn.click();
const isOpen = getSetting("Comfy.RightSidePanel.IsOpen");
if (typeof isOpen === "boolean") {
setSetting("Comfy.RightSidePanel.IsOpen", !isOpen);
requestAnimationFrame(() => syncClassicLayout());
} else {
const nativeBtn = findOriginalPropertiesButton();
if (nativeBtn) {
nativeBtn.click();
requestAnimationFrame(() => syncClassicLayout());
}
}
});
@@ -674,7 +671,7 @@ function syncPropertiesButton(isClassicLayoutEnabled) {
}
function syncActiveQueueIndicator(isClassicLayoutEnabled) {
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
const hideQueueIndicator = getSetting("Comfy Sidebar.Hide Junk.Override Stock Job History Tab", false);
const indicator = findActiveQueueIndicator();
if (indicator) {
if (isClassicLayoutEnabled && hideQueueIndicator) {
@@ -693,39 +690,9 @@ export function syncRuntimePreviewButton() {
if (!dock) {
dock = document.createElement("div");
dock.className = "comfy-sidebar-runtime-preview-dock";
Object.assign(dock.style, {
position: "sticky",
bottom: "0px",
left: "0px",
width: "100%",
padding: "8px 12px",
boxSizing: "border-box",
background: "var(--comfy-menu-bg, #181818)",
borderTop: "1px solid var(--border-color, rgba(255, 255, 255, 0.12))",
zIndex: "20",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: "auto"
});
const btn = document.createElement("button");
btn.className = "comfy-sidebar-runtime-preview-btn";
Object.assign(btn.style, {
width: "100%",
height: "34px",
borderRadius: "6px",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
fontSize: "12px",
fontWeight: "600",
fontFamily: "sans-serif",
transition: "all 0.15s ease",
outline: "none"
});
btn.onclick = (e) => {
e.preventDefault();
@@ -741,6 +708,7 @@ export function syncRuntimePreviewButton() {
const btn = dock.querySelector(".comfy-sidebar-runtime-preview-btn");
if (btn) {
const active = isRuntimePreviewEnabled();
btn.classList.toggle("active", active);
const lucidePlay = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><polygon points="6 3 20 12 6 21 6 3"/></svg>`;
const lucideEye = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.7;flex-shrink:0;"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`;
@@ -748,17 +716,9 @@ export function syncRuntimePreviewButton() {
const lucideSquare = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.5;flex-shrink:0;"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>`;
if (active) {
btn.style.background = "rgba(59, 130, 246, 0.18)";
btn.style.border = "1px solid #3b82f6";
btn.style.color = "#93c5fd";
btn.style.boxShadow = "0 0 8px rgba(59, 130, 246, 0.35)";
btn.innerHTML = `<span style="display:flex;align-items:center;gap:7px;">${lucidePlay}<span>Runtime Preview</span></span>${lucideSquareCheck}`;
btn.title = "Runtime Preview is ON (auto-tracking live generation). Click to turn OFF.";
} else {
btn.style.background = "rgba(255, 255, 255, 0.05)";
btn.style.border = "1px solid var(--border-color, rgba(255, 255, 255, 0.15))";
btn.style.color = "var(--desc-color, #aaa)";
btn.style.boxShadow = "none";
btn.innerHTML = `<span style="display:flex;align-items:center;gap:7px;">${lucideEye}<span>Runtime Preview</span></span>${lucideSquare}`;
btn.title = "Click to turn ON Runtime Preview (auto-focus live generation).";
}
@@ -769,24 +729,19 @@ export function syncClassicLayout() {
if (domObserver) domObserver.disconnect();
try {
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
const tabsPos = app.extensionManager?.setting?.get?.("Comfy.Workflow.WorkflowTabsPosition") ??
app.ui?.settings?.getSettingValue("Comfy.Workflow.WorkflowTabsPosition");
const isClassicLayoutEnabled = getSetting("Comfy Sidebar.Comfy Layout", false);
const tabsPos = getSetting("Comfy.Workflow.WorkflowTabsPosition");
// Real-time safeguard: if tabs are in Sidebar, disable Comfy Layout immediately
if (isClassicLayoutEnabled && tabsPos === "Sidebar") {
applyClassicLayout(false, false);
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set("Comfy Sidebar.Comfy Layout", false); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue("Comfy Sidebar.Comfy Layout", false); } catch (e) {}
}
setSetting("Comfy Sidebar.Comfy Layout", false);
return;
}
let canvasControlsMode = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Floating Canvas Controls");
let canvasControlsMode = getSetting("Comfy Sidebar.Hide Junk.Floating Canvas Controls");
if (canvasControlsMode === undefined || canvasControlsMode === null) {
const legacyDock = app.ui?.settings?.getSettingValue("Comfy Sidebar.Dock Canvas Controls");
const legacyDock = getSetting("Comfy Sidebar.Dock Canvas Controls");
canvasControlsMode = legacyDock ? "Dock" : "Default";
} else if (typeof canvasControlsMode === "boolean") {
canvasControlsMode = canvasControlsMode ? "Dock" : "Default";
@@ -961,14 +916,19 @@ export function setupPropertiesPanelToggleFix() {
}
/* Neutralize CanvasModeSelector inner selected backgrounds so idle mode is clean and transparent */
.comfy-sidebar-unfloated-canvas-menu [class*="bg-muted"],
.comfy-sidebar-unfloated-canvas-menu [class*="group-hover:bg-accent"],
.comfy-sidebar-unfloated-canvas-menu > button div,
.comfy-sidebar-unfloated-canvas-menu > div div,
.comfy-sidebar-unfloated-canvas-menu > div button,
.comfy-sidebar-unfloated-canvas-menu > div [role="button"],
.comfy-sidebar-unfloated-canvas-menu [class*="bg-interface-panel-selected-surface"],
.comfy-sidebar-unfloated-canvas-menu [class*="group-hover:bg-interface-button-hover-surface"] {
background: transparent !important;
background-color: transparent !important;
border-radius: 0 !important;
box-shadow: none !important;
padding: 0 !important;
}
/* Compact CanvasModeSelector padding and chevrons to eliminate extra sidebar width and ensure full-width highlight */
@@ -1071,7 +1031,7 @@ export function setupPropertiesPanelToggleFix() {
const isInternalSidebarMutation = (node) => {
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');
};
+2
View File
@@ -25,6 +25,8 @@ export const SettingIds = Object.freeze({
OVERRIDE_STOCK_HISTORY: "Comfy Sidebar.Hide Junk.Override Stock Job History Tab",
HIDE_GRAPH_BUTTON: "Comfy Sidebar.Hide Junk.Graph Button",
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",
DOCK_QUEUE: "Comfy.Queue.QPOV2",
SHOW_PROGRESS_BAR: "Comfy.Queue.ShowRunProgressBar",
+102 -22
View File
@@ -1,14 +1,39 @@
import { app } from "/scripts/app.js";
let currentLocale = "en";
let currentSettingRaw = null;
let loadSeq = 0;
let translations = {};
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) {
if (localeCache.has(lang)) {
return localeCache.get(lang);
}
const data = {};
for (const file of ["main.json", "settings.json"]) {
for (const file of ["main.json"]) {
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);
if (res.ok) {
const json = await res.json();
@@ -16,53 +41,108 @@ async function fetchLocaleFiles(lang) {
}
} catch (e) {}
}
localeCache.set(lang, data);
return data;
}
export async function initI18n() {
const seq = ++loadSeq;
try {
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) {}
}
const raw = (settingVal || "en").toLowerCase().replace("_", "-");
const raw = detectRawLocale();
const candidates = [raw];
const base = raw.split("-")[0];
if (base !== raw) candidates.push(base);
currentSettingRaw = raw;
currentLocale = candidates[0];
console.log("[ComfySidebar] Detected locale:", currentLocale);
// 1. Load English fallback
fallbackTranslations = await fetchLocaleFiles("en");
// 1. Load English fallback once
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") {
for (const cand of candidates) {
const locData = await fetchLocaleFiles(cand);
if (seq !== loadSeq) return;
if (Object.keys(locData).length > 0) {
translations = locData;
console.log(`[ComfySidebar] Loaded ${cand} with ${Object.keys(translations).length} translations`);
nextTranslations = locData;
break;
}
}
}
if (seq !== loadSeq) return;
translations = nextTranslations;
} catch (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 = {}) {
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)) {
str = str.replaceAll(`{${k}}`, v);
}
+1
View File
@@ -32,6 +32,7 @@ export const PersistenceAdapter = {
status: state.status,
images: cleanedImages,
texts: state.texts || [],
promptSearchText: state.promptSearchText,
nodeOutputs: state.nodeOutputs || {},
nodeTitles: state.nodeTitles || {}, // Always preserved across restarts
workflow: includeWorkflow ? state.workflow : null,
+1
View File
@@ -6,6 +6,7 @@ export function createPromptState(pid, initial = {}) {
status: initial.status || PromptStatus.PENDING,
images: Array.isArray(initial.images) ? [...initial.images] : [],
texts: Array.isArray(initial.texts) ? [...initial.texts] : [],
promptSearchText: typeof initial.promptSearchText === "string" ? initial.promptSearchText : null,
nodeOutputs: initial.nodeOutputs ? { ...initial.nodeOutputs } : {},
nodeTitles: initial.nodeTitles ? { ...initial.nodeTitles } : {},
progress: typeof initial.progress === "number" ? initial.progress : 0,
+27
View File
@@ -0,0 +1,27 @@
import { app } from "/scripts/app.js";
/**
* Safely retrieves a setting value from ComfyUI's extension manager or legacy settings.
*/
export function getSetting(id, fallback = undefined) {
const settings = app.extensionManager?.setting;
if (settings?.get) {
const val = settings.get(id);
if (val !== undefined && val !== null) return val;
}
const legacyVal = app.ui?.settings?.getSettingValue?.(id);
if (legacyVal !== undefined && legacyVal !== null) return legacyVal;
return fallback;
}
/**
* Safely sets a setting value across modern extensionManager and legacy UI settings.
*/
export function setSetting(id, value) {
const settings = app.extensionManager?.setting;
if (settings?.set) {
settings.set(id, value);
return;
}
app.ui?.settings?.setSettingValue?.(id, value);
}
-32
View File
@@ -1,32 +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": "إخفاء"
}
-32
View File
@@ -1,32 +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"
}
-32
View File
@@ -1,32 +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"
}
-32
View File
@@ -1,32 +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": "پنهان"
}
-32
View File
@@ -1,32 +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"
}
-32
View File
@@ -1,32 +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": "הסתר"
}
-32
View File
@@ -1,32 +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"
}
-32
View File
@@ -1,32 +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": "非表示"
}
-32
View File
@@ -1,32 +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": "숨기기"
}
-32
View File
@@ -1,32 +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"
}
-33
View File
@@ -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": "Компактный интерфейс",
"Classic Layout": "Компактный интерфейс",
"Hide Junk": "Скрыть лишнее",
"Default": "По умолчанию",
"Dock": "Закрепить",
"Hide": "Скрыть"
}
-32
View File
@@ -1,32 +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"
}
-32
View File
@@ -1,32 +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": "隱藏"
}
-32
View File
@@ -1,32 +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": "隐藏"
}
+3 -2
View File
@@ -6,6 +6,7 @@ import { createPromptState, activatePrompt, updatePromptProgress } from "../core
import { syncQueue, concludeRun, initSessionAndHistory, clearCancelledOrFailed } from "./queueService.js";
import { findImagesInOutputs, findTextsInOutputs } from "../utils/utils.js";
import { updateActiveComparisonPreview, isRuntimePreviewEnabled, focusRuntimePreviewTarget, getActiveComparisonViewer, showFullscreenPreview } from "../utils/comparison.js";
import { getSetting } from "../core/settings.js";
export function setupExecutionTracker(onTargetedProgressUpdate) {
const onStatus = () => syncQueue();
@@ -17,7 +18,7 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
};
const onExecutionStart = (e) => {
const autoClear = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Auto Clear Interrupted") ?? false;
const autoClear = getSetting(SettingIds.AUTO_CLEAR_INTERRUPTED, false);
if (autoClear) {
clearCancelledOrFailed();
}
@@ -70,7 +71,7 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
const onExecuting = (e) => {
const nodeId = e.detail;
const showWorkingNode = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Show Working Node Name") ?? true;
const showWorkingNode = getSetting(SettingIds.SHOW_WORKING_NODE_NAME, true);
const activePid = store.ui.currentlyActivePromptId;
if (activePid && store.hasPrompt(activePid)) {
+2 -1
View File
@@ -25,7 +25,8 @@ export function normalizeQueueItem(p, defaultIndex = 0) {
pid,
seq,
workflow: parseWorkflow(rawWorkflow),
outputs
outputs,
promptGraph: Array.isArray(p) ? (p[2] || null) : (p.prompt || null)
};
}
+65 -13
View File
@@ -1,7 +1,8 @@
import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
import { store } from "../core/store.js";
import { PromptStatus, StoreEvents } from "../core/constants.js";
import { PromptStatus, StoreEvents, SettingIds } from "../core/constants.js";
import { getSetting } from "../core/settings.js";
import { createPromptState, activatePrompt, finalizePrompt } from "../core/promptState.js";
import { normalizeQueueItem, normalizeHistoryItem } from "./queueNormalizer.js";
import { findImagesInOutputs, findTextsInOutputs, getPrimaryOutputImages, getNodeTitle } from "../utils/utils.js";
@@ -18,6 +19,54 @@ function extractNodeTitles(outputs, workflow, promptGraph = null) {
return titles;
}
export function extractPromptOutputs(outputs, workflow, promptGraph = null) {
return {
images: getPrimaryOutputImages(outputs, workflow),
texts: findTextsInOutputs(outputs, workflow),
nodeTitles: extractNodeTitles(outputs, workflow, promptGraph)
};
}
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() {
try {
const q = await api.getQueue();
@@ -26,29 +75,29 @@ export async function syncQueue() {
const runningIds = new Set();
const pendingIds = new Set();
const promptGraphs = [];
runningList.forEach((raw, idx) => {
const item = normalizeQueueItem(raw, idx);
if (!item.pid) return;
runningIds.add(item.pid);
if (item.promptGraph) promptGraphs.push({ pid: item.pid, promptGraph: item.promptGraph });
store.setActivePromptId(item.pid);
const existing = store.getPrompt(item.pid);
const activeWorkflow = item.workflow || (app.graph?.serialize?.() || null);
const titles = extractNodeTitles(item.outputs, activeWorkflow);
if (!existing) {
const seq = store.nextSequence();
const images = getPrimaryOutputImages(item.outputs, activeWorkflow);
const texts = findTextsInOutputs(item.outputs, activeWorkflow);
const { images, texts, nodeTitles } = extractPromptOutputs(item.outputs, activeWorkflow);
const newState = createPromptState(item.pid, {
status: PromptStatus.ACTIVE,
images,
texts,
nodeOutputs: item.outputs,
nodeTitles: titles,
nodeTitles,
progress: 0,
progressText: "Processing...",
timestamp: seq,
@@ -57,6 +106,7 @@ export async function syncQueue() {
});
store.setPrompt(item.pid, newState);
} else {
const titles = extractNodeTitles(item.outputs, activeWorkflow);
if (existing.status !== PromptStatus.ACTIVE) {
activatePrompt(existing, { workflow: activeWorkflow, nodeTitles: titles });
store.updatePrompt(item.pid, existing);
@@ -76,6 +126,7 @@ export async function syncQueue() {
normalizedPending.forEach((item, index) => {
const number = index + 1;
pendingIds.add(item.pid);
if (item.promptGraph) promptGraphs.push({ pid: item.pid, promptGraph: item.promptGraph });
const existing = store.getPrompt(item.pid);
if (!existing) {
@@ -100,6 +151,8 @@ export async function syncQueue() {
}
});
await updatePromptSearchTexts(promptGraphs);
for (const prompt of store.getAllPrompts()) {
if (prompt.status === PromptStatus.PENDING && !pendingIds.has(prompt.pid)) {
store.deletePrompt(prompt.pid);
@@ -145,10 +198,10 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
state.status = norm.status;
const promptGraph = hItem[key].prompt?.[2] || null;
const titles = extractNodeTitles(state.nodeOutputs, state.workflow, promptGraph);
await updatePromptSearchTexts([{ pid: key, promptGraph }]);
const { images: primaryImgs, texts: fetchedTexts, nodeTitles: titles } = extractPromptOutputs(norm.outputs, state.workflow, promptGraph);
Object.assign(state.nodeTitles, titles);
const primaryImgs = getPrimaryOutputImages(norm.outputs, state.workflow);
const hasActualSavedOutputs = primaryImgs.some(img => !img.isFallback);
// If cancelled while sampling, do NOT overwrite with older nodes; keep the exact step preview!
@@ -158,7 +211,6 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
}
}
const fetchedTexts = findTextsInOutputs(norm.outputs, state.workflow);
if (fetchedTexts.length > 0) state.texts = fetchedTexts;
break;
}
@@ -173,7 +225,7 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
}
}
const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64;
const maxItems = getSetting(SettingIds.MAX_HISTORY_ITEMS, 64);
store.pruneHistory(maxItems);
store.updatePrompt(key, state);
syncQueue();
@@ -204,9 +256,7 @@ export async function initSessionAndHistory() {
normalized.sort((a, b) => a.norm.queueNumber - b.norm.queueNumber);
normalized.forEach(({ norm: item, promptGraph }) => {
const images = getPrimaryOutputImages(item.outputs, item.workflow);
const texts = findTextsInOutputs(item.outputs, item.workflow);
const titles = extractNodeTitles(item.outputs, item.workflow, promptGraph);
const { images, texts, nodeTitles: titles } = extractPromptOutputs(item.outputs, item.workflow, promptGraph);
if (images.length === 0 && texts.length === 0 && item.status === PromptStatus.COMPLETED) {
return;
@@ -253,7 +303,9 @@ export async function initSessionAndHistory() {
store.setPrompt(item.pid, newState);
});
const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64;
await updatePromptSearchTexts(normalized.map(({ norm, promptGraph }) => ({ pid: norm.pid, promptGraph })));
const maxItems = getSetting(SettingIds.MAX_HISTORY_ITEMS, 64);
store.pruneHistory(maxItems);
await syncQueue();
} catch (err) {
+147 -134
View File
@@ -2,21 +2,54 @@ import { app } from "/scripts/app.js";
import { SettingIds } from "./core/constants.js";
import { injectStyles } from "./utils/styles.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 { findOurSidebarButton, applySidebarOverride } from "./comfy/adapter.js";
import { applySidebarOverride } from "./comfy/adapter.js";
import { syncAllNodeBadges, setupVueNodeObserver, toggleIgnoreActiveNode } from "./comfy/nodes.js";
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings, destroyLayoutFix } from "./comfy/layout.js";
import { initSessionAndHistory, syncQueue } from "./queue/queueService.js";
import { setupExecutionTracker } from "./queue/executionTracker.js";
import { setupSidebarView, renderSidebar, teardownSidebarView } from "./ui/sidebarView.js";
import { initI18n, t } from "./core/i18n.js";
import { setupGalleryView, teardownGalleryView } from "./ui/galleryView.js";
import { initI18n, t, setupLocaleWatcher, onLocaleChange } from "./core/i18n.js";
import { updateCardProgressTargeted, cardPool } from "./ui/cardRenderer.js";
import { getScrollContainer } from "./utils/utils.js";
import { store } from "./core/store.js";
import { getSetting, setSetting } from "./core/settings.js";
let isInitialized = false;
let activeKeydownHandler = null;
let cleanupFns = [];
function setupKeydownShortcuts() {
activeKeydownHandler = (e) => {
const activeEl = document.activeElement;
if (activeEl && (
activeEl.tagName === "INPUT" ||
activeEl.tagName === "TEXTAREA" ||
activeEl.isContentEditable ||
activeEl.tagName === "SELECT"
)) return;
// Ctrl+Q / Cmd+Q: Toggle Ignore Selected Node
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
toggleIgnoreActiveNode(() => renderSidebar());
}
// Alt+R: Renumber all nodes in execution order
if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
renumberNodesTopologically();
}
};
document.addEventListener("keydown", activeKeydownHandler, true);
}
function registerSettings() {
app.ui.settings.addSetting({
id: SettingIds.GRID_COLUMNS_THRESHOLD,
@@ -46,6 +79,32 @@ function registerSettings() {
defaultValue: false
});
const setGalleryButtonVisible = (visible) => {
try {
const buttons = document.querySelectorAll(".side-bar-button, [data-tab-id]");
for (const btn of buttons) {
if (btn.getAttribute("data-tab-id") === "classic-comfy-gallery" ||
btn.getAttribute("title")?.includes("Gallery") ||
btn.querySelector(".pi-folder")) {
btn.style.display = visible ? "" : "none";
break;
}
}
} catch (e) {
console.warn("[ComfySidebar] Error toggling gallery tab button:", e);
}
};
app.ui.settings.addSetting({
id: "Comfy Sidebar.Enable Gallery",
name: t("Enable Output Gallery Sidebar"),
type: "boolean",
defaultValue: true,
onChange: (val) => {
setGalleryButtonVisible(val ?? true);
}
});
app.ui.settings.addSetting({
id: "Comfy Sidebar.Permanent Delete on Disk Deletion",
name: t("Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)"),
@@ -114,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({
id: SettingIds.CLASSIC_LAYOUT,
name: t("Places the controls and the open workflow tabs on a single unified top bar"),
@@ -125,104 +192,14 @@ function registerSettings() {
}
}
});
}
function setupSettingsDialogObserver() {
const targetTexts = [
"Grid Columns Threshold",
"Show Pending Count Only",
"Show Working Node Name",
"Auto Clear Interrupted",
"Permanent Delete on Disk Deletion",
"Hide Junk",
"Override Stock History",
"Hide Graph Button",
"Hide Avatar",
"Floating Canvas Controls",
"Classic Layout",
"Comfy Layout",
"Default",
"Dock",
"Hide"
];
const translateDOM = (rootEl) => {
if (!rootEl) return;
const walker = document.createTreeWalker(
rootEl,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
const text = node.nodeValue?.trim();
return targetTexts.includes(text) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
}
);
const nodesToTranslate = [];
while (walker.nextNode()) {
nodesToTranslate.push(walker.currentNode);
}
for (const textNode of nodesToTranslate) {
const raw = textNode.nodeValue.trim();
const translated = t(raw);
if (translated && translated !== raw) {
textNode.nodeValue = textNode.nodeValue.replace(raw, translated);
}
}
};
const observer = new MutationObserver(() => {
// 1. Translate headers and selected value inside the Settings dialog
const dialog = document.querySelector('.p-dialog, .comfy-settings-dialog, [role="dialog"]');
if (dialog) {
translateDOM(dialog);
}
// 2. Translate dropdown popup menu items (floated directly on document.body)
const overlay = document.querySelector('.p-select-overlay, .p-dropdown-panel, .p-select-list, [data-pc-name="selectoverlay"]');
if (overlay) {
translateDOM(overlay);
}
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
});
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}
function setupKeydownShortcuts() {
activeKeydownHandler = (e) => {
const activeEl = document.activeElement;
if (activeEl && (
activeEl.tagName === "INPUT" ||
activeEl.tagName === "TEXTAREA" ||
activeEl.isContentEditable ||
activeEl.tagName === "SELECT"
)) return;
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
const ourBtn = findOurSidebarButton();
if (ourBtn) ourBtn.click();
}
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
toggleIgnoreActiveNode(() => renderSidebar());
}
// Alt+R: Renumber all nodes in execution order
if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
renumberNodesTopologically();
}
};
document.addEventListener("keydown", activeKeydownHandler, true);
}
function setupWorkflowTabsPositionWatcher() {
@@ -232,15 +209,11 @@ function setupWorkflowTabsPositionWatcher() {
const checkAndDisableIfSidebar = (val) => {
if (isSyncing) return;
if (val === "Sidebar") {
const isClassicEnabled = app.ui?.settings?.getSettingValue(SettingIds.CLASSIC_LAYOUT) ?? false;
const isClassicEnabled = getSetting(SettingIds.CLASSIC_LAYOUT, false);
if (isClassicEnabled) {
isSyncing = true;
try {
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
}
setSetting(SettingIds.CLASSIC_LAYOUT, false);
applyClassicLayout(false, false);
} finally {
isSyncing = false;
@@ -251,7 +224,6 @@ function setupWorkflowTabsPositionWatcher() {
const cleanups = [];
// 1. Hook the setting's native onChange callback directly (instant execution on click)
const attachNativeOnChange = (settingObj) => {
if (!settingObj || settingObj._classicWatcherAttached) return;
settingObj._classicWatcherAttached = true;
@@ -266,23 +238,19 @@ function setupWorkflowTabsPositionWatcher() {
});
};
// Check existing registered setting in legacy UI and modern extension manager
attachNativeOnChange(app.ui?.settings?.settings?.[TARGET_SETTING]);
attachNativeOnChange(app.extensionManager?.setting?.getSetting?.(TARGET_SETTING));
attachNativeOnChange(app.extensionManager?.setting?.settings?.[TARGET_SETTING]);
// 2. Hook Vue/Pinia reactive store subscription if available (zero polling, synchronous)
const settingStore = app.extensionManager?.setting?.settingStore;
if (settingStore?.$subscribe) {
const unsubscribe = settingStore.$subscribe(() => {
const val = app.extensionManager?.setting?.get?.(TARGET_SETTING) ??
app.ui?.settings?.getSettingValue(TARGET_SETTING);
const val = getSetting(TARGET_SETTING);
checkAndDisableIfSidebar(val);
});
cleanups.push(unsubscribe);
}
// 3. Hook addSetting in case core registers/re-registers the setting later
if (app.ui?.settings?.addSetting) {
const origAdd = app.ui.settings.addSetting.bind(app.ui.settings);
app.ui.settings.addSetting = (setting) => {
@@ -296,7 +264,6 @@ function setupWorkflowTabsPositionWatcher() {
});
}
// 4. Hook API setters as fallback
if (app.ui?.settings?.setSettingValue) {
const origSetSettingValue = app.ui.settings.setSettingValue.bind(app.ui.settings);
app.ui.settings.setSettingValue = (id, value) => {
@@ -332,15 +299,15 @@ app.registerExtension({
registerSettings();
},
commands: [
// Plain Q toggles the Queue tab through the frontend keybinding system (rebindable in Settings > Keybinding).
keybindings: [
{
id: "ComfySidebar.ToggleSidebar",
label: t("Toggle Comfy Queue Sidebar"),
function: () => {
const ourBtn = findOurSidebarButton();
if (ourBtn) ourBtn.click();
}
},
combo: { key: "q" },
commandId: "Workspace.ToggleSidebarTab.classic-comfy-sidebar"
}
],
commands: [
{
id: "ComfySidebar.ToggleIgnoreNode",
label: t("Toggle Ignore Selected Node(s) in Queue"),
@@ -396,23 +363,17 @@ app.registerExtension({
if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return;
await initI18n();
const tabsPos = app.extensionManager?.setting?.get?.("Comfy.Workflow.WorkflowTabsPosition") ??
app.ui.settings.getSettingValue("Comfy.Workflow.WorkflowTabsPosition");
let isClassicLayoutEnabled = app.ui.settings.getSettingValue(SettingIds.CLASSIC_LAYOUT) ?? false;
const tabsPos = getSetting("Comfy.Workflow.WorkflowTabsPosition");
let isClassicLayoutEnabled = getSetting(SettingIds.CLASSIC_LAYOUT, false);
// Startup safeguard: if workflows are set to Sidebar, disable Comfy Layout
if (isClassicLayoutEnabled && tabsPos === "Sidebar") {
isClassicLayoutEnabled = false;
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
}
setSetting(SettingIds.CLASSIC_LAYOUT, false);
}
applyClassicLayout(isClassicLayoutEnabled, false);
const isHistoryOverrideEnabled = app.ui.settings.getSettingValue(SettingIds.OVERRIDE_STOCK_HISTORY) ?? false;
const isHistoryOverrideEnabled = getSetting(SettingIds.OVERRIDE_STOCK_HISTORY, false);
if (isHistoryOverrideEnabled) {
syncStockHistoryAndProgressSettings(true);
}
@@ -424,15 +385,19 @@ app.registerExtension({
cleanupFns.push(setupWorkflowTabsPositionWatcher());
cleanupFns.push(setupDragAndDrop());
cleanupFns.push(setupLatinHotkeys());
cleanupFns.push(setupNewWorkflowQuickPicker());
cleanupFns.push(setupLocaleWatcher());
cleanupFns.push(onLocaleChange(() => {
renderSidebar();
}));
cleanupFns.push(setupVueNodeObserver());
cleanupFns.push(setupSettingsDialogObserver());
// Setup execution tracker with targeted fast-path update for active card
cleanupFns.push(setupExecutionTracker((pid, progress, nodeName) => {
const cardObj = cardPool.get(pid);
const state = store.getPrompt(pid);
if (cardObj && state) {
const showWorkingNode = app.ui.settings.getSettingValue(SettingIds.SHOW_WORKING_NODE_NAME) ?? true;
const showWorkingNode = getSetting(SettingIds.SHOW_WORKING_NODE_NAME, true);
updateCardProgressTargeted(cardObj, progress, nodeName, showWorkingNode, state);
} else {
renderSidebar();
@@ -446,15 +411,58 @@ app.registerExtension({
syncAllNodeBadges();
let queueScrollPos = 0;
let galleryScrollPos = 0;
let activeSidebarTab = "queue";
app.extensionManager.registerSidebarTab({
id: "classic-comfy-sidebar",
icon: "pi pi-images",
title: t("Queue"),
tooltip: `${t("Queue")} (Q)`,
type: "custom",
render: (el) => { el.appendChild(sidebarContainer); }
render: (el) => {
document.querySelectorAll(".comfy-sidebar-scroll-top-btn").forEach(btn => { btn.style.display = "none"; });
const scrollParent = getScrollContainer(el);
if (activeSidebarTab === "gallery" && scrollParent) {
galleryScrollPos = scrollParent.scrollTop;
}
activeSidebarTab = "queue";
el.replaceChildren(sidebarContainer);
requestAnimationFrame(() => {
const sp = getScrollContainer(el);
if (sp) sp.scrollTop = queueScrollPos;
});
}
});
const galleryContainer = setupGalleryView();
app.extensionManager.registerSidebarTab({
id: "classic-comfy-gallery",
icon: "pi pi-folder",
title: t("Gallery"),
tooltip: t("Gallery (Outputs)"),
type: "custom",
render: (el) => {
document.querySelectorAll(".comfy-sidebar-scroll-top-btn").forEach(btn => { btn.style.display = "none"; });
const scrollParent = getScrollContainer(el);
if (activeSidebarTab === "queue" && scrollParent) {
queueScrollPos = scrollParent.scrollTop;
}
activeSidebarTab = "gallery";
el.replaceChildren(galleryContainer);
requestAnimationFrame(() => {
const sp = getScrollContainer(el);
if (sp) sp.scrollTop = galleryScrollPos;
});
}
});
const isGalleryEnabled = getSetting("Comfy Sidebar.Enable Gallery", true);
if (!isGalleryEnabled) {
setTimeout(() => setGalleryButtonVisible(false), 50);
}
requestAnimationFrame(() => {
applySidebarOverride();
});
@@ -466,6 +474,11 @@ app.registerExtension({
}
destroyLayoutFix();
teardownSidebarView();
teardownGalleryView();
if (app.extensionManager?.unregisterSidebarTab) {
app.extensionManager.unregisterSidebarTab("classic-comfy-gallery");
}
if (activeKeydownHandler) {
document.removeEventListener("keydown", activeKeydownHandler, true);
@@ -476,4 +489,4 @@ app.registerExtension({
app.extensionManager.unregisterSidebarTab("classic-comfy-sidebar");
}
}
});
});
+45 -72
View File
@@ -5,7 +5,7 @@ import { store } from "../core/store.js";
import { renderCardImages } from "./mediaPreview.js";
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage, removeImageFromNodeOutputs } from "./mediaActions.js";
import { centerAndSelectCanvasNode } from "../comfy/adapter.js";
import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/utils.js";
import { getRunOutputs, extractWorkflowFromPng, isImageFormat, animateCardRemoval, downloadJson, getScrollContainer, attachDeleteConfirmation } from "../utils/utils.js";
import { showFullscreenPreview } from "../utils/comparison.js";
import { stopAllAudioPlayback } from "./audioController.js";
import { deleteHistoryItem, cancelPendingTask, interruptActive } from "../queue/queueService.js";
@@ -100,13 +100,7 @@ export function syncCardButtonVisibility(cardObj, state) {
alert(t("No workflow metadata found in this file."));
return;
}
const blob = new Blob([JSON.stringify(wf, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `workflow_${state.pid || "comfy"}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1500);
downloadJson(wf, `workflow_${state.pid || "comfy"}.json`);
};
} else {
cardObj.btnJson.style.setProperty("display", "none", "important");
@@ -304,9 +298,7 @@ export function syncCardButtonVisibility(cardObj, state) {
}
}
const scrollEl = document.querySelector('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| store.ui.cardStack?.parentElement
|| store.ui.cardStack;
const scrollEl = getScrollContainer(store.ui.cardStack);
if (scrollEl) {
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
}
@@ -386,18 +378,18 @@ export function getOrCreateCard(state, callbacks = {}) {
const pt = document.createElement("div");
Object.assign(pt.style, {
width: "100%", height: "4px", background: "#333", borderRadius: "2px", marginTop: "8px",
width: "100%", height: "4px", background: "var(--border, #333)", borderRadius: "2px", marginTop: "8px",
overflow: "hidden", display: "none"
});
const pb = document.createElement("div");
Object.assign(pb.style, {
width: "0%", height: "100%", background: "#3b82f6", transition: "width 0.1s linear"
width: "0%", height: "100%", background: "var(--primary, var(--p-primary-color, #ea580c))", transition: "width 0.1s linear"
});
pt.appendChild(pb);
const statusText = document.createElement("div");
Object.assign(statusText.style, {
fontSize: "11px", opacity: "0.9", color: "#3b82f6", textAlign: "center", marginTop: "6px",
fontSize: "11px", opacity: "0.9", color: "var(--primary, var(--p-primary-color, #ea580c))", textAlign: "center", marginTop: "6px",
display: "none", fontWeight: "bold"
});
@@ -503,71 +495,54 @@ export function getOrCreateCard(state, callbacks = {}) {
}
});
let deleteTimeout = null, isDeletePending = false, isDiskDelete = false;
const resetDeleteBtn = () => {
isDeletePending = false;
isDiskDelete = false;
cardObj.btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
cardObj.btnDel.title = t("Delete Card (Ctrl+Click to delete from disk)");
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
attachDeleteConfirmation(btnDel, {
canDiskDelete: () => cardObj.canDeleteFromDisk !== false,
idleTitle: () => cardObj.canDeleteFromDisk ? t("Delete Card (Hold Ctrl to delete file from disk)") : t("Delete Card"),
confirmTitle: t("Click again to confirm removing card"),
confirmDiskTitle: t("Ctrl+Click again to delete file from DISK / TRASH"),
onConfirm: async (shouldDeleteFromDisk) => {
animateCardRemoval(card, async () => {
const liveState = store.getPrompt(state.pid) || state;
btnDel.onclick = async (ev) => {
ev.stopPropagation();
const wantsDiskDelete = (ev.ctrlKey || ev.metaKey) && cardObj.canDeleteFromDisk !== false;
if (shouldDeleteFromDisk) {
const filesToDelete = [];
const seen = new Set();
const addFile = (item) => {
if (item && item.filename) {
const key = `${item.type || ""}/${item.subfolder || ""}/${item.filename}`;
if (!seen.has(key)) {
seen.add(key);
filesToDelete.push(item);
}
}
};
if (!isDeletePending) {
isDeletePending = true;
isDiskDelete = wantsDiskDelete;
cardObj.btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
cardObj.btnDel.title = wantsDiskDelete
? t("Ctrl+Click again to delete file from DISK / TRASH")
: t("Click again to confirm removing card");
deleteTimeout = setTimeout(resetDeleteBtn, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
resetDeleteBtn();
(liveState.images || []).forEach(addFile);
const allOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
allOutputs.forEach(out => (out.images || []).forEach(addFile));
const liveState = store.getPrompt(state.pid) || state;
if (shouldDeleteFromDisk) {
const filesToDelete = [];
const seen = new Set();
const addFile = (item) => {
if (item && item.filename) {
const key = `${item.type || ""}/${item.subfolder || ""}/${item.filename}`;
if (!seen.has(key)) {
seen.add(key);
filesToDelete.push(item);
let deletedCount = 0;
let lastAction = "trashed";
for (const fileItem of filesToDelete) {
const action = await deleteFileOnServer(fileItem);
if (action) {
deletedCount++;
lastAction = action;
}
}
};
(liveState.images || []).forEach(addFile);
const allOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
allOutputs.forEach(out => (out.images || []).forEach(addFile));
let deletedCount = 0;
let lastAction = "trashed";
for (const fileItem of filesToDelete) {
const action = await deleteFileOnServer(fileItem);
if (action) {
deletedCount++;
lastAction = action;
if (deletedCount > 0) {
const msg = lastAction === "trashed"
? `Moved ${deletedCount} file${deletedCount > 1 ? "s" : ""} to Recycle Bin.`
: `Permanently deleted ${deletedCount} file${deletedCount > 1 ? "s" : ""}.`;
showNotification(msg, "success", "Delete Files");
}
}
if (deletedCount > 0) {
const msg = lastAction === "trashed"
? `Moved ${deletedCount} file${deletedCount > 1 ? "s" : ""} to Recycle Bin.`
: `Permanently deleted ${deletedCount} file${deletedCount > 1 ? "s" : ""}.`;
showNotification(msg, "success", "Delete Files");
}
}
deleteHistoryItem(state.pid);
deleteHistoryItem(state.pid);
});
}
};
});
cardPool.set(state.pid, cardObj);
return cardObj;
@@ -643,9 +618,7 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
} else {
cardObj.placeholder.style.display = "none";
renderCardImages(cardObj, state, (st) => {
const scrollEl = document.querySelector('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| store.ui.cardStack?.parentElement
|| store.ui.cardStack;
const scrollEl = getScrollContainer(store.ui.cardStack);
if (scrollEl) {
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
}
+742
View File
@@ -0,0 +1,742 @@
import { app } from "/scripts/app.js";
import { SettingIds, PromptStatus } from "../core/constants.js";
import { t } from "../core/i18n.js";
import { deleteFileOnServer, openFileOrFolder } from "./mediaActions.js";
import { extractWorkflowFromPng, isImageFormat, animateCardRemoval, getScrollContainer, calculateGridColumns, downloadJson, setupSpotlightTracker, attachDeleteConfirmation } from "../utils/utils.js";
import { getSetting } from "../core/settings.js";
import { renderCardImages } from "./mediaPreview.js";
import { showNotification } from "../utils/nodeRenumber.js";
let galleryContainerEl = null;
let cardStackEl = null;
let scrollToTopBtnEl = null;
let currentColumnCount = 1;
let currentSubfolder = "";
let currentSearchQuery = "";
let stickyHeaderGroupEl = null;
let lastScrollTop = 0;
let scrollDeltaAccumulator = 0;
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
let sortOrder = "newest";
try {
sortOrder = localStorage.getItem("ComfySidebar.GallerySortOrder") || "newest";
} catch (e) {}
let cachedFolders = [];
let cachedFiles = [];
let displayedFilesCount = 0;
const BATCH_SIZE = 36;
let isLoading = false;
function formatFileSize(bytes) {
if (!bytes || bytes <= 0) return "";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
}
function updateScrollTopVisibility() {
if (!galleryContainerEl || !galleryContainerEl.isConnected) {
if (scrollToTopBtnEl) scrollToTopBtnEl.style.display = "none";
return;
}
const scrollEl = getScrollContainer(cardStackEl);
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) {
scrollToTopBtnEl.style.display = scrollEl.scrollTop > 200 ? "flex" : "none";
}
// Handle smart scroll-up reveal for header & navigation
handleHeaderScroll(scrollEl);
if (scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 300) {
loadNextBatch();
}
}
}
function handleHeaderScroll(scrollEl) {
if (!stickyHeaderGroupEl) return;
const currentScroll = scrollEl.scrollTop;
const searchContainer = stickyHeaderGroupEl.querySelector(".comfy-sidebar-search-container");
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)";
isHeaderHidden = false;
return;
}
const delta = currentScroll - lastScrollTop;
lastScrollTop = currentScroll;
// Near the very top: always keep visible & transparent
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) {
// Scrolling DOWN: hide after small buffer
if (scrollDeltaAccumulator < 0) scrollDeltaAccumulator = 0;
scrollDeltaAccumulator += delta;
if (scrollDeltaAccumulator > 25 && !isHeaderHidden) {
stickyHeaderGroupEl.style.transform = "translateY(-100%)";
isHeaderHidden = true;
}
} else if (delta < 0) {
// Scrolling UP: reveal as soon as user scrolls up a little bit (~15px)
if (scrollDeltaAccumulator > 0) scrollDeltaAccumulator = 0;
scrollDeltaAccumulator += delta;
if (scrollDeltaAccumulator < -15 && isHeaderHidden) {
stickyHeaderGroupEl.style.transform = "translateY(0)";
isHeaderHidden = false;
}
}
}
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 = "") {
// 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 {
isLoading = true;
const normalized = subfolder.replace(/\\/g, "/");
const queryParams = new URLSearchParams({ subfolder: normalized });
if (searchQuery) queryParams.set("search", searchQuery);
const res = await fetch(`/comfy-sidebar/gallery/list?${queryParams.toString()}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
cachedFolders = data.folders || [];
cachedFiles = data.files || [];
currentSubfolder = (data.subfolder || "").replace(/\\/g, "/");
applySorting();
displayedFilesCount = 0;
} catch (e) {
if (e?.name === "AbortError") return false;
console.error("[ComfySidebar] Failed to load gallery", e);
showNotification(t("Failed to load gallery from disk."), "error");
} finally {
if (galleryFetchController === controller) {
galleryFetchController = null;
isLoading = false;
}
}
return true;
}
function applySorting() {
if (sortOrder === "newest") {
cachedFiles.sort((a, b) => b.mtime - a.mtime);
} else {
cachedFiles.sort((a, b) => a.mtime - b.mtime);
}
}
function mountGalleryCards(cardStack, folderWrapperEl, cardEls, cols = 1, append = false) {
if (!cardStack) return;
const oldEmpty = cardStack.querySelector(".comfy-sidebar-gallery-empty-notice");
if (oldEmpty) oldEmpty.remove();
if (!append) {
let subfolderArea = cardStack.querySelector(".comfy-sidebar-subfolders-area");
if (!subfolderArea) {
subfolderArea = document.createElement("div");
subfolderArea.className = "comfy-sidebar-subfolders-area";
cardStack.insertBefore(subfolderArea, cardStack.firstChild);
}
subfolderArea.innerHTML = "";
if (folderWrapperEl) {
subfolderArea.appendChild(folderWrapperEl);
subfolderArea.style.display = "block";
} else {
subfolderArea.style.display = "none";
}
}
let columnsWrapper = cardStack.querySelector(".comfy-sidebar-masonry-wrapper");
if (!columnsWrapper) {
columnsWrapper = document.createElement("div");
columnsWrapper.className = "comfy-sidebar-masonry-wrapper";
Object.assign(columnsWrapper.style, { display: "flex", gap: "12px", alignItems: "flex-start", width: "100%" });
cardStack.appendChild(columnsWrapper);
}
if (!append && cardEls.length === 0 && !folderWrapperEl) {
columnsWrapper.innerHTML = "";
const emptyEl = document.createElement("div");
emptyEl.className = "comfy-sidebar-card comfy-sidebar-gallery-empty-notice";
Object.assign(emptyEl.style, {
padding: "24px", textAlign: "center", color: "var(--desc-color, #888)", fontSize: "12px", width: "100%"
});
emptyEl.textContent = isLoading ? t("Loading files...") : t("No files found in this folder.");
cardStack.appendChild(emptyEl);
return;
}
while (columnsWrapper.children.length < cols) {
const colDiv = document.createElement("div");
colDiv.className = "comfy-sidebar-masonry-col";
Object.assign(colDiv.style, { flex: "1", display: "flex", flexDirection: "column", gap: "12px", minWidth: "0" });
columnsWrapper.appendChild(colDiv);
}
while (columnsWrapper.children.length > cols) {
columnsWrapper.removeChild(columnsWrapper.lastChild);
}
const colHeights = Array.from(columnsWrapper.children).map(c => c.scrollHeight || 0);
cardEls.forEach((cardEl) => {
let shortestCol = 0;
for (let c = 1; c < cols; c++) {
if (colHeights[c] < colHeights[shortestCol]) shortestCol = c;
}
columnsWrapper.children[shortestCol].appendChild(cardEl);
const cardH = cardEl.offsetHeight > 0 ? cardEl.offsetHeight : 160;
colHeights[shortestCol] += cardH + 12;
});
}
function createFileCard(file) {
const card = document.createElement("div");
card.className = "comfy-sidebar-card completed";
card.style.position = "relative";
card.style.cursor = "pointer";
const previewUrl = `/view?filename=${encodeURIComponent(file.filename)}&type=output&subfolder=${encodeURIComponent(file.subfolder || "")}`;
const isPng = file.filename.toLowerCase().endsWith(".png");
const grid = document.createElement("div");
grid.style.display = "flex";
grid.style.flexDirection = "column";
grid.style.width = "100%";
const cardObj = {
element: card,
grid: grid,
dimEl: null,
currentImageIndex: 0,
firstImgElement: null
};
const fileAsset = {
filename: file.filename,
subfolder: file.subfolder,
type: "output",
url: previewUrl
};
// Render using Queue's exact media pipeline (Images, Videos, 3D, Audio, Files)
renderCardImages(cardObj, {
pid: `gallery-${file.filename}`,
status: PromptStatus.COMPLETED,
images: [fileAsset],
workflow: null
});
const sizeBadge = document.createElement("div");
sizeBadge.className = "comfy-sidebar-card-timer";
sizeBadge.textContent = formatFileSize(file.size);
sizeBadge.style.display = file.size ? "block" : "none";
const isMedia = isImageFormat(file.filename) || /\.(mp4|webm|mov|mkv|avi)$/i.test(file.filename);
let nameBadge = null;
if (isMedia) {
nameBadge = document.createElement("div");
nameBadge.className = "comfy-sidebar-card-filename-badge";
nameBadge.textContent = file.filename;
nameBadge.title = file.filename;
}
const hoverPanel = document.createElement("div");
hoverPanel.className = "comfy-sidebar-hover-panel";
Object.assign(hoverPanel.style, {
position: "absolute", bottom: "4px", right: "4px", flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnDownload = document.createElement("span");
btnDownload.className = "pi pi-download comfy-sidebar-card-action-btn";
btnDownload.title = t("Download Object");
btnDownload.onclick = (ev) => {
ev.stopPropagation();
const a = document.createElement("a");
a.href = previewUrl;
a.download = file.filename;
a.click();
};
let btnJson = null;
if (isPng) {
btnJson = document.createElement("span");
btnJson.className = "pi pi-file comfy-sidebar-card-action-btn";
btnJson.title = t("Download JSON");
btnJson.onclick = async (ev) => {
ev.stopPropagation();
const wf = await extractWorkflowFromPng(previewUrl);
if (!wf) {
alert(t("No workflow metadata found in this file."));
return;
}
downloadJson(wf, `workflow_${file.filename.replace(/\.[^/.]+$/, "")}.json`);
};
}
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del comfy-sidebar-gallery-btn-del";
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = t("Delete File");
btnDel.appendChild(btnDelLabel);
attachDeleteConfirmation(btnDel, {
idleTitle: t("Delete file (Hold Ctrl to delete file from disk)"),
confirmTitle: t("Click again to confirm deleting file"),
confirmDiskTitle: t("Ctrl+Click again to delete file from DISK / TRASH"),
onConfirm: async () => {
const action = await deleteFileOnServer(fileAsset);
if (action) {
const msg = action === "trashed" ? t("Moved file to Recycle Bin.") : t("Permanently deleted file.");
showNotification(msg, "success", t("Delete File"));
cachedFiles = cachedFiles.filter(f => f.filename !== file.filename || f.subfolder !== file.subfolder);
displayedFilesCount = Math.max(0, displayedFilesCount - 1);
animateCardRemoval(card, () => {
card.remove();
if (cachedFiles.length === 0) {
renderGallery();
}
});
}
}
});
hoverPanel.appendChild(btnDownload);
if (btnJson) hoverPanel.appendChild(btnJson);
hoverPanel.appendChild(btnDel);
const leftHoverPanel = document.createElement("div");
leftHoverPanel.className = "comfy-sidebar-left-hover-panel";
Object.assign(leftHoverPanel.style, {
position: "absolute", bottom: "4px", left: "4px", flexDirection: "column", gap: "4px", zIndex: "20"
});
const isImage = isImageFormat(file.filename);
const btnCopy = document.createElement("span");
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.title = t("Copy to Clipboard");
if (!isImage) {
btnCopy.style.display = "none";
} else {
btnCopy.onclick = async (ev) => {
ev.stopPropagation();
const success = await copyImageToClipboard(previewUrl);
if (success) {
btnCopy.className = "pi pi-check comfy-sidebar-card-action-btn";
btnCopy.style.color = "#4ade80";
setTimeout(() => {
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.style.color = "";
}, 1500);
}
};
}
leftHoverPanel.appendChild(btnCopy);
card.append(grid, sizeBadge);
if (nameBadge) card.append(nameBadge);
card.append(hoverPanel, leftHoverPanel);
return card;
}
function updateBreadcrumbUI() {
if (!galleryContainerEl) return;
const breadcrumbBar = galleryContainerEl.querySelector(".comfy-sidebar-breadcrumb-bar");
if (!breadcrumbBar) return;
breadcrumbBar.innerHTML = "";
const rootBtn = document.createElement("span");
rootBtn.className = "comfy-sidebar-breadcrumb-item";
rootBtn.innerHTML = `<i class="pi pi-home" style="margin-right: 4px; font-size: 11px; color: var(--primary, var(--p-primary-color, var(--primary-color, #a855f7)));"></i>output`;
rootBtn.onclick = () => navigateToSubfolder("");
breadcrumbBar.appendChild(rootBtn);
const normalized = (currentSubfolder || "").replace(/\\/g, "/");
if (normalized) {
const parts = normalized.split("/").filter(Boolean);
let accumulated = "";
parts.forEach((part) => {
accumulated = accumulated ? `${accumulated}/${part}` : part;
const targetPath = accumulated;
const sep = document.createElement("span");
sep.className = "comfy-sidebar-breadcrumb-sep";
sep.textContent = "/";
breadcrumbBar.appendChild(sep);
const crumb = document.createElement("span");
crumb.className = "comfy-sidebar-breadcrumb-item";
crumb.textContent = part;
crumb.onclick = () => navigateToSubfolder(targetPath);
breadcrumbBar.appendChild(crumb);
});
}
}
function loadNextBatch() {
if (displayedFilesCount >= cachedFiles.length) return;
const nextFiles = cachedFiles.slice(displayedFilesCount, displayedFilesCount + BATCH_SIZE);
displayedFilesCount += nextFiles.length;
const cardElements = nextFiles.map(file => createFileCard(file));
mountGalleryCards(cardStackEl, null, cardElements, currentColumnCount, true);
}
export function renderGallery() {
if (!galleryContainerEl || !cardStackEl) return;
updateBreadcrumbUI();
let subfolderWrapper = null;
if (cachedFolders.length > 0 && !currentSearchQuery) {
subfolderWrapper = document.createElement("div");
subfolderWrapper.className = "comfy-sidebar-folders-grid";
cachedFolders.forEach(folder => {
const folderCard = document.createElement("div");
folderCard.className = "comfy-sidebar-folder-card";
folderCard.title = folder.name;
const icon = document.createElement("span");
icon.className = "pi pi-folder comfy-sidebar-folder-icon";
const name = document.createElement("span");
name.className = "comfy-sidebar-folder-name";
name.textContent = folder.name;
const ctrlOverlay = document.createElement("div");
ctrlOverlay.className = "comfy-sidebar-ctrl-overlay";
ctrlOverlay.innerHTML = `<i class="pi pi-folder-open"></i><span>${t("Open Folder")}</span>`;
folderCard.appendChild(icon);
folderCard.appendChild(name);
folderCard.appendChild(ctrlOverlay);
folderCard.onclick = async (e) => {
if (e.ctrlKey || e.metaKey) {
e.stopPropagation();
await openFileOrFolder({ subfolder: folder.subfolder, type: "output" });
return;
}
await navigateToSubfolder(folder.subfolder);
};
subfolderWrapper.appendChild(folderCard);
});
}
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
const containerWidth = galleryContainerEl.clientWidth || 0;
if (containerWidth > 0) {
currentColumnCount = calculateGridColumns(containerWidth, threshold);
}
let columnsWrapper = cardStackEl.querySelector(".comfy-sidebar-masonry-wrapper");
if (columnsWrapper) columnsWrapper.innerHTML = "";
const initialFiles = cachedFiles.slice(0, BATCH_SIZE);
displayedFilesCount = initialFiles.length;
const cardElements = initialFiles.map(file => createFileCard(file));
mountGalleryCards(cardStackEl, subfolderWrapper, cardElements, currentColumnCount, false);
updateScrollTopVisibility();
}
async function navigateToSubfolder(subfolder) {
currentSubfolder = (subfolder || "").replace(/\\/g, "/");
currentSearchQuery = "";
await fetchGalleryData(currentSubfolder);
const scrollEl = getScrollContainer(cardStackEl);
if (scrollEl) scrollEl.scrollTop = 0;
renderGallery();
}
export function setupGalleryView() {
const container = document.createElement("div");
galleryContainerEl = container;
Object.assign(container.style, {
display: "flex", flexDirection: "column", height: "100%", padding: "14px", boxSizing: "border-box",
background: "transparent",
color: "var(--fg-color, var(--p-text-color, #eee))", position: "relative"
});
const header = document.createElement("div");
header.className = "comfy-sidebar-header-root";
Object.assign(header.style, { position: "relative", marginBottom: "8px", height: "26px", display: "flex", alignItems: "center" });
const standardHeader = document.createElement("div");
Object.assign(standardHeader.style, { display: "flex", width: "100%", alignItems: "center", justifyContent: "space-between" });
const titleGroup = document.createElement("div");
Object.assign(titleGroup.style, { display: "flex", alignItems: "center", gap: "8px" });
const searchIcon = document.createElement("span");
searchIcon.className = "pi pi-search comfy-sidebar-icon-btn";
searchIcon.title = t("Search Gallery (Filename or Prompt)");
const title = document.createElement("h3");
title.textContent = t("Gallery");
Object.assign(title.style, { margin: "0", fontSize: "14px", fontWeight: "bold", opacity: "0.9", color: "var(--fg-color, #eee)" });
titleGroup.appendChild(searchIcon);
titleGroup.appendChild(title);
standardHeader.appendChild(titleGroup);
const actionsGroup = document.createElement("div");
Object.assign(actionsGroup.style, { display: "flex", gap: "5px", alignItems: "center" });
const createHeaderBtn = (iconClass, tooltip, onClickFn) => {
const btn = document.createElement("button");
btn.className = `${iconClass} comfy-sidebar-header-btn`;
btn.title = tooltip;
btn.onclick = async (ev) => {
ev.stopPropagation();
btn.style.transform = "scale(0.88)";
setTimeout(() => { btn.style.transform = ""; }, 120);
await onClickFn();
};
return btn;
};
const initialSortIcon = sortOrder === "newest" ? "pi-sort-amount-down" : "pi-sort-amount-up";
const initialSortTitle = sortOrder === "newest" ? t("Sort: Newest First") : t("Sort: Oldest First");
const btnSort = createHeaderBtn(`pi ${initialSortIcon}`, initialSortTitle, () => {
sortOrder = sortOrder === "newest" ? "oldest" : "newest";
try { localStorage.setItem("ComfySidebar.GallerySortOrder", sortOrder); } catch (e) {}
btnSort.className = `pi ${sortOrder === "newest" ? "pi-sort-amount-down" : "pi-sort-amount-up"} comfy-sidebar-header-btn`;
btnSort.title = sortOrder === "newest" ? t("Sort: Newest First") : t("Sort: Oldest First");
applySorting();
renderGallery();
});
const btnUp = createHeaderBtn("pi pi-arrow-up", t("Go Up One Level"), async () => {
if (!currentSubfolder) return;
const parts = currentSubfolder.split("/").filter(Boolean);
parts.pop();
await navigateToSubfolder(parts.join("/"));
});
const btnOpenFolder = createHeaderBtn("pi pi-folder-open", t("Open Current Folder in File Manager"), async () => {
await openFileOrFolder({ subfolder: currentSubfolder, type: "output" });
});
const btnRefresh = createHeaderBtn("pi pi-refresh", t("Refresh Gallery"), async () => {
await fetchGalleryData(currentSubfolder, currentSearchQuery);
renderGallery();
});
actionsGroup.append(btnSort, btnUp, btnOpenFolder, btnRefresh);
standardHeader.appendChild(actionsGroup);
const searchContainer = document.createElement("div");
searchContainer.className = "comfy-sidebar-search-container";
Object.assign(searchContainer.style, {
display: "none", width: "100%", alignItems: "center",
background: "var(--card, var(--secondary, var(--comfy-input-bg, #181818)))",
border: "1px solid var(--border, var(--border-color, #555))", borderRadius: "4px", padding: "2px 8px",
boxSizing: "border-box", height: "26px"
});
const sIcon = document.createElement("span");
sIcon.className = "pi pi-search";
Object.assign(sIcon.style, { fontSize: "11px", opacity: "0.6", marginRight: "6px", transition: "transform 0.2s" });
const sInput = document.createElement("input");
Object.assign(sInput, { type: "text", placeholder: t("Search filenames or prompt metadata...") });
Object.assign(sInput.style, {
flex: "1", background: "transparent", border: "none", outline: "none",
color: "var(--comfy-input-color, var(--fg-color, #eee))", fontSize: "11px", padding: "0"
});
const sClear = document.createElement("span");
sClear.className = "pi pi-times comfy-sidebar-icon-btn";
sClear.title = t("Clear & Close Search");
Object.assign(sClear.style, { marginLeft: "6px" });
searchContainer.append(sIcon, sInput, sClear);
header.append(standardHeader, searchContainer);
const stickyHeaderGroup = document.createElement("div");
stickyHeaderGroup.className = "comfy-sidebar-sticky-header-group";
stickyHeaderGroupEl = stickyHeaderGroup;
stickyHeaderGroup.appendChild(header);
let scrollBeforeSearch = 0;
searchIcon.onclick = (e) => {
e.stopPropagation();
const scrollEl = getScrollContainer(cardStackEl);
scrollBeforeSearch = scrollEl ? scrollEl.scrollTop : 0;
standardHeader.style.display = "none";
searchContainer.style.display = "flex";
sInput.focus();
};
let searchDebounce = null;
let searchSeq = 0;
const closeSearch = () => {
const hadFilter = Boolean(currentSearchQuery || sInput.value.trim());
sInput.value = "";
currentSearchQuery = "";
sIcon.className = "pi pi-search";
searchContainer.style.display = "none";
standardHeader.style.display = "flex";
// If the user didn't type a search query, do NOT re-fetch or wipe cards.
// The scroll position stays right where you are.
if (hadFilter) {
fetchGalleryData(currentSubfolder, "").then(() => {
renderGallery();
const scrollEl = getScrollContainer(cardStackEl);
if (scrollEl && scrollBeforeSearch > 0) {
scrollEl.scrollTop = scrollBeforeSearch;
requestAnimationFrame(() => { scrollEl.scrollTop = scrollBeforeSearch; });
}
});
}
};
sClear.onclick = (e) => { e.stopPropagation(); closeSearch(); };
sInput.onkeydown = (e) => { if (e.key === "Escape") closeSearch(); };
sInput.oninput = () => {
currentSearchQuery = sInput.value.trim();
if (searchDebounce) clearTimeout(searchDebounce);
sIcon.className = "pi pi-spin pi-spinner";
const seq = ++searchSeq;
searchDebounce = setTimeout(async () => {
let applied = false;
try {
applied = (await fetchGalleryData(currentSubfolder, currentSearchQuery)) !== false;
if (applied) renderGallery();
} finally {
// Only the latest search may stop the spinner
if (seq === searchSeq) sIcon.className = "pi pi-search";
}
}, 350);
};
const breadcrumbBar = document.createElement("div");
breadcrumbBar.className = "comfy-sidebar-breadcrumb-bar";
stickyHeaderGroup.appendChild(breadcrumbBar);
container.appendChild(stickyHeaderGroup);
const cardStack = document.createElement("div");
cardStackEl = cardStack;
Object.assign(cardStack.style, {
flex: "1", overflowY: "visible", display: "flex", flexDirection: "column", paddingBottom: "46px"
});
container.appendChild(cardStack);
scrollToTopBtnEl = document.createElement("button");
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
scrollToTopBtnEl.title = t("Scroll to Top");
scrollToTopBtnEl.onclick = (e) => {
e.stopPropagation();
const scrollEl = getScrollContainer(cardStackEl);
if (scrollEl) scrollEl.scrollTo({ top: 0, behavior: "smooth" });
};
container.appendChild(scrollToTopBtnEl);
window.addEventListener("keydown", handleCtrlDown);
window.addEventListener("keyup", handleCtrlUp);
window.addEventListener("blur", handleCtrlBlur);
// Reveal Spotlight: track cursor position over Gallery card and folder borders
setupSpotlightTracker(container, ".comfy-sidebar-card, .comfy-sidebar-folder-card");
document.addEventListener("scroll", updateScrollTopVisibility, { capture: true, passive: true });
galleryResizeObserver = new ResizeObserver((entries) => {
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
const cols = calculateGridColumns(entries[0].contentRect.width, threshold);
if (cols !== currentColumnCount) {
currentColumnCount = cols;
renderGallery();
}
});
galleryResizeObserver.observe(container);
fetchGalleryData("").then(() => renderGallery());
return container;
}
export function teardownGalleryView() {
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 = [];
cachedFiles = [];
stickyHeaderGroupEl = null;
galleryContainerEl = null;
cardStackEl = null;
if (scrollToTopBtnEl && scrollToTopBtnEl.parentNode) {
scrollToTopBtnEl.parentNode.removeChild(scrollToTopBtnEl);
}
scrollToTopBtnEl = null;
}
+4 -4
View File
@@ -1,3 +1,5 @@
import { getSetting } from "../core/settings.js";
export async function copyImageToClipboard(src) {
try {
const res = await fetch(src);
@@ -24,8 +26,6 @@ export async function copyImageToClipboard(src) {
}
}
import { app } from "/scripts/app.js";
export async function openFileOrFolder(img) {
if (!img) return;
@@ -54,7 +54,7 @@ export async function openFileOrFolder(img) {
export async function deleteFileOnServer(fileItem) {
if (!fileItem || !fileItem.filename) return null;
const isPermanent = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Permanent Delete on Disk Deletion") ?? false;
const isPermanent = getSetting("Comfy Sidebar.Permanent Delete on Disk Deletion", false);
try {
const res = await fetch("/comfy-sidebar/delete-file", {
method: "POST",
@@ -124,4 +124,4 @@ export function findNodeIdForImage(state, img) {
}
}
return null;
}
}
+173 -147
View File
@@ -5,7 +5,8 @@ import { t } from "../core/i18n.js";
import { getOrCreateCard, updateCardDOM, resetAllCardHoverStates, cardPool } from "./cardRenderer.js";
import { clearCancelledOrFailed, clearAllHistory, cancelAllPending } from "../queue/queueService.js";
import { updateSidebarBadge, centerAndSelectCanvasNode } from "../comfy/adapter.js";
import { matchesFilter, getRunOutputs, isImageFormat } from "../utils/utils.js";
import { matchesFilter, getRunOutputs, isImageFormat, animateCardRemoval, getScrollContainer, calculateGridColumns, setupSpotlightTracker, attachDeleteConfirmation } from "../utils/utils.js";
import { getSetting } from "../core/settings.js";
import { renderCardImages } from "./mediaPreview.js";
import { deleteFileOnServer, removeImageFromNodeOutputs, copyImageToClipboard, findNodeIdForImage } from "./mediaActions.js";
import { toggleIgnoreActiveNode } from "../comfy/nodes.js";
@@ -16,6 +17,10 @@ let globalClickRegistered = false;
let renderAnimationFrameId = null;
let activeTimerInterval = null;
let currentColumnCount = 1;
let stickyHeaderGroupEl = null;
let lastScrollTop = 0;
let scrollDeltaAccumulator = 0;
let isHeaderHidden = false;
function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
if (!cardStack) return;
@@ -90,16 +95,65 @@ function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
}
}
function getScrollContainer() {
if (!store.ui.cardStack) return null;
return store.ui.cardStack.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| store.ui.cardStack.parentElement
|| store.ui.cardStack;
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() {
if (!scrollToTopBtnEl || !store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) return;
const scrollEl = getScrollContainer();
if (!scrollToTopBtnEl) return;
if (!store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) {
scrollToTopBtnEl.style.display = "none";
return;
}
const scrollEl = getScrollContainer(store.ui.cardStack);
if (scrollEl) {
const viewportEl = scrollEl.parentElement || store.ui.sidebarContainer;
if (viewportEl && scrollToTopBtnEl.parentNode !== viewportEl) {
@@ -114,6 +168,8 @@ function updateScrollTopBtnVisibility() {
} else {
scrollToTopBtnEl.style.display = "none";
}
handleQueueHeaderScroll(scrollEl);
}
}
@@ -162,7 +218,8 @@ export function setupSidebarView() {
Object.assign(container.style, {
display: "flex", flexDirection: "column", height: "100%", padding: "14px", boxSizing: "border-box",
background: "var(--comfy-menu-bg, #121212)", color: "var(--fg-color, #eee)", position: "relative"
background: "transparent",
color: "var(--fg-color, var(--p-text-color, #eee))", position: "relative"
});
const header = document.createElement("div");
@@ -213,8 +270,8 @@ export function setupSidebarView() {
const reset = () => {
isPending = false;
Object.assign(btn.style, {
color: "var(--desc-color, #aaa)", background: "transparent",
borderColor: "var(--border-color, #555)", boxShadow: "none"
color: "", background: "",
borderColor: "", boxShadow: ""
});
if (timeout) { clearTimeout(timeout); timeout = null; }
};
@@ -257,7 +314,7 @@ export function setupSidebarView() {
const divider = document.createElement("div");
Object.assign(divider.style, {
width: "1px", height: "14px", background: "var(--border-color, #444)",
width: "1px", height: "14px", background: "var(--border, var(--border-color, #444))",
margin: "0 2px", opacity: "0.6"
});
@@ -281,8 +338,9 @@ export function setupSidebarView() {
const searchContainer = document.createElement("div");
searchContainer.className = "comfy-sidebar-search-container";
Object.assign(searchContainer.style, {
display: "none", width: "100%", alignItems: "center", background: "var(--comfy-input-bg, #181818)",
border: "1px solid var(--border-color, #555)", borderRadius: "4px", padding: "2px 8px",
display: "none", width: "100%", alignItems: "center",
background: "var(--card, var(--secondary, var(--comfy-input-bg, #181818)))",
border: "1px solid var(--border, var(--border-color, #555))", borderRadius: "4px", padding: "2px 8px",
boxSizing: "border-box", height: "26px"
});
const searchInputIcon = document.createElement("span");
@@ -305,7 +363,12 @@ export function setupSidebarView() {
header.appendChild(standardHeader);
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) => {
e.stopPropagation();
@@ -336,7 +399,7 @@ export function setupSidebarView() {
overflowY: "visible",
display: "flex",
flexDirection: "column",
paddingBottom: "28px"
paddingBottom: "46px"
});
container.appendChild(cardStack);
@@ -346,17 +409,20 @@ export function setupSidebarView() {
scrollToTopBtnEl.addEventListener("click", (e) => {
e.stopPropagation();
const scrollEl = getScrollContainer();
const scrollEl = getScrollContainer(store.ui.cardStack);
if (scrollEl) scrollEl.scrollTo({ top: 0, behavior: "smooth" });
});
container.appendChild(scrollToTopBtnEl);
// Reveal Spotlight: track cursor position over Queue card borders
setupSpotlightTracker(container, ".comfy-sidebar-card");
document.addEventListener("scroll", updateScrollTopBtnVisibility, { capture: true, passive: true });
new ResizeObserver((entries) => {
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
const cols = Math.max(1, Math.floor(entries[0].contentRect.width / (threshold / 2)));
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
const cols = calculateGridColumns(entries[0].contentRect.width, threshold);
if (cols !== currentColumnCount) {
currentColumnCount = cols;
renderSidebar();
@@ -395,8 +461,8 @@ export function renderSidebar() {
renderAnimationFrameId = requestAnimationFrame(() => {
if (!store.ui.sidebarContainer || !store.ui.cardStack) return;
const showPendingSummary = app.ui?.settings?.getSettingValue?.(SettingIds.SHOW_PENDING_COUNT_ONLY) ?? true;
const showWorkingNode = app.ui?.settings?.getSettingValue?.(SettingIds.SHOW_WORKING_NODE_NAME) ?? true;
const showPendingSummary = getSetting(SettingIds.SHOW_PENDING_COUNT_ONLY, true);
const showWorkingNode = getSetting(SettingIds.SHOW_WORKING_NODE_NAME, true);
const headerTitle = store.ui.sidebarContainer.querySelector("h3");
const headerSearchIcon = store.ui.sidebarContainer.querySelector(".comfy-sidebar-header-root .pi-search");
@@ -481,79 +547,60 @@ export function renderSidebar() {
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
btnDel.title = t("Remove from batch (Hold Ctrl to delete file from disk)");
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = t("Delete File");
btnDel.appendChild(btnDelLabel);
let isDeletePending = false, isDiskDelete = false, deleteTimeout = null;
const resetDelete = () => {
isDeletePending = false;
isDiskDelete = false;
btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
btnDel.title = t("Remove from batch (Hold Ctrl to delete file from disk)");
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
btnDel.onclick = async (ev) => {
ev.stopPropagation();
const wantsDiskDelete = ev.ctrlKey || ev.metaKey;
if (!isDeletePending) {
isDeletePending = true;
isDiskDelete = wantsDiskDelete;
cardObj.btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
cardObj.btnDel.title = wantsDiskDelete
? t("Ctrl+Click again to delete file from DISK / TRASH")
: t("Click again to confirm removing from batch");
deleteTimeout = setTimeout(resetDelete, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
resetDelete();
if (shouldDeleteFromDisk && img && img.filename) {
const action = await deleteFileOnServer(img);
if (action) {
const msg = action === "trashed" ? t("Moved file to Recycle Bin.") : t("Permanently deleted file.");
showNotification(msg, "success", t("Delete File"));
attachDeleteConfirmation(btnDel, {
idleTitle: t("Remove from batch (Hold Ctrl to delete file from disk)"),
confirmTitle: t("Click again to confirm removing from batch"),
confirmDiskTitle: t("Ctrl+Click again to delete file from DISK / TRASH"),
onConfirm: async (shouldDeleteFromDisk) => {
animateCardRemoval(card, async () => {
if (shouldDeleteFromDisk && img && img.filename) {
const action = await deleteFileOnServer(img);
if (action) {
const msg = action === "trashed" ? t("Moved file to Recycle Bin.") : t("Permanently deleted file.");
showNotification(msg, "success", t("Delete File"));
}
}
}
const pid = batchInfo.parentPromptId || batchInfo.pid;
const state = store.getPrompt(pid);
const imgIdx = batchInfo.images.indexOf(img);
if (imgIdx > -1) batchInfo.images.splice(imgIdx, 1);
const pid = batchInfo.parentPromptId || batchInfo.pid;
const state = store.getPrompt(pid);
const imgIdx = batchInfo.images.indexOf(img);
if (imgIdx > -1) batchInfo.images.splice(imgIdx, 1);
if (state) {
if (Array.isArray(state.images)) {
const stIdx = state.images.findIndex(i => i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""));
if (stIdx > -1) state.images.splice(stIdx, 1);
if (state) {
if (Array.isArray(state.images)) {
const stIdx = state.images.findIndex(i => i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""));
if (stIdx > -1) state.images.splice(stIdx, 1);
}
if (state.nodeOutputs) {
removeImageFromNodeOutputs(state.nodeOutputs, img);
}
state.rendered = false;
store.updatePrompt(pid, state);
}
if (state.nodeOutputs) {
removeImageFromNodeOutputs(state.nodeOutputs, img);
const mainCardObj = cardPool.get(pid);
if (mainCardObj) {
mainCardObj.currentImageIndex = 0;
mainCardObj.lastImagesSignature = "";
}
state.rendered = false;
store.updatePrompt(pid, state);
}
const mainCardObj = cardPool.get(pid);
if (mainCardObj) {
mainCardObj.currentImageIndex = 0;
mainCardObj.lastImagesSignature = "";
}
if (batchInfo.images.length === 0) {
if (batchInfo.parentPromptId) {
store.openOutputsSubmenu(batchInfo.parentPromptId);
} else {
store.closeSubmenu();
if (batchInfo.images.length === 0) {
if (batchInfo.parentPromptId) {
store.openOutputsSubmenu(batchInfo.parentPromptId);
} else {
store.closeSubmenu();
}
}
}
renderSidebar();
renderSidebar();
});
}
};
});
hoverPanel.append(btnImg, btnDel);
@@ -634,7 +681,7 @@ export function renderSidebar() {
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
const scrollEl = getScrollContainer();
const scrollEl = getScrollContainer(store.ui.cardStack);
if (scrollEl) scrollEl.scrollTop = 0;
updateScrollTopBtnVisibility();
return;
@@ -752,68 +799,49 @@ export function renderSidebar() {
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
btnDel.title = t("Delete node output (Hold Ctrl to delete file from disk)");
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = t("Delete File");
btnDel.appendChild(btnDelLabel);
let isDeletePending = false, isDiskDelete = false, deleteTimeout = null;
const resetDelete = () => {
isDeletePending = false;
isDiskDelete = false;
btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
btnDel.title = t("Delete node output (Hold Ctrl to delete file from disk)");
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
btnDel.onclick = async (ev) => {
ev.stopPropagation();
const wantsDiskDelete = ev.ctrlKey || ev.metaKey;
if (!isDeletePending) {
isDeletePending = true;
isDiskDelete = wantsDiskDelete;
btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
btnDel.title = wantsDiskDelete
? t("Ctrl+Click again to delete this node's files from DISK / TRASH")
: t("Click again to confirm removing output");
deleteTimeout = setTimeout(resetDelete, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
resetDelete();
if (shouldDeleteFromDisk && out.images) {
let count = 0;
let lastAct = "trashed";
for (const imgItem of out.images) {
const action = await deleteFileOnServer(imgItem);
if (action) { count++; lastAct = action; }
attachDeleteConfirmation(btnDel, {
idleTitle: t("Delete node output (Hold Ctrl to delete file from disk)"),
confirmTitle: t("Click again to confirm removing output"),
confirmDiskTitle: t("Ctrl+Click again to delete this node's files from DISK / TRASH"),
onConfirm: async (shouldDeleteFromDisk) => {
animateCardRemoval(card, async () => {
if (shouldDeleteFromDisk && out.images) {
let count = 0;
let lastAct = "trashed";
for (const imgItem of out.images) {
const action = await deleteFileOnServer(imgItem);
if (action) { count++; lastAct = action; }
}
if (count > 0) {
const msg = lastAct === "trashed"
? `Moved ${count} file${count > 1 ? "s" : ""} to Recycle Bin.`
: `Permanently deleted ${count} file${count > 1 ? "s" : ""}.`;
showNotification(msg, "success", "Delete Files");
}
}
if (count > 0) {
const msg = lastAct === "trashed"
? `Moved ${count} file${count > 1 ? "s" : ""} to Recycle Bin.`
: `Permanently deleted ${count} file${count > 1 ? "s" : ""}.`;
showNotification(msg, "success", "Delete Files");
if (st.nodeOutputs && st.nodeOutputs[out.nodeId]) {
delete st.nodeOutputs[out.nodeId];
}
}
if (st.nodeOutputs && st.nodeOutputs[out.nodeId]) {
delete st.nodeOutputs[out.nodeId];
}
const remainingOutputs = getRunOutputs(st.nodeOutputs, st.workflow);
if (remainingOutputs.length > 0) {
st.images = remainingOutputs[remainingOutputs.length - 1].images || [];
} else {
st.images = [];
store.closeSubmenu();
}
const remainingOutputs = getRunOutputs(st.nodeOutputs, st.workflow);
if (remainingOutputs.length > 0) {
st.images = remainingOutputs[remainingOutputs.length - 1].images || [];
} else {
st.images = [];
store.closeSubmenu();
}
store.updatePrompt(st.pid, st);
renderSidebar();
store.updatePrompt(st.pid, st);
renderSidebar();
});
}
};
});
hoverPanel.append(btnImg, btnDel);
@@ -887,7 +915,7 @@ export function renderSidebar() {
nodeOutputs: st.nodeOutputs
}, () => {
// Clicking (1/N) opens batch drill-down for this specific intermediate node
const scrollEl = getScrollContainer();
const scrollEl = getScrollContainer(store.ui.cardStack);
if (scrollEl) {
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
}
@@ -956,18 +984,15 @@ export function renderSidebar() {
if (!pCard) {
const el = document.createElement("div");
Object.assign(el.style, {
background: "var(--comfy-input-bg, #181818)", border: "2px solid #6c757d", borderRadius: "4px", padding: "10px",
background: "var(--card, var(--secondary, var(--comfy-input-bg, #181818)))", border: "2px solid var(--border, #6c757d)", borderRadius: "4px", padding: "10px",
textAlign: "center", fontSize: "12px", fontWeight: "bold",
color: "var(--desc-color, #aaa)", display: "flex", flexDirection: "column", gap: "8px"
color: "var(--muted-foreground, var(--desc-color, #aaa))", display: "flex", flexDirection: "column", gap: "8px"
});
const textDiv = document.createElement("div");
el.appendChild(textDiv);
const cancelBtn = document.createElement("button");
cancelBtn.className = "comfy-sidebar-cancel-all-btn";
cancelBtn.textContent = t("Cancel All Pending");
Object.assign(cancelBtn.style, {
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "4px",
cursor: "pointer", fontSize: "11px", fontWeight: "bold"
});
cancelBtn.onclick = async () => {
await cancelAllPending();
};
@@ -981,11 +1006,8 @@ export function renderSidebar() {
let btnCard = cardPool.get("pending-cancel-all-standalone");
if (!btnCard) {
const btn = document.createElement("button");
btn.className = "comfy-sidebar-cancel-all-btn";
btn.textContent = t("Cancel All Pending");
Object.assign(btn.style, {
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "6px",
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%"
});
btn.onclick = async () => {
await cancelAllPending();
};
@@ -1000,10 +1022,10 @@ export function renderSidebar() {
activeTasks.forEach(st => cardElements.push(syncTaskCard(st)));
completedTasks.forEach(st => cardElements.push(syncTaskCard(st)));
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
const containerWidth = store.ui.sidebarContainer?.clientWidth || 0;
if (containerWidth > 0) {
currentColumnCount = Math.max(1, Math.floor(containerWidth / (threshold / 2)));
currentColumnCount = calculateGridColumns(containerWidth, threshold);
}
mountCards(store.ui.cardStack, bannerElements, cardElements, currentColumnCount);
@@ -1011,7 +1033,7 @@ export function renderSidebar() {
if (store.ui.mainQueueScrollTop !== null) {
const restorePos = store.ui.mainQueueScrollTop;
store.ui.mainQueueScrollTop = null;
const scrollEl = getScrollContainer();
const scrollEl = getScrollContainer(store.ui.cardStack);
if (scrollEl) {
scrollEl.scrollTop = restorePos;
requestAnimationFrame(() => { scrollEl.scrollTop = restorePos; });
@@ -1038,4 +1060,8 @@ export function teardownSidebarView() {
renderAnimationFrameId = null;
}
cardPool.clear();
if (scrollToTopBtnEl && scrollToTopBtnEl.parentNode) {
scrollToTopBtnEl.parentNode.removeChild(scrollToTopBtnEl);
}
scrollToTopBtnEl = null;
}
+28 -15
View File
@@ -328,6 +328,7 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
const createMediaElement = (src, muted = false) => {
const isVideo = isVideoFormat(src);
const el = isVideo ? document.createElement("video") : document.createElement("img");
el.className = "comfy-sidebar-comparison-media";
if (isVideo) {
el.muted = muted;
el.playsInline = true;
@@ -345,7 +346,10 @@ const createMediaElement = (src, muted = false) => {
pointerEvents: isVideo ? "none" : "auto",
userSelect: "none",
webkitUserSelect: "none",
display: "block"
display: "block",
border: "none",
outline: "none",
boxShadow: "none"
});
el.draggable = false;
el.ondragstart = (e) => e.preventDefault();
@@ -367,11 +371,11 @@ const setupVideoPlayback = (vid, container) => {
Object.assign(controlBar.style, {
position: "absolute", bottom: "16px", left: "50%",
display: "flex", alignItems: "center", gap: "12px",
background: "var(--comfy-input-bg, rgba(18, 18, 18, 0.9))",
border: "1px solid var(--border-color, rgba(255, 255, 255, 0.15))",
background: "color-mix(in srgb, var(--card, var(--secondary, #181818)) 90%, transparent)",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
padding: "8px 16px", borderRadius: "8px", zIndex: "40", fontSize: "11px",
fontFamily: "monospace",
color: "var(--fg-color, #eee)",
color: "var(--foreground, var(--fg-color, #eee))",
width: "80%", maxWidth: "500px",
boxShadow: "0 4px 12px rgba(0,0,0,0.3)", transform: "translate3d(-50%, 0, 0)",
pointerEvents: "auto"
@@ -398,7 +402,7 @@ const setupVideoPlayback = (vid, container) => {
position: "relative", cursor: "pointer"
});
const scrubberFill = document.createElement("div");
Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#3b82f6", borderRadius: "2px" });
Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "var(--primary, var(--p-primary-color, #38bdf8))", borderRadius: "2px" });
scrubberContainer.appendChild(scrubberFill);
const timeLabel = document.createElement("span");
@@ -492,12 +496,12 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
Object.assign(header.style, {
position: "absolute", top: "16px", left: "50%", transform: "translateX(-50%)",
display: "flex", gap: "16px", zIndex: "30",
color: "var(--desc-color, #aaa)",
color: "var(--muted-foreground, var(--desc-color, #aaa))",
fontSize: "12px",
fontFamily: "sans-serif", pointerEvents: "none",
background: "var(--comfy-input-bg, rgba(18, 18, 18, 0.85))",
border: "1px solid var(--border-color, rgba(255, 255, 255, 0.15))",
padding: "6px 14px", borderRadius: "6px", backdropFilter: "blur(6px)",
background: "color-mix(in srgb, var(--card, var(--secondary, #181818)) 90%, transparent)",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
padding: "6px 14px", borderRadius: "6px", backdropFilter: "blur(8px)",
boxShadow: "0 2px 8px rgba(0,0,0,0.3)", maxWidth: "85%", textAlign: "center"
});
const infoText = document.createElement("span");
@@ -546,7 +550,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
to { transform: rotate(360deg); }
}
</style>
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="animation: comfy-sidebar-spin 1.3s linear infinite; filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.45));">
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color: var(--primary, var(--p-primary-color, #38bdf8)); animation: comfy-sidebar-spin 1.3s linear infinite; filter: drop-shadow(0 0 12px rgba(168, 85, 247, 0.45));">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
`;
@@ -576,21 +580,30 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
}
const slider = document.createElement("div");
slider.className = "comfy-sidebar-comparison-slider";
Object.assign(slider.style, {
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",
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");
sliderButton.className = "comfy-sidebar-comparison-handle";
Object.assign(sliderButton.style, {
position: "absolute", top: "50%", left: "50%",
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",
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);
wrapper.appendChild(slider);
+126
View File
@@ -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);
};
}
+539
View File
@@ -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();
};
}
+23 -7
View File
@@ -83,7 +83,13 @@ export class SidebarOverlay {
// Live observer for opening/closing panels or layout changes
if (window.MutationObserver) {
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) {
updateScheduled = true;
requestAnimationFrame(() => {
@@ -118,6 +124,8 @@ export class SidebarOverlay {
}
updateOverlayBounds() {
// A pending rAF/ResizeObserver callback can fire after destroy(); don't re-attach observers
if (this.destroyed) return;
let leftOffset = 0;
let rightOffset = 0;
let topOffset = 0;
@@ -126,6 +134,18 @@ export class SidebarOverlay {
const midX = winW / 2;
try {
// 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']");
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
const r = bar.getBoundingClientRect();
if (r.top <= 2 && r.bottom > 20 && r.bottom < 120 && r.width > 200) {
topOffset = Math.max(topOffset, Math.round(r.bottom));
}
}
const { leftPanels, rightPanels } = findActiveSidebars();
[...leftPanels, ...rightPanels].forEach(p => this.observeElement(p));
@@ -135,9 +155,6 @@ export class SidebarOverlay {
if (rect.right > 0 && rect.right < midX) {
leftOffset = Math.max(leftOffset, Math.round(rect.right));
}
if (rect.top > 0 && rect.top < 120) {
topOffset = Math.max(topOffset, Math.round(rect.top));
}
}
for (const el of rightPanels) {
@@ -145,9 +162,6 @@ export class SidebarOverlay {
if (rect.left > midX && rect.left < winW) {
rightOffset = Math.max(rightOffset, Math.round(winW - rect.left));
}
if (rect.top > 0 && rect.top < 120) {
topOffset = Math.max(topOffset, Math.round(rect.top));
}
}
} catch (e) {
console.error("Comfy Sidebar: Error finding active sidebars:", e);
@@ -174,6 +188,8 @@ export class SidebarOverlay {
}
destroy() {
if (this.destroyed) return;
this.destroyed = true;
document.body.classList.remove("comfy-sidebar-overlay-active");
for (const fn of this.cleanupFns.splice(0)) {
+555 -46
View File
@@ -4,21 +4,55 @@ export function injectStyles() {
style.id = "comfy-sidebar-styles";
style.textContent = `
.comfy-sidebar-card {
background: var(--comfy-input-bg, #181818);
/* Uses the theme's main elevated surface color (matches folder background identically) */
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-radius: 4px; padding: 8px; position: relative;
min-height: 80px; margin-bottom: 0px; break-inside: avoid;
user-select: none; -webkit-user-select: none;
border: 2px solid var(--border-color, #333);
color: var(--comfy-input-color, var(--fg-color, #eee));
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)));
overflow: hidden;
}
.comfy-sidebar-card:hover {
border-color: var(--p-primary-color, var(--primary-color, #555)) !important;
border-color: color-mix(in srgb, var(--border, var(--border-color, #555)) 50%, var(--foreground, var(--fg-color, #fff)) 50%) !important;
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%) !important;
transition: border-color 0.2s, background-color 0.2s;
}
.comfy-sidebar-card.active { --border-color: #3b82f6; --hover-color: #60a5fa; }
.comfy-sidebar-card.pending { --border-color: #6c757d; --hover-color: #adb5bd; }
.comfy-sidebar-card.cancelled { --border-color: #ffc107; --hover-color: #ffe082; }
.comfy-sidebar-card.error { --border-color: #dc3545; --hover-color: #f87171; }
/* Border & Frame Spotlight (Uses the theme's main hue) */
.comfy-sidebar-card::before,
.comfy-sidebar-folder-card::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-card:hover::before,
.comfy-sidebar-folder-card:hover::before {
opacity: 0.65;
}
/* Active job uses the theme accent (orange); Cancelled & Error remain standout Yellow & Red */
.comfy-sidebar-card.active { border-color: var(--primary, var(--p-primary-color, #ea580c)) !important; --hover-color: var(--primary, var(--p-primary-color, #ea580c)); }
.comfy-sidebar-card.pending { border-color: #6c757d !important; --hover-color: #adb5bd; }
.comfy-sidebar-card.cancelled { border-color: #ffc107 !important; --hover-color: #ffe082; }
.comfy-sidebar-card.error { border-color: #dc3545 !important; --hover-color: #f87171; }
/* Progress bar track & fill on active cards */
.comfy-sidebar-card.active [class*="progress"] > div,
.comfy-sidebar-card.active [class*="progress-fill"],
.comfy-sidebar-card.active [class*="bar-fill"] {
background: var(--primary, var(--p-primary-color, #ea580c)) !important;
}
.comfy-sidebar-card.pending .comfy-sidebar-queue-cancel-btn,
.comfy-sidebar-card.active .comfy-sidebar-queue-cancel-btn {
@@ -44,25 +78,53 @@ export function injectStyles() {
padding: 2px 4px; border-radius: 3px; pointer-events: none; z-index: 10; color: #fff;
}
/* Card Cancel / Interrupt Button - matches native ComfyUI Topbar Cancel */
.comfy-sidebar-queue-cancel-btn {
display: none;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
background-color: #7f1d1d !important;
color: #e2e8f0 !important;
font-size: 11px !important;
width: 28px !important;
height: 28px !important;
background: var(--destructive, var(--p-button-danger-background, #ef4444)) !important;
color: var(--foreground, var(--fg-color, inherit)) !important;
border: 1px solid var(--destructive-border, transparent) !important;
font-size: 12px !important;
border-radius: 6px !important;
cursor: pointer !important;
transition: background-color 0.15s ease, color 0.15s ease !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
transition: background-color 0.15s ease, filter 0.15s ease !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important;
z-index: 25 !important;
}
.comfy-sidebar-queue-cancel-btn:hover {
background-color: #991b1b !important;
color: #ffffff !important;
background: color-mix(in srgb, var(--destructive, var(--p-button-danger-background, #ef4444)) 88%, var(--foreground, #fff) 12%) !important;
color: var(--foreground, var(--fg-color, inherit)) !important;
filter: brightness(1.06);
}
/* Cancel All Pending Button */
.comfy-sidebar-cancel-all-btn {
background: var(--destructive, var(--p-button-danger-background, #ef4444)) !important;
color: var(--foreground, var(--fg-color, inherit)) !important;
border: 1px solid var(--destructive-border, transparent) !important;
border-radius: 6px !important;
padding: 6px 12px !important;
cursor: pointer !important;
font-size: 11px !important;
font-weight: bold !important;
width: 100% !important;
box-sizing: border-box !important;
transition: background-color 0.15s ease, filter 0.15s ease !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2) !important;
}
.comfy-sidebar-cancel-all-btn:hover {
background: color-mix(in srgb, var(--destructive, var(--p-button-danger-background, #ef4444)) 88%, var(--foreground, #fff) 12%) !important;
filter: brightness(1.06);
}
.comfy-sidebar-cancel-all-btn:active {
filter: brightness(0.95);
}
.comfy-sidebar-hover-panel,
@@ -155,14 +217,14 @@ export function injectStyles() {
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(2px);
background: color-mix(in srgb, var(--card, var(--secondary, #12071f)) 85%, transparent);
backdrop-filter: blur(3px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: #38bdf8;
color: var(--primary, var(--p-primary-color, #ea580c));
z-index: 15;
opacity: 0;
pointer-events: none;
@@ -189,10 +251,10 @@ export function injectStyles() {
}
.comfy-sidebar-header-btn {
background: transparent;
color: var(--desc-color, #aaa);
border: 1px solid var(--border-color, #555);
border-radius: 3px;
background: var(--card, var(--secondary, var(--p-surface-800, rgba(255, 255, 255, 0.05))));
color: var(--muted-foreground, var(--desc-color, #aaa));
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)));
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 13px;
@@ -200,19 +262,22 @@ export function injectStyles() {
}
.comfy-sidebar-header-btn:hover {
border-color: var(--fg-color, #eee);
color: var(--fg-color, #eee);
border-color: color-mix(in srgb, var(--border, var(--border-color, #a855f7)) 65%, var(--foreground, #fff) 35%);
color: var(--foreground, #fff);
background: color-mix(in srgb, var(--card, var(--secondary, rgba(255, 255, 255, 0.12))) 65%, var(--border, var(--border-color, #a855f7)) 35%);
}
.comfy-sidebar-icon-btn {
cursor: pointer;
font-size: 13px;
opacity: 0.6;
transition: opacity 0.15s ease-in-out;
color: var(--foreground, var(--fg-color, inherit));
transition: opacity 0.15s ease-in-out, color 0.15s ease-in-out;
}
.comfy-sidebar-icon-btn:hover {
opacity: 1;
color: var(--primary, var(--p-primary-color, inherit));
}
.comfy-sidebar-scroll-top-btn {
@@ -222,25 +287,25 @@ export function injectStyles() {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(30, 30, 30, 0.9);
color: #eee;
border: 1px solid var(--border-color, #555);
background: var(--card, var(--secondary, rgba(30, 30, 30, 0.9)));
color: var(--foreground, #eee);
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)));
cursor: pointer;
font-size: 13px;
display: none;
align-items: center;
justify-content: center;
z-index: 999;
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
transition: all 0.15s ease-in-out;
opacity: 0.85;
}
.comfy-sidebar-scroll-top-btn:hover {
opacity: 1;
background: var(--p-primary-color, #3b82f6);
background: var(--primary, var(--p-primary-color, #ea580c));
color: #fff;
border-color: var(--p-primary-color, #3b82f6);
border-color: var(--primary, var(--p-primary-color, #ea580c));
}
/* 3D Preview Cards */
@@ -259,8 +324,8 @@ export function injectStyles() {
}
.comfy-sidebar-3d-wrapper:hover {
border-color: #3b82f6;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.35);
border-color: var(--primary, var(--p-primary-color, #ea580c));
box-shadow: 0 0 12px color-mix(in srgb, var(--primary, #ea580c) 35%, transparent);
}
.comfy-sidebar-3d-preview-img {
@@ -454,13 +519,13 @@ export function injectStyles() {
}
.comfy-sidebar-file-wrapper:hover {
border-color: #0ea5e9;
box-shadow: 0 0 14px rgba(14, 165, 233, 0.35);
border-color: var(--primary, var(--p-primary-color, #ea580c));
box-shadow: 0 0 14px color-mix(in srgb, var(--primary, #ea580c) 35%, transparent);
transform: translateY(-1px);
}
.comfy-sidebar-file-badge {
background: rgba(14, 165, 233, 0.9);
background: var(--primary, var(--p-primary-color, #ea580c));
color: #fff;
font-size: 9px;
font-weight: bold;
@@ -514,7 +579,7 @@ export function injectStyles() {
.comfy-sidebar-text-clamp {
font-size: 11px;
line-height: 1.45;
color: #cbd5e1;
color: var(--foreground, #cbd5e1);
max-height: 84px;
overflow: hidden;
display: -webkit-box;
@@ -522,17 +587,93 @@ export function injectStyles() {
-webkit-box-orient: vertical;
cursor: pointer;
padding: 8px 10px;
background: rgba(255, 255, 255, 0.04);
/* Solid theme surface that sits above the spotlight, keeping text crisp and readable */
background: var(--card, var(--secondary, var(--p-surface-800, #181818)));
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.12)));
margin-top: 18px;
word-break: break-word;
transition: all 0.15s ease-in-out;
position: relative;
z-index: 2;
}
.comfy-sidebar-text-clamp:hover {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.08);
/* Follows the theme accent (orange) */
border-color: var(--primary, var(--p-primary-color, #ea580c));
background: color-mix(in srgb, var(--primary, var(--p-primary-color, #ea580c)) 6%, var(--card, var(--secondary, #181818)) 94%);
}
/* Translucent theme backdrop behind Text Reader & Fullscreen Previews */
.comfy-sidebar-comparison-overlay,
.comfy-sidebar-overlay,
body.comfy-sidebar-overlay-active > div[style*="position: fixed"],
body.comfy-sidebar-overlay-active > div[style*="position:fixed"] {
background: color-mix(in srgb, var(--background, var(--card, var(--comfy-menu-bg, #13091f))) 82%, transparent) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
}
/* Comparison Media: Ensures no borders or outlines appear on compared images */
.comfy-sidebar-comparison-media {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
/* Comparison Slider Line & Handle */
.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;
}
body.comfy-sidebar-overlay-active button[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,
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-comparison-overlay button[style*="50%"]:hover,
div[class*="sidebar-overlay"] button[style*="50%"]:hover {
color: #ffffff !important;
border-color: var(--primary, var(--p-primary-color, #ea580c)) !important;
background: var(--primary, var(--p-primary-color, #ea580c)) !important;
}
body.comfy-sidebar-overlay-active [style*="50%"] svg,
body.comfy-sidebar-overlay-active [style*="50%"] i,
body.comfy-sidebar-overlay-active [style*="50%"] line,
body.comfy-sidebar-overlay-active [style*="50%"] path {
stroke: currentColor !important;
color: inherit !important;
}
/* Elevate outer sidebar dock containers above the preview overlay */
@@ -555,9 +696,11 @@ export function injectStyles() {
z-index: 1005 !important;
}
/* Explicit card element layering so badges and [X] are ALWAYS on top of images */
.comfy-sidebar-card .comfy-sidebar-media-wrapper {
z-index: 1 !important;
/* Media and text wrappers sit on z-index 2 so the spotlight shines on the border behind them */
.comfy-sidebar-card .comfy-sidebar-media-wrapper,
.comfy-sidebar-card .comfy-sidebar-text-clamp {
z-index: 2 !important;
position: relative;
}
.comfy-sidebar-card-timer {
z-index: 10 !important;
@@ -569,6 +712,20 @@ export function injectStyles() {
z-index: 30 !important;
}
/* Sidebar Tab Counter Badge (Queue count) */
.side-tool-bar-container [class*="badge"],
.comfyui-sidebar [class*="badge"],
nav [class*="badge"],
aside [class*="badge"],
.side-tool-bar-end [class*="badge"],
button:has(.pi-images) + span,
button:has([class*="queue"]) + span {
background-color: var(--primary, var(--p-primary-color, #ea580c)) !important;
background: var(--primary, var(--p-primary-color, #ea580c)) !important;
color: var(--primary-foreground, #ffffff) !important;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4) !important;
}
/* Slim down the Queue sidebar scrollbar so it stops colliding with the separator */
.comfyui-sidebar ::-webkit-scrollbar,
.comfy-sidebar ::-webkit-scrollbar,
@@ -584,11 +741,363 @@ export function injectStyles() {
border-radius: 3px !important;
}
/* Gallery delete button only shows in the Ctrl alternative actions menu */
.comfy-sidebar-gallery-btn-del {
display: none !important;
}
body.comfy-sidebar-ctrl-active .comfy-sidebar-gallery-btn-del,
.comfy-sidebar-gallery-btn-del.confirm-delete,
.comfy-sidebar-gallery-btn-del.confirm-delete-disk {
display: inline-flex !important;
}
/* Sticky Header & Nav Reveal (Seamlessly transparent at rest, frosted on scroll) */
.comfy-sidebar-sticky-header-group {
position: sticky;
top: 0;
margin: -14px -14px 6px -14px;
padding: 14px 14px 0 14px;
background: transparent;
z-index: 35;
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease, box-shadow 0.2s ease;
will-change: transform;
}
.comfy-sidebar-sticky-header-group.is-scrolled {
background: color-mix(in srgb, var(--card, var(--secondary, var(--background, var(--bg-color, #181818)))) 75%, transparent);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
border-bottom: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.08)));
}
/* Gallery Subfolders & Breadcrumb Styles */
.comfy-sidebar-breadcrumb-bar {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--muted-foreground, var(--desc-color, #aaa));
overflow-x: auto;
white-space: nowrap;
padding: 4px 0 8px 0;
border-bottom: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.08)));
margin-bottom: 10px;
}
.comfy-sidebar-breadcrumb-item {
cursor: pointer;
padding: 2px 6px;
border-radius: 3px;
background: var(--card, var(--secondary, var(--p-surface-800, rgba(255, 255, 255, 0.05))));
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.08)));
transition: all 0.15s ease-in-out;
color: var(--foreground, var(--fg-color, #eee));
text-decoration: none;
}
.comfy-sidebar-breadcrumb-item:hover {
background: color-mix(in srgb, var(--primary, var(--p-primary-color, #38bdf8)) 25%, transparent);
border-color: var(--primary, var(--p-primary-color, #38bdf8));
color: #fff;
}
.comfy-sidebar-breadcrumb-sep {
opacity: 0.4;
font-size: 10px;
}
.comfy-sidebar-folders-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 8px;
margin-bottom: 12px;
width: 100%;
}
.comfy-sidebar-folder-card {
/* Uses the theme's main elevated surface color */
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-radius: 4px;
padding: 8px 10px;
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)));
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.1s ease;
user-select: none;
min-width: 0;
position: relative;
overflow: hidden;
}
.comfy-sidebar-folder-card: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-folder-icon {
font-size: 16px;
/* Keeps the accent color (orange, blue, or purple) strictly for the icon */
color: var(--primary, var(--p-primary-color, var(--primary-color, #38bdf8)));
flex-shrink: 0;
}
body.comfy-sidebar-ctrl-active .comfy-sidebar-folder-card:hover {
cursor: alias !important;
border-color: var(--primary, var(--p-primary-color, var(--primary-color, #a855f7))) !important;
box-shadow: 0 0 0 1px var(--primary, var(--p-primary-color, var(--primary-color, #a855f7))), 0 0 10px rgba(168, 85, 247, 0.3) !important;
background: color-mix(in srgb, var(--primary, var(--p-primary-color, var(--primary-color, #a855f7))) 15%, transparent) !important;
}
.comfy-sidebar-folder-name {
font-size: 11px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.comfy-sidebar-card-filename-badge {
position: absolute;
top: 6px;
right: 8px;
font-size: 10px;
font-family: monospace;
opacity: 0.7;
background: rgba(0, 0, 0, 0.6);
padding: 2px 4px;
border-radius: 3px;
pointer-events: none;
z-index: 5;
color: #fff;
max-width: 62%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transform: translateZ(0);
}
/* Folder card layout & Ctrl overlay support */
.comfy-sidebar-folder-card {
position: relative;
overflow: hidden;
}
body.comfy-sidebar-ctrl-active .comfy-sidebar-folder-card:hover .comfy-sidebar-ctrl-overlay {
display: flex;
}
/* Comparison selected image */
.comfy-sidebar-card.comfy-sidebar-comparison-source {
outline: 2px solid #eab308 !important;
box-shadow: 0 0 12px rgba(234, 179, 8, 0.5);
}
/* Smooth animation for the circular loader */
@keyframes comfy-sidebar-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Runtime Preview Dock in Properties Panel */
.comfy-sidebar-runtime-preview-dock {
position: sticky;
bottom: 0px;
left: 0px;
width: 100%;
padding: 8px 12px;
box-sizing: border-box;
background: color-mix(in srgb, var(--card, var(--bg-color, var(--comfy-menu-bg, #181818))) 85%, transparent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-top: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.12)));
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
margin-top: auto;
}
.comfy-sidebar-runtime-preview-btn {
width: 100%;
height: 34px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
font-family: inherit;
transition: all 0.15s ease-in-out;
outline: none;
box-sizing: border-box;
background: var(--card, var(--secondary, rgba(255, 255, 255, 0.05)));
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)));
color: var(--muted-foreground, var(--desc-color, #aaa));
box-shadow: none;
}
.comfy-sidebar-runtime-preview-btn:hover {
border-color: color-mix(in srgb, var(--primary, var(--p-primary-color, #ea580c)) 65%, var(--border, #666) 35%);
color: var(--foreground, #fff);
background: color-mix(in srgb, var(--primary, var(--p-primary-color, #ea580c)) 8%, var(--card, rgba(255, 255, 255, 0.08)) 92%);
}
.comfy-sidebar-runtime-preview-btn.active {
background: color-mix(in srgb, var(--primary, var(--p-primary-color, var(--primary-color, #ea580c))) 18%, transparent) !important;
border: 1px solid var(--primary, var(--p-primary-color, var(--primary-color, #ea580c))) !important;
color: var(--primary, var(--p-primary-color, var(--primary-color, #ea580c))) !important;
box-shadow: 0 0 10px color-mix(in srgb, var(--primary, var(--p-primary-color, var(--primary-color, #ea580c))) 35%, transparent) !important;
}
.comfy-sidebar-runtime-preview-btn.active:hover {
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);
}
+30 -20
View File
@@ -40,7 +40,7 @@ function parseInlineMarkdown(html) {
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
html = html.replace(/~~(.*?)~~/g, '<span style="text-decoration:line-through;color:#888;">$1</span>');
html = html.replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" style="color:#61afef;text-decoration:none;border-bottom:1px dashed #61afef;cursor:pointer;">$1</a>');
html = html.replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" style="color:var(--primary, #ea580c);text-decoration:none;border-bottom:1px dashed var(--primary, #ea580c);cursor:pointer;">$1</a>');
html = html.replace(/\$(?!\s)([^\$]+?)(?<!\s)\$/g, '<span style="font-family:\'Times New Roman\',Times,serif;font-size:14px;color:#e5c07b;font-style:italic;padding:0 2px;">$1</span>');
return html;
}
@@ -91,8 +91,8 @@ function parseMarkdown(text) {
if (inCodeBlock) {
const rCode = codeContent.join("\n");
const langBadge = codeLang ? `<div style="position:absolute;top:6px;left:10px;color:#85c5ec;font-size:10px;font-family:sans-serif;font-weight:bold;text-transform:uppercase;opacity:0.8;user-select:none;">${codeLang}</div>` : '';
const btnStyle = "position:absolute;top:6px;right:6px;background:#333;color:#aaa;border:1px solid #555;border-radius:3px;padding:3px 8px;font-size:10px;cursor:pointer;";
htmlLines.push(`<div class="code-block-container" style="position:relative;margin:8px 0;">${langBadge}<button onclick="window.moonCopyCode(this)" style="${btnStyle}">${t("Copy")}</button><pre style="background:#1e1e1e;padding:10px;padding-top:28px;border-radius:4px;border:1px solid #333;font-family:monospace;overflow-x:auto;margin:0;color:#85c5ec;"><code>${rCode}</code></pre></div>`);
const btnStyle = "position:absolute;top:6px;right:6px;background:var(--secondary, #333);color:var(--muted-foreground, #aaa);border:1px solid var(--border, #555);border-radius:3px;padding:3px 8px;font-size:10px;cursor:pointer;";
htmlLines.push(`<div class="code-block-container" style="position:relative;margin:8px 0;">${langBadge}<button onclick="window.moonCopyCode(this)" style="${btnStyle}">${t("Copy")}</button><pre style="background:color-mix(in srgb, var(--card, #1e1e1e) 90%, black 10%);padding:10px;padding-top:28px;border-radius:4px;border:1px solid var(--border, #333);font-family:monospace;overflow-x:auto;margin:0;color:#85c5ec;"><code>${rCode}</code></pre></div>`);
codeContent = []; codeLang = ""; inCodeBlock = false;
} else {
inCodeBlock = true; codeLang = trimmed.slice(3).trim();
@@ -202,14 +202,14 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const readerBox = document.createElement("div");
Object.assign(readerBox.style, {
background: "var(--comfy-menu-bg, rgba(18, 18, 24, 0.95))",
backdropFilter: "blur(12px)",
border: "1px solid var(--border-color, rgba(255, 255, 255, 0.15))",
background: "color-mix(in srgb, var(--card, var(--secondary, var(--p-surface-800, #181818))) 94%, var(--background, #000) 6%)",
backdropFilter: "blur(16px)",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
borderRadius: "12px",
padding: "20px 24px", width: "85%", maxWidth: "840px", height: "80vh",
boxShadow: "0 12px 40px rgba(0,0,0,0.4)", display: "flex",
boxShadow: "0 12px 40px rgba(0,0,0,0.5)", display: "flex",
flexDirection: "column", gap: "14px", zIndex: "20",
color: "var(--fg-color, #eee)",
color: "var(--foreground, var(--fg-color, #eee))",
pointerEvents: "auto"
});
@@ -222,7 +222,7 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const headerRow = document.createElement("div");
Object.assign(headerRow.style, {
display: "flex", alignItems: "center", justifyContent: "space-between",
borderBottom: "1px solid rgba(255,255,255,0.1)", paddingBottom: "12px",
borderBottom: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.12)))", paddingBottom: "12px",
flexWrap: "wrap", gap: "10px"
});
@@ -242,7 +242,9 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const statsBadge = document.createElement("span");
Object.assign(statsBadge.style, {
fontSize: "10px", color: "#94a3b8", background: "rgba(255,255,255,0.06)",
fontSize: "10px", color: "var(--muted-foreground, #94a3b8)",
background: "var(--secondary, rgba(255,255,255,0.06))",
border: "1px solid var(--border, transparent)",
padding: "3px 8px", borderRadius: "4px", fontFamily: "monospace",
whiteSpace: "nowrap", flexShrink: "0"
});
@@ -254,7 +256,9 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const modeBtn = document.createElement("button");
Object.assign(modeBtn.style, {
background: "#334155", border: "1px solid #64748b", color: "#f8fafc",
background: "var(--primary, var(--p-primary-color, #a855f7))",
border: "1px solid var(--primary, var(--p-primary-color, #a855f7))",
color: "#ffffff",
borderRadius: "6px", padding: "5px 12px", fontSize: "11px", cursor: "pointer",
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px",
whiteSpace: "nowrap", minWidth: "96px", height: "28px", boxSizing: "border-box"
@@ -263,7 +267,9 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const copyBtn = document.createElement("button");
Object.assign(copyBtn.style, {
background: "#1e293b", border: "1px solid #475569", color: "#e2e8f0",
background: "var(--secondary, var(--p-surface-800, #1e293b))",
border: "1px solid var(--border, var(--border-color, #475569))",
color: "var(--foreground, #e2e8f0)",
borderRadius: "6px", padding: "5px 10px", fontSize: "11px", cursor: "pointer",
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px",
whiteSpace: "nowrap", height: "28px", boxSizing: "border-box"
@@ -272,7 +278,9 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const downloadBtn = document.createElement("button");
Object.assign(downloadBtn.style, {
background: "#1e293b", border: "1px solid #475569", color: "#e2e8f0",
background: "var(--secondary, var(--p-surface-800, #1e293b))",
border: "1px solid var(--border, var(--border-color, #475569))",
color: "var(--foreground, #e2e8f0)",
borderRadius: "6px", padding: "5px 10px", fontSize: "11px", cursor: "pointer",
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px",
whiteSpace: "nowrap", height: "28px", boxSizing: "border-box"
@@ -286,12 +294,12 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
Object.assign(textArea.style, {
flex: "1", overflowY: "auto", fontSize: "13px",
lineHeight: "1.6",
color: "var(--fg-color, #e2e8f0)",
color: "var(--foreground, var(--fg-color, #e2e8f0))",
wordBreak: "break-word",
userSelect: "text", "-webkit-user-select": "text",
background: "var(--comfy-input-bg, rgba(0, 0, 0, 0.2))",
background: "color-mix(in srgb, var(--background, var(--comfy-input-bg, #000)) 75%, transparent)",
padding: "16px 20px", borderRadius: "6px",
border: "1px solid var(--border-color, rgba(255, 255, 255, 0.08))",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.1)))",
scrollbarWidth: "thin"
});
@@ -301,15 +309,17 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
if (isMarkdownMode) {
modeBtn.innerHTML = `<i class="pi pi-code"></i><span>Markdown</span>`;
modeBtn.style.background = "#3b82f6";
modeBtn.style.borderColor = "#60a5fa";
modeBtn.style.background = "var(--primary, var(--p-primary-color, #a855f7))";
modeBtn.style.borderColor = "var(--primary, var(--p-primary-color, #a855f7))";
modeBtn.style.color = "#ffffff";
textArea.style.fontFamily = "sans-serif";
textArea.style.whiteSpace = "normal";
textArea.innerHTML = parseMarkdown(rawText);
} else {
modeBtn.innerHTML = `<i class="pi pi-align-left"></i><span>${t("Raw Text")}</span>`;
modeBtn.style.background = "#334155";
modeBtn.style.borderColor = "#64748b";
modeBtn.style.background = "var(--secondary, #334155)";
modeBtn.style.borderColor = "var(--border, #64748b)";
modeBtn.style.color = "var(--muted-foreground, #f8fafc)";
textArea.style.fontFamily = "monospace";
textArea.style.whiteSpace = "pre-wrap";
textArea.textContent = rawText;
+162 -11
View File
@@ -1,4 +1,5 @@
import { app } from "/scripts/app.js";
import { t } from "../core/i18n.js";
const FILE_EXT_REGEX = /\.(safetensors|ckpt|pt|pth|bin|latent|onnx|engine|gguf|lora|zip|tar|gz|7z|json|csv|parquet|txt|yaml|yml|xml|pdf|blend|fbx|obj|glb|gltf|stl|ply|splat|spz|ksplat|png|jpg|jpeg|webp|gif|bmp|tiff|svg|mp4|webm|wav|mp3|ogg|flac|m4a|aac|opus)$/i;
@@ -67,17 +68,59 @@ export function parseWorkflow(workflow) {
return null;
}
export function isNodeIgnored(nodeId, rawWorkflow) {
export function getWorkflowNodeMap(rawWorkflow) {
const workflow = parseWorkflow(rawWorkflow);
if (workflow && Array.isArray(workflow.nodes)) {
const node = workflow.nodes.find(n => String(n.id) === String(nodeId));
if (node?.properties?.ignoreInQueue !== undefined) {
return !!node.properties.ignoreInQueue;
if (!workflow || !Array.isArray(workflow.nodes)) return null;
if (workflow._nodeMap instanceof Map) {
return workflow._nodeMap;
}
const map = new Map();
for (const node of workflow.nodes) {
if (node && node.id !== undefined && node.id !== null) {
map.set(String(node.id), node);
}
}
if (!workflow && app.graph) {
const canvasNode = app.graph.getNodeById ? (app.graph.getNodeById(Number(nodeId)) || app.graph.getNodeById(String(nodeId))) : null;
// Attach as non-enumerable so JSON.stringify and Object.keys never see it
if (typeof workflow === "object") {
try {
Object.defineProperty(workflow, "_nodeMap", {
value: map,
writable: true,
configurable: true,
enumerable: false
});
} catch (_) {
workflow._nodeMap = map;
}
}
return map;
}
export function isNodeIgnored(nodeId, rawWorkflow) {
const idStr = String(nodeId);
const nodeMap = getWorkflowNodeMap(rawWorkflow);
if (nodeMap) {
const node = nodeMap.get(idStr);
if (node?.properties?.ignoreInQueue !== undefined) {
return !!node.properties.ignoreInQueue;
}
} else {
const workflow = parseWorkflow(rawWorkflow);
if (workflow && Array.isArray(workflow.nodes)) {
const node = workflow.nodes.find(n => String(n.id) === idStr);
if (node?.properties?.ignoreInQueue !== undefined) {
return !!node.properties.ignoreInQueue;
}
}
}
if (app.graph) {
const canvasNode = app.graph.getNodeById ? (app.graph.getNodeById(Number(nodeId)) || app.graph.getNodeById(idStr)) : null;
if (canvasNode?.properties?.ignoreInQueue !== undefined) {
return !!canvasNode.properties.ignoreInQueue;
}
@@ -87,13 +130,21 @@ export function isNodeIgnored(nodeId, rawWorkflow) {
export function getNodeTitle(nodeId, rawWorkflow, promptGraph = null) {
const idStr = String(nodeId);
const nodeMap = getWorkflowNodeMap(rawWorkflow);
const workflow = parseWorkflow(rawWorkflow);
if (workflow && Array.isArray(workflow.nodes)) {
const node = workflow.nodes.find(n => String(n.id) === idStr);
if (nodeMap) {
const node = nodeMap.get(idStr);
if (node) {
return node.title || node.type || null;
}
} else {
const workflow = parseWorkflow(rawWorkflow);
if (workflow && Array.isArray(workflow.nodes)) {
const node = workflow.nodes.find(n => String(n.id) === idStr);
if (node) {
return node.title || node.type || null;
}
}
}
if (promptGraph && promptGraph[idStr]) {
@@ -233,9 +284,10 @@ export function findImagesInOutputs(outputs, rawWorkflow) {
};
if (outputs) {
const nodeMap = getWorkflowNodeMap(workflow);
for (const nodeId in outputs) {
if (isNodeIgnored(nodeId, workflow)) continue;
const node = workflow?.nodes?.find(n => String(n.id) === String(nodeId));
const node = nodeMap ? nodeMap.get(String(nodeId)) : workflow?.nodes?.find(n => String(n.id) === String(nodeId));
const nodeType = node ? (node.type || "") : "";
scan(outputs[nodeId], "", "output", nodeType);
}
@@ -374,6 +426,7 @@ export function matchesFilter(state, query) {
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.promptSearchText && state.promptSearchText.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;
@@ -431,4 +484,102 @@ export async function extractWorkflowFromPng(imageUrl) {
console.warn("Comfy Sidebar: Could not extract workflow from PNG", e);
}
return null;
}
export function animateCardRemoval(cardEl, callback, duration = 200) {
if (!cardEl) {
if (callback) callback();
return;
}
cardEl.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`;
cardEl.style.opacity = "0";
cardEl.style.transform = "scale(0.85)";
setTimeout(() => {
if (callback) callback();
}, duration);
}
export function getScrollContainer(element) {
if (!element) return null;
return element.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| element.parentElement
|| element;
}
export function calculateGridColumns(width, threshold = 350) {
if (!width || width <= 0) return 1;
return Math.max(1, Math.floor(width / ((threshold || 350) / 2)));
}
export function downloadJson(data, filename) {
if (!data) return;
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename || "workflow.json";
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1500);
}
export function setupSpotlightTracker(container, cardSelector = ".comfy-sidebar-card") {
if (!container) return;
container.addEventListener("pointermove", (e) => {
const card = e.target.closest(cardSelector);
if (card) {
const rect = card.getBoundingClientRect();
card.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`);
card.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`);
}
}, { passive: true });
}
export function attachDeleteConfirmation(btn, options = {}) {
if (!btn) return () => {};
const {
canDiskDelete = true,
timeout = 2000,
idleTitle = t("Delete"),
confirmTitle = t("Click again to confirm"),
confirmDiskTitle = t("Ctrl+Click again to delete from disk"),
onConfirm
} = options;
let isPending = false;
let isDisk = false;
let timer = null;
const reset = () => {
isPending = false;
isDisk = false;
btn.classList.remove("confirm-delete", "confirm-delete-disk");
btn.title = typeof idleTitle === "function" ? idleTitle() : idleTitle;
if (timer) {
clearTimeout(timer);
timer = null;
}
};
reset();
btn.onclick = async (ev) => {
ev.stopPropagation();
const wantsDisk = (ev.ctrlKey || ev.metaKey) && (typeof canDiskDelete === "function" ? canDiskDelete() : !!canDiskDelete);
if (!isPending) {
isPending = true;
isDisk = wantsDisk;
btn.classList.add(wantsDisk ? "confirm-delete-disk" : "confirm-delete");
btn.title = wantsDisk
? (typeof confirmDiskTitle === "function" ? confirmDiskTitle() : confirmDiskTitle)
: (typeof confirmTitle === "function" ? confirmTitle() : confirmTitle);
timer = setTimeout(reset, timeout);
} else {
const finalDisk = isDisk || wantsDisk;
reset();
if (onConfirm) await onConfirm(finalDisk);
}
};
return reset;
}
+74 -30
View File
@@ -65,10 +65,14 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const header = document.createElement("div");
Object.assign(header.style, {
position: "absolute", top: "16px", left: "50%", transform: "translateX(-50%)",
display: "flex", gap: "16px", zIndex: "30", color: "#aaa", fontSize: "12px",
fontFamily: "monospace", pointerEvents: "none", background: "rgba(10,10,10,0.75)",
padding: "6px 14px", borderRadius: "6px", backdropFilter: "blur(4px)",
boxShadow: "0 2px 6px rgba(0,0,0,0.4)", maxWidth: "85%", textAlign: "center"
display: "flex", gap: "16px", zIndex: "30",
color: "var(--muted-foreground, var(--desc-color, #aaa))",
fontSize: "12px",
fontFamily: "monospace", pointerEvents: "none",
background: "color-mix(in srgb, var(--card, var(--secondary, #181818)) 90%, transparent)",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
padding: "6px 14px", borderRadius: "6px", backdropFilter: "blur(8px)",
boxShadow: "0 2px 8px rgba(0,0,0,0.4)", maxWidth: "85%", textAlign: "center"
});
const titleText = document.createElement("span");
titleText.textContent = getFilenameFromUrl(baseSrc) || t("3D Model");
@@ -85,9 +89,13 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const loadingSpinner = document.createElement("div");
Object.assign(loadingSpinner.style, {
position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)",
color: "#60a5fa", fontSize: "14px", fontFamily: "sans-serif", zIndex: "20",
background: "rgba(0,0,0,0.8)", padding: "10px 18px", borderRadius: "8px",
display: "flex", alignItems: "center", gap: "10px"
color: "var(--primary, var(--p-primary-color, #ea580c))",
fontSize: "14px", fontFamily: "sans-serif", zIndex: "20",
background: "color-mix(in srgb, var(--card, var(--secondary, #181818)) 92%, transparent)",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
padding: "10px 18px", borderRadius: "8px",
display: "flex", alignItems: "center", gap: "10px",
boxShadow: "0 8px 24px rgba(0,0,0,0.5)"
});
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner" style="font-size: 18px;"></span> ${t("Loading 3D Asset...")}`;
overlay.container.appendChild(loadingSpinner);
@@ -95,9 +103,13 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const settingsPanel = document.createElement("div");
Object.assign(settingsPanel.style, {
position: "absolute", top: "60px", right: "20px", width: "230px",
background: "rgba(18, 18, 24, 0.9)", backdropFilter: "blur(8px)",
border: "1px solid rgba(255, 255, 255, 0.12)", borderRadius: "8px",
padding: "14px", zIndex: "35", color: "#e2e8f0", fontSize: "11px",
background: "color-mix(in srgb, var(--card, var(--secondary, #181818)) 94%, transparent)",
backdropFilter: "blur(12px)",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
borderRadius: "8px",
padding: "14px", zIndex: "35",
color: "var(--foreground, #e2e8f0)",
fontSize: "11px",
boxShadow: "0 8px 24px rgba(0,0,0,0.6)", display: "none", flexDirection: "column", gap: "12px"
});
@@ -106,7 +118,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
Object.assign(row.style, { display: "flex", flexDirection: "column", gap: "4px" });
const lbl = document.createElement("label");
lbl.textContent = label;
Object.assign(lbl.style, { color: "#94a3b8", fontSize: "10px", textTransform: "uppercase", fontWeight: "bold" });
Object.assign(lbl.style, { color: "var(--muted-foreground, #94a3b8)", fontSize: "10px", textTransform: "uppercase", fontWeight: "bold" });
row.append(lbl, inputEl);
return row;
};
@@ -114,7 +126,9 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const createSelect = (options, value, onChange) => {
const sel = document.createElement("select");
Object.assign(sel.style, {
background: "#252530", color: "#eee", border: "1px solid #444",
background: "var(--secondary, #252530)",
color: "var(--foreground, #eee)",
border: "1px solid var(--border, var(--border-color, #444))",
borderRadius: "4px", padding: "4px 6px", fontSize: "11px", outline: "none"
});
options.forEach(opt => {
@@ -131,9 +145,11 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const toolbar = document.createElement("div");
Object.assign(toolbar.style, {
position: "absolute", bottom: "20px", left: "50%", transform: "translateX(-50%)",
display: "flex", gap: "8px", zIndex: "30", background: "rgba(20,20,25,0.85)",
padding: "6px 12px", borderRadius: "8px", backdropFilter: "blur(6px)",
boxShadow: "0 4px 12px rgba(0,0,0,0.6)", border: "1px solid rgba(255,255,255,0.1)",
display: "flex", gap: "8px", zIndex: "30",
background: "color-mix(in srgb, var(--card, var(--secondary, #181818)) 92%, transparent)",
padding: "6px 12px", borderRadius: "8px", backdropFilter: "blur(8px)",
boxShadow: "0 4px 12px rgba(0,0,0,0.6)",
border: "1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.15)))",
alignItems: "center"
});
@@ -141,19 +157,25 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const btn = document.createElement("button");
let active = activeDefault;
btn.className = "comfy-sidebar-3d-tool-btn";
const updateBtnTheme = () => {
btn.style.background = active ? "var(--primary, var(--p-primary-color, #ea580c))" : "transparent";
btn.style.color = active ? "#ffffff" : "var(--muted-foreground, #aaa)";
btn.style.borderColor = active ? "var(--primary, var(--p-primary-color, #ea580c))" : "var(--border, var(--border-color, #444))";
};
Object.assign(btn.style, {
background: active ? "#3b82f6" : "transparent", color: active ? "#fff" : "#aaa",
border: "1px solid " + (active ? "#3b82f6" : "#444"), borderRadius: "4px",
padding: "4px 10px", fontSize: "11px", cursor: "pointer", display: "flex",
alignItems: "center", gap: "6px", transition: "all 0.15s ease"
borderRadius: "4px", padding: "4px 10px", fontSize: "11px",
cursor: "pointer", display: "flex", alignItems: "center", gap: "6px",
transition: "all 0.15s ease", border: "1px solid"
});
updateBtnTheme();
btn.innerHTML = `<i class="${iconClass}"></i><span>${label}</span>`;
btn.onclick = (e) => {
e.stopPropagation();
active = !active;
btn.style.background = active ? "#3b82f6" : "transparent";
btn.style.color = active ? "#fff" : "#aaa";
btn.style.borderColor = active ? "#3b82f6" : "#444";
updateBtnTheme();
onClick(active);
};
return btn;
@@ -321,7 +343,19 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
dirLight2.position.set(-5, -5, -5);
scene.add(dirLight2);
gridHelper = new THREE.GridHelper(10, 20, 0x3b82f6, 0x22222a);
let gridPrimaryColor = 0xea580c;
try {
const tempDiv = document.createElement("div");
tempDiv.style.color = "var(--primary, var(--p-primary-color, #ea580c))";
document.body.appendChild(tempDiv);
const rgb = getComputedStyle(tempDiv).color.match(/\d+/g);
tempDiv.remove();
if (rgb && rgb.length >= 3) {
gridPrimaryColor = (Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2]);
}
} catch (e) {}
gridHelper = new THREE.GridHelper(10, 20, gridPrimaryColor, 0x22222a);
gridHelper.position.y = 0;
scene.add(gridHelper);
@@ -415,11 +449,21 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const btnResetCam = document.createElement("button");
Object.assign(btnResetCam.style, {
background: "transparent", color: "#aaa", border: "1px solid #444",
background: "transparent",
color: "var(--muted-foreground, #aaa)",
border: "1px solid var(--border, var(--border-color, #444))",
borderRadius: "4px", padding: "4px 10px", fontSize: "11px", cursor: "pointer",
display: "flex", alignItems: "center", gap: "6px"
display: "flex", alignItems: "center", gap: "6px", transition: "all 0.15s ease"
});
btnResetCam.innerHTML = `<i class="pi pi-compass"></i><span>${t("Reset View")}</span>`;
btnResetCam.onmouseenter = () => {
btnResetCam.style.borderColor = "var(--primary, var(--border-color, #fff))";
btnResetCam.style.color = "var(--foreground, #fff)";
};
btnResetCam.onmouseleave = () => {
btnResetCam.style.borderColor = "var(--border, var(--border-color, #444))";
btnResetCam.style.color = "var(--muted-foreground, #aaa)";
};
btnResetCam.onclick = (e) => {
e.stopPropagation();
if (currentModel) fitCameraToObject(camera, currentModel, controls, THREE);
@@ -440,9 +484,9 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
exportSelect.value = "";
}
});
exportSelect.style.background = "#1e293b";
exportSelect.style.borderColor = "#3b82f6";
exportSelect.style.color = "#93c5fd";
exportSelect.style.background = "var(--secondary, var(--card, #1e293b))";
exportSelect.style.borderColor = "var(--primary, var(--p-primary-color, #ea580c))";
exportSelect.style.color = "var(--foreground, #fff)";
toolbar.append(btnGrid, btnWire, btnRotate, btnResetCam, btnSettings, exportSelect);
overlay.container.appendChild(toolbar);
@@ -493,12 +537,12 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
} else if (ext === "stl" && THREE.STLLoader) {
const loader = new THREE.STLLoader();
const geometry = await loader.loadAsync(srcUrl);
const material = new THREE.MeshStandardMaterial({ color: 0x90caf9, roughness: 0.4, metalness: 0.2 });
const material = new THREE.MeshStandardMaterial({ color: 0xd1d5db, roughness: 0.4, metalness: 0.2 });
loadedObj = new THREE.Mesh(geometry, material);
} else if (ext === "ply" && THREE.PLYLoader) {
const loader = new THREE.PLYLoader();
const geometry = await loader.loadAsync(srcUrl);
const material = new THREE.MeshStandardMaterial({ color: 0x90caf9, roughness: 0.4 });
const material = new THREE.MeshStandardMaterial({ color: 0xd1d5db, roughness: 0.4 });
loadedObj = new THREE.Mesh(geometry, material);
} else if (THREE.GLTFLoader) {
const loader = new THREE.GLTFLoader();
@@ -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": "قائمة الانتظار",
"Search History": "البحث في السجل",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "تجاهل/تضمين العقد المحددة (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "تجاهل/تضمين العقد المحددة في قائمة الانتظار",
"Renumber All Nodes (Execution Order)": "إعادة ترقيم جميع العقد (ترتيب التنفيذ)",
"Set Selected Node ID...": "تعيين معرف العقدة المحددة...",
"Set Node ID...": "تعيين معرف العقدة..."
"Set Node ID...": "تعيين معرف العقدة...",
"Gallery": "المعرض",
"Gallery (Outputs)": "المعرض (المخرجات)",
"Go Up One Level": "الانتقال لمستوى أعلى",
"Open Current Folder in File Manager": "فتح المجلد الحالي في مدير الملفات",
"Refresh Gallery": "تحديث المعرض",
"Filter gallery files...": "تصفية ملفات المعرض...",
"Loading files...": "جاري تحميل الملفات...",
"No files found in this folder.": "لم يتم العثور على ملفات في هذا المجلد.",
"Failed to load gallery from disk.": "فشل تحميل المعرض من القرص.",
"Sort: Newest First": "الترتيب: الأحدث أولاً",
"Sort: Oldest First": "الترتيب: الأقدم أولاً",
"Search Gallery (Filename or Prompt)": "البحث في المعرض (اسم الملف أو الموجه)",
"Search filenames or prompt metadata...": "البحث في أسماء الملفات أو بيانات الموجه...",
"Type to load saved workflow...": "اكتب لتحميل سير عمل محفوظ...",
"No matching workflows": "لا توجد مسارات عمل مطابقة"
}
+66
View File
@@ -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",
"Search History": "Search History",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "Toggle Ignore Selected Node(s) in Queue",
"Renumber All Nodes (Execution Order)": "Renumber All Nodes (Execution Order)",
"Set Selected Node ID...": "Set Selected Node ID...",
"Set Node ID...": "Set Node ID..."
"Set Node ID...": "Set Node ID...",
"Gallery": "Gallery",
"Gallery (Outputs)": "Gallery (Outputs)",
"Go Up One Level": "Go Up One Level",
"Open Current Folder in File Manager": "Open Current Folder in File Manager",
"Refresh Gallery": "Refresh Gallery",
"Filter gallery files...": "Filter gallery files...",
"Loading files...": "Loading files...",
"No files found in this folder.": "No files found in this folder.",
"Failed to load gallery from disk.": "Failed to load gallery from disk.",
"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...": "Type to load saved workflow...",
"No matching workflows": "No matching workflows"
}
+66
View File
@@ -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": "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",
"Search History": "Buscar en el historial",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nodo(s) seleccionado(s) (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignorar/Incluir nodo(s) seleccionado(s) en la cola",
"Renumber All Nodes (Execution Order)": "Renumerar todos los nodos (orden de ejecución)",
"Set Selected Node ID...": "Establecer ID del nodo seleccionado...",
"Set Node ID...": "Establecer ID de nodo..."
"Set Node ID...": "Establecer ID de nodo...",
"Gallery": "Galería",
"Gallery (Outputs)": "Galería (Salidas)",
"Go Up One Level": "Subir un nivel",
"Open Current Folder in File Manager": "Abrir carpeta actual en el explorador de archivos",
"Refresh Gallery": "Actualizar galería",
"Filter gallery files...": "Filtrar archivos de la galería...",
"Loading files...": "Cargando archivos...",
"No files found in this folder.": "No se encontraron archivos en esta carpeta.",
"Failed to load gallery from disk.": "Error al cargar la galería desde el disco.",
"Sort: Newest First": "Ordenar: más recientes primero",
"Sort: Oldest First": "Ordenar: más antiguos primero",
"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...",
"Type to load saved workflow...": "Escribe para cargar un flujo guardado...",
"No matching workflows": "No hay flujos coincidentes"
}
+66
View File
@@ -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": "صف",
"Search History": "جستجو در تاریخچه",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "نادیده گرفتن گره‌های انتخابی در صف (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "نادیده گرفتن/فعال‌سازی گره‌های انتخاب‌شده در صف",
"Renumber All Nodes (Execution Order)": "شماره‌گذاری مجدد همه گره‌ها (ترتیب اجرا)",
"Set Selected Node ID...": "تنظیم شناسه گره انتخاب‌شده...",
"Set Node ID...": "تنظیم شناسه گره..."
"Set Node ID...": "تنظیم شناسه گره...",
"Gallery": "گالری",
"Gallery (Outputs)": "گالری (خروجی‌ها)",
"Go Up One Level": "یک سطح به بالا",
"Open Current Folder in File Manager": "باز کردن پوشه فعلی در مدیریت فایل",
"Refresh Gallery": "به‌روزرسانی گالری",
"Filter gallery files...": "فیلتر کردن فایل‌های گالری...",
"Loading files...": "در حال بارگذاری فایل‌ها...",
"No files found in this folder.": "هیچ فایلی در این پوشه یافت نشد.",
"Failed to load gallery from disk.": "بارگذاری گالری از دیسک ناموفق بود.",
"Sort: Newest First": "مرتب‌سازی: جدیدترین‌ها در ابتدا",
"Sort: Oldest First": "مرتب‌سازی: قدیمی‌ترین‌ها در ابتدا",
"Search Gallery (Filename or Prompt)": "جستجو در گالری (نام فایل یا پرامپت)",
"Search filenames or prompt metadata...": "جستجوی نام فایل‌ها یا متادیتای پرامپت...",
"Type to load saved workflow...": "برای بارگذاری گردش کار ذخیره‌شده تایپ کنید...",
"No matching workflows": "گردش کار مطابقی یافت نشد"
}
+66
View File
@@ -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",
"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)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignorer/Activer les nœuds sélectionnés dans la file",
"Renumber All Nodes (Execution Order)": "Renuméroter tous les nœuds (ordre d'exécution)",
"Set Selected Node ID...": "Définir l'ID du nœud sélectionné...",
"Set Node ID...": "Définir l'ID du nœud..."
"Set Node ID...": "Définir l'ID du nœud...",
"Gallery": "Galerie",
"Gallery (Outputs)": "Galerie (Sorties)",
"Go Up One Level": "Dossier parent",
"Open Current Folder in File Manager": "Ouvrir le dossier actuel dans le gestionnaire de fichiers",
"Refresh Gallery": "Actualiser la galerie",
"Filter gallery files...": "Filtrer les fichiers de la galerie...",
"Loading files...": "Chargement des fichiers...",
"No files found in this folder.": "Aucun fichier trouvé dans ce dossier.",
"Failed to load gallery from disk.": "Échec du chargement de la galerie depuis le disque.",
"Sort: Newest First": "Trier : Plus récents 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 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"
}
+66
View File
@@ -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": "תור",
"Search History": "חיפוש בהיסטוריה",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "התעלם/כלול צמתים נבחרים בתור (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "התעלם/כלול צמתים נבחרים בתור",
"Renumber All Nodes (Execution Order)": "מספר מחדש את כל הצמתים (סדר ביצוע)",
"Set Selected Node ID...": "הגדר מזהה צומת נבחר...",
"Set Node ID...": "הגדר מזהה צומת..."
"Set Node ID...": "הגדר מזהה צומת...",
"Gallery": "גלריה",
"Gallery (Outputs)": "גלריה (תוצרים)",
"Go Up One Level": "עלה שלב אחד למעלה",
"Open Current Folder in File Manager": "פתח תיקייה נוכחית בסייר הקבצים",
"Refresh Gallery": "רענן גלריה",
"Filter gallery files...": "סנן קבצי גלריה...",
"Loading files...": "טוען קבצים...",
"No files found in this folder.": "לא נמצאו קבצים בתיקייה זו.",
"Failed to load gallery from disk.": "טעינת הגלריה מהכונן נכשלה.",
"Sort: Newest First": "מיון: החדשים ביותר קודם",
"Sort: Oldest First": "מיון: הישנים ביותר קודם",
"Search Gallery (Filename or Prompt)": "חיפוש בגלריה (שם קובץ או פרומפט)",
"Search filenames or prompt metadata...": "חיפוש שמות קבצים או מטא-דאטה של פרומפט...",
"Type to load saved workflow...": "הקלד כדי לטעון תהליך עבודה שמור...",
"No matching workflows": "לא נמצאו תהליכי עבודה תואמים"
}
+66
View File
@@ -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",
"Search History": "Cerca nella cronologia",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignora/Attiva nodo/i selezionato/i (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignora/Attiva nodi selezionati nella coda",
"Renumber All Nodes (Execution Order)": "Rinumera tutti i nodi (ordine di esecuzione)",
"Set Selected Node ID...": "Imposta ID nodo selezionato...",
"Set Node ID...": "Imposta ID nodo..."
"Set Node ID...": "Imposta ID nodo...",
"Gallery": "Galleria",
"Gallery (Outputs)": "Galleria (Output)",
"Go Up One Level": "Sali di un livello",
"Open Current Folder in File Manager": "Apri cartella corrente in Esplora file",
"Refresh Gallery": "Aggiorna galleria",
"Filter gallery files...": "Filtra file della galleria...",
"Loading files...": "Caricamento file...",
"No files found in this folder.": "Nessun file trovato in questa cartella.",
"Failed to load gallery from disk.": "Impossibile caricare la galleria dal disco.",
"Sort: Newest First": "Ordina: Più recenti prima",
"Sort: Oldest First": "Ordina: Più vecchi prima",
"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...",
"Type to load saved workflow...": "Digita per caricare un workflow salvato...",
"No matching workflows": "Nessun workflow corrispondente"
}
+66
View File
@@ -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": "キュー",
"Search History": "履歴を検索",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "選択したノードをキューで無視/解除 (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "キューで選択したノードを無視/解除",
"Renumber All Nodes (Execution Order)": "すべてのノード番号を再割り当て (実行順)",
"Set Selected Node ID...": "選択したノードIDを設定...",
"Set Node ID...": "ノードIDを設定..."
"Set Node ID...": "ノードIDを設定...",
"Gallery": "ギャラリー",
"Gallery (Outputs)": "ギャラリー (出力)",
"Go Up One Level": "1つ上の階層へ",
"Open Current Folder in File Manager": "現在のフォルダーをファイルマネージャーで開く",
"Refresh Gallery": "ギャラリーを更新",
"Filter gallery files...": "ギャラリーファイルを絞り込み...",
"Loading files...": "ファイルを読み込み中...",
"No files found in this folder.": "このフォルダーにはファイルがありません。",
"Failed to load gallery from disk.": "ディスクからギャラリーを読み込めませんでした。",
"Sort: Newest First": "並び替え:新しい順",
"Sort: Oldest First": "並び替え:古い順",
"Search Gallery (Filename or Prompt)": "ギャラリーを検索 (ファイル名またはプロンプト)",
"Search filenames or prompt metadata...": "ファイル名またはプロンプトを検索...",
"Type to load saved workflow...": "保存済みワークフローを読み込むには入力...",
"No matching workflows": "一致するワークフローがありません"
}
+66
View File
@@ -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": "대기열",
"Search History": "기록 검색",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "대기열에서 선택한 노드 무시/해제 (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "대기열에서 선택한 노드 무시/해제",
"Renumber All Nodes (Execution Order)": "모든 노드 재번호 매기기 (실행 순서)",
"Set Selected Node ID...": "선택한 노드 ID 설정...",
"Set Node ID...": "노드 ID 설정..."
"Set Node ID...": "노드 ID 설정...",
"Gallery": "갤러리",
"Gallery (Outputs)": "갤러리 (출력)",
"Go Up One Level": "한 수준 위로",
"Open Current Folder in File Manager": "파일 탐색기에서 현재 폴더 열기",
"Refresh Gallery": "갤러리 새로고침",
"Filter gallery files...": "갤러리 파일 필터링...",
"Loading files...": "파일 로딩 중...",
"No files found in this folder.": "이 폴더에 파일이 없습니다.",
"Failed to load gallery from disk.": "디스크에서 갤러리를 불러오지 못했습니다.",
"Sort: Newest First": "정렬: 최신순",
"Sort: Oldest First": "정렬: 오래된순",
"Search Gallery (Filename or Prompt)": "갤러리 검색 (파일명 또는 프롬프트)",
"Search filenames or prompt metadata...": "파일명 또는 프롬프트 메타데이터 검색...",
"Type to load saved workflow...": "저장된 워크플로우를 불러오려면 입력하세요...",
"No matching workflows": "일치하는 워크플로우가 없습니다"
}
+66
View File
@@ -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",
"Search History": "Pesquisar histórico",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nó(s) selecionado(s) na fila (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignorar/Incluir nós selecionados na fila",
"Renumber All Nodes (Execution Order)": "Renumerar todos os nós (ordem de execução)",
"Set Selected Node ID...": "Definir ID do nó selecionado...",
"Set Node ID...": "Definir ID do nó..."
"Set Node ID...": "Definir ID do nó...",
"Gallery": "Galeria",
"Gallery (Outputs)": "Galeria (Saídas)",
"Go Up One Level": "Subir um nível",
"Open Current Folder in File Manager": "Abrir pasta atual no gerenciador de arquivos",
"Refresh Gallery": "Atualizar galeria",
"Filter gallery files...": "Filtrar arquivos da galeria...",
"Loading files...": "Carregando arquivos...",
"No files found in this folder.": "Nenhum arquivo encontrado nesta pasta.",
"Failed to load gallery from disk.": "Falha ao carregar a galeria do disco.",
"Sort: Newest First": "Ordenar: Mais recentes primeiro",
"Sort: Oldest First": "Ordenar: Mais antigos primeiro",
"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...",
"Type to load saved workflow...": "Digite para carregar um fluxo salvo...",
"No matching workflows": "Nenhum fluxo correspondente"
}
+66
View File
@@ -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": "Очередь",
"Search History": "Поиск по истории",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Игнорировать выбранные ноды в очереди (Ctrl+Q)",
@@ -101,5 +114,21 @@
"Toggle Ignore Selected Node(s) in Queue": "Игнорировать выбранные ноды в очереди",
"Renumber All Nodes (Execution Order)": "Перенумеровать ноды (порядок выполнения)",
"Set Selected Node ID...": "Задать ID выбранной ноды...",
"Set Node ID...": "Задать ID ноды..."
}
"Set Node ID...": "Задать ID ноды...",
"Gallery": "Галерея",
"Gallery (Outputs)": "Галерея (Результаты)",
"Go Up One Level": "На один уровень вверх",
"Open Current Folder in File Manager": "Открыть текущую папку в проводнике",
"Refresh Gallery": "Обновить галерею",
"Filter gallery files...": "Фильтр файлов галереи...",
"Loading files...": "Загрузка файлов...",
"No files found in this folder.": "В этой папке нет файлов.",
"Failed to load gallery from disk.": "Не удалось загрузить галерею с диска.",
"Sort: Newest First": "Сортировка: сначала новые",
"Sort: Oldest First": "Сортировка: сначала старые",
"Search Gallery (Filename or Prompt)": "Поиск по галерее (имя файла или промпт)",
"Search filenames or prompt metadata...": "Поиск по именам файлов или промптам...",
"Type to load saved workflow...": "Найти сохранённый рабочий процесс...",
"No matching workflows": "Подходящие рабочие процессы не найдены"
}
+66
View File
@@ -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",
"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)",
@@ -101,5 +127,20 @@
"Toggle Ignore Selected Node(s) in Queue": "Kuyrukta Seçili Düğümleri Yoksay/Etkinleştir",
"Renumber All Nodes (Execution Order)": "Tüm Düğümleri Yeniden Numaralandır (Çalıştırma Sırası)",
"Set Selected Node ID...": "Seçili Düğüm Kimliğini Belirle...",
"Set Node ID...": "Düğüm Kimliğini Belirle..."
"Set Node ID...": "Düğüm Kimliğini Belirle...",
"Gallery": "Galeri",
"Gallery (Outputs)": "Galeri (Çıktılar)",
"Go Up One Level": "Bir Düzey Yukarı Çık",
"Open Current Folder in File Manager": "Mevcut Klasörü Dosya Yöneticisinde Aç",
"Refresh Gallery": "Galeriyi Yenile",
"Filter gallery files...": "Galeri dosyalarını filtrele...",
"Loading files...": "Dosyalar yükleniyor...",
"No files found in this folder.": "Bu klasörde dosya bulunamadı.",
"Failed to load gallery from disk.": "Galeri diskten yüklenemedi.",
"Sort: Newest First": "Sıralama: Önce En Yeniler",
"Sort: Oldest First": "Sıralama: Önce En Eskiler",
"Search Gallery (Filename or Prompt)": "Galeride Ara (Dosya Adı veya Prompt)",
"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"
}
+66
View File
@@ -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": "隊列",
"Search History": "搜尋歷史",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在隊列中忽略選取節點 (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "在隊列中忽略選取節點",
"Renumber All Nodes (Execution Order)": "重新編號所有節點 (按執行順序)",
"Set Selected Node ID...": "設定選取節點 ID...",
"Set Node ID...": "設定節點 ID..."
"Set Node ID...": "設定節點 ID...",
"Gallery": "圖庫",
"Gallery (Outputs)": "圖庫 (輸出)",
"Go Up One Level": "返回上一層",
"Open Current Folder in File Manager": "在檔案管理器中開啟目前資料夾",
"Refresh Gallery": "重新整理圖庫",
"Filter gallery files...": "篩選圖庫檔案...",
"Loading files...": "正在載入檔案...",
"No files found in this folder.": "此資料夾中未找到檔案。",
"Failed to load gallery from disk.": "從磁碟載入圖庫失敗。",
"Sort: Newest First": "排序:最新優先",
"Sort: Oldest First": "排序:最舊優先",
"Search Gallery (Filename or Prompt)": "搜尋圖庫 (檔案名稱或提示詞)",
"Search filenames or prompt metadata...": "搜尋檔案名稱或提示詞元資料...",
"Type to load saved workflow...": "輸入以載入已儲存的工作流程...",
"No matching workflows": "沒有符合的工作流程"
}
+66
View File
@@ -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": "队列",
"Search History": "搜索历史",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在队列中忽略所选节点 (Ctrl+Q)",
@@ -101,5 +114,20 @@
"Toggle Ignore Selected Node(s) in Queue": "在队列中忽略所选节点",
"Renumber All Nodes (Execution Order)": "重新编号所有节点 (按执行顺序)",
"Set Selected Node ID...": "设置所选节点 ID...",
"Set Node ID...": "设置节点 ID..."
"Set Node ID...": "设置节点 ID...",
"Gallery": "图库",
"Gallery (Outputs)": "图库 (输出)",
"Go Up One Level": "返回上一级",
"Open Current Folder in File Manager": "在文件管理器中打开当前文件夹",
"Refresh Gallery": "刷新图库",
"Filter gallery files...": "筛选图库文件...",
"Loading files...": "正在加载文件...",
"No files found in this folder.": "此文件夹中未找到文件。",
"Failed to load gallery from disk.": "从磁盘加载图库失败。",
"Sort: Newest First": "排序:最新优先",
"Sort: Oldest First": "排序:最早优先",
"Search Gallery (Filename or Prompt)": "搜索图库 (文件名或提示词)",
"Search filenames or prompt metadata...": "搜索文件名或提示词元数据...",
"Type to load saved workflow...": "输入以加载已保存的工作流...",
"No matching workflows": "没有匹配的工作流"
}
+66
View File
@@ -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": "创建新的空白工作流时显示快速工作流选择器"
}
}
+199
View File
@@ -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
View File
@@ -1,7 +1,7 @@
[project]
name = "comfy-sidebar"
description = "A compact and powerful layout overhaul for ComfyUI that makes generation results easier to browse, preview, compare, and manage."
version = "1.7.4"
version = "1.8.4"
license = { file = "LICENSE" }
dependencies = [
"send2trash>=1.8.0",