Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ab9e1946 | ||
|
|
2db8592267 | ||
|
|
9cb8f7faba | ||
|
|
a95d2ca1b7 | ||
|
|
f5f5016346 | ||
|
|
e9c7205e25 | ||
|
|
6b06b18bca | ||
|
|
1c6886712a | ||
|
|
2286083b9b | ||
|
|
a23ba4fae2 | ||
|
|
09677772c6 | ||
|
|
37522e484a | ||
|
|
270616a744 | ||
|
|
2941e48879 | ||
|
|
f7436011c4 | ||
|
|
e783422e7c | ||
|
|
41d2e38f2f | ||
|
|
56247cfa8b | ||
|
|
328915a7ee | ||
|
|
543e9e999a | ||
|
|
421361edb8 | ||
|
|
647c95a5df | ||
|
|
7421dfa756 | ||
|
|
60883fdf63 | ||
|
|
b6143a3abf | ||
|
|
828d510891 | ||
|
|
024309c050 | ||
|
|
2e09cf3963 | ||
|
|
ef424be4b1 | ||
|
|
a1fa872e2e | ||
|
|
d31852e5ab | ||
|
|
bb98171982 | ||
|
|
004744e8e2 | ||
|
|
94933a0246 | ||
|
|
c942976c02 | ||
|
|
0d19394c8f | ||
|
|
4bca1435a6 | ||
|
|
4eb67f25f0 | ||
|
|
40a9cf3db9 | ||
|
|
ff0466957e | ||
|
|
294e610eb1 | ||
|
|
a33052141a | ||
|
|
e2a3c0c596 | ||
|
|
557d910ab4 | ||
|
|
be5c8c41c9 | ||
|
|
408517273a | ||
|
|
e8b6622919 | ||
|
|
4001848e37 | ||
|
|
0733d001f3 | ||
|
|
33878502af | ||
|
|
2c9ded2cbf | ||
|
|
1a848e2b00 | ||
|
|
700b85408c | ||
|
|
6a0b0cd71a | ||
|
|
56452eb855 | ||
|
|
81d98f908b | ||
|
|
cad4098496 | ||
|
|
6c3422e5c0 | ||
|
|
2554e6dc45 |
+2
-1
@@ -1,4 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
desktop.ini
|
||||
@@ -18,6 +18,10 @@ A compact and powerful layout overhaul for ComfyUI that makes generation results
|
||||
|
||||
<img src="assets/compare.png" alt="ComfySidebar compare" width="100%">
|
||||
|
||||
* **[NEW] Output Gallery sidebar:** Browse files and subfolders directly from your ComfyUI `output` directory without leaving the interface. Features dedicated cards for images, videos, audio, and 3D assets (`.glb`/`.gltf`), breadcrumb navigation, 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%">
|
||||
@@ -58,6 +62,8 @@ 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
|
||||
|
||||
@@ -70,12 +76,16 @@ Open **Settings → Comfy Sidebar** to configure:
|
||||
| `Shift+Click` | Compare another result |
|
||||
| `Ctrl+Click` | Open the file location / alternative action |
|
||||
|
||||
`Q` is a regular ComfyUI keybinding, so you can see and change it in **Settings → Keybinding**.
|
||||
|
||||
You can also use the available actions from result cards, the sidebar header, and the node/canvas context menus.
|
||||
|
||||
## 📝 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).
|
||||
|
||||
+176
-3
@@ -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,150 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@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 +326,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)
|
||||
|
||||
+9
-5
@@ -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();
|
||||
|
||||
+40
-81
@@ -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,8 @@ 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 onLeft = isSidebarOnLeft();
|
||||
|
||||
popovers.forEach((pop) => {
|
||||
if (pop.style.display !== "none" && !pop.classList.contains("p-overlay-hidden")) {
|
||||
@@ -425,11 +421,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 +445,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 +493,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 +506,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 +548,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 +643,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 +670,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 +689,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 +707,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 +715,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 +728,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 +915,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 */
|
||||
|
||||
@@ -25,6 +25,7 @@ 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",
|
||||
MAX_HISTORY_ITEMS: "Comfy.Queue.MaxHistoryItems",
|
||||
DOCK_QUEUE: "Comfy.Queue.QPOV2",
|
||||
SHOW_PROGRESS_BAR: "Comfy.Queue.ShowRunProgressBar",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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...": "البحث في أسماء الملفات أو بيانات الموجه..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "إخفاء العناصر غير المرغوبة",
|
||||
"Default": "افتراضي",
|
||||
"Dock": "إرساء",
|
||||
"Hide": "إخفاء"
|
||||
"Hide": "إخفاء",
|
||||
"Enable Output Gallery Sidebar": "تمكين الشريط الجانبي لمعرض المخرجات",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "إصلاح اختصارات المفاتيح غير ASCII (اختصارات تعمل على أي تخطيط للوحة المفاتيح)",
|
||||
"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.": "يعامل الأحرف غير اللاتينية (مثل السيريلية) على أنها المفتاح اللاتيني في الموضع الفعلي نفسه، فتعمل الاختصارات مثل Q أو Ctrl+Z عند تفعيل تخطيط آخر. ولا تتأثر الكتابة في حقول النص."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "Hide Junk",
|
||||
"Default": "Default",
|
||||
"Dock": "Dock",
|
||||
"Hide": "Hide"
|
||||
"Hide": "Hide",
|
||||
"Enable Output Gallery Sidebar": "Enable Output Gallery Sidebar",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "Fix non-ASCII keybinds (hotkeys on any keyboard layout)",
|
||||
"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.": "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."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "Ocultar elementos innecesarios",
|
||||
"Default": "Por defecto",
|
||||
"Dock": "Acoplar",
|
||||
"Hide": "Ocultar"
|
||||
"Hide": "Ocultar",
|
||||
"Enable Output Gallery Sidebar": "Habilitar barra lateral de galería de salidas",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "Corregir atajos con caracteres no ASCII (atajos en cualquier distribución de teclado)",
|
||||
"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.": "Trata las letras no latinas (p. ej., cirílico) como la tecla latina en la misma posición física, de modo que atajos como Q o Ctrl+Z funcionen con otra distribución activa. No afecta a la escritura en campos de texto."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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...": "جستجوی نام فایلها یا متادیتای پرامپت..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "پنهان کردن موارد غیرضروری",
|
||||
"Default": "پیشفرض",
|
||||
"Dock": "اتصال",
|
||||
"Hide": "پنهان"
|
||||
"Hide": "پنهان",
|
||||
"Enable Output Gallery Sidebar": "فعالسازی نوار کناری گالری خروجیها",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "رفع مشکل کلیدهای میانبر غیر ASCII (میانبرها در هر چیدمان صفحهکلید)",
|
||||
"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.": "حروف غیر لاتین (مثلاً سیریلیک) را معادل کلید لاتین در همان موقعیت فیزیکی در نظر میگیرد تا میانبرهایی مانند Q یا Ctrl+Z هنگام فعال بودن چیدمان دیگر نیز کار کنند. تایپ در فیلدهای متنی تحت تأثیر قرار نمیگیرد."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "Masquer les éléments inutiles",
|
||||
"Default": "Par défaut",
|
||||
"Dock": "Ancrer",
|
||||
"Hide": "Masquer"
|
||||
"Hide": "Masquer",
|
||||
"Enable Output Gallery Sidebar": "Activer la barre latérale de la galerie de sorties",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "Corriger les raccourcis non ASCII (raccourcis sur toutes les dispositions de clavier)",
|
||||
"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.": "Traite les lettres non latines (p. ex. cyrillique) comme la touche latine située au même emplacement physique, afin que des raccourcis comme Q ou Ctrl+Z fonctionnent avec une autre disposition active. La saisie dans les champs de texte n'est pas affectée."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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...": "חיפוש שמות קבצים או מטא-דאטה של פרומפט..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "הסתר פריטים מיותרים",
|
||||
"Default": "ברירת מחדל",
|
||||
"Dock": "הצמד",
|
||||
"Hide": "הסתר"
|
||||
"Hide": "הסתר",
|
||||
"Enable Output Gallery Sidebar": "הפעל סרגל צד של גלריית תוצרים",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "תיקון קיצורי מקשים שאינם ASCII (קיצורים בכל פריסת מקלדת)",
|
||||
"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.": "מתייחס לאותיות שאינן לטיניות (למשל קירילית) כמקש הלטיני באותו מיקום פיזי, כך שקיצורים כמו Q או Ctrl+Z פועלים גם כשפריסה אחרת פעילה. ההקלדה בשדות טקסט אינה מושפעת."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "Nascondi elementi inutili",
|
||||
"Default": "Predefinito",
|
||||
"Dock": "Aggancia",
|
||||
"Hide": "Nascondi"
|
||||
"Hide": "Nascondi",
|
||||
"Enable Output Gallery Sidebar": "Abilita barra laterale galleria di output",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "Correggi le scorciatoie non ASCII (scorciatoie con qualsiasi layout di tastiera)",
|
||||
"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.": "Considera le lettere non latine (ad es. cirillico) come il tasto latino nella stessa posizione fisica, così scorciatoie come Q o Ctrl+Z funzionano anche con un altro layout attivo. La digitazione nei campi di testo non è interessata."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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...": "ファイル名またはプロンプトを検索..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "不要な項目を非表示",
|
||||
"Default": "デフォルト",
|
||||
"Dock": "ドック",
|
||||
"Hide": "非表示"
|
||||
"Hide": "非表示",
|
||||
"Enable Output Gallery Sidebar": "出力ギャラリーのサイドバーを有効化",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "非ASCIIのキーバインドを修正(どのキーボード配列でもショートカットを使用)",
|
||||
"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.": "ラテン文字以外の文字(例:キリル文字)を同じ物理位置のラテン文字キーとして扱い、別の配列が有効な状態でも Q や Ctrl+Z などのショートカットが動作するようにします。テキスト入力欄での入力には影響しません。"
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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...": "파일명 또는 프롬프트 메타데이터 검색..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "불필요한 항목 숨기기",
|
||||
"Default": "기본값",
|
||||
"Dock": "도킹",
|
||||
"Hide": "숨기기"
|
||||
"Hide": "숨기기",
|
||||
"Enable Output Gallery Sidebar": "출력 갤러리 사이드바 활성화",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "비ASCII 단축키 수정(모든 키보드 배열에서 단축키 사용)",
|
||||
"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.": "라틴 문자가 아닌 글자(예: 키릴 문자)를 같은 물리적 위치의 라틴 키로 처리하여, 다른 자판 배열이 활성화되어 있어도 Q, Ctrl+Z 같은 단축키가 작동합니다. 텍스트 입력란에서의 입력에는 영향을 주지 않습니다."
|
||||
}
|
||||
@@ -101,5 +101,18 @@
|
||||
"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..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "Ocultar itens desnecessários",
|
||||
"Default": "Padrão",
|
||||
"Dock": "Fixar",
|
||||
"Hide": "Ocultar"
|
||||
"Hide": "Ocultar",
|
||||
"Enable Output Gallery Sidebar": "Ativar barra lateral da galeria de saídas",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "Corrigir atalhos não ASCII (atalhos em qualquer layout de teclado)",
|
||||
"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.": "Trata letras não latinas (por exemplo, cirílico) como a tecla latina na mesma posição física, para que atalhos como Q ou Ctrl+Z funcionem com outro layout ativo. A digitação em campos de texto não é afetada."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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...": "Поиск по именам файлов или промптам..."
|
||||
}
|
||||
@@ -22,12 +22,14 @@
|
||||
"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": "Скрыть"
|
||||
"Hide": "Скрыть",
|
||||
"Enable Output Gallery Sidebar": "Включить боковую панель галереи результатов",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "Исправить не-ASCII горячие клавиши (работа в любой раскладке)",
|
||||
"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.": "Нелатинские буквы (например, кириллица) считаются латинской клавишей в том же физическом положении, поэтому горячие клавиши вроде Q или Ctrl+Z работают при любой активной раскладке. Ввод текста в полях не затрагивается."
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "Gereksiz Öğeleri Gizle",
|
||||
"Default": "Varsayılan",
|
||||
"Dock": "Sabitle",
|
||||
"Hide": "Gizle"
|
||||
"Hide": "Gizle",
|
||||
"Enable Output Gallery Sidebar": "Çıktı Galerisi Kenar Çubuğunu Etkinleştir",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "ASCII olmayan kısayolları düzelt (her klavye düzeninde kısayollar)",
|
||||
"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.": "Latin olmayan harfleri (örn. Kiril) aynı fiziksel konumdaki Latin tuş olarak kabul eder; böylece başka bir düzen etkinken Q veya Ctrl+Z gibi kısayollar çalışır. Metin alanlarına yazma etkilenmez."
|
||||
}
|
||||
@@ -101,5 +101,18 @@
|
||||
"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...": "搜尋檔案名稱或提示詞元資料..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "隱藏雜項",
|
||||
"Default": "預設",
|
||||
"Dock": "停靠",
|
||||
"Hide": "隱藏"
|
||||
"Hide": "隱藏",
|
||||
"Enable Output Gallery Sidebar": "啟用輸出圖庫側邊欄",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "修正非 ASCII 快速鍵(任何鍵盤配置下皆可使用)",
|
||||
"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.": "將非拉丁字母(如西里爾字母)視為相同實體位置的拉丁按鍵,因此在使用其他鍵盤配置時,Q 或 Ctrl+Z 等快速鍵也能正常運作。在文字欄位中輸入不受影響。"
|
||||
}
|
||||
+14
-1
@@ -101,5 +101,18 @@
|
||||
"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...": "搜索文件名或提示词元数据..."
|
||||
}
|
||||
@@ -28,5 +28,8 @@
|
||||
"Hide Junk": "隐藏杂项",
|
||||
"Default": "默认",
|
||||
"Dock": "停靠",
|
||||
"Hide": "隐藏"
|
||||
"Hide": "隐藏",
|
||||
"Enable Output Gallery Sidebar": "启用输出图库侧边栏",
|
||||
"Fix non-ASCII keybinds (hotkeys on any keyboard layout)": "修复非 ASCII 快捷键(任意键盘布局下可用)",
|
||||
"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.": "将非拉丁字母(如西里尔字母)视为同一物理位置的拉丁按键,这样在启用其他键盘布局时,Q 或 Ctrl+Z 等快捷键也能正常使用。在文本框中输入不受影响。"
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
+134
-135
@@ -2,21 +2,53 @@ 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 { 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 { setupGalleryView, teardownGalleryView } from "./ui/galleryView.js";
|
||||
import { initI18n, t } 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 +78,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 +172,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"),
|
||||
@@ -127,104 +193,6 @@ 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);
|
||||
}
|
||||
});
|
||||
|
||||
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() {
|
||||
let isSyncing = false;
|
||||
const TARGET_SETTING = "Comfy.Workflow.WorkflowTabsPosition";
|
||||
@@ -232,15 +200,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 +215,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 +229,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 +255,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 +290,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 +354,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 +376,14 @@ app.registerExtension({
|
||||
|
||||
cleanupFns.push(setupWorkflowTabsPositionWatcher());
|
||||
cleanupFns.push(setupDragAndDrop());
|
||||
cleanupFns.push(setupLatinHotkeys());
|
||||
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 +397,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 +460,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 +475,4 @@ app.registerExtension({
|
||||
app.extensionManager.unregisterSidebarTab("classic-comfy-sidebar");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
+45
-72
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
+23
-7
@@ -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)) {
|
||||
|
||||
+407
-46
@@ -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,215 @@ 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);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
+30
-20
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Extract searchable prompt text from ComfyUI prompt graphs and PNG outputs.
|
||||
|
||||
This module has no ComfyUI imports, so it can be used and tested on its own.
|
||||
|
||||
How it works
|
||||
------------
|
||||
* Only the PNG ``prompt`` text chunk is read. Other chunks (notably the often
|
||||
multi-megabyte ``workflow`` chunk) are skipped with a seek, never parsed.
|
||||
* From API-format prompt graphs (whether supplied by Queue or read from PNGs),
|
||||
collect only the strings that actually
|
||||
feed a text-encoder node (``CLIPTextEncode`` and friends), following links to
|
||||
upstream string/primitive/concat nodes. Model names, sampler names, file
|
||||
names and other string widgets are ignored.
|
||||
* Extracted text is cached per file (keyed by mtime + size), so repeated
|
||||
searches only pay for files that are new or changed.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import zlib
|
||||
from collections import OrderedDict
|
||||
|
||||
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
# Input names that can carry prompt text (substring match, lower-case).
|
||||
# Deliberately does not match plain "clip", "model", "vae", "image", etc.
|
||||
_PROMPT_KEY_HINTS = (
|
||||
"text", "prompt", "positive", "negative", "caption",
|
||||
"string", "value", "t5xxl", "clip_l", "clip_g",
|
||||
)
|
||||
_MAX_LINK_DEPTH = 6
|
||||
_MAX_PROMPT_CHUNK_BYTES = 16 * 1024 * 1024
|
||||
_MAX_TEXT_CHARS_PER_FILE = 50_000
|
||||
|
||||
_CACHE_MAX_ENTRIES = 20_000
|
||||
_cache = OrderedDict() # path -> ((mtime_ns, size), lowercased prompt text)
|
||||
_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Prompt graph -> text
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _is_prompt_key(name):
|
||||
lowered = str(name).lower()
|
||||
return any(hint in lowered for hint in _PROMPT_KEY_HINTS)
|
||||
|
||||
|
||||
def _is_encoder(class_type):
|
||||
lowered = str(class_type or "").lower()
|
||||
return "textencode" in lowered or "prompt" in lowered
|
||||
|
||||
|
||||
def _is_link(value):
|
||||
# API-format link: [source_node_id, output_slot]
|
||||
return isinstance(value, list) and len(value) == 2 and isinstance(value[1], int)
|
||||
|
||||
|
||||
def extract_prompt_texts(prompt):
|
||||
"""Return the prompt strings found in an API-format ComfyUI prompt dict."""
|
||||
if not isinstance(prompt, dict):
|
||||
return []
|
||||
|
||||
texts = []
|
||||
seen = set()
|
||||
|
||||
def visit(node_id, depth):
|
||||
key = str(node_id)
|
||||
if key in seen or depth > _MAX_LINK_DEPTH:
|
||||
return
|
||||
seen.add(key)
|
||||
node = prompt.get(key)
|
||||
inputs = node.get("inputs") if isinstance(node, dict) else None
|
||||
if not isinstance(inputs, dict):
|
||||
return
|
||||
for name, value in inputs.items():
|
||||
if not _is_prompt_key(name):
|
||||
continue
|
||||
if isinstance(value, str):
|
||||
if value.strip():
|
||||
texts.append(value)
|
||||
elif _is_link(value):
|
||||
visit(value[0], depth + 1)
|
||||
|
||||
starts = [
|
||||
node_id for node_id, node in prompt.items()
|
||||
if isinstance(node, dict) and _is_encoder(node.get("class_type"))
|
||||
]
|
||||
if not starts:
|
||||
# Unusual graph with no recognisable text-encoder node: fall back to
|
||||
# every node, still restricted to prompt-like input names.
|
||||
starts = list(prompt)
|
||||
for node_id in starts:
|
||||
visit(node_id, 0)
|
||||
return texts
|
||||
|
||||
|
||||
def get_prompt_search_text_from_graph(prompt):
|
||||
"""Return lower-cased text-encoder prompt text from an API-format graph."""
|
||||
return "\n".join(extract_prompt_texts(prompt)).lower()[:_MAX_TEXT_CHARS_PER_FILE]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# PNG chunk reading
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _inflate(data, limit=_MAX_PROMPT_CHUNK_BYTES):
|
||||
# Bounded decompress: refuses to expand beyond `limit` bytes.
|
||||
return zlib.decompressobj().decompress(data, limit)
|
||||
|
||||
|
||||
def _decode_text_chunk(chunk_type, data, nul):
|
||||
"""Decode tEXt / zTXt / iTXt chunk data (keyword already located at `nul`)."""
|
||||
if chunk_type == b"tEXt":
|
||||
return data[nul + 1:].decode("latin-1")
|
||||
|
||||
if chunk_type == b"zTXt":
|
||||
if data[nul + 1] != 0: # only compression method 0 (zlib) exists
|
||||
return None
|
||||
return _inflate(data[nul + 2:]).decode("latin-1")
|
||||
|
||||
# iTXt: keyword\0 compression_flag compression_method language\0 translated\0 text
|
||||
flag = data[nul + 1]
|
||||
rest = data[nul + 3:]
|
||||
lang_end = rest.index(b"\x00")
|
||||
translated_end = rest.index(b"\x00", lang_end + 1)
|
||||
body = rest[translated_end + 1:]
|
||||
if flag == 1:
|
||||
body = _inflate(body)
|
||||
return body.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def read_png_prompt(fileobj):
|
||||
"""Return the parsed ``prompt`` JSON from an open PNG file, or None."""
|
||||
if fileobj.read(8) != _PNG_SIGNATURE:
|
||||
return None
|
||||
while True:
|
||||
header = fileobj.read(8)
|
||||
if len(header) < 8:
|
||||
return None
|
||||
length = int.from_bytes(header[:4], "big")
|
||||
chunk_type = header[4:]
|
||||
if chunk_type in (b"IDAT", b"IEND"):
|
||||
return None # ComfyUI writes its text chunks before the image data
|
||||
|
||||
if chunk_type in (b"tEXt", b"zTXt", b"iTXt"):
|
||||
head = fileobj.read(min(length, 80)) # keyword is at most 79 bytes
|
||||
nul = head.find(b"\x00")
|
||||
if nul != -1 and head[:nul] == b"prompt":
|
||||
if length > _MAX_PROMPT_CHUNK_BYTES:
|
||||
return None
|
||||
data = head + fileobj.read(length - len(head))
|
||||
text = _decode_text_chunk(chunk_type, data, nul)
|
||||
return json.loads(text) if text else None
|
||||
fileobj.seek(length - len(head) + 4, 1) # rest of data + CRC
|
||||
else:
|
||||
fileobj.seek(length + 4, 1)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Cached per-file text
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def get_prompt_search_text(path):
|
||||
"""Lower-cased prompt text of a PNG ('' if none). Cached by mtime + size."""
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except OSError:
|
||||
return ""
|
||||
signature = (st.st_mtime_ns, st.st_size)
|
||||
|
||||
with _cache_lock:
|
||||
hit = _cache.get(path)
|
||||
if hit and hit[0] == signature:
|
||||
_cache.move_to_end(path)
|
||||
return hit[1]
|
||||
|
||||
text = ""
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
prompt = read_png_prompt(f)
|
||||
if prompt is not None:
|
||||
text = get_prompt_search_text_from_graph(prompt)
|
||||
except Exception:
|
||||
# Corrupt / truncated / non-ComfyUI PNG: treat as "no prompt".
|
||||
text = ""
|
||||
|
||||
with _cache_lock:
|
||||
_cache[path] = (signature, text)
|
||||
_cache.move_to_end(path)
|
||||
while len(_cache) > _CACHE_MAX_ENTRIES:
|
||||
_cache.popitem(last=False)
|
||||
return text
|
||||
|
||||
|
||||
def clear_cache():
|
||||
with _cache_lock:
|
||||
_cache.clear()
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
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.3"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"send2trash>=1.8.0",
|
||||
|
||||
Reference in New Issue
Block a user