diff --git a/js/layout.js b/js/layout.js new file mode 100644 index 0000000..762cca5 --- /dev/null +++ b/js/layout.js @@ -0,0 +1,349 @@ +// layout.js - Variant 1 (Media Queries) with Properties Panel Toggle Fix, Indicator Filter & Sidebar Visibility +import { app } from "/scripts/app.js"; + +const STYLE_ID = "comfy-sidebar-classic-layout-override"; + +const CLASSIC_LAYOUT_CSS_MEDIA = ` +/* Style and align the custom plugins bar (Manager, LoRA, etc.) */ +[class*="actionbar"]:not(.actionbar), +[class*="actionbar-buttons"], +.actionbar-buttons { + position: fixed !important; + top: -5px !important; + right: 4px !important; + left: auto !important; + transform: none !important; + z-index: 1010 !important; + + /* Nuke the background rectangle, border, and shadows */ + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; + margin: 0 !important; + display: flex !important; + align-items: center !important; +} + +/* Base rules: Large/Wide desktop screens (>= 1600px width) */ +.p-tabview-nav-content, +[class*="tabview"] { + padding-left: 150px !important; + padding-right: 580px !important; +} + +/* Medium desktop screens (between 1200px and 1599px width) */ +@media (max-width: 1599px) { + .p-tabview-nav-content, + [class*="tabview"] { + padding-left: 100px !important; + padding-right: 420px !important; + } +} + +/* Standard laptops / small monitors (between 900px and 1199px width) */ +@media (max-width: 1199px) { + .p-tabview-nav-content, + [class*="tabview"] { + padding-left: 40px !important; + padding-right: 280px !important; + } +} + +/* Very narrow or heavily zoomed displays (< 900px width) */ +@media (max-width: 899px) { + .p-tabview-nav-content, + [class*="tabview"] { + padding-left: 10px !important; + padding-right: 180px !important; + } +} +`; + +export function applyClassicLayout(enable) { + let styleEl = document.getElementById(STYLE_ID); + + if (enable) { + if (app.ui && app.ui.settings) { + app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar"); + } + + if (!styleEl) { + styleEl = document.createElement("style"); + styleEl.id = STYLE_ID; + document.head.appendChild(styleEl); + } + styleEl.textContent = CLASSIC_LAYOUT_CSS_MEDIA; + + } else { + if (styleEl) styleEl.remove(); + + if (app.ui && app.ui.settings) { + app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar"); + } + } +} + +/* --- Properties Panel Toggle Inline Correction --- */ + +let savedButtonData = null; + +function findOriginalButton() { + const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]'); + if (icon) { + const btn = icon.closest('button, [role="button"], .comfyui-menu-item, .p-button'); + if (btn && !btn.classList.contains("comfy-sidebar-custom-properties-toggle")) { + return btn; + } + } + return null; +} + +function findTopbarContainer() { + const container = document.querySelector('[class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]'); + if (container) return container; + + const orig = findOriginalButton(); + if (orig && orig.parentNode) return orig.parentNode; + + const buttons = document.querySelectorAll("button, .p-button"); + for (const btn of buttons) { + if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue; + const title = (btn.getAttribute("title") || btn.getAttribute("aria-label") || "").toLowerCase(); + if (title.includes("run") || title.includes("queue") || title.includes("manager")) { + if (btn.parentNode) return btn.parentNode; + } + } + return null; +} + +function isPropertiesPanelOpen() { + const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]'); + return !!(icon && icon.className.includes("lucide--panel-left")); +} + +function findActiveQueueIndicator() { + const container = findTopbarContainer(); + if (!container) return null; + + const buttons = container.querySelectorAll("button, .p-button, .comfyui-menu-item"); + for (const btn of buttons) { + if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue; + + const text = (btn.textContent || "").toLowerCase(); + if (text.includes("active") || text.includes("queued") || text.includes("running") || text.includes("pending")) { + return btn; + } + } + return null; +} + +function updateSidebarTabsVisibility() { + const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]'); + if (!sidebar) return; + + // Direct mapping from tab settings to their unique class-based HTML icon tags + const tabIconSelectors = { + "Assets": '[class*="comfy--image-ai-edit"]', + "Nodes": '[class*="comfy--node"]', + "Models": '[class*="comfy--ai-model"]', + "Workflows": '[class*="comfy--workflow"]', + "Apps": '[class*="lucide--panels-top-left"]', + "NodesMap": '[class*="pi-sitemap"]', + "Templates": '[class*="comfy--template"]' + }; + + Object.entries(tabIconSelectors).forEach(([tab, selector]) => { + const shouldHide = app.ui?.settings?.getSettingValue(`Comfy Sidebar.Hide Sidebar Tabs.${tab}`) ?? false; + + // Find the specific icon element inside the sidebar + const icon = sidebar.querySelector(selector); + if (icon) { + // Find its closest parent button or tab item wrapper + const tabBtn = icon.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || icon; + + if (shouldHide) { + if (tabBtn.style.display !== "none") { + tabBtn.style.setProperty("display", "none", "important"); + } + } else { + if (tabBtn.style.display === "none") { + tabBtn.style.removeProperty("display"); + } + } + } + }); + + // 4. Handle "Override Stock Job History Tab" using its exact icon selector + const hideStockHistory = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Sidebar Tabs.Override Stock Job History Tab") ?? false; + const historyIcon = sidebar.querySelector('[class*="lucide--history"]'); + if (historyIcon) { + const historyBtn = historyIcon.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || historyIcon; + if (hideStockHistory) { + if (historyBtn.style.display !== "none") { + historyBtn.style.setProperty("display", "none", "important"); + } + } else { + if (historyBtn.style.display === "none") { + historyBtn.style.removeProperty("display"); + } + } + } +} + +export function setupPropertiesPanelToggleFix() { + const style = document.createElement("style"); + style.textContent = ` + /* Hide native button completely */ + .comfy-sidebar-hide-original-properties-btn { + display: none !important; + } + + /* Spacing when the properties panel is CLOSED */ + .comfy-sidebar-custom-properties-toggle { + margin-left: -8px !important; + margin-right: 0px !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + padding: 0 !important; /* Forces perfect centering of the icon */ + } + + /* Spacing when the properties panel is OPEN */ + .comfy-sidebar-custom-properties-toggle.comfy-panel-open { + margin-left: 0px !important; + margin-right: 0px !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + padding: 0 !important; /* Forces perfect centering of the icon */ + } + `; + document.head.appendChild(style); + + function frameLoop() { + const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Classic Layout") ?? false; + + // If Classic Layout is disabled, cleanly restore stock elements and exit early + if (!isClassicLayoutEnabled) { + const originalBtn = findOriginalButton(); + if (originalBtn && originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) { + originalBtn.classList.remove("comfy-sidebar-hide-original-properties-btn"); + } + + const customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle"); + if (customBtn) { + customBtn.remove(); + } + + const indicator = findActiveQueueIndicator(); + if (indicator && indicator.style.display === "none") { + indicator.style.removeProperty("display"); + } + + updateSidebarTabsVisibility(); + + requestAnimationFrame(frameLoop); + return; + } + + // --- Active Classic Layout Toggle Correction --- + const originalBtn = findOriginalButton(); + const container = findTopbarContainer(); + const openState = isPropertiesPanelOpen(); + + // 1. Capture original template data when native button is alive + 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, + tagName: originalBtn.tagName, + attributes: Array.from(originalBtn.attributes).map(attr => ({ + name: attr.name, + value: attr.value + })) + }; + } + + let customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle"); + + // 2. Build and insert our custom button inside topbar + if (!customBtn && savedButtonData && container) { + customBtn = document.createElement(savedButtonData.tagName); + + customBtn.className = savedButtonData.className + " comfy-sidebar-custom-properties-toggle"; + customBtn.innerHTML = savedButtonData.innerHTML; + + for (const attr of savedButtonData.attributes) { + if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") { + customBtn.setAttribute(attr.name, attr.value); + } + } + + // Standard inline rendering + Object.assign(customBtn.style, { + position: "relative", + zIndex: "1000", + margin: "0", + boxSizing: "border-box", + opacity: "1", + pointerEvents: "auto", + display: "inline-flex" + }); + + customBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + + const nativeBtn = findOriginalButton(); + if (nativeBtn) nativeBtn.click(); + }); + + container.appendChild(customBtn); + } else if (customBtn && container && container.lastChild !== customBtn) { + container.appendChild(customBtn); + } + + // 3. Keep the state-specific CSS classes synchronized + if (customBtn) { + if (openState) { + if (!customBtn.classList.contains("comfy-panel-open")) { + customBtn.classList.add("comfy-panel-open"); + } + } else { + if (customBtn.classList.contains("comfy-panel-open")) { + customBtn.classList.remove("comfy-panel-open"); + } + } + customBtn.style.display = "inline-flex"; + } + + // 4. Handle "0 active" indicator visibility based on user settings (Sub-grouped Path) + const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Sidebar Tabs.Override Stock Job History Tab") ?? false; + const indicator = findActiveQueueIndicator(); + if (indicator) { + if (hideQueueIndicator) { + if (indicator.style.display !== "none") { + indicator.style.setProperty("display", "none", "important"); + } + } else { + if (indicator.style.display === "none") { + indicator.style.removeProperty("display"); + } + } + } + + // 5. Update Sidebar tab elements visibility + updateSidebarTabsVisibility(); + + // Run checking loop continuously synced with screen refresh cycles + requestAnimationFrame(frameLoop); + } + + // Start loop synced with the screen rendering rate + requestAnimationFrame(frameLoop); +} \ No newline at end of file diff --git a/js/sidebar.js b/js/sidebar.js index 7773c5e..fcd6605 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -1,8 +1,10 @@ +// sidebar.js import { app } from "/scripts/app.js"; import { injectStyles } from "./styles.js"; import { setupDragAndDrop } from "./dragdrop.js"; import { setupSidebarUI, applySidebarOverride, findOurSidebarButton, setSyncQueue, updateSidebarBadge, renderDOM } from "./ui.js"; import { setupApiListeners, initSessionAndHistory, syncQueue, setUIDependencies } from "./queue.js"; +import { applyClassicLayout, setupPropertiesPanelToggleFix } from "./layout.js"; // Import properties panel fix app.registerExtension({ name: "ComfySidebar.ClassicRestore", @@ -12,15 +14,51 @@ app.registerExtension({ app.ui.settings.addSetting({ id: "Comfy Sidebar.Keep Object Aspect Ratio", name: "If disabled, cards in the queue will be cropped to the same size.", type: "boolean", defaultValue: true }); app.ui.settings.addSetting({ id: "Comfy Sidebar.Show Pending Count Only", name: "If disabled, each queued job will have a separate individual card", type: "boolean", defaultValue: true }); app.ui.settings.addSetting({ id: "Comfy Sidebar.Show Working Node Name", name: "Shows the name of the node which is currently in the process", type: "boolean", defaultValue: true }); - app.ui.settings.addSetting({ id: "Comfy Sidebar.Override Stock Job History Tab", name: "Replaces the stock Job History sidebar with Comfy Queue", type: "boolean", defaultValue: false }); // Settings toggle for Auto-clear Interrupted runs app.ui.settings.addSetting({ id: "Comfy Sidebar.Auto Clear Interrupted", name: "Auto-clear cancelled & failed jobs on new generation", type: "boolean", defaultValue: false }); + + // Add settings to hide individual sidebar tabs under a shared sub-category group + const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "NodesMap", "Templates"]; + sidebarTabs.forEach(tab => { + app.ui.settings.addSetting({ + id: `Comfy Sidebar.Hide Sidebar Tabs.${tab}`, + name: `Hide Tab: ${tab}`, + type: "boolean", + defaultValue: false + }); + }); + + // Group the Stock History Tab override into the same sub-category group + app.ui.settings.addSetting({ + id: "Comfy Sidebar.Hide Sidebar Tabs.Override Stock Job History Tab", + name: "Replaces the stock Job History sidebar with Comfy Queue", + type: "boolean", + defaultValue: false + }); + + // Add the toggle setting for Classic / Comfy Layout + app.ui.settings.addSetting({ + id: "Comfy Sidebar.Comfy Layout", + name: "Places the controls and the open workflow tabs on a single unified top bar", + type: "boolean", + defaultValue: false, + onChange: (value) => { + applyClassicLayout(value); + } + }); }, async setup() { if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return; + // Apply saved layout configuration on startup + const isClassicLayoutEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false; + applyClassicLayout(isClassicLayoutEnabled); + + // Run properties panel toggle correction + setupPropertiesPanelToggleFix(); + // Safely resolve circular dependencies using Dependency Injection Hooks setSyncQueue(syncQueue); setUIDependencies(renderDOM, updateSidebarBadge); diff --git a/js/styles.js b/js/styles.js index ef0d60c..a568354 100644 --- a/js/styles.js +++ b/js/styles.js @@ -1,3 +1,4 @@ +// styles.js export function injectStyles() { const style = document.createElement("style"); style.textContent = ` @@ -16,14 +17,42 @@ export function injectStyles() { .comfy-sidebar-card.cancelled { --border-color: #ffc107; --hover-color: #ffe082; } .comfy-sidebar-card.error { --border-color: #dc3545; --hover-color: #f87171; } - .comfy-sidebar-card.pending .pi-times { display: none !important; } - .comfy-sidebar-card.pending:hover .pi-times { display: flex !important; } + .comfy-sidebar-card.pending .comfy-sidebar-queue-cancel-btn, + .comfy-sidebar-card.active .comfy-sidebar-queue-cancel-btn { + display: none !important; + } + + .comfy-sidebar-card.pending:hover .comfy-sidebar-queue-cancel-btn, + .comfy-sidebar-card.active:hover .comfy-sidebar-queue-cancel-btn { + display: flex !important; + } .comfy-sidebar-card-timer { position: absolute; top: 6px; left: 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; } + + /* --- Custom Queue Cancel Button to Mimic ComfyUI Native Destructive Button --- */ + .comfy-sidebar-queue-cancel-btn { + display: none; + align-items: center !important; + justify-content: center !important; + width: 32px !important; + height: 32px !important; + background-color: #7f1d1d !important; /* Native deep red */ + color: #e2e8f0 !important; /* Native slate white icon */ + font-size: 11px !important; /* Muted native cross size */ + border-radius: 6px !important; /* Native corner rounding */ + cursor: pointer !important; + transition: background-color 0.15s ease, color 0.15s ease !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important; + } + + .comfy-sidebar-queue-cancel-btn:hover { + background-color: #991b1b !important; /* Slightly brighter red on hover */ + color: #ffffff !important; + } `; document.head.appendChild(style); } \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index b7dc0d9..ca20aad 100644 --- a/js/ui.js +++ b/js/ui.js @@ -85,7 +85,7 @@ export const findStandardQueueButton = () => { }; export const applySidebarOverride = () => { - const overrideStock = app.ui.settings.getSettingValue("Comfy Sidebar.Override Stock Job History Tab") ?? false; + const overrideStock = app.ui.settings.getSettingValue("Comfy Sidebar.Hide Sidebar Tabs.Override Stock Job History Tab") ?? false; const stdBtn = findStandardQueueButton(); const ourBtn = findOurSidebarButton(); @@ -282,8 +282,15 @@ export function renderDOM() { if (!cardObj) { const card = document.createElement("div"); const timerEl = document.createElement("div"); timerEl.className = "comfy-sidebar-card-timer"; - const cancelX = document.createElement("span"); cancelX.className = "pi pi-times"; - Object.assign(cancelX.style, { position: "absolute", top: "4px", right: "4px", color: "#dc3545", cursor: "pointer", fontSize: "20px", display: "none", zIndex: "10", background: "rgba(0,0,0,0.85)", padding: "8px 12px", borderRadius: "4px", transition: "color 0.2s" }); + const cancelX = document.createElement("span"); + cancelX.className = "pi pi-times comfy-sidebar-queue-cancel-btn"; + Object.assign(cancelX.style, { + position: "absolute", + top: "4px", + right: "4px", + display: "none", + zIndex: "10" + }); const sBadge = document.createElement("div"); Object.assign(sBadge.style, { position: "absolute", top: "6px", right: "8px", fontSize: "9px", fontWeight: "bold", padding: "2px 6px", borderRadius: "2px", textTransform: "uppercase", display: "none", pointerEvents: "none", zIndex: "10" }); const grid = document.createElement("div"); grid.style.display = "flex"; grid.style.flexDirection = "column"; grid.style.gap = "6px"; @@ -300,7 +307,16 @@ export function renderDOM() { const btnImg = document.createElement("span"); btnImg.className = "pi pi-image"; Object.assign(btnImg.style, { cursor: "pointer", fontSize: "20px", color: "#aaa" }); const btnJson = document.createElement("span"); btnJson.className = "pi pi-file"; Object.assign(btnJson.style, { cursor: "pointer", fontSize: "20px", color: "#aaa" }); - const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash"; Object.assign(btnDel.style, { cursor: "pointer", fontSize: "20px", color: "#dc3545", transition: "all 0.1s ease-in-out" }); + const btnDel = document.createElement("span"); + btnDel.className = "pi pi-trash"; + Object.assign(btnDel.style, { + cursor: "pointer", + fontSize: "20px", + color: "#dc3545", + transition: "all 0.1s ease-in-out", + padding: "2px", + transform: "scale(1)" + }); hoverPanel.append(btnImg, btnJson, btnDel); card.append(timerEl, cancelX, sBadge, grid, p, pt, statusText, hoverPanel); @@ -344,8 +360,21 @@ export function renderDOM() { if (state.status === "pending" && !showPendingSummary) { cardObj.cancelBtn.style.display = "flex"; - cardObj.cancelBtn.onclick = async (ev) => { ev.stopPropagation(); await api.fetchApi("/queue", { method: "POST", body: JSON.stringify({ delete: [state.pid] }) }); await syncQueueFn(); }; - } else cardObj.cancelBtn.style.display = "none"; + cardObj.cancelBtn.onclick = async (ev) => { + ev.stopPropagation(); + await api.fetchApi("/queue", { method: "POST", body: JSON.stringify({ delete: [state.pid] }) }); + await syncQueueFn(); + }; + } else if (state.status === "active") { + cardObj.cancelBtn.style.display = "flex"; + cardObj.cancelBtn.onclick = async (ev) => { + ev.stopPropagation(); + await api.interrupt(); // Natively interrupts current execution + await syncQueueFn(); + }; + } else { + cardObj.cancelBtn.style.display = "none"; + } if (state.images && state.images.length > 0) { cardObj.btnImg.style.display = "inline"; @@ -358,14 +387,36 @@ export function renderDOM() { } else cardObj.btnJson.style.display = "none"; let deleteTimeout = null, isDeletePending = false; - const resetDeleteBtn = () => { isDeletePending = false; Object.assign(cardObj.btnDel.style, { color: "#dc3545", background: "none", padding: "0" }); cardObj.btnDel.title = "Delete Element from History"; if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; } }; + const resetDeleteBtn = () => { + isDeletePending = false; + Object.assign(cardObj.btnDel.style, { + color: "#dc3545", + background: "none", + padding: "2px", + transform: "scale(1)" + }); + cardObj.btnDel.title = "Delete Element from History"; + if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; } + }; + cardObj.btnDel.onclick = async (ev) => { ev.stopPropagation(); if (!isDeletePending) { - isDeletePending = true; Object.assign(cardObj.btnDel.style, { color: "#fff", background: "#dc3545", borderRadius: "4px", padding: "2px" }); cardObj.btnDel.title = "Click again to confirm deletion"; + isDeletePending = true; + Object.assign(cardObj.btnDel.style, { + color: "#fff", + background: "#dc3545", + borderRadius: "4px", + padding: "2px", + transform: "scale(1.2)" + }); + cardObj.btnDel.title = "Click again to confirm deletion"; deleteTimeout = setTimeout(resetDeleteBtn, 1000); } else { - resetDeleteBtn(); promptStates.delete(state.pid); await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ delete: [state.pid] }) }); renderDOM(); + resetDeleteBtn(); + promptStates.delete(state.pid); + await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ delete: [state.pid] }) }); + renderDOM(); } };