Compare commits

...
6 Commits
40 changed files with 1841 additions and 265 deletions
+3
View File
@@ -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
View File
@@ -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:
+127 -25
View File
@@ -1,4 +1,5 @@
import { app } from "/scripts/app.js";
import { SettingIds } from "../core/constants.js";
export function findOriginalPropertiesButton() {
const byTestId = document.querySelector('[data-testid="properties-panel-toggle"]');
@@ -26,7 +27,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();
@@ -156,6 +157,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 +236,135 @@ 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 = (
app.extensionManager?.setting?.get?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Override Stock History") ??
false
);
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;
}
}
+72 -39
View File
@@ -6,8 +6,10 @@ import {
findActiveQueueIndicator,
findNativeExtensionsPanel,
findGraphButton,
findPropertiesPanel
findPropertiesPanel,
applySidebarOverride
} from "./adapter.js";
import { SettingIds } from "../core/constants.js";
import { isRuntimePreviewEnabled, setRuntimePreviewEnabled, setRuntimePreviewStateListener } from "../utils/comparison.js";
// Listen for runtime preview state changes to keep button UI in sync
@@ -18,20 +20,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 {
@@ -102,9 +106,27 @@ export function syncStockHistoryAndProgressSettings(enable) {
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 sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
if (!sidebar) return false;
@@ -371,7 +393,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;
@@ -491,7 +513,13 @@ function updateSidebarTabsVisibility() {
}
});
const hideStockHistory = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
const hideStockHistory = (
app.extensionManager?.setting?.get?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.(SettingIds.OVERRIDE_STOCK_HISTORY) ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ??
app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Override Stock History") ??
false
);
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;
@@ -578,10 +606,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 +615,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");
@@ -621,31 +665,11 @@ 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";
}
}
@@ -779,6 +803,10 @@ export function syncClassicLayout() {
syncPropertiesButton(isClassicLayoutEnabled);
syncActiveQueueIndicator(isClassicLayoutEnabled);
updateSidebarTabsVisibility();
applySidebarOverride();
if (!sidebarTopObserver) {
setupSidebarTopObserver();
}
syncRuntimePreviewButton();
} catch (err) {
@@ -796,6 +824,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 +841,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;
@@ -1087,6 +1116,10 @@ export function destroyLayoutFix() {
domObserver.disconnect();
domObserver = null;
}
if (sidebarTopObserver) {
sidebarTopObserver.disconnect();
sidebarTopObserver = null;
}
if (actionbarResizeObserver) {
actionbarResizeObserver.disconnect();
actionbarResizeObserver = null;
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "تجاهل/تضمين العقد المحددة في قائمة الانتظار",
"Renumber All Nodes (Execution Order)": "إعادة ترقيم جميع العقد (ترتيب التنفيذ)",
"Set Selected Node ID...": "تعيين معرف العقدة المحددة...",
"Set Node ID...": "تعيين معرف العقدة..."
"Set Node ID...": "تعيين معرف العقدة...",
"Gallery": "المعرض",
"Gallery (Outputs)": "المعرض (المخرجات)",
"Go Up One Level": "الانتقال لمستوى أعلى",
"Open Current Folder in File Manager": "فتح المجلد الحالي في مدير الملفات",
"Refresh Gallery": "تحديث المعرض",
"Filter gallery files...": "تصفية ملفات المعرض...",
"Loading files...": "جاري تحميل الملفات...",
"No files found in this folder.": "لم يتم العثور على ملفات في هذا المجلد.",
"Failed to load gallery from disk.": "فشل تحميل المعرض من القرص.",
"Sort: Newest First": "الترتيب: الأحدث أولاً",
"Sort: Oldest First": "الترتيب: الأقدم أولاً",
"Search Gallery (Filename or Prompt)": "البحث في المعرض (اسم الملف أو الموجه)",
"Search filenames or prompt metadata...": "البحث في أسماء الملفات أو بيانات الموجه..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "إخفاء العناصر غير المرغوبة",
"Default": "افتراضي",
"Dock": "إرساء",
"Hide": "إخفاء"
"Hide": "إخفاء",
"Enable Output Gallery Sidebar": "تمكين الشريط الجانبي لمعرض المخرجات"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "Toggle Ignore Selected Node(s) in Queue",
"Renumber All Nodes (Execution Order)": "Renumber All Nodes (Execution Order)",
"Set Selected Node ID...": "Set Selected Node ID...",
"Set Node ID...": "Set Node ID..."
"Set Node ID...": "Set Node ID...",
"Gallery": "Gallery",
"Gallery (Outputs)": "Gallery (Outputs)",
"Go Up One Level": "Go Up One Level",
"Open Current Folder in File Manager": "Open Current Folder in File Manager",
"Refresh Gallery": "Refresh Gallery",
"Filter gallery files...": "Filter gallery files...",
"Loading files...": "Loading files...",
"No files found in this folder.": "No files found in this folder.",
"Failed to load gallery from disk.": "Failed to load gallery from disk.",
"Sort: Newest First": "Sort: Newest First",
"Sort: Oldest First": "Sort: Oldest First",
"Search Gallery (Filename or Prompt)": "Search Gallery (Filename or Prompt)",
"Search filenames or prompt metadata...": "Search filenames or prompt metadata..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "Hide Junk",
"Default": "Default",
"Dock": "Dock",
"Hide": "Hide"
"Hide": "Hide",
"Enable Output Gallery Sidebar": "Enable Output Gallery Sidebar"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignorar/Incluir nodo(s) seleccionado(s) en la cola",
"Renumber All Nodes (Execution Order)": "Renumerar todos los nodos (orden de ejecución)",
"Set Selected Node ID...": "Establecer ID del nodo seleccionado...",
"Set Node ID...": "Establecer ID de nodo..."
"Set Node ID...": "Establecer ID de nodo...",
"Gallery": "Galería",
"Gallery (Outputs)": "Galería (Salidas)",
"Go Up One Level": "Subir un nivel",
"Open Current Folder in File Manager": "Abrir carpeta actual en el explorador de archivos",
"Refresh Gallery": "Actualizar galería",
"Filter gallery files...": "Filtrar archivos de la galería...",
"Loading files...": "Cargando archivos...",
"No files found in this folder.": "No se encontraron archivos en esta carpeta.",
"Failed to load gallery from disk.": "Error al cargar la galería desde el disco.",
"Sort: Newest First": "Ordenar: más recientes primero",
"Sort: Oldest First": "Ordenar: más antiguos primero",
"Search Gallery (Filename or Prompt)": "Buscar en galería (nombre de archivo o prompt)",
"Search filenames or prompt metadata...": "Buscar nombres de archivo o metadatos de prompt..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "Ocultar elementos innecesarios",
"Default": "Por defecto",
"Dock": "Acoplar",
"Hide": "Ocultar"
"Hide": "Ocultar",
"Enable Output Gallery Sidebar": "Habilitar barra lateral de galería de salidas"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "نادیده گرفتن/فعال‌سازی گره‌های انتخاب‌شده در صف",
"Renumber All Nodes (Execution Order)": "شماره‌گذاری مجدد همه گره‌ها (ترتیب اجرا)",
"Set Selected Node ID...": "تنظیم شناسه گره انتخاب‌شده...",
"Set Node ID...": "تنظیم شناسه گره..."
"Set Node ID...": "تنظیم شناسه گره...",
"Gallery": "گالری",
"Gallery (Outputs)": "گالری (خروجی‌ها)",
"Go Up One Level": "یک سطح به بالا",
"Open Current Folder in File Manager": "باز کردن پوشه فعلی در مدیریت فایل",
"Refresh Gallery": "به‌روزرسانی گالری",
"Filter gallery files...": "فیلتر کردن فایل‌های گالری...",
"Loading files...": "در حال بارگذاری فایل‌ها...",
"No files found in this folder.": "هیچ فایلی در این پوشه یافت نشد.",
"Failed to load gallery from disk.": "بارگذاری گالری از دیسک ناموفق بود.",
"Sort: Newest First": "مرتب‌سازی: جدیدترین‌ها در ابتدا",
"Sort: Oldest First": "مرتب‌سازی: قدیمی‌ترین‌ها در ابتدا",
"Search Gallery (Filename or Prompt)": "جستجو در گالری (نام فایل یا پرامپت)",
"Search filenames or prompt metadata...": "جستجوی نام فایل‌ها یا متادیتای پرامپت..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "پنهان کردن موارد غیرضروری",
"Default": "پیش‌فرض",
"Dock": "اتصال",
"Hide": "پنهان"
"Hide": "پنهان",
"Enable Output Gallery Sidebar": "فعال‌سازی نوار کناری گالری خروجی‌ها"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignorer/Activer les nœuds sélectionnés dans la file",
"Renumber All Nodes (Execution Order)": "Renuméroter tous les nœuds (ordre d'exécution)",
"Set Selected Node ID...": "Définir l'ID du nœud sélectionné...",
"Set Node ID...": "Définir l'ID du nœud..."
"Set Node ID...": "Définir l'ID du nœud...",
"Gallery": "Galerie",
"Gallery (Outputs)": "Galerie (Sorties)",
"Go Up One Level": "Dossier parent",
"Open Current Folder in File Manager": "Ouvrir le dossier actuel dans le gestionnaire de fichiers",
"Refresh Gallery": "Actualiser la galerie",
"Filter gallery files...": "Filtrer les fichiers de la galerie...",
"Loading files...": "Chargement des fichiers...",
"No files found in this folder.": "Aucun fichier trouvé dans ce dossier.",
"Failed to load gallery from disk.": "Échec du chargement de la galerie depuis le disque.",
"Sort: Newest First": "Trier : Plus récents d'abord",
"Sort: Oldest First": "Trier : Plus anciens d'abord",
"Search Gallery (Filename or Prompt)": "Rechercher dans la galerie (nom de fichier ou prompt)",
"Search filenames or prompt metadata...": "Rechercher des noms de fichiers ou métadonnées de prompt..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "Masquer les éléments inutiles",
"Default": "Par défaut",
"Dock": "Ancrer",
"Hide": "Masquer"
"Hide": "Masquer",
"Enable Output Gallery Sidebar": "Activer la barre latérale de la galerie de sorties"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "התעלם/כלול צמתים נבחרים בתור",
"Renumber All Nodes (Execution Order)": "מספר מחדש את כל הצמתים (סדר ביצוע)",
"Set Selected Node ID...": "הגדר מזהה צומת נבחר...",
"Set Node ID...": "הגדר מזהה צומת..."
"Set Node ID...": "הגדר מזהה צומת...",
"Gallery": "גלריה",
"Gallery (Outputs)": "גלריה (תוצרים)",
"Go Up One Level": "עלה שלב אחד למעלה",
"Open Current Folder in File Manager": "פתח תיקייה נוכחית בסייר הקבצים",
"Refresh Gallery": "רענן גלריה",
"Filter gallery files...": "סנן קבצי גלריה...",
"Loading files...": "טוען קבצים...",
"No files found in this folder.": "לא נמצאו קבצים בתיקייה זו.",
"Failed to load gallery from disk.": "טעינת הגלריה מהכונן נכשלה.",
"Sort: Newest First": "מיון: החדשים ביותר קודם",
"Sort: Oldest First": "מיון: הישנים ביותר קודם",
"Search Gallery (Filename or Prompt)": "חיפוש בגלריה (שם קובץ או פרומפט)",
"Search filenames or prompt metadata...": "חיפוש שמות קבצים או מטא-דאטה של פרומפט..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "הסתר פריטים מיותרים",
"Default": "ברירת מחדל",
"Dock": "הצמד",
"Hide": "הסתר"
"Hide": "הסתר",
"Enable Output Gallery Sidebar": "הפעל סרגל צד של גלריית תוצרים"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignora/Attiva nodi selezionati nella coda",
"Renumber All Nodes (Execution Order)": "Rinumera tutti i nodi (ordine di esecuzione)",
"Set Selected Node ID...": "Imposta ID nodo selezionato...",
"Set Node ID...": "Imposta ID nodo..."
"Set Node ID...": "Imposta ID nodo...",
"Gallery": "Galleria",
"Gallery (Outputs)": "Galleria (Output)",
"Go Up One Level": "Sali di un livello",
"Open Current Folder in File Manager": "Apri cartella corrente in Esplora file",
"Refresh Gallery": "Aggiorna galleria",
"Filter gallery files...": "Filtra file della galleria...",
"Loading files...": "Caricamento file...",
"No files found in this folder.": "Nessun file trovato in questa cartella.",
"Failed to load gallery from disk.": "Impossibile caricare la galleria dal disco.",
"Sort: Newest First": "Ordina: Più recenti prima",
"Sort: Oldest First": "Ordina: Più vecchi prima",
"Search Gallery (Filename or Prompt)": "Cerca nella galleria (nome file o prompt)",
"Search filenames or prompt metadata...": "Cerca nomi di file o metadati del prompt..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "Nascondi elementi inutili",
"Default": "Predefinito",
"Dock": "Aggancia",
"Hide": "Nascondi"
"Hide": "Nascondi",
"Enable Output Gallery Sidebar": "Abilita barra laterale galleria di output"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "キューで選択したノードを無視/解除",
"Renumber All Nodes (Execution Order)": "すべてのノード番号を再割り当て (実行順)",
"Set Selected Node ID...": "選択したノードIDを設定...",
"Set Node ID...": "ノードIDを設定..."
"Set Node ID...": "ノードIDを設定...",
"Gallery": "ギャラリー",
"Gallery (Outputs)": "ギャラリー (出力)",
"Go Up One Level": "1つ上の階層へ",
"Open Current Folder in File Manager": "現在のフォルダーをファイルマネージャーで開く",
"Refresh Gallery": "ギャラリーを更新",
"Filter gallery files...": "ギャラリーファイルを絞り込み...",
"Loading files...": "ファイルを読み込み中...",
"No files found in this folder.": "このフォルダーにはファイルがありません。",
"Failed to load gallery from disk.": "ディスクからギャラリーを読み込めませんでした。",
"Sort: Newest First": "並び替え:新しい順",
"Sort: Oldest First": "並び替え:古い順",
"Search Gallery (Filename or Prompt)": "ギャラリーを検索 (ファイル名またはプロンプト)",
"Search filenames or prompt metadata...": "ファイル名またはプロンプトを検索..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "不要な項目を非表示",
"Default": "デフォルト",
"Dock": "ドック",
"Hide": "非表示"
"Hide": "非表示",
"Enable Output Gallery Sidebar": "出力ギャラリーのサイドバーを有効化"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "대기열에서 선택한 노드 무시/해제",
"Renumber All Nodes (Execution Order)": "모든 노드 재번호 매기기 (실행 순서)",
"Set Selected Node ID...": "선택한 노드 ID 설정...",
"Set Node ID...": "노드 ID 설정..."
"Set Node ID...": "노드 ID 설정...",
"Gallery": "갤러리",
"Gallery (Outputs)": "갤러리 (출력)",
"Go Up One Level": "한 수준 위로",
"Open Current Folder in File Manager": "파일 탐색기에서 현재 폴더 열기",
"Refresh Gallery": "갤러리 새로고침",
"Filter gallery files...": "갤러리 파일 필터링...",
"Loading files...": "파일 로딩 중...",
"No files found in this folder.": "이 폴더에 파일이 없습니다.",
"Failed to load gallery from disk.": "디스크에서 갤러리를 불러오지 못했습니다.",
"Sort: Newest First": "정렬: 최신순",
"Sort: Oldest First": "정렬: 오래된순",
"Search Gallery (Filename or Prompt)": "갤러리 검색 (파일명 또는 프롬프트)",
"Search filenames or prompt metadata...": "파일명 또는 프롬프트 메타데이터 검색..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "불필요한 항목 숨기기",
"Default": "기본값",
"Dock": "도킹",
"Hide": "숨기기"
"Hide": "숨기기",
"Enable Output Gallery Sidebar": "출력 갤러리 사이드바 활성화"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "Ignorar/Incluir nós selecionados na fila",
"Renumber All Nodes (Execution Order)": "Renumerar todos os nós (ordem de execução)",
"Set Selected Node ID...": "Definir ID do nó selecionado...",
"Set Node ID...": "Definir ID do nó..."
"Set Node ID...": "Definir ID do nó...",
"Gallery": "Galeria",
"Gallery (Outputs)": "Galeria (Saídas)",
"Go Up One Level": "Subir um nível",
"Open Current Folder in File Manager": "Abrir pasta atual no gerenciador de arquivos",
"Refresh Gallery": "Atualizar galeria",
"Filter gallery files...": "Filtrar arquivos da galeria...",
"Loading files...": "Carregando arquivos...",
"No files found in this folder.": "Nenhum arquivo encontrado nesta pasta.",
"Failed to load gallery from disk.": "Falha ao carregar a galeria do disco.",
"Sort: Newest First": "Ordenar: Mais recentes primeiro",
"Sort: Oldest First": "Ordenar: Mais antigos primeiro",
"Search Gallery (Filename or Prompt)": "Pesquisar na galeria (nome do arquivo ou prompt)",
"Search filenames or prompt metadata...": "Pesquisar nomes de arquivos ou metadados de prompt..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "Ocultar itens desnecessários",
"Default": "Padrão",
"Dock": "Fixar",
"Hide": "Ocultar"
"Hide": "Ocultar",
"Enable Output Gallery Sidebar": "Ativar barra lateral da galeria de saídas"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "Игнорировать выбранные ноды в очереди",
"Renumber All Nodes (Execution Order)": "Перенумеровать ноды (порядок выполнения)",
"Set Selected Node ID...": "Задать ID выбранной ноды...",
"Set Node ID...": "Задать ID ноды..."
"Set Node ID...": "Задать ID ноды...",
"Gallery": "Галерея",
"Gallery (Outputs)": "Галерея (Результаты)",
"Go Up One Level": "На один уровень вверх",
"Open Current Folder in File Manager": "Открыть текущую папку в проводнике",
"Refresh Gallery": "Обновить галерею",
"Filter gallery files...": "Фильтр файлов галереи...",
"Loading files...": "Загрузка файлов...",
"No files found in this folder.": "В этой папке нет файлов.",
"Failed to load gallery from disk.": "Не удалось загрузить галерею с диска.",
"Sort: Newest First": "Сортировка: сначала новые",
"Sort: Oldest First": "Сортировка: сначала старые",
"Search Gallery (Filename or Prompt)": "Поиск по галерее (имя файла или промпт)",
"Search filenames or prompt metadata...": "Поиск по именам файлов или промптам..."
}
+2 -1
View File
@@ -29,5 +29,6 @@
"Hide Junk": "Скрыть лишнее",
"Default": "По умолчанию",
"Dock": "Закрепить",
"Hide": "Скрыть"
"Hide": "Скрыть",
"Enable Output Gallery Sidebar": "Включить боковую панель галереи результатов"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "Kuyrukta Seçili Düğümleri Yoksay/Etkinleştir",
"Renumber All Nodes (Execution Order)": "Tüm Düğümleri Yeniden Numaralandır (Çalıştırma Sırası)",
"Set Selected Node ID...": "Seçili Düğüm Kimliğini Belirle...",
"Set Node ID...": "Düğüm Kimliğini Belirle..."
"Set Node ID...": "Düğüm Kimliğini Belirle...",
"Gallery": "Galeri",
"Gallery (Outputs)": "Galeri (Çıktılar)",
"Go Up One Level": "Bir Düzey Yukarı Çık",
"Open Current Folder in File Manager": "Mevcut Klasörü Dosya Yöneticisinde Aç",
"Refresh Gallery": "Galeriyi Yenile",
"Filter gallery files...": "Galeri dosyalarını filtrele...",
"Loading files...": "Dosyalar yükleniyor...",
"No files found in this folder.": "Bu klasörde dosya bulunamadı.",
"Failed to load gallery from disk.": "Galeri diskten yüklenemedi.",
"Sort: Newest First": "Sıralama: Önce En Yeniler",
"Sort: Oldest First": "Sıralama: Önce En Eskiler",
"Search Gallery (Filename or Prompt)": "Galeride Ara (Dosya Adı veya Prompt)",
"Search filenames or prompt metadata...": "Dosya adlarını veya prompt meta verilerini ara..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "Gereksiz Öğeleri Gizle",
"Default": "Varsayılan",
"Dock": "Sabitle",
"Hide": "Gizle"
"Hide": "Gizle",
"Enable Output Gallery Sidebar": "Çıktı Galerisi Kenar Çubuğunu Etkinleştir"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "在隊列中忽略選取節點",
"Renumber All Nodes (Execution Order)": "重新編號所有節點 (按執行順序)",
"Set Selected Node ID...": "設定選取節點 ID...",
"Set Node ID...": "設定節點 ID..."
"Set Node ID...": "設定節點 ID...",
"Gallery": "圖庫",
"Gallery (Outputs)": "圖庫 (輸出)",
"Go Up One Level": "返回上一層",
"Open Current Folder in File Manager": "在檔案管理器中開啟目前資料夾",
"Refresh Gallery": "重新整理圖庫",
"Filter gallery files...": "篩選圖庫檔案...",
"Loading files...": "正在載入檔案...",
"No files found in this folder.": "此資料夾中未找到檔案。",
"Failed to load gallery from disk.": "從磁碟載入圖庫失敗。",
"Sort: Newest First": "排序:最新優先",
"Sort: Oldest First": "排序:最舊優先",
"Search Gallery (Filename or Prompt)": "搜尋圖庫 (檔案名稱或提示詞)",
"Search filenames or prompt metadata...": "搜尋檔案名稱或提示詞元資料..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "隱藏雜項",
"Default": "預設",
"Dock": "停靠",
"Hide": "隱藏"
"Hide": "隱藏",
"Enable Output Gallery Sidebar": "啟用輸出圖庫側邊欄"
}
+14 -1
View File
@@ -101,5 +101,18 @@
"Toggle Ignore Selected Node(s) in Queue": "在队列中忽略所选节点",
"Renumber All Nodes (Execution Order)": "重新编号所有节点 (按执行顺序)",
"Set Selected Node ID...": "设置所选节点 ID...",
"Set Node ID...": "设置节点 ID..."
"Set Node ID...": "设置节点 ID...",
"Gallery": "图库",
"Gallery (Outputs)": "图库 (输出)",
"Go Up One Level": "返回上一级",
"Open Current Folder in File Manager": "在文件管理器中打开当前文件夹",
"Refresh Gallery": "刷新图库",
"Filter gallery files...": "筛选图库文件...",
"Loading files...": "正在加载文件...",
"No files found in this folder.": "此文件夹中未找到文件。",
"Failed to load gallery from disk.": "从磁盘加载图库失败。",
"Sort: Newest First": "排序:最新优先",
"Sort: Oldest First": "排序:最早优先",
"Search Gallery (Filename or Prompt)": "搜索图库 (文件名或提示词)",
"Search filenames or prompt metadata...": "搜索文件名或提示词元数据..."
}
+2 -1
View File
@@ -28,5 +28,6 @@
"Hide Junk": "隐藏杂项",
"Default": "默认",
"Dock": "停靠",
"Hide": "隐藏"
"Hide": "隐藏",
"Enable Output Gallery Sidebar": "启用输出图库侧边栏"
}
+93 -3
View File
@@ -3,12 +3,13 @@ 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 { store } from "./core/store.js";
@@ -46,6 +47,32 @@ function registerSettings() {
defaultValue: false
});
const setGalleryButtonVisible = (visible) => {
try {
const buttons = document.querySelectorAll(".side-bar-button, [data-tab-id]");
for (const btn of buttons) {
if (btn.getAttribute("data-tab-id") === "classic-comfy-gallery" ||
btn.getAttribute("title")?.includes("Gallery") ||
btn.querySelector(".pi-folder")) {
btn.style.display = visible ? "" : "none";
break;
}
}
} catch (e) {
console.warn("[ComfySidebar] Error toggling gallery tab button:", e);
}
};
app.ui.settings.addSetting({
id: "Comfy Sidebar.Enable Gallery",
name: t("Enable Output Gallery Sidebar"),
type: "boolean",
defaultValue: true,
onChange: (val) => {
setGalleryButtonVisible(val ?? true);
}
});
app.ui.settings.addSetting({
id: "Comfy Sidebar.Permanent Delete on Disk Deletion",
name: t("Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)"),
@@ -75,7 +102,11 @@ function registerSettings() {
if (isInitialized && app.ui?.settings) {
syncStockHistoryAndProgressSettings(value);
}
setTimeout(() => { syncClassicLayout(); }, 0);
applySidebarOverride();
setTimeout(() => {
applySidebarOverride();
syncClassicLayout();
}, 50);
}
});
@@ -442,13 +473,67 @@ app.registerExtension({
syncAllNodeBadges();
let queueScrollPos = 0;
let galleryScrollPos = 0;
let activeSidebarTab = "queue";
const getRealScrollEl = (childEl) => {
if (!childEl) return null;
return childEl.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| childEl.parentElement
|| childEl;
};
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 = getRealScrollEl(el);
if (activeSidebarTab === "gallery" && scrollParent) {
galleryScrollPos = scrollParent.scrollTop;
}
activeSidebarTab = "queue";
el.replaceChildren(sidebarContainer);
requestAnimationFrame(() => {
const sp = getRealScrollEl(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 = getRealScrollEl(el);
if (activeSidebarTab === "queue" && scrollParent) {
queueScrollPos = scrollParent.scrollTop;
}
activeSidebarTab = "gallery";
el.replaceChildren(galleryContainer);
requestAnimationFrame(() => {
const sp = getRealScrollEl(el);
if (sp) sp.scrollTop = galleryScrollPos;
});
}
});
const isGalleryEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Enable Gallery") ?? true;
if (!isGalleryEnabled) {
setTimeout(() => setGalleryButtonVisible(false), 50);
}
requestAnimationFrame(() => {
applySidebarOverride();
});
},
@@ -458,6 +543,11 @@ app.registerExtension({
}
destroyLayoutFix();
teardownSidebarView();
teardownGalleryView();
if (app.extensionManager?.unregisterSidebarTab) {
app.extensionManager.unregisterSidebarTab("classic-comfy-gallery");
}
if (activeKeydownHandler) {
document.removeEventListener("keydown", activeKeydownHandler, true);
+34 -32
View File
@@ -5,7 +5,7 @@ import { store } from "../core/store.js";
import { renderCardImages } from "./mediaPreview.js";
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage, removeImageFromNodeOutputs } from "./mediaActions.js";
import { centerAndSelectCanvasNode } from "../comfy/adapter.js";
import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/utils.js";
import { getRunOutputs, extractWorkflowFromPng, isImageFormat, animateCardRemoval } from "../utils/utils.js";
import { showFullscreenPreview } from "../utils/comparison.js";
import { stopAllAudioPlayback } from "./audioController.js";
import { deleteHistoryItem, cancelPendingTask, interruptActive } from "../queue/queueService.js";
@@ -528,44 +528,46 @@ export function getOrCreateCard(state, callbacks = {}) {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
resetDeleteBtn();
const liveState = store.getPrompt(state.pid) || state;
animateCardRemoval(card, async () => {
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);
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);
}
}
};
(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;
}
}
};
(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);
});
}
};
+732
View File
@@ -0,0 +1,732 @@
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 } from "../utils/utils.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 getScrollContainer() {
if (!cardStackEl) return null;
return cardStackEl.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| cardStackEl.parentElement
|| cardStackEl;
}
function updateScrollTopVisibility() {
if (!galleryContainerEl || !galleryContainerEl.isConnected) {
if (scrollToTopBtnEl) scrollToTopBtnEl.style.display = "none";
return;
}
const scrollEl = getScrollContainer();
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;
}
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_${file.filename.replace(/\.[^/.]+$/, "")}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1500);
};
}
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del comfy-sidebar-gallery-btn-del";
btnDel.title = t("Delete file (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 file (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 file from DISK / TRASH")
: t("Click again to confirm deleting file");
deleteTimeout = setTimeout(resetDelete, 2000);
} else {
resetDelete();
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(--p-primary-color, #0ea5e9);"></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 = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
const containerWidth = galleryContainerEl.clientWidth || 0;
if (containerWidth > 0) {
currentColumnCount = Math.max(1, Math.floor(containerWidth / (threshold / 2)));
}
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();
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(--comfy-input-bg, #181818)",
border: "1px solid 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();
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();
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();
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
container.addEventListener("pointermove", (e) => {
const card = e.target.closest(".comfy-sidebar-card, .comfy-sidebar-folder-card");
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 });
document.addEventListener("scroll", updateScrollTopVisibility, { 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)));
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;
}
+79 -60
View File
@@ -5,7 +5,7 @@ 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 } from "../utils/utils.js";
import { renderCardImages } from "./mediaPreview.js";
import { deleteFileOnServer, removeImageFromNodeOutputs, copyImageToClipboard, findNodeIdForImage } from "./mediaActions.js";
import { toggleIgnoreActiveNode } from "../comfy/nodes.js";
@@ -98,7 +98,11 @@ function getScrollContainer() {
}
function updateScrollTopBtnVisibility() {
if (!scrollToTopBtnEl || !store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) return;
if (!scrollToTopBtnEl) return;
if (!store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) {
scrollToTopBtnEl.style.display = "none";
return;
}
const scrollEl = getScrollContainer();
if (scrollEl) {
const viewportEl = scrollEl.parentElement || store.ui.sidebarContainer;
@@ -162,7 +166,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");
@@ -352,6 +357,16 @@ export function setupSidebarView() {
container.appendChild(scrollToTopBtnEl);
// Reveal Spotlight: track cursor position over Queue card borders
container.addEventListener("pointermove", (e) => {
const card = e.target.closest(".comfy-sidebar-card");
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 });
document.addEventListener("scroll", updateScrollTopBtnVisibility, { capture: true, passive: true });
new ResizeObserver((entries) => {
@@ -512,46 +527,48 @@ export function renderSidebar() {
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"));
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();
});
}
};
@@ -783,35 +800,37 @@ export function renderSidebar() {
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; }
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();
});
}
};
+130 -72
View File
@@ -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);
+200 -5
View File
@@ -4,17 +4,69 @@ export function injectStyles() {
style.id = "comfy-sidebar-styles";
style.textContent = `
.comfy-sidebar-card {
background: var(--comfy-input-bg, #181818);
background: var(--comfy-input-bg, var(--p-surface-800, rgba(128, 128, 128, 0.08)));
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);
border: 2px solid var(--border-color, rgba(128, 128, 128, 0.25));
color: var(--comfy-input-color, var(--fg-color, #eee));
overflow: hidden;
}
/* Subtle neutral border hover (No blue clash with running jobs) */
.comfy-sidebar-card:hover {
border-color: var(--p-primary-color, var(--primary-color, #555)) !important;
border-color: color-mix(in srgb, var(--border-color, #555) 50%, var(--fg-color, #fff) 50%) !important;
background: color-mix(in srgb, var(--comfy-input-bg, var(--p-surface-800, rgba(128, 128, 128, 0.1))) 85%, var(--fg-color, #888) 15%) !important;
transition: border-color 0.2s, background-color 0.2s;
}
/* Border & Frame Spotlight (Renders BEHIND the image so PNG is never washed out) */
.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(--p-primary-color, #38bdf8) 30%, var(--fg-color, #fff) 70%),
transparent 75%
);
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.35;
}
/* Windows 10 Reveal Spotlight Effect (UI Folder Buttons Only) */
.comfy-sidebar-folder-card {
position: relative;
overflow: hidden;
}
.comfy-sidebar-folder-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(
140px circle at var(--mouse-x, -999px) var(--mouse-y, -999px),
rgba(255, 255, 255, 0.16),
transparent 75%
);
opacity: 0;
transition: opacity 0.25s ease;
pointer-events: none;
z-index: 3;
}
.comfy-sidebar-folder-card:hover::before {
opacity: 1;
}
.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; }
@@ -555,9 +607,10 @@ export function injectStyles() {
z-index: 1005 !important;
}
/* Explicit card element layering so badges and [X] are ALWAYS on top of images */
/* Media wrapper sits on z-index 2 so the spotlight shines on the border behind it */
.comfy-sidebar-card .comfy-sidebar-media-wrapper {
z-index: 1 !important;
z-index: 2 !important;
position: relative;
}
.comfy-sidebar-card-timer {
z-index: 10 !important;
@@ -584,6 +637,148 @@ 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(--p-content-background, var(--bg-color, var(--comfy-menu-bg, #181818))) 65%, transparent);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
}
/* Gallery Subfolders & Breadcrumb Styles */
.comfy-sidebar-breadcrumb-bar {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--desc-color, #aaa);
overflow-x: auto;
white-space: nowrap;
padding: 4px 0 8px 0;
border-bottom: 1px solid 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: rgba(255, 255, 255, 0.04);
transition: all 0.15s ease-in-out;
color: var(--fg-color, #eee);
text-decoration: none;
}
.comfy-sidebar-breadcrumb-item:hover {
background: var(--p-primary-color, rgba(59, 130, 246, 0.2));
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 {
background: var(--comfy-input-bg, var(--p-surface-800, rgba(128, 128, 128, 0.08)));
border-radius: 4px;
padding: 8px 10px;
border: 1px solid var(--border-color, rgba(128, 128, 128, 0.25));
color: var(--comfy-input-color, var(--fg-color, #eee));
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: border-color 0.15s ease, background 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-color, #555) 50%, var(--fg-color, #fff) 50%);
background: color-mix(in srgb, var(--comfy-input-bg, var(--p-surface-800, rgba(128, 128, 128, 0.1))) 85%, var(--fg-color, #888) 15%);
}
.comfy-sidebar-folder-icon {
font-size: 16px;
color: var(--p-primary-color, #0ea5e9);
flex-shrink: 0;
}
body.comfy-sidebar-ctrl-active .comfy-sidebar-folder-card:hover {
cursor: alias !important;
border-color: var(--p-primary-color, #0ea5e9) !important;
box-shadow: 0 0 0 1px var(--p-primary-color, #0ea5e9), 0 0 10px rgba(14, 165, 233, 0.3) !important;
background: rgba(14, 165, 233, 0.15) !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); }
+13
View File
@@ -431,4 +431,17 @@ 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);
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfy-sidebar"
description = "A compact and powerful layout overhaul for ComfyUI that makes generation results easier to browse, preview, compare, and manage."
version = "1.7.3"
version = "1.8.1"
license = { file = "LICENSE" }
dependencies = [
"send2trash>=1.8.0",