From 9797d1ac7ab0e03665bb2c8d454bc8b78a1f6c64 Mon Sep 17 00:00:00 2001 From: m0rtus59 Date: Mon, 10 Aug 2026 22:00:17 +0500 Subject: [PATCH] refactor: align layout, settings, and node hooks with native ComfyUI frontend API --- __init__.py | 14 +----- js/layout.js | 67 +++++++++++------------- js/queue.js | 6 +++ js/sidebar.js | 137 ++++++++++++++++++++++++++++++++++++++++---------- js/styles.js | 57 +++++++++++++++++++++ js/ui.js | 63 ++++++----------------- 6 files changed, 221 insertions(+), 123 deletions(-) diff --git a/__init__.py b/__init__.py index 65213d5..f5f3245 100644 --- a/__init__.py +++ b/__init__.py @@ -1,20 +1,8 @@ # Standard ComfyUI extension loader -from server import PromptServer -from aiohttp import web -import uuid - NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} # Tells ComfyUI to serve and load the contents of the js/ folder WEB_DIRECTORY = "./js" -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] - -# Create a unique session ID for this backend boot to track server restarts -server_session_id = str(uuid.uuid4()) - -# Register a custom route to check for backend restarts -@PromptServer.instance.routes.get("/classic-sidebar/session") -async def get_session_id(request): - return web.json_response({"session_id": server_session_id}) \ No newline at end of file +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/js/layout.js b/js/layout.js index b20d1e7..2c7140a 100644 --- a/js/layout.js +++ b/js/layout.js @@ -97,46 +97,26 @@ button:has([data-pc-name="avatar"]), } `; +/** + * Native settings synchronization without direct DOM element querying or simulated clicks. + */ export function syncStockHistoryAndProgressSettings(enable) { - if (!app.ui?.settings) return; - const dockedVal = !!enable; const progressVal = !enable; const QPOV2_ID = "Comfy.Queue.QPOV2"; - - try { - app.ui.settings.setSettingValue(QPOV2_ID, dockedVal); - localStorage.setItem(QPOV2_ID, JSON.stringify(dockedVal)); - } catch (e) {} - - try { - const qpovContainer = document.querySelector('[data-setting-id="Comfy.Queue.QPOV2"]') || - document.getElementById(QPOV2_ID)?.closest('.setting-item, tr, div'); - if (qpovContainer) { - const toggle = qpovContainer.querySelector('input[type="checkbox"], [role="switch"], .p-toggleswitch'); - if (toggle) { - const isChecked = toggle.checked || - toggle.getAttribute('data-p-checked') === 'true' || - toggle.classList.contains('p-toggleswitch-checked'); - if (isChecked !== dockedVal) toggle.click(); - } - } - } catch (e) {} - const progressKey = "Comfy.Queue.ShowRunProgressBar"; - try { - app.ui.settings.setSettingValue(progressKey, progressVal); - localStorage.setItem(progressKey, JSON.stringify(progressVal)); - } catch (e) {} - try { - const progressBarBtn = document.querySelector('[data-testid="show-run-progress-bar-action"]'); - if (progressBarBtn) { - const isChecked = !!progressBarBtn.querySelector('[class*="lucide--check"], .icon-\\[lucide--check\\]'); - if (isChecked !== progressVal) progressBarBtn.click(); - } - } catch (e) {} + // Use official extensionManager setting API if present, with fallback to app.ui.settings + if (app.extensionManager?.setting) { + try { app.extensionManager.setting.set(QPOV2_ID, dockedVal); } catch (e) {} + try { app.extensionManager.setting.set(progressKey, progressVal); } catch (e) {} + } else if (app.ui?.settings) { + try { app.ui.settings.setSettingValue(QPOV2_ID, dockedVal); } catch (e) {} + try { app.ui.settings.setSettingValue(progressKey, progressVal); } catch (e) {} + } + try { localStorage.setItem(QPOV2_ID, JSON.stringify(dockedVal)); } catch (e) {} + try { localStorage.setItem(progressKey, JSON.stringify(progressVal)); } catch (e) {} try { window.dispatchEvent(new Event("storage")); } catch (e) {} } @@ -144,8 +124,12 @@ export function applyClassicLayout(enable, updateSetting = false) { let styleEl = document.getElementById(STYLE_ID); if (enable) { - if (updateSetting && app.ui?.settings) { - app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar"); + if (updateSetting) { + if (app.extensionManager?.setting) { + app.extensionManager.setting.set("Comfy.Workflow.WorkflowTabsPosition", "Topbar"); + } else if (app.ui?.settings) { + app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar"); + } } if (!styleEl) { styleEl = document.createElement("style"); @@ -155,8 +139,12 @@ export function applyClassicLayout(enable, updateSetting = false) { styleEl.textContent = CLASSIC_LAYOUT_CSS_MEDIA; } else { if (styleEl) styleEl.remove(); - if (updateSetting && app.ui?.settings) { - app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar"); + if (updateSetting) { + if (app.extensionManager?.setting) { + app.extensionManager.setting.set("Comfy.Workflow.WorkflowTabsPosition", "Sidebar"); + } else if (app.ui?.settings) { + app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar"); + } } } } @@ -166,6 +154,11 @@ let domObserver = null; let syncScheduled = false; function findOriginalButton() { + const byTestId = document.querySelector('[data-testid="properties-panel-toggle"]'); + if (byTestId && !byTestId.classList.contains("comfy-sidebar-custom-properties-toggle")) { + return byTestId; + } + const icons = document.querySelectorAll('[class*="lucide--panel-right"], [class*="lucide--panel-left"], [class*="lucide--panel-bottom"]'); for (const icon of icons) { const btn = icon.closest('button, [role="button"], .comfyui-menu-item, .p-button'); diff --git a/js/queue.js b/js/queue.js index 39486be..defc1b7 100644 --- a/js/queue.js +++ b/js/queue.js @@ -115,6 +115,12 @@ const concludeRun = async (pid, statusStr) => { export function setupApiListeners() { api.addEventListener("status", syncQueue); + api.addEventListener("reconnected", async () => { + console.log("Comfy Sidebar: Server reconnected, syncing state and history"); + await initSessionAndHistory(); + await syncQueue(); + }); + api.addEventListener("execution_start", (e) => { if (app.ui.settings.getSettingValue("Comfy Sidebar.Auto Clear Interrupted") ?? false) { const toDelete = []; diff --git a/js/sidebar.js b/js/sidebar.js index 1aca9a8..a4f620e 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -6,6 +6,8 @@ import { setupApiListeners, initSessionAndHistory, syncQueue, setUIDependencies import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings } from "./layout.js"; let isInitialized = false; +let activeKeydownHandler = null; +let nodeDOMObserver = null; // Helper to scan node container for the exact text element rendering the title (Vue Mode) function findHeaderByText(parent, text) { @@ -24,7 +26,12 @@ function findHeaderByText(parent, text) { } function syncNodeVueBadge(node, isIgnored) { - const nodeEl = document.querySelector(`.comfy-node[data-node-id="${node.id}"], [data-node-id="${node.id}"]`); + // Robust multi-selector search for Vue node containers across different frontend versions + const nodeEl = document.querySelector( + `.comfy-node[data-node-id="${node.id}"], ` + + `[data-node-id="${node.id}"], ` + + `[data-id="${node.id}"]` + ); if (!nodeEl) return; const titleTextNode = findHeaderByText(nodeEl, node.title || node.type); @@ -92,6 +99,40 @@ export function syncAllNodeBadges() { }); } +function setupVueNodeObserver() { + if (nodeDOMObserver) nodeDOMObserver.disconnect(); + + let timeoutId = null; + nodeDOMObserver = new MutationObserver((mutations) => { + let hasNodeMutation = false; + for (const m of mutations) { + for (const node of m.addedNodes) { + if (node.nodeType === 1) { + if (node.hasAttribute?.("data-node-id") || + node.classList?.contains("comfy-node") || + node.querySelector?.('[data-node-id], .comfy-node')) { + hasNodeMutation = true; + break; + } + } + } + if (hasNodeMutation) break; + } + + if (hasNodeMutation) { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + syncAllNodeBadges(); + }, 50); + } + }); + + nodeDOMObserver.observe(document.body, { + childList: true, + subtree: true + }); +} + function toggleIgnoreActiveNode() { const canvas = app.canvas; if (!canvas) return; @@ -188,6 +229,33 @@ app.registerExtension({ }); }, + commands: [ + { + id: "ComfySidebar.ToggleSidebar", + label: "Toggle Comfy Queue Sidebar", + function: () => { + const ourBtn = findOurSidebarButton(); + if (ourBtn) ourBtn.click(); + } + }, + { + id: "ComfySidebar.ToggleIgnoreNode", + label: "Toggle Ignore Selected Node(s) in Queue", + function: () => toggleIgnoreActiveNode() + } + ], + + nodeCreated(node) { + if (node && node.properties?.ignoreInQueue) { + node.boxcolor = "#ff3333"; + syncNodeVueBadge(node, true); + } + }, + + afterConfigureGraph() { + requestAnimationFrame(() => syncAllNodeBadges()); + }, + async setup() { if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return; @@ -215,41 +283,42 @@ app.registerExtension({ await initSessionAndHistory(); - // Keyboard shortcuts listener - document.addEventListener("keydown", (e) => { + // Watch for Vue DOM node mounting events (e.g. LiteGraph ↔ Vue mode switches) + setupVueNodeObserver(); + + // Remove any existing keydown listener from previous hot-reload + if (activeKeydownHandler) { + document.removeEventListener("keydown", activeKeydownHandler, true); + activeKeydownHandler = null; + } + + // Clean, direct keyboard listener + activeKeydownHandler = (e) => { const activeEl = document.activeElement; - if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable || activeEl.tagName === "SELECT")) return; + if (activeEl && ( + activeEl.tagName === "INPUT" || + activeEl.tagName === "TEXTAREA" || + activeEl.isContentEditable || + activeEl.tagName === "SELECT" + )) return; + // Toggle sidebar with Q if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) { - e.preventDefault(); e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); const ourBtn = findOurSidebarButton(); if (ourBtn) ourBtn.click(); } - if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); e.stopPropagation(); + // Toggle ignore node with Ctrl+Q or Cmd+Q (Mac) + if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { + e.preventDefault(); + e.stopPropagation(); toggleIgnoreActiveNode(); } - }, true); - - // Event hooks for node additions and workflow loading (eliminates continuous polling loops) - if (app.graph) { - const originalConfigure = app.graph.configure; - app.graph.configure = function() { - const res = originalConfigure ? originalConfigure.apply(this, arguments) : undefined; - requestAnimationFrame(() => syncAllNodeBadges()); - return res; - }; + }; - const originalOnNodeAdded = app.graph.onNodeAdded; - app.graph.onNodeAdded = function(node) { - if (originalOnNodeAdded) originalOnNodeAdded.apply(this, arguments); - if (node && node.properties?.ignoreInQueue) { - node.boxcolor = "#ff3333"; - syncNodeVueBadge(node, true); - } - }; - } + document.addEventListener("keydown", activeKeydownHandler, true); // Apply sidebar override when initializing applySidebarOverride(); @@ -262,5 +331,21 @@ app.registerExtension({ type: "custom", render: (el) => { el.appendChild(sidebarContainer); } }); + }, + + destroy() { + if (nodeDOMObserver) { + nodeDOMObserver.disconnect(); + nodeDOMObserver = null; + } + + if (activeKeydownHandler) { + document.removeEventListener("keydown", activeKeydownHandler, true); + activeKeydownHandler = null; + } + + if (app.extensionManager?.unregisterSidebarTab) { + app.extensionManager.unregisterSidebarTab("classic-comfy-sidebar"); + } } }); \ No newline at end of file diff --git a/js/styles.js b/js/styles.js index d8832c8..aa6931c 100644 --- a/js/styles.js +++ b/js/styles.js @@ -97,6 +97,63 @@ export function injectStyles() { color: #ffffff !important; box-shadow: 0 0 8px rgba(220, 53, 69, 0.6) !important; } + + /* Header Icons & Header Action Buttons */ + .comfy-sidebar-header-btn { + background: transparent; + color: var(--desc-color, #aaa); + border: 1px solid var(--border-color, #555); + border-radius: 3px; + padding: 4px 8px; + cursor: pointer; + font-size: 13px; + transition: all 0.15s ease-in-out; + } + + .comfy-sidebar-header-btn:hover { + border-color: var(--fg-color, #eee); + color: var(--fg-color, #eee); + } + + .comfy-sidebar-icon-btn { + cursor: pointer; + font-size: 13px; + opacity: 0.6; + transition: opacity 0.15s ease-in-out; + } + + .comfy-sidebar-icon-btn:hover { + opacity: 1; + } + + /* Scroll To Top Floating Button */ + .comfy-sidebar-scroll-top-btn { + position: absolute; + bottom: 12px; + right: 12px; + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(30, 30, 30, 0.9); + color: #eee; + border: 1px solid var(--border-color, #555); + cursor: pointer; + font-size: 13px; + display: none; + align-items: center; + justify-content: center; + z-index: 999; + box-shadow: 0 4px 12px rgba(0,0,0,0.6); + transition: all 0.15s ease-in-out; + opacity: 0.85; + } + + .comfy-sidebar-scroll-top-btn:hover { + opacity: 1; + background: var(--p-primary-color, #3b82f6); + color: #fff; + border-color: var(--p-primary-color, #3b82f6); + } `; document.head.appendChild(style); } \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index da8b51b..5594fc1 100644 --- a/js/ui.js +++ b/js/ui.js @@ -162,11 +162,8 @@ export function setupSidebarUI() { Object.assign(titleGroup.style, { display: "flex", alignItems: "center", gap: "8px" }); const searchIcon = document.createElement("span"); - searchIcon.className = "pi pi-search"; + searchIcon.className = "pi pi-search comfy-sidebar-icon-btn"; searchIcon.title = "Search History"; - Object.assign(searchIcon.style, { cursor: "pointer", fontSize: "13px", opacity: "0.6", transition: "opacity 0.15s ease-in-out" }); - searchIcon.onmouseenter = () => searchIcon.style.opacity = "1"; - searchIcon.onmouseleave = () => searchIcon.style.opacity = "0.6"; const title = document.createElement("h3"); title.textContent = "Queue"; @@ -181,12 +178,8 @@ export function setupSidebarUI() { const createActionBtn = (iconClass, tooltip, hoverColor, onClickFn) => { const btn = document.createElement("button"); - btn.className = iconClass; + btn.className = `${iconClass} comfy-sidebar-header-btn`; btn.title = tooltip; - Object.assign(btn.style, { - background: "transparent", color: "var(--desc-color, #aaa)", border: "1px solid var(--border-color, #555)", - borderRadius: "3px", padding: "4px 8px", cursor: "pointer", fontSize: "13px", transition: "all 0.15s ease-in-out" - }); let timeout = null, isPending = false; const reset = () => { @@ -195,10 +188,7 @@ export function setupSidebarUI() { if (timeout) { clearTimeout(timeout); timeout = null; } }; - btn.onmouseenter = () => { if (!isPending) { btn.style.borderColor = "var(--fg-color, #eee)"; btn.style.color = "var(--fg-color, #eee)"; } }; - btn.onmouseleave = () => { if (!isPending) { btn.style.borderColor = "var(--border-color, #555)"; btn.style.color = "var(--desc-color, #aaa)"; } }; - - btn.onclick = async (ev) => { + btn.addEventListener("click", async (ev) => { ev.stopPropagation(); if (!isPending) { isPending = true; @@ -208,7 +198,7 @@ export function setupSidebarUI() { reset(); await onClickFn(); } - }; + }); return btn; }; @@ -251,59 +241,38 @@ export function setupSidebarUI() { Object.assign(searchInput, { type: "text", placeholder: "Filter by text, images, nodes..." }); Object.assign(searchInput.style, { flex: "1", background: "transparent", border: "none", outline: "none", color: "var(--comfy-input-color, var(--fg-color, #eee))", fontSize: "11px", padding: "0" }); const clearSearchBtn = document.createElement("span"); - clearSearchBtn.className = "pi pi-times"; + clearSearchBtn.className = "pi pi-times comfy-sidebar-icon-btn"; clearSearchBtn.title = "Clear & Close Search"; - Object.assign(clearSearchBtn.style, { cursor: "pointer", fontSize: "11px", opacity: "0.6", marginLeft: "6px", transition: "opacity 0.15s ease" }); - clearSearchBtn.onmouseenter = () => clearSearchBtn.style.opacity = "1"; clearSearchBtn.onmouseleave = () => clearSearchBtn.style.opacity = "0.6"; + Object.assign(clearSearchBtn.style, { marginLeft: "6px" }); searchContainer.appendChild(searchInputIcon); searchContainer.appendChild(searchInput); searchContainer.appendChild(clearSearchBtn); header.appendChild(standardHeader); header.appendChild(searchContainer); State.sidebarContainer.appendChild(header); - searchIcon.onclick = (e) => { e.stopPropagation(); standardHeader.style.display = "none"; searchContainer.style.display = "flex"; searchInput.focus(); }; + searchIcon.addEventListener("click", (e) => { e.stopPropagation(); standardHeader.style.display = "none"; searchContainer.style.display = "flex"; searchInput.focus(); }); const closeSearch = () => { searchInput.value = ""; State.currentSearchQuery = ""; searchContainer.style.display = "none"; standardHeader.style.display = "flex"; renderDOM(); }; - clearSearchBtn.onclick = (e) => { e.stopPropagation(); closeSearch(); }; - searchInput.onkeydown = (e) => { if (e.key === "Escape") closeSearch(); }; - searchInput.oninput = () => { State.currentSearchQuery = searchInput.value.trim(); renderDOM(); }; + clearSearchBtn.addEventListener("click", (e) => { e.stopPropagation(); closeSearch(); }); + searchInput.addEventListener("keydown", (e) => { if (e.key === "Escape") closeSearch(); }); + searchInput.addEventListener("input", () => { State.currentSearchQuery = searchInput.value.trim(); renderDOM(); }); State.cardStack = document.createElement("div"); Object.assign(State.cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" }); State.sidebarContainer.appendChild(State.cardStack); scrollToTopBtnEl = document.createElement("button"); - scrollToTopBtnEl.className = "pi pi-chevron-up"; + scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn"; scrollToTopBtnEl.title = "Scroll to Top"; - Object.assign(scrollToTopBtnEl.style, { - position: "absolute", bottom: "12px", right: "12px", width: "32px", height: "32px", - borderRadius: "50%", background: "rgba(30, 30, 30, 0.9)", color: "#eee", - border: "1px solid var(--border-color, #555)", cursor: "pointer", fontSize: "13px", - display: "none", alignItems: "center", justifyContent: "center", zIndex: "999", - boxShadow: "0 4px 12px rgba(0,0,0,0.6)", transition: "all 0.15s ease-in-out", opacity: "0.85" - }); - scrollToTopBtnEl.onmouseenter = () => { - scrollToTopBtnEl.style.opacity = "1"; - scrollToTopBtnEl.style.background = "var(--p-primary-color, #3b82f6)"; - scrollToTopBtnEl.style.color = "#fff"; - scrollToTopBtnEl.style.borderColor = "var(--p-primary-color, #3b82f6)"; - }; - scrollToTopBtnEl.onmouseleave = () => { - scrollToTopBtnEl.style.opacity = "0.85"; - scrollToTopBtnEl.style.background = "rgba(30, 30, 30, 0.9)"; - scrollToTopBtnEl.style.color = "#eee"; - scrollToTopBtnEl.style.borderColor = "var(--border-color, #555)"; - }; - - scrollToTopBtnEl.onclick = (e) => { + scrollToTopBtnEl.addEventListener("click", (e) => { e.stopPropagation(); const scrollEl = getScrollContainer(); if (scrollEl) { scrollEl.scrollTo({ top: 0, behavior: "smooth" }); } - }; + }); State.sidebarContainer.appendChild(scrollToTopBtnEl); - State.sidebarContainer.onclick = (e) => { + State.sidebarContainer.addEventListener("click", (e) => { if (State.activeSubmenuPromptId || State.activeSubmenuBatchImages) { if (e.target.closest('img, video, .comfy-sidebar-card-timer, .pi-times, .comfy-sidebar-left-hover-btn, .comfy-sidebar-queue-cancel-btn, button, span')) return; State.activeSubmenuPromptId = null; @@ -313,7 +282,7 @@ export function setupSidebarUI() { document.removeEventListener("click", handleGlobalClick, true); globalClickRegistered = false; } - }; + }); new ResizeObserver((entries) => { const threshold = app.ui.settings.getSettingValue("Comfy Sidebar.Grid Columns Threshold") ?? 350; @@ -471,7 +440,7 @@ function renderCardImages(cardObj, state, keepAspect) { showFullscreenPreview([src], ev.shiftKey); }; - // Auto-prune card if file was deleted by startup temp cleanup script + // Auto-prune card if file was deleted mediaEl.onerror = () => { if (src && !src.startsWith("blob:")) { deletePromptState(state.pid);