Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a848e2b00 | ||
|
|
700b85408c | ||
|
|
6a0b0cd71a | ||
|
|
56452eb855 | ||
|
|
81d98f908b | ||
|
|
cad4098496 | ||
|
|
6c3422e5c0 | ||
|
|
2554e6dc45 | ||
|
|
cc8337f6e6 | ||
|
|
b4a21ef852 | ||
|
|
65e53946d2 | ||
|
|
b1d784e9e4 | ||
|
|
2b91e5ef72 |
+2
-1
@@ -1,4 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
desktop.ini
|
||||
@@ -18,6 +18,8 @@ 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, prompt metadata search across subfolders, and persistent sorting.
|
||||
|
||||
* **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 +60,7 @@ Open **Settings → Comfy Sidebar** to configure:
|
||||
* Graph button visibility
|
||||
* Unified top bar layout
|
||||
* Permanent deletion behavior
|
||||
* Output Gallery sidebar toggle
|
||||
|
||||
## ⌨️ Controls
|
||||
|
||||
|
||||
+133
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import json
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
import folder_paths
|
||||
@@ -117,6 +118,138 @@ def open_file_in_os(target_path):
|
||||
raise RuntimeError("Headless Linux server detected (no desktop GUI available).")
|
||||
|
||||
|
||||
def check_png_prompt_match(filepath, search_term):
|
||||
"""Checks PNG prompt metadata strictly for text inputs matching search."""
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
if f.read(8) != b"\x89PNG\r\n\x1a\n":
|
||||
return False
|
||||
while True:
|
||||
len_b = f.read(4)
|
||||
if len(len_b) < 4:
|
||||
break
|
||||
length = int.from_bytes(len_b, "big")
|
||||
ctype = f.read(4)
|
||||
if ctype in (b"tEXt", b"iTXt"):
|
||||
data = f.read(length)
|
||||
if data.startswith(b"prompt\x00") or b'"inputs"' in data:
|
||||
text = data.decode("utf-8", errors="ignore")
|
||||
idx = text.find("{")
|
||||
if idx != -1:
|
||||
try:
|
||||
prompt_dict = json.loads(text[idx:])
|
||||
for node in prompt_dict.values():
|
||||
if isinstance(node, dict) and "inputs" in node:
|
||||
for val in node["inputs"].values():
|
||||
if isinstance(val, str) and search_term in val.lower():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
elif ctype in (b"IDAT", b"IEND"):
|
||||
break
|
||||
else:
|
||||
f.seek(length, 1)
|
||||
f.seek(4, 1)
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@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()
|
||||
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 web.json_response({"error": "Unsafe path"}, status=403)
|
||||
|
||||
if not os.path.exists(target_dir) or not os.path.isdir(target_dir):
|
||||
return web.json_response({"subfolder": subfolder, "folders": [], "files": []})
|
||||
|
||||
folders = []
|
||||
files = []
|
||||
|
||||
supported_exts = {
|
||||
".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif",
|
||||
".mp4", ".webm", ".mov", ".mkv",
|
||||
".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac",
|
||||
".glb", ".gltf", ".obj", ".fbx", ".stl",
|
||||
".json"
|
||||
}
|
||||
|
||||
if search:
|
||||
# Recursive search in current directory and all subfolders
|
||||
for root, dirs, fnames in os.walk(target_dir):
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
for fname in fnames:
|
||||
if fname.startswith("."):
|
||||
continue
|
||||
ext = os.path.splitext(fname)[1].lower()
|
||||
if ext in supported_exts:
|
||||
fpath = os.path.join(root, fname)
|
||||
matched = search in fname.lower()
|
||||
if not matched and ext == ".png":
|
||||
matched = check_png_prompt_match(fpath, search)
|
||||
|
||||
if matched:
|
||||
rel_sub = os.path.relpath(root, output_dir).replace("\\", "/")
|
||||
if rel_sub == ".":
|
||||
rel_sub = ""
|
||||
stat = os.stat(fpath)
|
||||
files.append({
|
||||
"filename": fname,
|
||||
"subfolder": rel_sub,
|
||||
"type": "output",
|
||||
"mtime": stat.st_mtime,
|
||||
"size": stat.st_size
|
||||
})
|
||||
else:
|
||||
# Instant single-directory listing without reading files from disk
|
||||
with os.scandir(target_dir) as entries:
|
||||
for entry in entries:
|
||||
if entry.name.startswith("."):
|
||||
continue
|
||||
try:
|
||||
if entry.is_dir(follow_symlinks=False):
|
||||
rel_sub = os.path.relpath(entry.path, output_dir).replace("\\", "/")
|
||||
folders.append({
|
||||
"name": entry.name,
|
||||
"subfolder": rel_sub,
|
||||
"mtime": entry.stat().st_mtime
|
||||
})
|
||||
elif entry.is_file(follow_symlinks=False):
|
||||
ext = os.path.splitext(entry.name)[1].lower()
|
||||
if ext in supported_exts:
|
||||
rel_sub = os.path.relpath(os.path.dirname(entry.path), output_dir).replace("\\", "/")
|
||||
if rel_sub == ".":
|
||||
rel_sub = ""
|
||||
stat = entry.stat()
|
||||
files.append({
|
||||
"filename": entry.name,
|
||||
"subfolder": rel_sub,
|
||||
"type": "output",
|
||||
"mtime": stat.st_mtime,
|
||||
"size": stat.st_size
|
||||
})
|
||||
except (PermissionError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
folders.sort(key=lambda x: x["name"].lower())
|
||||
files.sort(key=lambda x: x["mtime"], reverse=True)
|
||||
|
||||
return web.json_response({
|
||||
"subfolder": subfolder.replace("\\", "/"),
|
||||
"folders": folders,
|
||||
"files": files
|
||||
})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/comfy-sidebar/open-folder")
|
||||
async def open_folder_handler(request):
|
||||
try:
|
||||
|
||||
+131
-25
@@ -1,4 +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"]');
|
||||
@@ -26,7 +28,7 @@ export function findOriginalPropertiesButton() {
|
||||
}
|
||||
|
||||
export function findTopbarContainer() {
|
||||
const container = document.querySelector('[data-testid="action-bar-buttons"], [class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
|
||||
const container = document.querySelector('.actionbar-container, [data-testid="action-bar-buttons"], [class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
|
||||
if (container) return container;
|
||||
|
||||
const orig = findOriginalPropertiesButton();
|
||||
@@ -70,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;
|
||||
|
||||
@@ -156,6 +163,11 @@ export function findNativeExtensionsPanel() {
|
||||
}
|
||||
|
||||
export function findGraphButton() {
|
||||
const byTestId = document.querySelector('[data-testid="view-mode-toggle"]');
|
||||
if (byTestId && !byTestId.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings, .comfyui-sidebar, .comfy-sidebar')) {
|
||||
return byTestId;
|
||||
}
|
||||
|
||||
const elements = document.querySelectorAll('.p-1.bg-base-background.rounded-lg, .bg-base-background.rounded-lg, .bg-secondary-background.rounded-lg.items-center.inline-flex.pointer-events-auto, [data-testid="graph-view-button"]');
|
||||
for (const el of elements) {
|
||||
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings, .comfyui-sidebar, .comfy-sidebar')) continue;
|
||||
@@ -230,39 +242,133 @@ export function updateSidebarBadge(count) {
|
||||
}
|
||||
}
|
||||
|
||||
export function applySidebarOverride() {
|
||||
const overrideStock = app.ui.settings?.getSettingValue?.("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
|
||||
const stdBtn = findStandardQueueButton();
|
||||
const ourBtn = findOurSidebarButton();
|
||||
let isApplyingSidebarOverride = false;
|
||||
|
||||
if (stdBtn) {
|
||||
if (!stdBtn._originalDisplay) {
|
||||
stdBtn._originalDisplay = window.getComputedStyle(stdBtn).display || "block";
|
||||
export function findSidebarMenuButton(sidebar) {
|
||||
const root = sidebar || document.querySelector('[data-testid="sidebar-top-group"]') ||
|
||||
document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar');
|
||||
if (!root) return null;
|
||||
|
||||
// 1. Look for explicit menu or logo test ID
|
||||
const byTestId = root.querySelector(
|
||||
'[data-testid*="menu-button"], [data-testid*="app-menu"], [data-testid*="comfy-logo"], [data-testid="sidebar-menu-button"]'
|
||||
);
|
||||
if (byTestId) {
|
||||
return byTestId.closest('button, [role="button"], .comfyui-sidebar-tab, .comfyui-sidebar-item') || byTestId;
|
||||
}
|
||||
|
||||
// 2. Look for title, tooltip, or aria-label containing "menu"
|
||||
const candidates = Array.from(root.querySelectorAll('button, [role="button"], a, .comfyui-sidebar-tab, .comfyui-sidebar-item'));
|
||||
const menuBtn = candidates.find(el => {
|
||||
if (el.classList.contains('comfy-sidebar-tab') || el.id?.includes('classic-comfy-sidebar')) return false;
|
||||
const title = (el.getAttribute('title') || el.getAttribute('aria-label') || el.textContent || '').trim().toLowerCase();
|
||||
return title === 'menu' || title.startsWith('menu');
|
||||
});
|
||||
if (menuBtn) return menuBtn;
|
||||
|
||||
// 3. Fallback: the first child element that is not our button and not the stock queue button
|
||||
const ourBtn = findOurSidebarButton();
|
||||
const stdBtn = findStandardQueueButton();
|
||||
for (const child of root.children) {
|
||||
if (child !== ourBtn && child !== stdBtn) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function applySidebarOverride() {
|
||||
if (isApplyingSidebarOverride) return;
|
||||
isApplyingSidebarOverride = true;
|
||||
|
||||
try {
|
||||
const overrideStock = (
|
||||
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();
|
||||
const topGroup = document.querySelector('[data-testid="sidebar-top-group"]') ||
|
||||
(ourBtn ? ourBtn.closest('[data-testid="sidebar-top-group"]') : null);
|
||||
|
||||
if (ourBtn && !ourBtn._capturedOriginal) {
|
||||
ourBtn._capturedOriginal = true;
|
||||
ourBtn._originalParent = ourBtn.parentNode;
|
||||
ourBtn._originalNextSibling = ourBtn.nextSibling;
|
||||
}
|
||||
|
||||
if (overrideStock) {
|
||||
stdBtn.style.setProperty("display", "none", "important");
|
||||
if (!stdBtn._overrideClickListener) {
|
||||
stdBtn._overrideClickListener = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const b = findOurSidebarButton();
|
||||
if (b) b.click();
|
||||
};
|
||||
stdBtn.addEventListener('click', stdBtn._overrideClickListener, true);
|
||||
if (stdBtn) {
|
||||
if (!stdBtn._originalDisplay) {
|
||||
stdBtn._originalDisplay = window.getComputedStyle(stdBtn).display || "block";
|
||||
}
|
||||
if (stdBtn.style.display !== "none") {
|
||||
stdBtn.style.setProperty("display", "none", "important");
|
||||
}
|
||||
if (!stdBtn._overrideClickListener) {
|
||||
stdBtn._overrideClickListener = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const b = findOurSidebarButton();
|
||||
if (b) b.click();
|
||||
};
|
||||
stdBtn.addEventListener('click', stdBtn._overrideClickListener, true);
|
||||
}
|
||||
}
|
||||
if (ourBtn && stdBtn.parentNode && ourBtn.nextSibling !== stdBtn) {
|
||||
stdBtn.parentNode.insertBefore(ourBtn, stdBtn);
|
||||
|
||||
if (ourBtn) {
|
||||
const parent = (topGroup && topGroup.contains(ourBtn)) ? topGroup : (stdBtn ? stdBtn.parentNode : ourBtn.parentNode);
|
||||
if (parent) {
|
||||
const menuBtn = findSidebarMenuButton(parent);
|
||||
if (menuBtn && menuBtn.parentNode === parent) {
|
||||
// Place Queue immediately below the Menu button
|
||||
if (menuBtn.nextSibling !== ourBtn) {
|
||||
parent.insertBefore(ourBtn, menuBtn.nextSibling);
|
||||
}
|
||||
} else if (stdBtn && stdBtn.parentNode === parent) {
|
||||
// Place Queue right where stock Job History was
|
||||
if (stdBtn.previousSibling !== ourBtn && stdBtn !== ourBtn) {
|
||||
parent.insertBefore(ourBtn, stdBtn);
|
||||
}
|
||||
} else {
|
||||
// Fallback: top of the group
|
||||
const first = parent.firstElementChild;
|
||||
if (first && first !== ourBtn) {
|
||||
parent.insertBefore(ourBtn, first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stdBtn.style.setProperty("display", stdBtn._originalDisplay === "none" ? "block" : stdBtn._originalDisplay);
|
||||
if (stdBtn._overrideClickListener) {
|
||||
stdBtn.removeEventListener('click', stdBtn._overrideClickListener, true);
|
||||
stdBtn._overrideClickListener = null;
|
||||
if (stdBtn) {
|
||||
if (stdBtn.style.display === "none") {
|
||||
stdBtn.style.removeProperty("display");
|
||||
if (stdBtn._originalDisplay && stdBtn._originalDisplay !== "none") {
|
||||
stdBtn.style.display = stdBtn._originalDisplay;
|
||||
}
|
||||
}
|
||||
if (stdBtn._overrideClickListener) {
|
||||
stdBtn.removeEventListener('click', stdBtn._overrideClickListener, true);
|
||||
stdBtn._overrideClickListener = null;
|
||||
}
|
||||
}
|
||||
if (ourBtn && stdBtn.parentNode && ourBtn.parentNode === stdBtn.parentNode && ourBtn !== stdBtn.parentNode.lastChild) {
|
||||
stdBtn.parentNode.appendChild(ourBtn);
|
||||
|
||||
if (ourBtn) {
|
||||
if (ourBtn._originalNextSibling && ourBtn._originalNextSibling.parentNode === ourBtn.parentNode) {
|
||||
if (ourBtn.nextSibling !== ourBtn._originalNextSibling) {
|
||||
ourBtn.parentNode.insertBefore(ourBtn, ourBtn._originalNextSibling);
|
||||
}
|
||||
} else if (ourBtn._originalParent && ourBtn.parentNode === ourBtn._originalParent) {
|
||||
if (ourBtn !== ourBtn.parentNode.lastElementChild) {
|
||||
ourBtn.parentNode.appendChild(ourBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isApplyingSidebarOverride = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+107
-115
@@ -6,9 +6,12 @@ import {
|
||||
findActiveQueueIndicator,
|
||||
findNativeExtensionsPanel,
|
||||
findGraphButton,
|
||||
findPropertiesPanel
|
||||
findPropertiesPanel,
|
||||
applySidebarOverride
|
||||
} 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(() => {
|
||||
@@ -18,20 +21,22 @@ setRuntimePreviewStateListener(() => {
|
||||
const STYLE_ID = "comfy-sidebar-classic-layout-override";
|
||||
|
||||
const CLASSIC_LAYOUT_CSS_MEDIA = `
|
||||
/* Neutralize the leftover floating shell box to prevent empty dark square artifacts */
|
||||
.actionbar-container,
|
||||
.shadow-interface.rounded-lg.bg-comfy-menu-bg:not(.floating-panel):not([role="toolbar"]),
|
||||
.shadow-interface.rounded-lg.border-interface-stroke:not(.floating-panel):not([role="toolbar"]),
|
||||
.shadow-interface.border.rounded-lg:has([class*="actionbar-buttons"]),
|
||||
.shadow-interface.border.rounded-lg:has(.actionbar-buttons),
|
||||
div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]),
|
||||
div.shadow-interface.py-1\\.75 {
|
||||
/* Neutralize the leftover floating shell box to prevent empty dark square/circle artifacts */
|
||||
[data-testid="action-bar-card"],
|
||||
.floating-panel:has(.actionbar-container),
|
||||
.floating-panel:has([class*="actionbar"]),
|
||||
div.shadow-interface:has(.actionbar-container),
|
||||
div.shadow-interface:has([class*="actionbar-buttons"]),
|
||||
div:has(> .actionbar-container):not(header):not(.topbar),
|
||||
div:has(> [class*="actionbar"]):not(header):not(.topbar):not(.actionbar-container) {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Dock actionbar buttons in the topbar row with transparent background */
|
||||
.actionbar-container,
|
||||
[class*="actionbar"]:not(.actionbar),
|
||||
[class*="actionbar-buttons"],
|
||||
.actionbar-buttons {
|
||||
@@ -84,28 +89,42 @@ div.shadow-interface.py-1\\.75 {
|
||||
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;
|
||||
let domObserver = null;
|
||||
let sidebarTopObserver = null;
|
||||
let syncScheduled = false;
|
||||
let actionbarResizeObserver = null;
|
||||
|
||||
function setupSidebarTopObserver() {
|
||||
if (sidebarTopObserver) return;
|
||||
const target = document.querySelector('[data-testid="sidebar-top-group"]') ||
|
||||
document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar');
|
||||
if (!target) return;
|
||||
|
||||
sidebarTopObserver = new MutationObserver(() => {
|
||||
applySidebarOverride();
|
||||
updateSidebarTabsVisibility();
|
||||
});
|
||||
|
||||
sidebarTopObserver.observe(target, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -344,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")) {
|
||||
@@ -371,7 +389,7 @@ function setupCanvasMenuPopoverRelocator() {
|
||||
}
|
||||
|
||||
function updateTopMetrics() {
|
||||
const bar = document.querySelector('.actionbar-buttons, [class*="actionbar-buttons"]') || findTopbarContainer();
|
||||
const bar = document.querySelector('.actionbar-container, .actionbar-buttons, [class*="actionbar-buttons"]') || findTopbarContainer();
|
||||
const tabsEl = document.querySelector('.workflow-tabs-container');
|
||||
const topbar = tabsEl?.closest('header, .topbar, [class*="topbar"]') || tabsEl;
|
||||
|
||||
@@ -403,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");
|
||||
@@ -431,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);
|
||||
|
||||
@@ -479,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;
|
||||
@@ -491,7 +505,11 @@ function updateSidebarTabsVisibility() {
|
||||
}
|
||||
});
|
||||
|
||||
const hideStockHistory = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
|
||||
const hideStockHistory = (
|
||||
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) {
|
||||
const historyBtn = historyTarget.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || historyTarget;
|
||||
@@ -530,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) {
|
||||
@@ -578,10 +596,6 @@ function syncPropertiesButton(isClassicLayoutEnabled) {
|
||||
const openState = isPropertiesPanelOpen();
|
||||
|
||||
if (originalBtn) {
|
||||
if (!originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
|
||||
originalBtn.classList.add("comfy-sidebar-hide-original-properties-btn");
|
||||
}
|
||||
|
||||
savedButtonData = {
|
||||
className: originalBtn.className.replace("comfy-sidebar-hide-original-properties-btn", "").trim(),
|
||||
innerHTML: originalBtn.innerHTML,
|
||||
@@ -591,6 +605,26 @@ function syncPropertiesButton(isClassicLayoutEnabled) {
|
||||
value: attr.value
|
||||
}))
|
||||
};
|
||||
|
||||
// When closed: keep originalBtn visible (ComfyUI renders error outlines & dot natively).
|
||||
// When open: hide originalBtn so our customBtn takes its pinned place.
|
||||
if (openState) {
|
||||
if (!originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
|
||||
originalBtn.classList.add("comfy-sidebar-hide-original-properties-btn");
|
||||
}
|
||||
} else {
|
||||
if (originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
|
||||
originalBtn.classList.remove("comfy-sidebar-hide-original-properties-btn");
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking the native button to open the panel triggers an instant layout sync
|
||||
if (!originalBtn._syncAttached) {
|
||||
originalBtn._syncAttached = true;
|
||||
originalBtn.addEventListener("click", () => {
|
||||
requestAnimationFrame(() => syncClassicLayout());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
|
||||
@@ -609,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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -621,36 +661,16 @@ function syncPropertiesButton(isClassicLayoutEnabled) {
|
||||
}
|
||||
|
||||
if (customBtn) {
|
||||
if (originalBtn) {
|
||||
const cleanedHTML = cleanHTML(originalBtn.innerHTML);
|
||||
if (cleanedHTML && customBtn.innerHTML !== cleanedHTML) {
|
||||
const existingDot = customBtn.querySelector('.comfy-sidebar-error-dot');
|
||||
customBtn.innerHTML = cleanedHTML;
|
||||
if (existingDot) customBtn.appendChild(existingDot);
|
||||
}
|
||||
|
||||
const titleVal = originalBtn.getAttribute("title") || originalBtn.getAttribute("aria-label") || "Toggle properties panel";
|
||||
customBtn.setAttribute("title", titleVal);
|
||||
customBtn.setAttribute("aria-label", titleVal);
|
||||
|
||||
for (const cls of originalBtn.classList) {
|
||||
if (cls !== "comfy-sidebar-hide-original-properties-btn" && !customBtn.classList.contains(cls)) {
|
||||
customBtn.classList.add(cls);
|
||||
}
|
||||
}
|
||||
|
||||
syncErrorBadge(originalBtn, customBtn);
|
||||
} else if (!customBtn.getAttribute("title")) {
|
||||
if (!customBtn.getAttribute("title")) {
|
||||
customBtn.setAttribute("title", "Toggle properties panel");
|
||||
customBtn.setAttribute("aria-label", "Toggle properties panel");
|
||||
}
|
||||
customBtn.classList.toggle("comfy-panel-open", openState);
|
||||
customBtn.style.display = "inline-flex";
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -669,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();
|
||||
@@ -717,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>`;
|
||||
@@ -724,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).";
|
||||
}
|
||||
@@ -745,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";
|
||||
@@ -779,6 +757,10 @@ export function syncClassicLayout() {
|
||||
syncPropertiesButton(isClassicLayoutEnabled);
|
||||
syncActiveQueueIndicator(isClassicLayoutEnabled);
|
||||
updateSidebarTabsVisibility();
|
||||
applySidebarOverride();
|
||||
if (!sidebarTopObserver) {
|
||||
setupSidebarTopObserver();
|
||||
}
|
||||
syncRuntimePreviewButton();
|
||||
|
||||
} catch (err) {
|
||||
@@ -796,6 +778,7 @@ export function syncClassicLayout() {
|
||||
export function setupPropertiesPanelToggleFix() {
|
||||
setupCanvasMenuTooltipRelocator();
|
||||
setupCanvasMenuPopoverRelocator();
|
||||
setupSidebarTopObserver();
|
||||
|
||||
if (!document.getElementById("comfy-sidebar-layout-fix-styles")) {
|
||||
const style = document.createElement("style");
|
||||
@@ -812,15 +795,15 @@ export function setupPropertiesPanelToggleFix() {
|
||||
order: 99999 !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
margin-left: -8px !important;
|
||||
margin-left: 0px !important;
|
||||
margin-right: 0px !important;
|
||||
display: inline-flex !important;
|
||||
display: none !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.comfy-sidebar-custom-properties-toggle.comfy-panel-open {
|
||||
margin-left: 0px !important;
|
||||
display: inline-flex !important;
|
||||
}
|
||||
.comfy-sidebar-custom-properties-toggle > span.comfy-sidebar-error-dot {
|
||||
display: inline-flex !important;
|
||||
@@ -932,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 */
|
||||
@@ -1087,6 +1075,10 @@ export function destroyLayoutFix() {
|
||||
domObserver.disconnect();
|
||||
domObserver = null;
|
||||
}
|
||||
if (sidebarTopObserver) {
|
||||
sidebarTopObserver.disconnect();
|
||||
sidebarTopObserver = null;
|
||||
}
|
||||
if (actionbarResizeObserver) {
|
||||
actionbarResizeObserver.disconnect();
|
||||
actionbarResizeObserver = null;
|
||||
|
||||
+24
-37
@@ -4,19 +4,28 @@ let currentLocale = "en";
|
||||
let translations = {};
|
||||
let fallbackTranslations = {};
|
||||
|
||||
/**
|
||||
* Initializes the localization engine by reading ComfyUI's language setting
|
||||
* and dynamically loading the corresponding JSON dictionary.
|
||||
*/
|
||||
async function fetchLocaleFiles(lang) {
|
||||
const data = {};
|
||||
for (const file of ["main.json", "settings.json"]) {
|
||||
try {
|
||||
const url = new URL(`../locales/${lang}/${file}`, import.meta.url).href;
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
Object.assign(data, json);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function initI18n() {
|
||||
try {
|
||||
// ComfyUI's internal setting ID is "Comfy.Locale"
|
||||
let settingVal = app?.extensionManager?.setting?.get?.("Comfy.Locale")
|
||||
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale")
|
||||
|| app?.extensionManager?.setting?.get?.("Comfy.Locale.Language")
|
||||
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale.Language");
|
||||
|
||||
// Fallback check in localStorage in case settings API hasn't populated yet
|
||||
if (!settingVal) {
|
||||
try {
|
||||
settingVal = localStorage.getItem("Comfy.Settings.Comfy.Locale")
|
||||
@@ -25,7 +34,6 @@ export async function initI18n() {
|
||||
}
|
||||
|
||||
const raw = (settingVal || "en").toLowerCase().replace("_", "-");
|
||||
// Check regional locale first (e.g. "zh-tw", "pt-br"), then base ("zh", "pt")
|
||||
const candidates = [raw];
|
||||
const base = raw.split("-")[0];
|
||||
if (base !== raw) candidates.push(base);
|
||||
@@ -33,34 +41,18 @@ export async function initI18n() {
|
||||
currentLocale = candidates[0];
|
||||
console.log("[ComfySidebar] Detected locale:", currentLocale);
|
||||
|
||||
// 1. Load English fallback dictionary
|
||||
try {
|
||||
const enUrl = new URL("../locales/en.json", import.meta.url).href;
|
||||
const enRes = await fetch(enUrl);
|
||||
if (enRes.ok) {
|
||||
fallbackTranslations = await enRes.json();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[ComfySidebar] Failed to load en.json fallback", e);
|
||||
}
|
||||
// 1. Load English fallback
|
||||
fallbackTranslations = await fetchLocaleFiles("en");
|
||||
|
||||
// 2. If language is not English, load target locale
|
||||
// 2. Load active locale if not English
|
||||
if (base !== "en") {
|
||||
let loaded = false;
|
||||
for (const cand of candidates) {
|
||||
try {
|
||||
const locUrl = new URL(`../locales/${cand}.json`, import.meta.url).href;
|
||||
const locRes = await fetch(locUrl);
|
||||
if (locRes.ok) {
|
||||
translations = await locRes.json();
|
||||
console.log(`[ComfySidebar] Loaded ${cand}.json with`, Object.keys(translations).length, "translations");
|
||||
loaded = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!loaded) {
|
||||
console.warn(`[ComfySidebar] No translation file found for: ${candidates.join(" / ")}`);
|
||||
const locData = await fetchLocaleFiles(cand);
|
||||
if (Object.keys(locData).length > 0) {
|
||||
translations = locData;
|
||||
console.log(`[ComfySidebar] Loaded ${cand} with ${Object.keys(translations).length} translations`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -68,11 +60,6 @@ export async function initI18n() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a phrase.
|
||||
* If translation is missing in current language, falls back to English.
|
||||
* If missing in English too, returns the original text.
|
||||
*/
|
||||
export function t(key, vars = {}) {
|
||||
if (!key) return "";
|
||||
let str = translations[key] || fallbackTranslations[key] || key;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "إعادة ضبط المنظور",
|
||||
"Settings": "الإعدادات",
|
||||
"Export As...": "تصدير كـ...",
|
||||
"Failed to load 3D format": "فشل تحميل تنسيق 3D"
|
||||
"Failed to load 3D format": "فشل تحميل تنسيق 3D",
|
||||
"Toggle Comfy Queue Sidebar": "تبديل الشريط الجانبي لـ Comfy Queue",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "تجاهل/تضمين العقد المحددة في قائمة الانتظار",
|
||||
"Renumber All Nodes (Execution Order)": "إعادة ترقيم جميع العقد (ترتيب التنفيذ)",
|
||||
"Set Selected 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...": "البحث في أسماء الملفات أو بيانات الموجه..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "حد أعمدة قائمة الانتظار",
|
||||
"Width Threshold for Queue Columns (px)": "العرض بالبكسل لتقسيم القائمة إلى عدة أعمدة",
|
||||
"Show Pending Count Only": "إظهار عدد المهام المعلقة فقط",
|
||||
"If disabled, each queued job will have a separate individual card": "في حال التعطيل، ستظهر كل مهمة في القائمة ببطاقة مستقلة",
|
||||
"Show Working Node Name": "إظهار اسم العقدة النشطة",
|
||||
"Shows the name of the node which is currently in the process": "يعرض اسم العقدة التي تتم معالجتها حالياً",
|
||||
"Auto Clear Interrupted": "مسح تلقائي للمهام المقاطعة",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "مسح المهام الملغاة والفاشلة تلقائياً عند بدء توليد جديد",
|
||||
"Permanent Delete on Disk Deletion": "حذف دائم من القرص",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "حذف الملفات نهائياً عند المسح من القرص (دون نقلها لسلة المحذوفات)",
|
||||
"Hide Tab: {tab}": "إخفاء التبويب: {tab}",
|
||||
"Assets": "الأصول",
|
||||
"Nodes": "العقد",
|
||||
"Models": "النماذج",
|
||||
"Workflows": "مخططات العمل",
|
||||
"Apps": "التطبيقات",
|
||||
"Templates": "القوالب",
|
||||
"Override Stock History": "استبدال السجل الافتراضي",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "استبدال الشريط الجانبي لسجل المهام الافتراضي بـ Comfy Queue",
|
||||
"Hide Graph Button": "إخفاء زر 'المخطط'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "إخفاء الزر العائم 'المخطط' (خريطة العقد)",
|
||||
"Hide Avatar": "إخفاء الصورة الرمزية",
|
||||
"Floating Canvas Controls": "أدوات التحكم العائمة في اللوحة",
|
||||
"Classic Layout": "المظهر الكلاسيكي",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "وضع عناصر التحكم وتبويبات العمل المفتوحة في شريط علوي موحد",
|
||||
"Comfy Layout": "مظهر مدمج",
|
||||
"Hide Junk": "إخفاء العناصر غير المرغوبة",
|
||||
"Default": "افتراضي",
|
||||
"Dock": "إرساء",
|
||||
"Hide": "إخفاء",
|
||||
"Enable Output Gallery Sidebar": "تمكين الشريط الجانبي لمعرض المخرجات"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "Reset View",
|
||||
"Settings": "Settings",
|
||||
"Export As...": "Export As...",
|
||||
"Failed to load 3D format": "Failed to load 3D format"
|
||||
"Failed to load 3D format": "Failed to load 3D format",
|
||||
"Toggle Comfy Queue Sidebar": "Toggle Comfy Queue Sidebar",
|
||||
"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...",
|
||||
"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..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "Grid Columns Threshold",
|
||||
"Width Threshold for Queue Columns (px)": "Width Threshold for Queue Columns (px)",
|
||||
"Show Pending Count Only": "Show Pending Count Only",
|
||||
"If disabled, each queued job will have a separate individual card": "If disabled, each queued job will have a separate individual card",
|
||||
"Show Working Node Name": "Show Working Node Name",
|
||||
"Shows the name of the node which is currently in the process": "Shows the name of the node which is currently in the process",
|
||||
"Auto Clear Interrupted": "Auto Clear Interrupted",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "Auto-clear cancelled & failed jobs on new generation",
|
||||
"Permanent Delete on Disk Deletion": "Permanent Delete on Disk Deletion",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)",
|
||||
"Hide Tab: {tab}": "Hide Tab: {tab}",
|
||||
"Assets": "Assets",
|
||||
"Nodes": "Nodes",
|
||||
"Models": "Models",
|
||||
"Workflows": "Workflows",
|
||||
"Apps": "Apps",
|
||||
"Templates": "Templates",
|
||||
"Override Stock History": "Override Stock History",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "Replace the stock Job History sidebar with Comfy Queue",
|
||||
"Hide Graph Button": "Hide Graph Button",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Hide floating 'Graph' (Workflow/Node Map) button",
|
||||
"Hide Avatar": "Hide Avatar",
|
||||
"Floating Canvas Controls": "Floating Canvas Controls",
|
||||
"Classic Layout": "Classic Layout",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "Places the controls and the open workflow tabs on a single unified top bar",
|
||||
"Comfy Layout": "Comfy Layout",
|
||||
"Hide Junk": "Hide Junk",
|
||||
"Default": "Default",
|
||||
"Dock": "Dock",
|
||||
"Hide": "Hide",
|
||||
"Enable Output Gallery Sidebar": "Enable Output Gallery Sidebar"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "Restablecer vista",
|
||||
"Settings": "Ajustes",
|
||||
"Export As...": "Exportar como...",
|
||||
"Failed to load 3D format": "Error al cargar el formato 3D"
|
||||
"Failed to load 3D format": "Error al cargar el formato 3D",
|
||||
"Toggle Comfy Queue Sidebar": "Alternar barra lateral Comfy Queue",
|
||||
"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...",
|
||||
"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..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "Umbral de columnas de la cola",
|
||||
"Width Threshold for Queue Columns (px)": "Ancho en píxeles para dividir la cola en varias columnas",
|
||||
"Show Pending Count Only": "Mostrar solo total en espera",
|
||||
"If disabled, each queued job will have a separate individual card": "Si se desactiva, cada tarea en cola tendrá una tarjeta individual",
|
||||
"Show Working Node Name": "Mostrar nombre del nodo en proceso",
|
||||
"Shows the name of the node which is currently in the process": "Muestra el nombre del nodo que se está procesando actualmente",
|
||||
"Auto Clear Interrupted": "Limpieza auto de interrumpidos",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "Borrar automáticamente tareas canceladas y fallidas al iniciar una nueva generación",
|
||||
"Permanent Delete on Disk Deletion": "Eliminación permanente del disco",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Eliminar permanentemente archivos al borrarlos (sin pasar por la papelera)",
|
||||
"Hide Tab: {tab}": "Ocultar pestaña: {tab}",
|
||||
"Assets": "Recursos",
|
||||
"Nodes": "Nodos",
|
||||
"Models": "Modelos",
|
||||
"Workflows": "Flujos",
|
||||
"Apps": "Apps",
|
||||
"Templates": "Plantillas",
|
||||
"Override Stock History": "Reemplazar historial estándar",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "Reemplaza la barra de historial estándar con Comfy Queue",
|
||||
"Hide Graph Button": "Ocultar botón 'Grafo'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Oculta el botón flotante 'Grafo' (mapa de nodos)",
|
||||
"Hide Avatar": "Ocultar avatar",
|
||||
"Floating Canvas Controls": "Controles flotantes del lienzo",
|
||||
"Classic Layout": "Diseño clásico",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "Coloca los controles y las pestañas abiertas en una barra superior unificada",
|
||||
"Comfy Layout": "Diseño compacto",
|
||||
"Hide Junk": "Ocultar elementos innecesarios",
|
||||
"Default": "Por defecto",
|
||||
"Dock": "Acoplar",
|
||||
"Hide": "Ocultar",
|
||||
"Enable Output Gallery Sidebar": "Habilitar barra lateral de galería de salidas"
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"Queue": "صف",
|
||||
"Search History": "جستجو در تاریخچه",
|
||||
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "نادیده گرفتن گرههای انتخابی در صف (Ctrl+Q)",
|
||||
"No nodes selected. Click a node on the canvas first to toggle ignore.": "هیچ گرهی انتخاب نشده است. ابتدا روی یک گره در بوم کلیک کنید.",
|
||||
"Renumber All Nodes by Execution Order (Alt+R)": "شمارهگذاری مجدد گرهها بر اساس ترتیب اجرا (Alt+R)",
|
||||
"Clear Cancelled & Failed": "پاک کردن موارد لغو شده و ناموفق",
|
||||
"Clear All History": "پاک کردن تمام تاریخچه",
|
||||
"Filter by text, images, nodes...": "فیلتر بر اساس متن، تصویر، گره...",
|
||||
"Clear & Close Search": "پاک کردن و بستن جستجو",
|
||||
"Scroll to Top": "رفتن به بالا",
|
||||
"Batch of #": "دسته #",
|
||||
"Go Back": "بازگشت",
|
||||
"Go Back to Queue": "بازگشت به صف",
|
||||
"Outputs of #": "خروجیهای #",
|
||||
"No separate intermediate outputs found.": "هیچ خروجی میانی مجزایی پیدا نشد.",
|
||||
"Cancel All Pending": "لغو تمام کارهای در انتظار",
|
||||
"Pending Queue:": "صف در انتظار:",
|
||||
"tasks": "کار",
|
||||
"Download Object": "دانلود فایل",
|
||||
"Copy to Clipboard": "کپی در کلیپبورد",
|
||||
"Download JSON": "دانلود JSON",
|
||||
"Delete Card (Hold Ctrl to delete file from disk)": "حذف کارت (Ctrl برای حذف از دیسک)",
|
||||
"Delete Card": "حذف کارت",
|
||||
"Delete File": "حذف فایل",
|
||||
"Delete Files": "حذف فایلها",
|
||||
"Show Node": "نمایش گره",
|
||||
"Show Stopped/Failed Node": "نمایش گره متوقف یا ناموفق",
|
||||
"View all intermediate outputs": "مشاهده تمام خروجیهای میانی",
|
||||
"Delete Card (Ctrl+Click to delete from disk)": "حذف کارت (Ctrl+کلیک برای حذف از دیسک)",
|
||||
"Ctrl+Click again to delete file from DISK / TRASH": "برای حذف نهایی از دیسک مجدداً Ctrl+کلیک کنید",
|
||||
"Click again to confirm removing card": "برای تأیید حذف مجدداً کلیک کنید",
|
||||
"Remove from batch (Hold Ctrl to delete file from disk)": "حذف از دسته (Ctrl برای حذف از دیسک)",
|
||||
"Click again to confirm removing from batch": "برای تأیید حذف از دسته مجدداً کلیک کنید",
|
||||
"Delete node output (Hold Ctrl to delete file from disk)": "حذف خروجی گره (Ctrl برای حذف از دیسک)",
|
||||
"Ctrl+Click again to delete this node's files from DISK / TRASH": "برای حذف فایلهای این گره از دیسک مجدداً Ctrl+کلیک کنید",
|
||||
"Click again to confirm removing output": "برای تأیید حذف خروجی مجدداً کلیک کنید",
|
||||
"Moved file to Recycle Bin.": "فایل به سطل بازیافت منتقل شد.",
|
||||
"Permanently deleted file.": "فایل به طور دائمی حذف شد.",
|
||||
"Cancelled": "لغو شد",
|
||||
"Error": "خطا",
|
||||
"Warning": "هشدار",
|
||||
"Cancel Queued Task": "لغو کار در صف",
|
||||
"Interrupt Execution": "توقف اجرا",
|
||||
"Click to read full text": "برای خواندن متن کامل کلیک کنید",
|
||||
"No Outputs": "بدون خروجی",
|
||||
"Processing...": "در حال پردازش...",
|
||||
"Finishing...": "در حال اتمام...",
|
||||
"No workflow metadata found in this file.": "متاداده جریان کاری در این فایل یافت نشد.",
|
||||
"Open Folder": "باز کردن پوشه",
|
||||
"Open Location": "باز کردن مسیر",
|
||||
"View all images in this batch": "مشاهده تمام تصاویر این دسته",
|
||||
"No nodes found on canvas to renumber.": "گرهی برای شمارهگذاری مجدد روی بوم یافت نشد.",
|
||||
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "شمارهگذاری مجدد، شناسه تمام گرهها را طبق ترتیب اجرا بازنویسی میکند.\n\nاین کار خروجیهای میانی را اصلاح میکند اما ممکن است اسکریپتهای وابسته به شناسهها را مختل کند.\n\nآیا ادامه میدهید؟",
|
||||
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "{count} گره طبق ترتیب اجرا شمارهگذاری شدند. آخرین گره: #{id} ({name}).",
|
||||
"Failed to renumber nodes: ": "شمارهگذاری مجدد گرهها ناموفق بود: ",
|
||||
"Please select a node first to change its ID.": "لطفاً ابتدا یک گره را برای تغییر شناسه انتخاب کنید.",
|
||||
"Enter new ID for node #{id} ({name}):": "شناسه جدید برای گره #{id} ({name}) را وارد کنید:",
|
||||
"Please enter a valid positive number.": "لطفاً یک عدد صحیح مثبت معتبر وارد کنید.",
|
||||
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "شناسه #{id} قبلاً توسط \"{name}\" استفاده شده است. آیا میخواهید شناسهها جابجا شوند؟",
|
||||
"Assigned node ID #{id} to \"{name}\".": "شناسه #{id} به گره \"{name}\" اختصاص یافت.",
|
||||
"Failed to set node ID: ": "تنظیم شناسه گره ناموفق بود: ",
|
||||
"Video playback. Press Esc to close.": "پخش ویدیو. برای بستن Esc را بزنید.",
|
||||
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "کلیک برای بزرگنمایی (100%/تناسب) | Shift+کلیک روی کارت دیگر برای مقایسه.",
|
||||
"Toggle Loop": "تکرار پخش",
|
||||
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "عدم تطابق نسبت ابعاد ({wA}x{hA} در برابر {wB}x{hB}). تصاویر برای مقایسه باید نسبت برابری داشته باشند.",
|
||||
"Click image to zoom | Drag image to compare slider | Esc to close.": "کلیک برای بزرگنمایی | کشیدن اسلایدر برای مقایسه | Esc برای بستن.",
|
||||
"Copy": "کپی",
|
||||
"Copied!": "کپی شد!",
|
||||
"Text Output": "خروجی متنی",
|
||||
"Text Output #": "خروجی متنی #",
|
||||
"Raw Text": "متن خام",
|
||||
"{wc} words • {chars} chars": "{wc} کلمه • {chars} نویسه",
|
||||
"3D Model": "مدل 3D",
|
||||
"Loading 3D Asset...": "در حال بارگذاری مدل سهبعدی...",
|
||||
"Exporting {format}...": "در حال صدور به فرمت {format}...",
|
||||
"Failed to load 3D engine.": "موتور سهبعدی بارگذاری نشد.",
|
||||
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "امکان دانلود کتابخانههای 3D وجود ندارد. اتصال اینترنت خود را بررسی کنید.",
|
||||
"Background Color": "رنگ پسزمینه",
|
||||
"Up Direction": "جهت بالا",
|
||||
"Material Mode": "حالت متریال",
|
||||
"Camera Type": "نوع دوربین",
|
||||
"FOV": "میدان دید (FOV)",
|
||||
"Light Intensity": "شدت نور",
|
||||
"Original (Y-Up)": "اصلی (Y رو به بالا)",
|
||||
"Z-Up (+90°)": "Z رو به بالا (+90°)",
|
||||
"Inverted (-90°)": "معکوس (-90°)",
|
||||
"Original / Textured": "اصلی / بافتدار",
|
||||
"Clay / Studio Matte": "گلی / مات استودیویی",
|
||||
"Normals": "نرمالها",
|
||||
"Perspective": "پرسپکتیو",
|
||||
"Orthographic": "ارتوگرافیک",
|
||||
"Grid": "شبکه",
|
||||
"Auto-Rotate": "چرخش خودکار",
|
||||
"Wireframe": "قاب سیمی (Wireframe)",
|
||||
"Reset View": "بازنشانی دید",
|
||||
"Settings": "تنظیمات",
|
||||
"Export As...": "صادرات به عنوان...",
|
||||
"Failed to load 3D format": "بارگذاری فرمت 3D ناموفق بود",
|
||||
"Toggle Comfy Queue Sidebar": "تغییر وضعیت نوار کناری Comfy Queue",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "نادیده گرفتن/فعالسازی گرههای انتخابشده در صف",
|
||||
"Renumber All Nodes (Execution Order)": "شمارهگذاری مجدد همه گرهها (ترتیب اجرا)",
|
||||
"Set Selected 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...": "جستجوی نام فایلها یا متادیتای پرامپت..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "آستانه ستونهای صف",
|
||||
"Width Threshold for Queue Columns (px)": "عرض بر حسب پیکسل برای تقسیم صف به چند ستون",
|
||||
"Show Pending Count Only": "فقط نمایش تعداد در انتظار",
|
||||
"If disabled, each queued job will have a separate individual card": "در صورت غیرفعال بودن، هر کار در صف یک کارت جداگانه خواهد داشت",
|
||||
"Show Working Node Name": "نمایش نام گره در حال پردازش",
|
||||
"Shows the name of the node which is currently in the process": "نام گرهی را که در حال حاضر پردازش میشود نشان میدهد",
|
||||
"Auto Clear Interrupted": "پاکسازی خودکار متوقفشدهها",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "پاک کردن خودکار کارهای لغو شده و ناموفق در تولید جدید",
|
||||
"Permanent Delete on Disk Deletion": "حذف دائمی از دیسک",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "حذف دائمی فایلها هنگام پاک کردن از دیسک (بدون انتقال به سطل بازیافت)",
|
||||
"Hide Tab: {tab}": "پنهان کردن برگه: {tab}",
|
||||
"Assets": "داراییها",
|
||||
"Nodes": "گرهها",
|
||||
"Models": "مدلها",
|
||||
"Workflows": "جریانهای کاری",
|
||||
"Apps": "برنامهها",
|
||||
"Templates": "الگوها",
|
||||
"Override Stock History": "جایگزینی تاریخچه پیشفرض",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "نوار کناری تاریخچه کارهای پیشفرض را با Comfy Queue جایگزین میکند",
|
||||
"Hide Graph Button": "پنهان کردن دکمه 'نمودار'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "دکمه شناور 'نمودار' (نقشه گرهها) را پنهان میکند",
|
||||
"Hide Avatar": "پنهان کردن آواتار",
|
||||
"Floating Canvas Controls": "کنترلهای شناور بوم",
|
||||
"Classic Layout": "طرحبندی کلاسیک",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "کنترلها و برگههای باز جریان کار را در یک نوار بالایی یکپارچه قرار میدهد",
|
||||
"Comfy Layout": "طرحبندی فشرده",
|
||||
"Hide Junk": "پنهان کردن موارد غیرضروری",
|
||||
"Default": "پیشفرض",
|
||||
"Dock": "اتصال",
|
||||
"Hide": "پنهان",
|
||||
"Enable Output Gallery Sidebar": "فعالسازی نوار کناری گالری خروجیها"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "Réinitialiser la vue",
|
||||
"Settings": "Paramètres",
|
||||
"Export As...": "Exporter sous...",
|
||||
"Failed to load 3D format": "Échec du chargement du format 3D"
|
||||
"Failed to load 3D format": "Échec du chargement du format 3D",
|
||||
"Toggle Comfy Queue Sidebar": "Basculer la barre latérale Comfy Queue",
|
||||
"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...",
|
||||
"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..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "Seuil de colonnes",
|
||||
"Width Threshold for Queue Columns (px)": "Largeur en pixels pour diviser la file en plusieurs colonnes",
|
||||
"Show Pending Count Only": "Afficher uniquement le total en attente",
|
||||
"If disabled, each queued job will have a separate individual card": "Si désactivé, chaque tâche en file aura une carte individuelle",
|
||||
"Show Working Node Name": "Afficher le nom du nœud actif",
|
||||
"Shows the name of the node which is currently in the process": "Affiche le nom du nœud en cours de traitement",
|
||||
"Auto Clear Interrupted": "Nettoyage auto des interruptions",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "Effacer automatiquement les tâches annulées et échouées lors d'une nouvelle génération",
|
||||
"Permanent Delete on Disk Deletion": "Suppression définitive du disque",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Supprimer définitivement les fichiers lors de la suppression (sans passer par la corbeille)",
|
||||
"Hide Tab: {tab}": "Masquer l'onglet : {tab}",
|
||||
"Assets": "Ressources",
|
||||
"Nodes": "Nœuds",
|
||||
"Models": "Modèles",
|
||||
"Workflows": "Workflows",
|
||||
"Apps": "Applications",
|
||||
"Templates": "Modèles",
|
||||
"Override Stock History": "Remplacer l'historique par défaut",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "Remplace la barre latérale d'historique par défaut par Comfy Queue",
|
||||
"Hide Graph Button": "Masquer le bouton 'Graphe'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Masque le bouton flottant 'Graphe' (carte des nœuds)",
|
||||
"Hide Avatar": "Masquer l'avatar",
|
||||
"Floating Canvas Controls": "Contrôles flottants du canevas",
|
||||
"Classic Layout": "Disposition classique",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "Regroupe les contrôles et les onglets ouverts dans une barre supérieure unique",
|
||||
"Comfy Layout": "Disposition compacte",
|
||||
"Hide Junk": "Masquer les éléments inutiles",
|
||||
"Default": "Par défaut",
|
||||
"Dock": "Ancrer",
|
||||
"Hide": "Masquer",
|
||||
"Enable Output Gallery Sidebar": "Activer la barre latérale de la galerie de sorties"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "אפס תצוגה",
|
||||
"Settings": "הגדרות",
|
||||
"Export As...": "ייצא כ...",
|
||||
"Failed to load 3D format": "טעינת פורמט 3D נכשלה"
|
||||
"Failed to load 3D format": "טעינת פורמט 3D נכשלה",
|
||||
"Toggle Comfy Queue Sidebar": "הפעל/השבת סרגל צד Comfy Queue",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "התעלם/כלול צמתים נבחרים בתור",
|
||||
"Renumber All Nodes (Execution Order)": "מספר מחדש את כל הצמתים (סדר ביצוע)",
|
||||
"Set Selected 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...": "חיפוש שמות קבצים או מטא-דאטה של פרומפט..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "סף עמודות תור",
|
||||
"Width Threshold for Queue Columns (px)": "רוחב בפיקסלים לחלוקת התור לעמודות מרובות",
|
||||
"Show Pending Count Only": "הצג רק ספירת ממתינים",
|
||||
"If disabled, each queued job will have a separate individual card": "אם מושבת, לכל משימה בתור יהיה כרטיס נפרד",
|
||||
"Show Working Node Name": "הצג שם צומת פעיל",
|
||||
"Shows the name of the node which is currently in the process": "מציג את שם הצומת המעובד כעת",
|
||||
"Auto Clear Interrupted": "ניקוי אוטומטי של משימות שהופסקו",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "נקה אוטומטית משימות שבוטלו או נכשלו בעת יצירה חדשה",
|
||||
"Permanent Delete on Disk Deletion": "מחיקה לצמיתות מהדיסק",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "מחק קבצים לצמיתות בעת מחיקה (עוקף את סל המיחזור)",
|
||||
"Hide Tab: {tab}": "הסתר לשונית: {tab}",
|
||||
"Assets": "נכסים",
|
||||
"Nodes": "צמתים",
|
||||
"Models": "מודלים",
|
||||
"Workflows": "תהליכי עבודה",
|
||||
"Apps": "יישומים",
|
||||
"Templates": "תבניות",
|
||||
"Override Stock History": "החלף היסטוריית ברירת מחדל",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "מחליף את סרגל הצד של היסטוריית העבודות הרגילה ב-Comfy Queue",
|
||||
"Hide Graph Button": "הסתר כפתור 'גרף'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "מסתיר את הכפתור הצף 'גרף' (מפת הצמתים)",
|
||||
"Hide Avatar": "הסתר אווטאר",
|
||||
"Floating Canvas Controls": "פקדי לוח צפים",
|
||||
"Classic Layout": "פריסה קלאסית",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "ממקם את הפקדים ואת לשוניות תהליך העבודה הפתוחות בסרגל עליון מאוחד",
|
||||
"Comfy Layout": "פריסה קומפקטית",
|
||||
"Hide Junk": "הסתר פריטים מיותרים",
|
||||
"Default": "ברירת מחדל",
|
||||
"Dock": "הצמד",
|
||||
"Hide": "הסתר",
|
||||
"Enable Output Gallery Sidebar": "הפעל סרגל צד של גלריית תוצרים"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "Ripristina visuale",
|
||||
"Settings": "Impostazioni",
|
||||
"Export As...": "Esporta come...",
|
||||
"Failed to load 3D format": "Caricamento formato 3D non riuscito"
|
||||
"Failed to load 3D format": "Caricamento formato 3D non riuscito",
|
||||
"Toggle Comfy Queue Sidebar": "Attiva/Disattiva barra laterale Comfy Queue",
|
||||
"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...",
|
||||
"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..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "Soglia colonne coda",
|
||||
"Width Threshold for Queue Columns (px)": "Larghezza in pixel per dividere la coda in più colonne",
|
||||
"Show Pending Count Only": "Mostra solo conteggio in attesa",
|
||||
"If disabled, each queued job will have a separate individual card": "Se disabilitato, ogni task in coda avrà una scheda separata",
|
||||
"Show Working Node Name": "Mostra nome nodo in elaborazione",
|
||||
"Shows the name of the node which is currently in the process": "Mostra il nome del nodo attualmente in elaborazione",
|
||||
"Auto Clear Interrupted": "Pulizia automatica interrotti",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "Cancella automaticamente i task annullati e falliti a ogni nuova generazione",
|
||||
"Permanent Delete on Disk Deletion": "Eliminazione definitiva dal disco",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Elimina definitivamente i file al momento della cancellazione (senza passare dal cestino)",
|
||||
"Hide Tab: {tab}": "Nascondi scheda: {tab}",
|
||||
"Assets": "Risorse",
|
||||
"Nodes": "Nodi",
|
||||
"Models": "Modelli",
|
||||
"Workflows": "Flussi di lavoro",
|
||||
"Apps": "Applicazioni",
|
||||
"Templates": "Modelli predefiniti",
|
||||
"Override Stock History": "Sostituisci cronologia standard",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "Sostituisce la barra laterale della cronologia standard con Comfy Queue",
|
||||
"Hide Graph Button": "Nascondi pulsante 'Grafo'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Nasconde il pulsante mobile 'Grafo' (mappa dei nodi)",
|
||||
"Hide Avatar": "Nascondi avatar",
|
||||
"Floating Canvas Controls": "Controlli mobili dell'area di lavoro",
|
||||
"Classic Layout": "Layout classico",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "Posiziona i controlli e le schede aperte su una barra superiore unificata",
|
||||
"Comfy Layout": "Layout compatto",
|
||||
"Hide Junk": "Nascondi elementi inutili",
|
||||
"Default": "Predefinito",
|
||||
"Dock": "Aggancia",
|
||||
"Hide": "Nascondi",
|
||||
"Enable Output Gallery Sidebar": "Abilita barra laterale galleria di output"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "視点をリセット",
|
||||
"Settings": "設定",
|
||||
"Export As...": "エクスポート...",
|
||||
"Failed to load 3D format": "3D形式の読み込みに失敗しました"
|
||||
"Failed to load 3D format": "3D形式の読み込みに失敗しました",
|
||||
"Toggle Comfy Queue Sidebar": "Comfy Queue サイドバーの切り替え",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "キューで選択したノードを無視/解除",
|
||||
"Renumber All Nodes (Execution Order)": "すべてのノード番号を再割り当て (実行順)",
|
||||
"Set Selected 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...": "ファイル名またはプロンプトを検索..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "列分割の幅しきい値",
|
||||
"Width Threshold for Queue Columns (px)": "キューを複数列に分割する幅のしきい値 (px)",
|
||||
"Show Pending Count Only": "待機数のみ表示",
|
||||
"If disabled, each queued job will have a separate individual card": "無効にすると、キュー内の各タスクが個別のカードとして表示されます",
|
||||
"Show Working Node Name": "処理中のノード名を表示",
|
||||
"Shows the name of the node which is currently in the process": "現在処理されているノードの名前を表示します",
|
||||
"Auto Clear Interrupted": "中断タスクの自動クリア",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "新規生成時にキャンセルおよび失敗したタスクを自動的に消去します",
|
||||
"Permanent Delete on Disk Deletion": "ディスクから完全消去",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "ディスクからの削除時にゴミ箱を経由せず完全に消去します",
|
||||
"Hide Tab: {tab}": "タブを非表示: {tab}",
|
||||
"Assets": "アセット",
|
||||
"Nodes": "ノード",
|
||||
"Models": "モデル",
|
||||
"Workflows": "ワークフロー",
|
||||
"Apps": "アプリ",
|
||||
"Templates": "テンプレート",
|
||||
"Override Stock History": "標準の履歴を置き換える",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "標準の履歴サイドバーを Comfy Queue に置き換えます",
|
||||
"Hide Graph Button": "「グラフ」ボタンを非表示",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "フローティングの「グラフ」(ノードマップ) ボタンを非表示にします",
|
||||
"Hide Avatar": "アバターを非表示",
|
||||
"Floating Canvas Controls": "フローティングキャンバス操作",
|
||||
"Classic Layout": "クラシックレイアウト",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "操作ボタンと開いているワークフロータブを統合された単一の上部バーに配置します",
|
||||
"Comfy Layout": "コンパクトレイアウト",
|
||||
"Hide Junk": "不要な項目を非表示",
|
||||
"Default": "デフォルト",
|
||||
"Dock": "ドック",
|
||||
"Hide": "非表示",
|
||||
"Enable Output Gallery Sidebar": "出力ギャラリーのサイドバーを有効化"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "시점 초기화",
|
||||
"Settings": "설정",
|
||||
"Export As...": "내보내기...",
|
||||
"Failed to load 3D format": "3D 형식 로드 실패"
|
||||
"Failed to load 3D format": "3D 형식 로드 실패",
|
||||
"Toggle Comfy Queue Sidebar": "Comfy Queue 사이드바 전환",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "대기열에서 선택한 노드 무시/해제",
|
||||
"Renumber All Nodes (Execution Order)": "모든 노드 재번호 매기기 (실행 순서)",
|
||||
"Set Selected 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...": "파일명 또는 프롬프트 메타데이터 검색..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "열 분할 너비 기준값",
|
||||
"Width Threshold for Queue Columns (px)": "대기열을 여러 열로 나눌 너비 기준값 (px)",
|
||||
"Show Pending Count Only": "대기 수량만 표시",
|
||||
"If disabled, each queued job will have a separate individual card": "비활성화 시 대기열의 각 작업이 개별 카드로 표시됩니다",
|
||||
"Show Working Node Name": "진행 중인 노드 이름 표시",
|
||||
"Shows the name of the node which is currently in the process": "현재 처리 중인 노드의 이름을 표시합니다",
|
||||
"Auto Clear Interrupted": "중단된 작업 자동 삭제",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "새 생성 시 취소되거나 실패한 작업을 자동으로 삭제합니다",
|
||||
"Permanent Delete on Disk Deletion": "디스크에서 완전 삭제",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "디스크에서 삭제 시 휴지통을 거치지 않고 영구 삭제합니다",
|
||||
"Hide Tab: {tab}": "탭 숨기기: {tab}",
|
||||
"Assets": "에셋",
|
||||
"Nodes": "노드",
|
||||
"Models": "모델",
|
||||
"Workflows": "워크플로",
|
||||
"Apps": "앱",
|
||||
"Templates": "템플릿",
|
||||
"Override Stock History": "기본 작업 기록 대체",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "기본 작업 기록 사이드바를 Comfy Queue로 대체합니다",
|
||||
"Hide Graph Button": "'그래프' 버튼 숨기기",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "플로팅 '그래프'(워크플로 맵) 버튼을 숨깁니다",
|
||||
"Hide Avatar": "아바타 숨기기",
|
||||
"Floating Canvas Controls": "플로팅 캔버스 제어 도구",
|
||||
"Classic Layout": "클래식 레이아웃",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "제어 도구와 열린 워크플로 탭을 하나의 상단 바로 통합합니다",
|
||||
"Comfy Layout": "컴팩트 레이아웃",
|
||||
"Hide Junk": "불필요한 항목 숨기기",
|
||||
"Default": "기본값",
|
||||
"Dock": "도킹",
|
||||
"Hide": "숨기기",
|
||||
"Enable Output Gallery Sidebar": "출력 갤러리 사이드바 활성화"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "Redefinir visão",
|
||||
"Settings": "Configurações",
|
||||
"Export As...": "Exportar como...",
|
||||
"Failed to load 3D format": "Falha ao carregar formato 3D"
|
||||
"Failed to load 3D format": "Falha ao carregar formato 3D",
|
||||
"Toggle Comfy Queue Sidebar": "Alternar barra lateral Comfy Queue",
|
||||
"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ó...",
|
||||
"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..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "Limite de colunas da fila",
|
||||
"Width Threshold for Queue Columns (px)": "Largura em pixels para dividir a fila em várias colunas",
|
||||
"Show Pending Count Only": "Mostrar apenas contagem pendente",
|
||||
"If disabled, each queued job will have a separate individual card": "Se desativado, cada tarefa na fila terá um cartão individual",
|
||||
"Show Working Node Name": "Mostrar nome do nó em execução",
|
||||
"Shows the name of the node which is currently in the process": "Mostra o nome do nó que está sendo processado no momento",
|
||||
"Auto Clear Interrupted": "Limpeza automática de interrompidos",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "Limpar automaticamente tarefas canceladas e com falha ao iniciar nova geração",
|
||||
"Permanent Delete on Disk Deletion": "Exclusão permanente do disco",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Excluir arquivos permanentemente (sem passar pela Lixeira)",
|
||||
"Hide Tab: {tab}": "Ocultar aba: {tab}",
|
||||
"Assets": "Recursos",
|
||||
"Nodes": "Nós",
|
||||
"Models": "Modelos",
|
||||
"Workflows": "Fluxos de trabalho",
|
||||
"Apps": "Aplicativos",
|
||||
"Templates": "Modelos prévios",
|
||||
"Override Stock History": "Substituir histórico padrão",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "Substitui a barra lateral de histórico padrão pelo Comfy Queue",
|
||||
"Hide Graph Button": "Ocultar botão 'Grafo'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Oculta o botão flutuante 'Grafo' (mapa de nós)",
|
||||
"Hide Avatar": "Ocultar avatar",
|
||||
"Floating Canvas Controls": "Controles flutuantes da tela",
|
||||
"Classic Layout": "Layout clássico",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "Posiciona os controles e as abas abertas em uma barra superior unificada",
|
||||
"Comfy Layout": "Layout compacto",
|
||||
"Hide Junk": "Ocultar itens desnecessários",
|
||||
"Default": "Padrão",
|
||||
"Dock": "Fixar",
|
||||
"Hide": "Ocultar",
|
||||
"Enable Output Gallery Sidebar": "Ativar barra lateral da galeria de saídas"
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"Remove from batch (Hold Ctrl to delete file from disk)": "Удалить из пакета (Ctrl для удаления с диска)",
|
||||
"Click again to confirm removing from batch": "Нажмите еще раз для удаления из пакета",
|
||||
"Delete node output (Hold Ctrl to delete file from disk)": "Удалить результат ноды (Ctrl для удаления с диска)",
|
||||
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Клик еще раз для удаления с ДИСКА",
|
||||
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Клик еще раз для удаления файлов с ДИСКА",
|
||||
"Click again to confirm removing output": "Нажмите еще раз для подтверждения удаления",
|
||||
"Moved file to Recycle Bin.": "Файл перемещен в корзину.",
|
||||
"Permanently deleted file.": "Файл удален навсегда.",
|
||||
@@ -59,12 +59,12 @@
|
||||
"Please enter a valid positive number.": "Пожалуйста, введите положительное число.",
|
||||
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "ID ноды #{id} уже используется нодой \"{name}\". Поменять их местами?",
|
||||
"Assigned node ID #{id} to \"{name}\".": "Ноде \"{name}\" присвоен ID #{id}.",
|
||||
"Failed to set node ID: ": "Не удалось установить ID ноды: " ,
|
||||
"Failed to set node ID: ": "Не удалось установить ID ноды: ",
|
||||
"Video playback. Press Esc to close.": "Воспроизведение видео. Нажмите Esc для закрытия.",
|
||||
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Клик для масштабирования (100%/Вписать) | Shift+Клик по другой карточке для сравнения.",
|
||||
"Toggle Loop": "Зациклить",
|
||||
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "Несовпадение пропорций ({wA}x{hA} против {wB}x{hB}). Изображения должны иметь одинаковые пропорции для сравнения.",
|
||||
"Click image to zoom | Drag image to compare slider | Esc to close.": "Клик для масштабирования | Перетащите для сдвига слайдера | Esc для закрытия.",
|
||||
"Click image to zoom | Drag image to compare slider | Esc to close.": "Клик для масштабирования | Перетащите слайдер для сравнения | Esc для закрытия.",
|
||||
"Copy": "Копировать",
|
||||
"Copied!": "Скопировано!",
|
||||
"Text Output": "Текстовый результат",
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "Сброс вида",
|
||||
"Settings": "Настройки",
|
||||
"Export As...": "Экспорт...",
|
||||
"Failed to load 3D format": "Не удалось загрузить 3D-формат"
|
||||
"Failed to load 3D format": "Не удалось загрузить 3D-формат",
|
||||
"Toggle Comfy Queue Sidebar": "Переключить боковую панель Comfy Queue",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "Игнорировать выбранные ноды в очереди",
|
||||
"Renumber All Nodes (Execution Order)": "Перенумеровать ноды (порядок выполнения)",
|
||||
"Set Selected 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...": "Поиск по именам файлов или промптам..."
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Grid Columns Threshold": "Порог колонок очереди",
|
||||
"Width Threshold for Queue Columns (px)": "Ширина в пикселях для разделения очереди на несколько колонок",
|
||||
"Show Pending Count Only": "Только количество в очереди",
|
||||
"If disabled, each queued job will have a separate individual card": "Если выключено, каждая задача в очереди будет отдельной карточкой",
|
||||
"Show Working Node Name": "Имя выполняющейся ноды",
|
||||
"Shows the name of the node which is currently in the process": "Показывать имя ноды, выполняющейся в данный момент",
|
||||
"Auto Clear Interrupted": "Автоочистка отменённых",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "Автоматически очищать отменённые и неудачные задачи при новой генерации",
|
||||
"Permanent Delete on Disk Deletion": "Безвозвратное удаление с диска",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Удалять файлы навсегда при удалении с диска (минуя корзину)",
|
||||
"Hide Tab: {tab}": "Скрыть вкладку: {tab}",
|
||||
"Assets": "Ресурсы",
|
||||
"Nodes": "Ноды",
|
||||
"Models": "Модели",
|
||||
"Workflows": "Рабочие процессы",
|
||||
"Apps": "Приложения",
|
||||
"Templates": "Шаблоны",
|
||||
"Override Stock History": "Замена стандартной истории",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "Заменить стандартную панель истории на Comfy Queue",
|
||||
"Hide Graph Button": "Скрыть кнопку 'Граф'",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Скрыть плавающую кнопку 'Граф' (карта нод)",
|
||||
"Hide Avatar": "Скрыть аватар",
|
||||
"Floating Canvas Controls": "Плавающие элементы управления",
|
||||
"Classic Layout": "Классический интерфейс",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "Разместить элементы управления и открытые вкладки на единой верхней панели",
|
||||
"Comfy Layout": "Компактный интерфейс",
|
||||
"Classic Layout": "Компактный интерфейс",
|
||||
"Hide Junk": "Скрыть лишнее",
|
||||
"Default": "По умолчанию",
|
||||
"Dock": "Закрепить",
|
||||
"Hide": "Скрыть",
|
||||
"Enable Output Gallery Sidebar": "Включить боковую панель галереи результатов"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "Görünümü sıfırla",
|
||||
"Settings": "Ayarlar",
|
||||
"Export As...": "Farklı dışa aktar...",
|
||||
"Failed to load 3D format": "3D formatı yüklenemedi"
|
||||
"Failed to load 3D format": "3D formatı yüklenemedi",
|
||||
"Toggle Comfy Queue Sidebar": "Comfy Queue Kenar Çubuğunu Aç/Kapat",
|
||||
"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...",
|
||||
"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..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "Kuyruk sütun genişliği eşiği",
|
||||
"Width Threshold for Queue Columns (px)": "Kuyruğu birden fazla sütuna bölmek için piksel genişliği",
|
||||
"Show Pending Count Only": "Yalnızca bekleyen sayısını göster",
|
||||
"If disabled, each queued job will have a separate individual card": "Devre dışı bırakılırsa kuyruktaki her görev ayrı bir karta sahip olur",
|
||||
"Show Working Node Name": "Çalışan düğüm adını göster",
|
||||
"Shows the name of the node which is currently in the process": "Şu anda işlenmekte olan düğümün adını gösterir",
|
||||
"Auto Clear Interrupted": "Kesilenleri otomatik temizle",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "Yeni üretimde iptal edilen ve başarısız olan görevleri otomatik olarak temizle",
|
||||
"Permanent Delete on Disk Deletion": "Diskten kalıcı silme",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "Diskten silerken dosyaları kalıcı olarak sil (Geri Dönüşüm Kutusu'nu atlar)",
|
||||
"Hide Tab: {tab}": "Sekmeyi gizle: {tab}",
|
||||
"Assets": "Varlıklar",
|
||||
"Nodes": "Düğümler",
|
||||
"Models": "Modeller",
|
||||
"Workflows": "İş Akışları",
|
||||
"Apps": "Uygulamalar",
|
||||
"Templates": "Şablonlar",
|
||||
"Override Stock History": "Varsayılan geçmişi değiştir",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "Varsayılan Görev Geçmişi kenar çubuğunu Comfy Queue ile değiştirir",
|
||||
"Hide Graph Button": "'Grafik' düğmesini gizle",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "Kayan 'Grafik' (İş akışı haritası) düğmesini gizler",
|
||||
"Hide Avatar": "Avatarı gizle",
|
||||
"Floating Canvas Controls": "Kayan tuval kontrolleri",
|
||||
"Classic Layout": "Klasik düzen",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "Kontrolleri ve açık iş akışı sekmelerini tek bir birleşik üst çubukta toplar",
|
||||
"Comfy Layout": "Kompakt Düzen",
|
||||
"Hide Junk": "Gereksiz Öğeleri Gizle",
|
||||
"Default": "Varsayılan",
|
||||
"Dock": "Sabitle",
|
||||
"Hide": "Gizle",
|
||||
"Enable Output Gallery Sidebar": "Çıktı Galerisi Kenar Çubuğunu Etkinleştir"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "重設視角",
|
||||
"Settings": "設定",
|
||||
"Export As...": "匯出為...",
|
||||
"Failed to load 3D format": "載入 3D 格式失敗"
|
||||
"Failed to load 3D format": "載入 3D 格式失敗",
|
||||
"Toggle Comfy Queue Sidebar": "切換 Comfy Queue 側邊欄",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "在隊列中忽略選取節點",
|
||||
"Renumber All Nodes (Execution Order)": "重新編號所有節點 (按執行順序)",
|
||||
"Set Selected 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...": "搜尋檔案名稱或提示詞元資料..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "隊列欄寬閾值",
|
||||
"Width Threshold for Queue Columns (px)": "隊列顯示多欄的寬度閾值 (像素)",
|
||||
"Show Pending Count Only": "僅顯示等待數量",
|
||||
"If disabled, each queued job will have a separate individual card": "若停用,每個排隊任務將顯示為獨立卡片",
|
||||
"Show Working Node Name": "顯示執行中節點名稱",
|
||||
"Shows the name of the node which is currently in the process": "顯示目前正在處理的節點名稱",
|
||||
"Auto Clear Interrupted": "自動清除中斷任務",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "新生成時自動清除已取消與失敗的任務",
|
||||
"Permanent Delete on Disk Deletion": "從磁碟永久刪除",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "從磁碟刪除時永久刪除檔案 (略過資源回收筒)",
|
||||
"Hide Tab: {tab}": "隱藏標籤頁: {tab}",
|
||||
"Assets": "資產",
|
||||
"Nodes": "節點",
|
||||
"Models": "模型",
|
||||
"Workflows": "工作流",
|
||||
"Apps": "應用",
|
||||
"Templates": "範本",
|
||||
"Override Stock History": "取代原生任務歷史",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "使用 Comfy Queue 取代原生任務歷史側邊欄",
|
||||
"Hide Graph Button": "隱藏「圖」按鈕",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "隱藏懸浮的「圖」(工作流/節點圖) 按鈕",
|
||||
"Hide Avatar": "隱藏頭像",
|
||||
"Floating Canvas Controls": "懸浮畫布控制項",
|
||||
"Classic Layout": "經典版面配置",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "將控制項與已開啟的工作流標籤頁整合於統一的頂端列",
|
||||
"Comfy Layout": "緊湊版面配置",
|
||||
"Hide Junk": "隱藏雜項",
|
||||
"Default": "預設",
|
||||
"Dock": "停靠",
|
||||
"Hide": "隱藏",
|
||||
"Enable Output Gallery Sidebar": "啟用輸出圖庫側邊欄"
|
||||
}
|
||||
@@ -96,5 +96,23 @@
|
||||
"Reset View": "重置视角",
|
||||
"Settings": "设置",
|
||||
"Export As...": "导出为...",
|
||||
"Failed to load 3D format": "加载 3D 格式失败"
|
||||
"Failed to load 3D format": "加载 3D 格式失败",
|
||||
"Toggle Comfy Queue Sidebar": "切换 Comfy Queue 侧边栏",
|
||||
"Toggle Ignore Selected Node(s) in Queue": "在队列中忽略所选节点",
|
||||
"Renumber All Nodes (Execution Order)": "重新编号所有节点 (按执行顺序)",
|
||||
"Set Selected 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...": "搜索文件名或提示词元数据..."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Grid Columns Threshold": "队列列宽阈值",
|
||||
"Width Threshold for Queue Columns (px)": "队列显示多列的宽度阈值 (像素)",
|
||||
"Show Pending Count Only": "仅显示等待数量",
|
||||
"If disabled, each queued job will have a separate individual card": "如果禁用,每个排队的任务将拥有单独的卡片",
|
||||
"Show Working Node Name": "显示正在执行的节点名称",
|
||||
"Shows the name of the node which is currently in the process": "显示当前正在处理的节点名称",
|
||||
"Auto Clear Interrupted": "自动清除中断任务",
|
||||
"Auto-clear cancelled & failed jobs on new generation": "新生成时自动清除已取消和失败的任务",
|
||||
"Permanent Delete on Disk Deletion": "从磁盘永久删除",
|
||||
"Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)": "从磁盘删除时永久删除文件 (绕过回收站)",
|
||||
"Hide Tab: {tab}": "隐藏标签页: {tab}",
|
||||
"Assets": "资产",
|
||||
"Nodes": "节点",
|
||||
"Models": "模型",
|
||||
"Workflows": "工作流",
|
||||
"Apps": "应用",
|
||||
"Templates": "模板",
|
||||
"Override Stock History": "替换原生任务历史",
|
||||
"Replace the stock Job History sidebar with Comfy Queue": "使用 Comfy Queue 替换原生任务历史侧边栏",
|
||||
"Hide Graph Button": "隐藏'图'按钮",
|
||||
"Hide floating 'Graph' (Workflow/Node Map) button": "隐藏悬浮的'图' (工作流/节点图) 按钮",
|
||||
"Hide Avatar": "隐藏头像",
|
||||
"Floating Canvas Controls": "悬浮画布控件",
|
||||
"Classic Layout": "经典布局",
|
||||
"Places the controls and the open workflow tabs on a single unified top bar": "将控件与已打开的工作流标签页整合在统一的顶部栏",
|
||||
"Comfy Layout": "紧凑布局",
|
||||
"Hide Junk": "隐藏杂项",
|
||||
"Default": "默认",
|
||||
"Dock": "停靠",
|
||||
"Hide": "隐藏",
|
||||
"Enable Output Gallery Sidebar": "启用输出图库侧边栏"
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
+17
-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,14 @@ 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)
|
||||
};
|
||||
}
|
||||
|
||||
export async function syncQueue() {
|
||||
try {
|
||||
const q = await api.getQueue();
|
||||
@@ -36,19 +45,17 @@ export async function syncQueue() {
|
||||
|
||||
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 +64,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);
|
||||
@@ -145,10 +153,9 @@ 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);
|
||||
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 +165,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 +179,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 +210,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 +257,7 @@ export async function initSessionAndHistory() {
|
||||
store.setPrompt(item.pid, newState);
|
||||
});
|
||||
|
||||
const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64;
|
||||
const maxItems = getSetting(SettingIds.MAX_HISTORY_ITEMS, 64);
|
||||
store.pruneHistory(maxItems);
|
||||
await syncQueue();
|
||||
} catch (err) {
|
||||
|
||||
+217
-149
@@ -3,125 +3,23 @@ import { SettingIds } from "./core/constants.js";
|
||||
import { injectStyles } from "./utils/styles.js";
|
||||
import { setupDragAndDrop } from "./utils/dragdrop.js";
|
||||
import { renumberNodesTopologically, setManualNodeId } from "./utils/nodeRenumber.js";
|
||||
import { findOurSidebarButton } from "./comfy/adapter.js";
|
||||
import { findOurSidebarButton, 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 registerSettings() {
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.GRID_COLUMNS_THRESHOLD,
|
||||
name: "Width Threshold for Queue Columns (px)",
|
||||
type: "number",
|
||||
defaultValue: 350
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.SHOW_PENDING_COUNT_ONLY,
|
||||
name: "If disabled, each queued job will have a separate individual card",
|
||||
type: "boolean",
|
||||
defaultValue: true
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.SHOW_WORKING_NODE_NAME,
|
||||
name: "Shows the name of the node which is currently in the process",
|
||||
type: "boolean",
|
||||
defaultValue: true
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.AUTO_CLEAR_INTERRUPTED,
|
||||
name: "Auto-clear cancelled & failed jobs on new generation",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Permanent Delete on Disk Deletion",
|
||||
name: "Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
});
|
||||
|
||||
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"];
|
||||
sidebarTabs.forEach(tab => {
|
||||
app.ui.settings.addSetting({
|
||||
id: `Comfy Sidebar.Hide Junk.${tab}`,
|
||||
name: `Hide Tab: ${tab}`,
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.OVERRIDE_STOCK_HISTORY,
|
||||
name: "Replace the stock Job History sidebar with Comfy Queue",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: (value) => {
|
||||
if (isInitialized && app.ui?.settings) {
|
||||
syncStockHistoryAndProgressSettings(value);
|
||||
}
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.HIDE_GRAPH_BUTTON,
|
||||
name: "Hide floating 'Graph' (Workflow/Node Map) button",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Avatar",
|
||||
name: "Hide Avatar",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Floating Canvas Controls",
|
||||
name: "Floating Canvas Controls",
|
||||
type: "combo",
|
||||
options: ["Default", "Dock", "Hide"],
|
||||
defaultValue: "Default",
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.CLASSIC_LAYOUT,
|
||||
name: "Places the controls and the open workflow tabs on a single unified top bar",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: (value) => {
|
||||
if (isInitialized) {
|
||||
applyClassicLayout(value, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupKeydownShortcuts() {
|
||||
activeKeydownHandler = (e) => {
|
||||
const activeEl = document.activeElement;
|
||||
@@ -132,13 +30,19 @@ function setupKeydownShortcuts() {
|
||||
activeEl.tagName === "SELECT"
|
||||
)) return;
|
||||
|
||||
// Q: Toggle Queue Sidebar
|
||||
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const ourBtn = findOurSidebarButton();
|
||||
if (ourBtn) ourBtn.click();
|
||||
if (app.extensionManager?.command?.hasCommand?.("Workspace.ToggleSidebarTab.classic-comfy-sidebar")) {
|
||||
app.extensionManager.command.execute("Workspace.ToggleSidebarTab.classic-comfy-sidebar");
|
||||
} else {
|
||||
const ourBtn = findOurSidebarButton();
|
||||
if (ourBtn) ourBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+Q / Cmd+Q: Toggle Ignore Selected Node
|
||||
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -156,6 +60,142 @@ function setupKeydownShortcuts() {
|
||||
document.addEventListener("keydown", activeKeydownHandler, true);
|
||||
}
|
||||
|
||||
function registerSettings() {
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.GRID_COLUMNS_THRESHOLD,
|
||||
name: t("Width Threshold for Queue Columns (px)"),
|
||||
type: "number",
|
||||
defaultValue: 350
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.SHOW_PENDING_COUNT_ONLY,
|
||||
name: t("If disabled, each queued job will have a separate individual card"),
|
||||
type: "boolean",
|
||||
defaultValue: true
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.SHOW_WORKING_NODE_NAME,
|
||||
name: t("Shows the name of the node which is currently in the process"),
|
||||
type: "boolean",
|
||||
defaultValue: true
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.AUTO_CLEAR_INTERRUPTED,
|
||||
name: t("Auto-clear cancelled & failed jobs on new generation"),
|
||||
type: "boolean",
|
||||
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)"),
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
});
|
||||
|
||||
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"];
|
||||
sidebarTabs.forEach(tab => {
|
||||
app.ui.settings.addSetting({
|
||||
id: `Comfy Sidebar.Hide Junk.${tab}`,
|
||||
name: t("Hide Tab: {tab}", { tab: t(tab) }),
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.OVERRIDE_STOCK_HISTORY,
|
||||
name: t("Replace the stock Job History sidebar with Comfy Queue"),
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: (value) => {
|
||||
if (isInitialized && app.ui?.settings) {
|
||||
syncStockHistoryAndProgressSettings(value);
|
||||
}
|
||||
applySidebarOverride();
|
||||
setTimeout(() => {
|
||||
applySidebarOverride();
|
||||
syncClassicLayout();
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.HIDE_GRAPH_BUTTON,
|
||||
name: t("Hide floating 'Graph' (Workflow/Node Map) button"),
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Avatar",
|
||||
name: t("Hide Avatar"),
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Floating Canvas Controls",
|
||||
name: t("Floating Canvas Controls"),
|
||||
type: "combo",
|
||||
options: ["Default", "Dock", "Hide"],
|
||||
defaultValue: "Default",
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.CLASSIC_LAYOUT,
|
||||
name: t("Places the controls and the open workflow tabs on a single unified top bar"),
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: (value) => {
|
||||
if (isInitialized) {
|
||||
applyClassicLayout(value, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupWorkflowTabsPositionWatcher() {
|
||||
let isSyncing = false;
|
||||
const TARGET_SETTING = "Comfy.Workflow.WorkflowTabsPosition";
|
||||
@@ -163,15 +203,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;
|
||||
@@ -182,7 +218,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;
|
||||
@@ -197,23 +232,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) => {
|
||||
@@ -227,7 +258,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) => {
|
||||
@@ -258,32 +288,25 @@ function setupWorkflowTabsPositionWatcher() {
|
||||
app.registerExtension({
|
||||
name: "ComfySidebar.ClassicRestore",
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
await initI18n();
|
||||
registerSettings();
|
||||
},
|
||||
|
||||
commands: [
|
||||
{
|
||||
id: "ComfySidebar.ToggleSidebar",
|
||||
label: "Toggle Comfy Queue Sidebar",
|
||||
function: () => {
|
||||
const ourBtn = findOurSidebarButton();
|
||||
if (ourBtn) ourBtn.click();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "ComfySidebar.ToggleIgnoreNode",
|
||||
label: "Toggle Ignore Selected Node(s) in Queue",
|
||||
label: t("Toggle Ignore Selected Node(s) in Queue"),
|
||||
function: () => toggleIgnoreActiveNode(() => renderSidebar())
|
||||
},
|
||||
{
|
||||
id: "ComfySidebar.RenumberNodes",
|
||||
label: "Renumber All Nodes (Execution Order)",
|
||||
label: t("Renumber All Nodes (Execution Order)"),
|
||||
function: () => renumberNodesTopologically()
|
||||
},
|
||||
{
|
||||
id: "ComfySidebar.SetNodeId",
|
||||
label: "Set Selected Node ID...",
|
||||
label: t("Set Selected Node ID..."),
|
||||
function: () => setManualNodeId()
|
||||
}
|
||||
],
|
||||
@@ -292,7 +315,7 @@ app.registerExtension({
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: "Set Node ID...",
|
||||
content: t("Set Node ID..."),
|
||||
callback: () => setManualNodeId(node)
|
||||
}
|
||||
];
|
||||
@@ -302,7 +325,7 @@ app.registerExtension({
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: "Renumber All Nodes (Execution Order)",
|
||||
content: t("Renumber All Nodes (Execution Order)"),
|
||||
callback: () => renumberNodesTopologically()
|
||||
}
|
||||
];
|
||||
@@ -326,23 +349,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);
|
||||
}
|
||||
@@ -356,12 +373,11 @@ app.registerExtension({
|
||||
cleanupFns.push(setupDragAndDrop());
|
||||
cleanupFns.push(setupVueNodeObserver());
|
||||
|
||||
// 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();
|
||||
@@ -375,13 +391,60 @@ 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();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -391,6 +454,11 @@ app.registerExtension({
|
||||
}
|
||||
destroyLayoutFix();
|
||||
teardownSidebarView();
|
||||
teardownGalleryView();
|
||||
|
||||
if (app.extensionManager?.unregisterSidebarTab) {
|
||||
app.extensionManager.unregisterSidebarTab("classic-comfy-gallery");
|
||||
}
|
||||
|
||||
if (activeKeydownHandler) {
|
||||
document.removeEventListener("keydown", activeKeydownHandler, true);
|
||||
@@ -401,4 +469,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,695 @@
|
||||
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;
|
||||
|
||||
// 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) {
|
||||
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;
|
||||
|
||||
// Never hide header if search bar is currently active
|
||||
const searchContainer = stickyHeaderGroupEl.querySelector(".comfy-sidebar-search-container");
|
||||
if (currentSearchQuery || (searchContainer && searchContainer.style.display !== "none")) {
|
||||
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
||||
isHeaderHidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentScroll = scrollEl.scrollTop;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGalleryData(subfolder = "", searchQuery = "") {
|
||||
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()}`);
|
||||
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) {
|
||||
console.error("[ComfySidebar] Failed to load gallery", e);
|
||||
showNotification(t("Failed to load gallery from disk."), "error");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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";
|
||||
|
||||
searchDebounce = setTimeout(async () => {
|
||||
try {
|
||||
await fetchGalleryData(currentSubfolder, currentSearchQuery);
|
||||
renderGallery();
|
||||
} finally {
|
||||
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: "28px"
|
||||
});
|
||||
container.appendChild(cardStack);
|
||||
|
||||
scrollToTopBtnEl = document.createElement("button");
|
||||
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
|
||||
scrollToTopBtnEl.title = t("Scroll to Top");
|
||||
Object.assign(scrollToTopBtnEl.style, {
|
||||
position: "sticky",
|
||||
bottom: "14px",
|
||||
marginLeft: "auto",
|
||||
zIndex: "30"
|
||||
});
|
||||
scrollToTopBtnEl.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const scrollEl = getScrollContainer(cardStackEl);
|
||||
if (scrollEl) scrollEl.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
container.appendChild(scrollToTopBtnEl);
|
||||
|
||||
const handleCtrlDown = (e) => {
|
||||
if (e.key === "Control" || e.key === "Meta") document.body.classList.add("comfy-sidebar-ctrl-active");
|
||||
};
|
||||
const handleCtrlUp = (e) => {
|
||||
if (e.key === "Control" || e.key === "Meta") document.body.classList.remove("comfy-sidebar-ctrl-active");
|
||||
};
|
||||
window.addEventListener("keydown", handleCtrlDown);
|
||||
window.addEventListener("keyup", handleCtrlUp);
|
||||
|
||||
// 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 });
|
||||
|
||||
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();
|
||||
}
|
||||
}).observe(container);
|
||||
|
||||
fetchGalleryData("").then(() => renderGallery());
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
export function teardownGalleryView() {
|
||||
document.removeEventListener("scroll", updateScrollTopVisibility, { capture: true, passive: true });
|
||||
cachedFolders = [];
|
||||
cachedFiles = [];
|
||||
stickyHeaderGroupEl = null;
|
||||
galleryContainerEl = null;
|
||||
cardStackEl = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
+106
-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";
|
||||
@@ -90,16 +91,13 @@ 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 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) {
|
||||
@@ -162,7 +160,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 +212,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 +256,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 +280,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");
|
||||
@@ -346,17 +346,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 +398,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 +484,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 +618,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 +736,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 +852,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 +921,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 +943,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 +959,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 +970,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; });
|
||||
|
||||
@@ -367,11 +367,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 +398,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 +492,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 +546,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>
|
||||
`;
|
||||
|
||||
+130
-72
@@ -56,6 +56,106 @@ export function showNotification(message, type = "success", title = null) {
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function remapWidgetValue(val, oldToNew) {
|
||||
if (typeof val === "string") {
|
||||
const match = val.match(/^\$([^-]+)-(.*)$/);
|
||||
if (match) {
|
||||
const mapped = oldToNew.get(match[1]) ?? oldToNew.get(Number(match[1]));
|
||||
if (mapped != null) {
|
||||
return `$${mapped}-${match[2]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
function applyIdMappingToGraph(oldToNew) {
|
||||
if (!app.graph) return;
|
||||
const graphData = app.graph.serialize();
|
||||
if (!graphData) return;
|
||||
|
||||
// 1. Remap node IDs and internal widget references
|
||||
if (Array.isArray(graphData.nodes)) {
|
||||
for (const node of graphData.nodes) {
|
||||
const oldId = node.id;
|
||||
const newId = oldToNew.get(oldId) ?? oldToNew.get(String(oldId)) ?? oldToNew.get(Number(oldId));
|
||||
if (newId != null) {
|
||||
node.id = newId;
|
||||
}
|
||||
if (Array.isArray(node.widgets_values)) {
|
||||
node.widgets_values = node.widgets_values.map(val => remapWidgetValue(val, oldToNew));
|
||||
} else if (node.widgets_values && typeof node.widgets_values === "object") {
|
||||
for (const key of Object.keys(node.widgets_values)) {
|
||||
node.widgets_values[key] = remapWidgetValue(node.widgets_values[key], oldToNew);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remap link endpoints (both array tuples and object representations)
|
||||
const linkList = Array.isArray(graphData.links)
|
||||
? graphData.links
|
||||
: (graphData.links && typeof graphData.links === "object" ? Object.values(graphData.links) : []);
|
||||
|
||||
for (const link of linkList) {
|
||||
if (!link) continue;
|
||||
if (Array.isArray(link)) {
|
||||
// [id, origin_id, origin_slot, target_id, target_slot, type]
|
||||
const originId = link[1];
|
||||
const targetId = link[3];
|
||||
const newOrigin = oldToNew.get(originId) ?? oldToNew.get(String(originId)) ?? oldToNew.get(Number(originId));
|
||||
if (newOrigin != null) link[1] = newOrigin;
|
||||
const newTarget = oldToNew.get(targetId) ?? oldToNew.get(String(targetId)) ?? oldToNew.get(Number(targetId));
|
||||
if (newTarget != null) link[3] = newTarget;
|
||||
} else if (typeof link === "object") {
|
||||
const originId = link.origin_id;
|
||||
const targetId = link.target_id;
|
||||
const newOrigin = oldToNew.get(originId) ?? oldToNew.get(String(originId)) ?? oldToNew.get(Number(originId));
|
||||
if (newOrigin != null) link.origin_id = newOrigin;
|
||||
const newTarget = oldToNew.get(targetId) ?? oldToNew.get(String(targetId)) ?? oldToNew.get(Number(targetId));
|
||||
if (newTarget != null) link.target_id = newTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Remap group membership if defined
|
||||
if (Array.isArray(graphData.groups)) {
|
||||
for (const group of graphData.groups) {
|
||||
if (Array.isArray(group.nodes)) {
|
||||
group.nodes = group.nodes.map(id => oldToNew.get(id) ?? oldToNew.get(String(id)) ?? oldToNew.get(Number(id)) ?? id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update graph last_node_id counter
|
||||
let maxId = 0;
|
||||
for (const val of oldToNew.values()) {
|
||||
const num = Number(val);
|
||||
if (!isNaN(num) && num > maxId) maxId = num;
|
||||
}
|
||||
graphData.last_node_id = Math.max(graphData.last_node_id || 0, maxId, graphData.nodes?.length || 0);
|
||||
|
||||
// 5. Preserve canvas pan/zoom coordinates so the screen doesn't jump
|
||||
const currentScale = app.canvas?.ds?.scale;
|
||||
const currentOffset = app.canvas?.ds?.offset ? [...app.canvas.ds.offset] : null;
|
||||
|
||||
app.graph.configure(graphData);
|
||||
|
||||
if (app.canvas?.ds && currentScale != null && currentOffset != null) {
|
||||
app.canvas.ds.scale = currentScale;
|
||||
app.canvas.ds.offset[0] = currentOffset[0];
|
||||
app.canvas.ds.offset[1] = currentOffset[1];
|
||||
}
|
||||
|
||||
if (typeof app.graph.afterChange === "function") {
|
||||
app.graph.afterChange();
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
if (app.canvas?.draw) {
|
||||
app.canvas.draw(true, true);
|
||||
}
|
||||
syncAllNodeBadges();
|
||||
}
|
||||
|
||||
export async function renumberNodesTopologically() {
|
||||
if (!app.graph || !app.graph._nodes || app.graph._nodes.length === 0) {
|
||||
showNotification(t("No nodes found on canvas to renumber."), "warn");
|
||||
@@ -88,55 +188,21 @@ export async function renumberNodesTopologically() {
|
||||
return posYA - posYB;
|
||||
});
|
||||
|
||||
const isStringId = typeof sortedNodes[0]?.id === "string";
|
||||
const oldToNew = new Map();
|
||||
sortedNodes.forEach((node, index) => {
|
||||
const newId = index + 1;
|
||||
const newId = isStringId ? String(index + 1) : (index + 1);
|
||||
oldToNew.set(node.id, newId);
|
||||
oldToNew.set(String(node.id), newId);
|
||||
oldToNew.set(Number(node.id), newId);
|
||||
});
|
||||
|
||||
// 1. Remap node IDs in-place on app.graph
|
||||
app.graph._nodes_by_id = {};
|
||||
sortedNodes.forEach(node => {
|
||||
const oldId = node.id;
|
||||
const newId = oldToNew.get(oldId);
|
||||
if (newId != null) {
|
||||
// Update internal widget IDs like "$2034-0" (PreviewBridge)
|
||||
if (Array.isArray(node.widgets)) {
|
||||
for (const w of node.widgets) {
|
||||
if (typeof w.value === "string" && w.value.startsWith(`$${oldId}-`)) {
|
||||
w.value = w.value.replace(`$${oldId}-`, `$${newId}-`);
|
||||
}
|
||||
}
|
||||
}
|
||||
node.id = newId;
|
||||
app.graph._nodes_by_id[newId] = node;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Remap links
|
||||
if (app.graph.links) {
|
||||
const links = app.graph.links instanceof Map
|
||||
? Array.from(app.graph.links.values())
|
||||
: Object.values(app.graph.links);
|
||||
|
||||
links.forEach(link => {
|
||||
if (!link) return;
|
||||
if (oldToNew.has(link.origin_id)) link.origin_id = oldToNew.get(link.origin_id);
|
||||
if (oldToNew.has(link.target_id)) link.target_id = oldToNew.get(link.target_id);
|
||||
});
|
||||
}
|
||||
|
||||
app.graph.last_node_id = sortedNodes.length;
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
if (app.canvas?.draw) {
|
||||
app.canvas.draw(true, true);
|
||||
}
|
||||
syncAllNodeBadges();
|
||||
applyIdMappingToGraph(oldToNew);
|
||||
|
||||
const lastNode = sortedNodes[sortedNodes.length - 1];
|
||||
const lastNodeName = lastNode ? (lastNode.title || lastNode.type) : "SaveImage";
|
||||
showNotification(t("Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).", { count: sortedNodes.length, id: lastNode ? lastNode.id : "", name: lastNodeName }), "success");
|
||||
const lastNewId = lastNode ? oldToNew.get(lastNode.id) : "";
|
||||
showNotification(t("Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).", { count: sortedNodes.length, id: lastNewId, name: lastNodeName }), "success");
|
||||
} catch (err) {
|
||||
console.error("ComfySidebar: Failed to renumber nodes", err);
|
||||
showNotification(t("Failed to renumber nodes: ") + err.message, "error");
|
||||
@@ -159,55 +225,47 @@ export async function setManualNodeId(targetNode = null) {
|
||||
const input = prompt(t("Enter new ID for node #{id} ({name}):", { id: currentId, name: node.title || node.type }), String(currentId));
|
||||
if (!input) return;
|
||||
|
||||
const newId = parseInt(input.trim(), 10);
|
||||
if (isNaN(newId) || newId <= 0) {
|
||||
const newIdNum = parseInt(input.trim(), 10);
|
||||
if (isNaN(newIdNum) || newIdNum <= 0) {
|
||||
showNotification(t("Please enter a valid positive number."), "error");
|
||||
return;
|
||||
}
|
||||
if (newId === currentId) return;
|
||||
if (String(newIdNum) === String(currentId)) return;
|
||||
|
||||
try {
|
||||
const existingNode = app.graph.getNodeById ? app.graph.getNodeById(newId) : null;
|
||||
const existingNode = app.graph.getNodeById ? (app.graph.getNodeById(newIdNum) || app.graph.getNodeById(String(newIdNum))) : null;
|
||||
if (existingNode && existingNode !== node) {
|
||||
const confirmSwap = confirm(t("Node ID #{id} is already used by \"{name}\". Swap their IDs?", { id: newId, name: existingNode.title || existingNode.type }));
|
||||
const confirmSwap = confirm(t("Node ID #{id} is already used by \"{name}\". Swap their IDs?", { id: newIdNum, name: existingNode.title || existingNode.type }));
|
||||
if (!confirmSwap) return;
|
||||
}
|
||||
|
||||
const isStringId = typeof currentId === "string";
|
||||
const newId = isStringId ? String(newIdNum) : newIdNum;
|
||||
const oldId = currentId;
|
||||
|
||||
const oldToNew = new Map();
|
||||
if (existingNode && existingNode !== node) {
|
||||
oldToNew.set(currentId, newId);
|
||||
oldToNew.set(newId, currentId);
|
||||
existingNode.id = currentId;
|
||||
const existingId = existingNode.id;
|
||||
oldToNew.set(oldId, newId);
|
||||
oldToNew.set(String(oldId), newId);
|
||||
oldToNew.set(Number(oldId), newId);
|
||||
|
||||
oldToNew.set(existingId, oldId);
|
||||
oldToNew.set(String(existingId), oldId);
|
||||
oldToNew.set(Number(existingId), oldId);
|
||||
} else {
|
||||
oldToNew.set(currentId, newId);
|
||||
}
|
||||
node.id = newId;
|
||||
|
||||
// Rebuild lookup map
|
||||
app.graph._nodes_by_id = {};
|
||||
for (const n of app.graph._nodes) {
|
||||
app.graph._nodes_by_id[n.id] = n;
|
||||
oldToNew.set(oldId, newId);
|
||||
oldToNew.set(String(oldId), newId);
|
||||
oldToNew.set(Number(oldId), newId);
|
||||
}
|
||||
|
||||
// Remap links
|
||||
if (app.graph.links) {
|
||||
const links = app.graph.links instanceof Map
|
||||
? Array.from(app.graph.links.values())
|
||||
: Object.values(app.graph.links);
|
||||
applyIdMappingToGraph(oldToNew);
|
||||
|
||||
links.forEach(link => {
|
||||
if (!link) return;
|
||||
if (oldToNew.has(link.origin_id)) link.origin_id = oldToNew.get(link.origin_id);
|
||||
if (oldToNew.has(link.target_id)) link.target_id = oldToNew.get(link.target_id);
|
||||
});
|
||||
const updatedNode = app.graph.getNodeById ? (app.graph.getNodeById(newId) || app.graph.getNodeById(String(newId))) : null;
|
||||
if (updatedNode && app.canvas?.selectNode) {
|
||||
app.canvas.selectNode(updatedNode);
|
||||
}
|
||||
|
||||
app.graph.last_node_id = Math.max(app.graph.last_node_id || 0, newId, app.graph._nodes.length);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
if (app.canvas?.draw) {
|
||||
app.canvas.draw(true, true);
|
||||
}
|
||||
syncAllNodeBadges();
|
||||
showNotification(t("Assigned node ID #{id} to \"{name}\".", { id: newId, name: node.title || node.type }), "success");
|
||||
} catch (err) {
|
||||
console.error("ComfySidebar: Failed to set node ID", err);
|
||||
|
||||
+10
-6
@@ -126,6 +126,16 @@ 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) {
|
||||
// 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 +145,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 +152,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);
|
||||
|
||||
+375
-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,61 @@ 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;
|
||||
}
|
||||
|
||||
/* Round (X) Close Button across all preview overlays (matches button, div, or span) */
|
||||
body.comfy-sidebar-overlay-active [style*="50%"],
|
||||
.comfy-sidebar-comparison-overlay [style*="50%"],
|
||||
.comfy-sidebar-3d-overlay-root [style*="50%"],
|
||||
div[class*="sidebar-overlay"] [style*="50%"] {
|
||||
background: var(--card, var(--secondary, rgba(28, 28, 35, 0.9))) !important;
|
||||
border: 1px solid var(--border, var(--border-color, rgba(255, 255, 255, 0.2))) !important;
|
||||
color: var(--muted-foreground, #cbd5e1) !important;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5) !important;
|
||||
transition: all 0.15s ease-in-out !important;
|
||||
}
|
||||
|
||||
body.comfy-sidebar-overlay-active [style*="50%"]:hover,
|
||||
.comfy-sidebar-comparison-overlay [style*="50%"]:hover,
|
||||
.comfy-sidebar-3d-overlay-root [style*="50%"]:hover,
|
||||
div[class*="sidebar-overlay"] [style*="50%"]:hover {
|
||||
color: #ffffff !important;
|
||||
border-color: var(--primary, var(--p-primary-color, #ea580c)) !important;
|
||||
background: color-mix(in srgb, var(--primary, var(--p-primary-color, #ea580c)) 25%, var(--card, var(--secondary, #222)) 75%) !important;
|
||||
transform: scale(1.08) !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 +664,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 +680,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 +709,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;
|
||||
|
||||
+161
-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);
|
||||
}
|
||||
@@ -431,4 +483,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();
|
||||
|
||||
+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.2"
|
||||
version = "1.8.2"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"send2trash>=1.8.0",
|
||||
|
||||
Reference in New Issue
Block a user