Compare commits

...
12 Commits
12 changed files with 662 additions and 327 deletions
+20
View File
@@ -0,0 +1,20 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+5 -2
View File
@@ -13,7 +13,8 @@ A lightweight, high-performance ComfyUI extension that replaces the cluttered st
* **Live Queue Tracking**: Stay informed on your server state. Includes a visual progress bar that tracks generations dynamically and displays the exact node title currently being executed.
* A compact topbar layout displaying both workflow tabs and the control panel on a single unified top bar. See the 'Comfy Layout' toggle in the settings.
* Toggles to hide buttons you don't need from the side panel.
* **[NEW]** Preview compare slider - just click the result image preview in the sidebar, and then click another to load it alongside the first one for pixel by pixel comparison.
* **[NEW]** Preview compare slider - just click the result image preview in the sidebar, and then `Shift+Click` another to load it alongside the first one for pixel by pixel comparison.
* **[NEW]** Prevent selected nodes from showing the output in the queue bar and intermediate results. `Ctrl+Q` to toggle.
<img width="385" height="336" alt="image" src="https://github.com/user-attachments/assets/743fbf85-9281-4561-9dd0-cd9af4d8c0e4" />
@@ -32,8 +33,10 @@ Restart your ComfyUI server and refresh the browser tab.
## ⚙️ Configuration
Additional tweaks are seamlessly integrated into the standard ComfyUI Settings panel. From there, you can adjust thresholds or fully replace the stock V2 panel by enabling options like **"Override Stock Job History Tab"**.
See additional tweaks in ComfyUI settings menu > Comfy Sidebar. From there, you can adjust the layout or fully replace the stock V2 panel by enabling options like **"Comfy Layout"** and **"Replace the stock Job History sidebar with Comfy Queue"**.
## ⌨️ Shortcuts
* **`Q`**: Instantly toggles the sidebar panel open or closed. Safely ignores inputs if you are currently typing inside a text field, input area, or combo box.
* **`Ctrl+Q on a selected node`**: Sets the node to be ignored in the sidebar panel results.
* **`Click`** on a card to enter preview. **`Shift+Click`** on another card in preview to enter compare mode.
+97 -51
View File
@@ -2,8 +2,14 @@ import { State } from "./state.js";
import { isVideoFormat } from "./utils.js";
let activeComparisonViewer = null;
let videoSyncActive = false;
let syncAnimationFrameId = null;
let globalKeydownHandler = null;
// Safe clientX resolver for mouse and touch events
const getClientX = (e) => {
if (e.touches && e.touches.length > 0) return e.touches[0].clientX;
if (e.changedTouches && e.changedTouches.length > 0) return e.changedTouches[0].clientX;
return e.clientX || 0;
};
const createMediaElement = (src, muted = false) => {
const isVideo = isVideoFormat(src);
@@ -13,7 +19,7 @@ const createMediaElement = (src, muted = false) => {
el.playsInline = true;
el.autoplay = true;
el.loop = true;
el.controls = false; // Disable native controls to prevent layout shifts
el.controls = false;
}
Object.assign(el.style, {
gridArea: "1 / 1", maxWidth: "100%", maxHeight: "80vh",
@@ -23,9 +29,12 @@ const createMediaElement = (src, muted = false) => {
return el;
};
// Video Playback Manager: Binds timeline progress bar, timeline scrubbing, and play/pause triggers
const setupVideoPlayback = (vid, container) => {
if (!vid) return;
if (!vid) return () => {};
// Localized animation loop states to prevent cross-instance contamination
let videoSyncActive = true;
let syncAnimationFrameId = null;
const onMetaLoaded = () => {
vid.play().catch(()=>{});
@@ -33,10 +42,9 @@ const setupVideoPlayback = (vid, container) => {
vid.addEventListener("loadedmetadata", onMetaLoaded);
if (vid.readyState >= 1) onMetaLoaded();
// Synced Scrubber Progress Bar Overlay
const controlBar = document.createElement("div");
Object.assign(controlBar.style, {
position: "absolute", bottom: "16px", left: "50%", transform: "translateX(-50%)",
position: "absolute", bottom: "16px", left: "50%",
display: "flex", alignItems: "center", gap: "12px", background: "rgba(10,10,10,0.85)",
padding: "8px 16px", borderRadius: "8px", zIndex: "40", fontSize: "11px",
fontFamily: "monospace", color: "#eee", width: "80%", maxWidth: "500px",
@@ -75,7 +83,7 @@ const setupVideoPlayback = (vid, container) => {
const scrub = (e) => {
const rect = scrubberContainer.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientX = getClientX(e);
const percent = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width));
const duration = vid.duration || 0;
if (duration > 0) {
@@ -98,11 +106,9 @@ const setupVideoPlayback = (vid, container) => {
controlBar.append(playBtn, scrubberContainer, timeLabel);
container.appendChild(controlBar);
videoSyncActive = true;
const syncLoop = () => {
if (!videoSyncActive) return;
// Update progress & labels
const cur = vid.currentTime || 0;
const dur = vid.duration || 0;
if (dur > 0) {
@@ -115,14 +121,14 @@ const setupVideoPlayback = (vid, container) => {
timeLabel.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
}
// Dynamically match play button in case video loops
playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause";
syncAnimationFrameId = requestAnimationFrame(syncLoop);
};
requestAnimationFrame(syncLoop);
return () => {
videoSyncActive = false;
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
window.removeEventListener("mousemove", handleWindowMove);
window.removeEventListener("touchmove", handleWindowMove);
window.removeEventListener("mouseup", handleWindowUp);
@@ -133,14 +139,15 @@ const setupVideoPlayback = (vid, container) => {
};
function createComparisonViewer(baseSrc) {
const isBaseVideo = isVideoFormat(baseSrc);
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
// Mount the viewer overlay container directly inside ComfyUI's canvas parent.
// This allows menus/sidebars outside the canvas area to naturally render on top.
const isBaseVideo = isVideoFormat(baseSrc);
const canvasEl = document.querySelector("#graph-canvas, canvas");
const targetContainer = canvasEl ? canvasEl.parentNode : document.body;
// Ensure the canvas container is positioned relatively so our overlay stretches accurately
if (targetContainer && targetContainer !== document.body) {
targetContainer.style.position = "relative";
}
@@ -151,7 +158,7 @@ function createComparisonViewer(baseSrc) {
position: "absolute", top: "0", left: "0", width: "100%", height: "100%",
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center",
zIndex: "10", // Above base LGraph grid but strictly below other UI components
zIndex: "10",
pointerEvents: "auto", userSelect: "none", "-webkit-user-select": "none"
});
@@ -164,11 +171,10 @@ function createComparisonViewer(baseSrc) {
const infoText = document.createElement("span");
infoText.textContent = isBaseVideo
? "Video playback. Press Esc to close."
: "Reference loaded. Click another card image in the sidebar to compare.";
: "Reference loaded. Click to replace | Shift+Click another card image to compare.";
header.appendChild(infoText);
container.appendChild(header);
// Close button
const closeBtn = document.createElement("span");
closeBtn.className = "pi pi-times";
closeBtn.title = "Close Comparison (Esc)";
@@ -180,7 +186,6 @@ function createComparisonViewer(baseSrc) {
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
container.appendChild(closeBtn);
// Workspace wrapper holding both elements aligned pixel-perfectly
const wrapper = document.createElement("div");
Object.assign(wrapper.style, {
position: "relative", display: "grid", placeItems: "center",
@@ -188,21 +193,37 @@ function createComparisonViewer(baseSrc) {
});
container.appendChild(wrapper);
// Close strictly on clicking empty background space of the overlay backdrop
// Bottom tooltip helper (Font size increased to 12px for legibility)
const hintPrompt = document.createElement("div");
Object.assign(hintPrompt.style, {
position: "absolute", bottom: "16px", zIndex: "30",
color: "#888", fontSize: "12px", fontFamily: "sans-serif",
pointerEvents: "none"
});
hintPrompt.innerHTML = 'Tip: Hold <span style="color:#aaa;font-weight:bold;">Shift</span> while clicking sidebar cards to compare outputs side-by-side.';
if (isBaseVideo) {
hintPrompt.style.display = "none";
}
container.appendChild(hintPrompt);
// Track drag actions to prevent mouseup outside the slider from triggering modal exit
let wasDragging = false;
container.onclick = (e) => {
if (e.target === container) {
if (wasDragging) {
wasDragging = false; // Ignore synthetic click caused by releasing drag on backdrop
return;
}
destroy();
}
};
// Media A (Base reference - Left. Created unmuted to enable audio playback)
let mediaA = createMediaElement(baseSrc, false);
wrapper.appendChild(mediaA);
// Media B (Comparison target - Right)
let mediaB = null;
// Draggable Split Slider Handle
const slider = document.createElement("div");
Object.assign(slider.style, {
position: "absolute", top: "0", bottom: "0", left: "50%",
@@ -223,7 +244,7 @@ function createComparisonViewer(baseSrc) {
slider.appendChild(sliderButton);
wrapper.appendChild(slider);
let splitRatio = 50; // Percentage
let splitRatio = 50;
let isDragging = false;
let destroyVideoPlaybackFn = null;
@@ -231,7 +252,6 @@ function createComparisonViewer(baseSrc) {
splitRatio = Math.max(0, Math.min(100, percent));
slider.style.left = `${splitRatio}%`;
if (mediaB) {
// Symmetrical split comparison layout: Image A on Left, Image B on Right
mediaB.style.clipPath = `polygon(${splitRatio}% 0, 100% 0, 100% 100%, ${splitRatio}% 100%)`;
}
};
@@ -240,23 +260,25 @@ function createComparisonViewer(baseSrc) {
e.preventDefault();
e.stopPropagation();
isDragging = true;
wasDragging = false;
// Symmetrical Click Teleportation: Snaps split-line directly to mouse coordinates
const rect = wrapper.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientX = getClientX(e);
const percent = ((clientX - rect.left) / rect.width) * 100;
updateSliderPosition(percent);
};
const doDrag = (e) => {
if (!isDragging) return;
wasDragging = true;
const rect = wrapper.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientX = getClientX(e);
const percent = ((clientX - rect.left) / rect.width) * 100;
updateSliderPosition(percent);
};
const endDrag = () => { isDragging = false; };
const endDrag = () => {
isDragging = false;
};
// Allows dragging/clicking from any region of the wrapper
wrapper.addEventListener("mousedown", startDrag);
wrapper.addEventListener("touchstart", startDrag);
@@ -265,17 +287,30 @@ function createComparisonViewer(baseSrc) {
window.addEventListener("mouseup", endDrag);
window.addEventListener("touchend", endDrag);
const cleanupMedia = (el) => {
if (el && el.tagName === "VIDEO") {
el.pause();
el.src = "";
el.load();
}
};
const destroy = () => {
window.removeEventListener("mousemove", doDrag);
window.removeEventListener("touchmove", doDrag);
window.removeEventListener("mouseup", endDrag);
window.removeEventListener("touchend", endDrag);
document.removeEventListener("keydown", handleKeys);
videoSyncActive = false;
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
if (destroyVideoPlaybackFn) destroyVideoPlaybackFn();
cleanupMedia(mediaA);
cleanupMedia(mediaB);
container.remove();
activeComparisonViewer = null;
};
@@ -289,53 +324,64 @@ function createComparisonViewer(baseSrc) {
updateSliderPosition(splitRatio > 50 ? 0 : 100);
}
};
document.addEventListener("keydown", handleKeys);
globalKeydownHandler = handleKeys;
document.addEventListener("keydown", globalKeydownHandler);
targetContainer.appendChild(container);
// Initialize single-video playbacks if the base output is an MP4
if (isBaseVideo) {
destroyVideoPlaybackFn = setupVideoPlayback(mediaA, container);
}
return {
loadTarget(targetSrc) {
loadTarget(targetSrc, isShiftClick) {
const isTargetVideo = isVideoFormat(targetSrc);
if (isBaseVideo) {
// If in Video Playback Mode: clicking another item replaces the player or opens image comparison
destroy();
showFullscreenPreview([targetSrc]);
showFullscreenPreview([targetSrc], isShiftClick);
return;
}
// If in Image Comparison Mode: ignore video targets ("images compare only to other images")
if (isTargetVideo) {
return;
}
if (mediaB) mediaB.remove();
if (isShiftClick) {
if (mediaB) {
cleanupMedia(mediaB);
mediaB.remove();
}
mediaB = createMediaElement(targetSrc, false);
mediaB.style.pointerEvents = "none";
wrapper.appendChild(mediaB);
mediaB = createMediaElement(targetSrc, false);
mediaB.style.pointerEvents = "none";
wrapper.appendChild(mediaB);
slider.style.display = "block";
infoText.textContent = "Drag the slider to compare. Click other card images to update target | Esc to close.";
updateSliderPosition(50);
slider.style.display = "block";
infoText.textContent = "Drag the slider to compare. Shift+Click other card images to update target | Esc to close.";
updateSliderPosition(50);
} else {
if (mediaB) {
cleanupMedia(mediaB);
mediaB.remove();
mediaB = null;
}
slider.style.display = "none";
mediaA.src = targetSrc;
infoText.textContent = "Reference loaded. Click to replace | Shift+Click another card image to compare.";
}
}
};
}
export function showFullscreenPreview(imgSrcs) {
export function showFullscreenPreview(imgSrcs, isShiftClick = false) {
if (!imgSrcs || imgSrcs.length === 0) return;
// If the comparison overlay is already open, dynamically load the target
if (activeComparisonViewer) {
activeComparisonViewer.loadTarget(imgSrcs[0]);
activeComparisonViewer.loadTarget(imgSrcs[0], isShiftClick);
return;
}
// Otherwise, open a fresh side-by-side comparison workspace
activeComparisonViewer = createComparisonViewer(imgSrcs[0]);
}
+19 -8
View File
@@ -1,9 +1,11 @@
import { app } from "/scripts/app.js";
import { State } from "./state.js";
async function uploadDroppedImageToInput(imageObj) {
const src = `/view?filename=${encodeURIComponent(imageObj.filename)}&type=${imageObj.type || 'output'}&subfolder=${encodeURIComponent(imageObj.subfolder || '')}`;
try {
const response = await fetch(src);
if (!response.ok) return null;
const blob = await response.blob();
const file = new File([blob], imageObj.filename || "dropped_image.png", { type: blob.type });
const formData = new FormData();
@@ -11,6 +13,7 @@ async function uploadDroppedImageToInput(imageObj) {
formData.append("overwrite", "true");
formData.append("type", "input");
const uploadRes = await fetch("/upload/image", { method: "POST", body: formData });
if (!uploadRes.ok) return null;
const uploadData = await uploadRes.json();
return uploadData.name;
} catch(e) {
@@ -26,6 +29,14 @@ export function setupDragAndDrop() {
});
document.addEventListener("drop", async (e) => {
const isSidebarDrop = e.target.closest('.comfyui-sidebar, .comfy-sidebar, [class*="sidebar"]') ||
(State.sidebarContainer && State.sidebarContainer.contains(e.target));
if (isSidebarDrop) {
e.preventDefault();
e.stopPropagation();
return;
}
const canvas = app.canvas;
if (!canvas || !canvas.graph) return;
@@ -71,9 +82,8 @@ export function setupDragAndDrop() {
targetNode = canvas.graph.getNodeOnPos((e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale, (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale);
}
if (targetNode && (targetNode.type.includes("LoadImage") || targetNode.widgets?.some(w => w.name === "image"))) {
// Dropped on a node with image input: Copy/Upload image to inputs
const widget = targetNode.widgets.find(w => w.name === "image");
if (targetNode && (targetNode.type?.includes("LoadImage") || targetNode.widgets?.some(w => w.name === "image"))) {
const widget = targetNode.widgets?.find(w => w.name === "image");
if (widget) {
const newFilename = await uploadDroppedImageToInput({ filename, type, subfolder });
if (newFilename) {
@@ -84,14 +94,15 @@ export function setupDragAndDrop() {
}
}
} else {
// Dropped on empty canvas: Load workflow natively
const src = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`;
const res = await fetch(src);
const blob = await res.blob();
const file = new File([blob], filename || "workflow.png", { type: blob.type });
if (res.ok) {
const blob = await res.blob();
const file = new File([blob], filename || "workflow.png", { type: blob.type });
if (app.handleFile) await app.handleFile(file);
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
if (app.handleFile) await app.handleFile(file);
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
}
}
}
}
+236 -128
View File
@@ -1,4 +1,3 @@
// layout.js - Fully Fixed & Renamed to 'Hide Junk'
import { app } from "/scripts/app.js";
const STYLE_ID = "comfy-sidebar-classic-layout-override";
@@ -15,7 +14,6 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
transform: none !important;
z-index: 1010 !important;
/* Nuke the background rectangle, border, and shadows */
background: transparent !important;
border: none !important;
box-shadow: none !important;
@@ -25,6 +23,45 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
align-items: center !important;
}
/* Style overrides for the new native Extensions button container when embedded in top actionbar */
.comfy-sidebar-extensions-override {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 8px 0 0 !important;
height: auto !important;
position: static !important;
order: -10 !important;
display: inline-flex !important;
align-items: center !important;
}
/* Scale down performance indicators (Crystools) uniformly */
#crysmonitor-monitors-root,
#crystools-monitors-root,
.crysmonitor-monitors-container {
zoom: 0.8 !important;
display: inline-flex !important;
align-items: center !important;
}
/* Hide user account profile button and PrimeVue components */
img[alt*="User Avatar"],
img[alt*="user avatar"],
button:has(img[alt*="User Avatar"]),
[role="button"]:has(img[alt*="User Avatar"]),
button:has(img[alt*="user avatar"]),
[role="button"]:has(img[alt*="user avatar"]),
.p-avatar,
[data-pc-name="avatar"],
button:has(.p-avatar),
[role="button"]:has(.p-avatar),
button:has([data-pc-name="avatar"]),
[role="button"]:has([data-pc-name="avatar"]) {
display: none !important;
}
/* Base rules: Large/Wide desktop screens (>= 1600px width) */
.p-tabview-nav-content,
[class*="tabview"] {
@@ -60,11 +97,61 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
}
`;
export function applyClassicLayout(enable) {
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) {}
try {
window.dispatchEvent(new Event("storage"));
} catch (e) {}
}
export function applyClassicLayout(enable, updateSetting = false) {
let styleEl = document.getElementById(STYLE_ID);
if (enable) {
if (app.ui && app.ui.settings) {
if (updateSetting && app.ui && app.ui.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
}
@@ -78,14 +165,12 @@ export function applyClassicLayout(enable) {
} else {
if (styleEl) styleEl.remove();
if (app.ui && app.ui.settings) {
if (updateSetting && app.ui && app.ui.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
}
}
}
/* --- Properties Panel Toggle Corrections --- */
let savedButtonData = null;
let domObserver = null;
let syncScheduled = false;
@@ -102,7 +187,7 @@ function findOriginalButton() {
}
function findTopbarContainer() {
const container = document.querySelector('[class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
const container = document.querySelector('[data-testid="action-bar-buttons"], [class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
if (container) return container;
const orig = findOriginalButton();
@@ -121,7 +206,9 @@ function findTopbarContainer() {
function isPropertiesPanelOpen() {
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
return !!(icon && icon.className.includes("lucide--panel-left"));
if (!icon) return false;
const classStr = icon.getAttribute("class") || "";
return classStr.includes("lucide--panel-left");
}
function findActiveQueueIndicator() {
@@ -140,23 +227,26 @@ function findActiveQueueIndicator() {
return null;
}
function findNativeExtensionsPanel() {
return Array.from(document.querySelectorAll('.shadow-interface:not(.actionbar-container)'))
.find(el => el.textContent.toLowerCase().includes('extensions') || el.querySelector('button')?.textContent.toLowerCase().includes('extensions'));
}
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
// NodesMap removed from tabIconSelectors
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]) => {
// Read from renamed "Hide Junk" path
const shouldHide = app.ui?.settings?.getSettingValue(`Comfy Sidebar.Hide Junk.${tab}`) ?? false;
const icon = sidebar.querySelector(selector);
@@ -175,7 +265,6 @@ function updateSidebarTabsVisibility() {
}
});
// Handle "Override Stock Job History Tab" using its renamed setting path
const hideStockHistory = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
const historyIcon = sidebar.querySelector('[class*="lucide--history"]');
if (historyIcon) {
@@ -193,17 +282,14 @@ function updateSidebarTabsVisibility() {
}
function findGraphButton() {
// 1. Target elements with Tailwind classes, but IGNORE those inside modal settings/sidebars
const elements = document.querySelectorAll('.bg-secondary-background.rounded-lg.items-center.inline-flex.pointer-events-auto');
for (const el of elements) {
// Filter out dialogs, sidebar overlays, settings drawers, and modals
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) {
continue;
}
return el;
}
// 2. Same fallback logic, excluding modals
const fallbacks = document.querySelectorAll('button, .bg-secondary-background');
for (const el of fallbacks) {
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) {
@@ -218,121 +304,151 @@ function findGraphButton() {
}
export function syncClassicLayout() {
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
if (domObserver) domObserver.disconnect();
// 1. Handle "Hide Graph Button" Toggle under the renamed "Hide Junk" path
const hideGraphBtn = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Graph Button") ?? false;
const graphBtn = findGraphButton();
if (graphBtn) {
if (hideGraphBtn) {
if (graphBtn.style.display !== "none") {
graphBtn.style.setProperty("display", "none", "important");
}
} else {
if (graphBtn.style.display === "none") {
graphBtn.style.removeProperty("display");
try {
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
const hideGraphBtn = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Graph Button") ?? false;
const graphBtn = findGraphButton();
if (graphBtn) {
if (hideGraphBtn) {
if (graphBtn.style.display !== "none") {
graphBtn.style.setProperty("display", "none", "important");
}
} else {
if (graphBtn.style.display === "none") {
graphBtn.style.removeProperty("display");
}
}
}
}
// 2. Exit early and clean up overrides if Classic Topbar layout is off
if (!isClassicLayoutEnabled) {
const extensionsPanel = findNativeExtensionsPanel();
if (extensionsPanel) {
if (isClassicLayoutEnabled) {
const container = findTopbarContainer();
if (container && extensionsPanel.parentNode !== container) {
if (!extensionsPanel._originalParent) {
extensionsPanel._originalParent = extensionsPanel.parentNode;
extensionsPanel._originalNextSibling = extensionsPanel.nextSibling;
}
container.appendChild(extensionsPanel);
extensionsPanel.classList.add("comfy-sidebar-extensions-override");
}
} else {
if (extensionsPanel._originalParent && extensionsPanel.parentNode !== extensionsPanel._originalParent) {
extensionsPanel._originalParent.insertBefore(extensionsPanel, extensionsPanel._originalNextSibling || null);
extensionsPanel.classList.remove("comfy-sidebar-extensions-override");
}
}
}
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();
return;
}
const originalBtn = findOriginalButton();
if (originalBtn && originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
originalBtn.classList.remove("comfy-sidebar-hide-original-properties-btn");
const container = findTopbarContainer();
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,
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");
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);
}
}
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);
}
const customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
if (customBtn) {
customBtn.remove();
customBtn.classList.toggle("comfy-panel-open", openState);
customBtn.style.display = "inline-flex";
}
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
const indicator = findActiveQueueIndicator();
if (indicator && indicator.style.display === "none") {
indicator.style.removeProperty("display");
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");
}
}
}
updateSidebarTabsVisibility();
return;
}
// 3. Handle Active Layout elements synchronization
const originalBtn = findOriginalButton();
const container = findTopbarContainer();
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,
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");
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);
}
}
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);
}
if (customBtn) {
customBtn.classList.toggle("comfy-panel-open", openState);
customBtn.style.display = "inline-flex";
}
// 4. Handle "0 active" indicator visibility based on history tab settings
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.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");
}
} catch (err) {
console.error("Comfy Sidebar: Error inside layout sync routine:", err);
} finally {
if (domObserver) {
domObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
updateSidebarTabsVisibility();
}
export function setupPropertiesPanelToggleFix() {
@@ -369,27 +485,17 @@ export function setupPropertiesPanelToggleFix() {
domObserver.disconnect();
}
syncClassicLayout();
domObserver = new MutationObserver((mutations) => {
let shouldSync = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
// Ensure mutation is on an actual Element node (nodeType 1) and not just text node updates
const hasElementMutation = Array.from(mutation.addedNodes).some(node => node.nodeType === 1) ||
Array.from(mutation.removedNodes).some(node => node.nodeType === 1);
if (!hasElementMutation) continue;
// Ignore mutations on our own components to avoid feedback loops
const isOurNode = Array.from(mutation.addedNodes).some(node =>
node.classList?.contains?.("comfy-sidebar-custom-properties-toggle") ||
node.id === "comfy-sidebar-layout-fix-styles"
);
if (isOurNode) continue;
shouldSync = true;
break;
if (hasElementMutation) {
shouldSync = true;
break;
}
}
}
if (shouldSync && !syncScheduled) {
@@ -405,4 +511,6 @@ export function setupPropertiesPanelToggleFix() {
childList: true,
subtree: true
});
syncClassicLayout();
}
+24 -18
View File
@@ -1,6 +1,6 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { State, promptStates, pruneHistory, loadStatesFromLocalStorage } from "./state.js";
import { State, promptStates, pruneHistory, loadStatesFromLocalStorage, cardElements } from "./state.js";
import { findImagesInOutputs, findTextsInOutputs } from "./utils.js";
export let renderDOMFn = () => {};
@@ -69,6 +69,13 @@ const concludeRun = async (pid, statusStr) => {
if (State.currentlyActivePromptId === pid) State.currentlyActivePromptId = null;
const st = promptStates.get(pid);
// FIX: Revoke live preview blob URL to prevent browser memory leaks
if (st._previewBlobUrl) {
try { URL.revokeObjectURL(st._previewBlobUrl); } catch(e){}
delete st._previewBlobUrl;
}
st.status = statusStr;
st.progressText = "";
st.rendered = false;
@@ -80,9 +87,9 @@ const concludeRun = async (pid, statusStr) => {
const hItem = await res.json();
if (hItem && hItem[pid]) {
if (!st.workflow) st.workflow = hItem[pid].extra_data?.extra_pnginfo?.workflow || null;
st.nodeOutputs = hItem[pid].outputs; // Store full outputs dictionary!
if (st.images.length === 0) st.images = findImagesInOutputs(hItem[pid].outputs);
st.texts = findTextsInOutputs(hItem[pid].outputs);
st.nodeOutputs = hItem[pid].outputs;
if (st.images.length === 0) st.images = findImagesInOutputs(hItem[pid].outputs, st.workflow);
st.texts = findTextsInOutputs(hItem[pid].outputs, st.workflow);
}
} catch (err) {}
pruneHistory(app);
@@ -93,7 +100,6 @@ export function setupApiListeners() {
api.addEventListener("status", syncQueue);
api.addEventListener("execution_start", (e) => {
// Auto-sweep cancelled and errored jobs if setting is enabled
if (app.ui.settings.getSettingValue("Comfy Sidebar.Auto Clear Interrupted") ?? false) {
const toDelete = [];
for (const [p, s] of promptStates.entries()) {
@@ -123,7 +129,7 @@ export function setupApiListeners() {
workflow: activeWorkspaceWorkflow, startTime: Date.now(), duration: null
});
}
renderDOMFn(); // Render immediately to prevent pop-in delay!
renderDOMFn();
syncQueue();
});
@@ -165,18 +171,15 @@ export function setupApiListeners() {
api.addEventListener("executed", (e) => {
if (promptStates.has(e.detail.prompt_id)) {
const st = promptStates.get(e.detail.prompt_id);
const finalImgs = findImagesInOutputs({ output: e.detail.output });
const finalImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (finalImgs.length > 0) {
// Overwrite with latest images to avoid accumulation across sequential nodes
st.images = finalImgs;
}
const finalTexts = findTextsInOutputs({ output: e.detail.output });
const finalTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (finalTexts.length > 0) {
// Overwrite with latest texts to avoid clearing previously set text outputs
st.texts = finalTexts;
}
// Dynamically capture each executed node's individual outputs
if (!st.nodeOutputs) st.nodeOutputs = {};
st.nodeOutputs[e.detail.node] = e.detail.output;
@@ -203,26 +206,29 @@ export async function initSessionAndHistory() {
}
const storedSessionId = localStorage.getItem("comfy_sidebar_backend_session_id");
if (backendSessionId && backendSessionId === storedSessionId) {
loadStatesFromLocalStorage();
} else {
if (backendSessionId && backendSessionId !== storedSessionId) {
promptStates.clear();
cardElements.clear();
localStorage.removeItem("comfy_sidebar_prompt_states");
if (backendSessionId) localStorage.setItem("comfy_sidebar_backend_session_id", backendSessionId);
} else if (!storedSessionId && backendSessionId) {
localStorage.setItem("comfy_sidebar_backend_session_id", backendSessionId);
}
const historyData = await api.getHistory();
const ids = Object.keys(historyData).sort((a,b) => Number(a)-Number(b));
ids.forEach(id => {
if (promptStates.has(id)) return;
const images = findImagesInOutputs(historyData[id].outputs);
const texts = findTextsInOutputs(historyData[id].outputs);
const workflow = historyData[id].extra_data?.extra_pnginfo?.workflow || null;
const images = findImagesInOutputs(historyData[id].outputs, workflow);
const texts = findTextsInOutputs(historyData[id].outputs, workflow);
if (images.length === 0 && texts.length === 0) return;
State.globalOrderCounter++;
promptStates.set(id, {
pid: id, status: "completed", images, texts,
nodeOutputs: historyData[id].outputs, // Store full outputs dictionary!
workflow: historyData[id].extra_data?.extra_pnginfo?.workflow || null,
nodeOutputs: historyData[id].outputs,
workflow: workflow,
progressText: "", timestamp: State.globalOrderCounter, rendered: true
});
});
+158 -33
View File
@@ -1,25 +1,120 @@
// sidebar.js - Fully Fixed Real-Time UI Sync (With setTimeout deferred updates)
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, syncClassicLayout } from "./layout.js";
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings } from "./layout.js";
let isInitialized = false;
// Helper to scan node container for the exact text element rendering the title (Vue Mode)
function findHeaderByText(parent, text) {
if (!text) return null;
const cleanText = text.trim();
const elements = parent.querySelectorAll("span, div, h1, h2, h3, h4, p, [class*='title'], [class*='text']");
for (const el of elements) {
if (el.textContent.trim() === cleanText) {
if (el.offsetWidth > 0 && el.offsetHeight > 0 && el.offsetWidth < parent.offsetWidth * 0.9) {
return el;
}
}
}
return null;
}
function syncNodeVueBadge(node, isIgnored) {
const nodeEl = document.querySelector(`.comfy-node[data-node-id="${node.id}"], [data-node-id="${node.id}"]`);
if (!nodeEl) return;
const titleTextNode = findHeaderByText(nodeEl, node.title || node.type);
if (!titleTextNode) {
const badge = nodeEl.querySelector(".comfy-sidebar-ignore-badge");
if (badge && !isIgnored) badge.remove();
return;
}
const headerEl = titleTextNode.parentNode;
let badge = headerEl.querySelector(".comfy-sidebar-ignore-badge");
if (isIgnored) {
if (!badge) {
badge = document.createElement("div");
badge.className = "comfy-sidebar-ignore-badge";
badge.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/>
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/>
<path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/>
<line x1="2" y1="2" x2="22" y2="22"/>
</svg>
`;
Object.assign(badge.style, {
marginLeft: "auto",
marginRight: "6px",
float: "right",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
color: "inherit",
pointerEvents: "none"
});
headerEl.appendChild(badge);
}
} else {
if (badge) badge.remove();
}
}
function toggleIgnoreActiveNode() {
const canvas = app.canvas;
if (!canvas) return;
let nodes = [];
if (canvas.selected_nodes && Object.keys(canvas.selected_nodes).length > 0) {
nodes = Object.values(canvas.selected_nodes);
} else if (canvas.current_active_node) {
nodes = [canvas.current_active_node];
}
if (nodes.length === 0) return;
nodes.forEach(node => {
if (!node.properties) node.properties = {};
node.properties.ignoreInQueue = !node.properties.ignoreInQueue;
const isIgnored = !!node.properties.ignoreInQueue;
if (isIgnored) {
if (node.boxcolor !== "#ff3333") {
node._oldBoxcolor = node.boxcolor || "";
node.boxcolor = "#ff3333";
}
} else {
if (node.boxcolor === "#ff3333") {
node.boxcolor = node._oldBoxcolor || "";
delete node._oldBoxcolor;
}
}
syncNodeVueBadge(node, isIgnored);
});
if (app.graph) app.graph.setDirtyCanvas(true, true);
if (app.canvas) app.canvas.setDirty(true, true);
}
app.registerExtension({
name: "ComfySidebar.ClassicRestore",
init() {
// Standard Comfy Queue Settings
app.ui.settings.addSetting({ id: "Comfy Sidebar.Grid Columns Threshold", name: "Width Threshold for Queue Columns (px)", type: "number", defaultValue: 350 });
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.Auto Clear Interrupted", name: "Auto-clear cancelled & failed jobs on new generation", type: "boolean", defaultValue: false });
// Add settings to hide individual sidebar tabs under the "Hide Junk" sub-category
// We use setTimeout to let Comfy finish writing the setting value before reading it
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "NodesMap", "Templates"];
// NodesMap removed from sidebarTabs list
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"];
sidebarTabs.forEach(tab => {
app.ui.settings.addSetting({
id: `Comfy Sidebar.Hide Junk.${tab}`,
@@ -27,47 +122,43 @@ app.registerExtension({
type: "boolean",
defaultValue: false,
onChange: () => {
setTimeout(() => {
if (typeof syncClassicLayout === "function") syncClassicLayout();
}, 0);
setTimeout(() => { syncClassicLayout(); }, 0);
}
});
});
// Group the Stock History Tab override into "Hide Junk"
app.ui.settings.addSetting({
id: "Comfy Sidebar.Hide Junk.Override Stock Job History Tab",
name: "Replaces the stock Job History sidebar with Comfy Queue",
name: "Replace the stock Job History sidebar with Comfy Queue",
type: "boolean",
defaultValue: false,
onChange: () => {
setTimeout(() => {
if (typeof syncClassicLayout === "function") syncClassicLayout();
}, 0);
onChange: (value) => {
if (isInitialized && app.ui && app.ui.settings) {
syncStockHistoryAndProgressSettings(value);
}
setTimeout(() => { syncClassicLayout(); }, 0);
}
});
// Add the toggle setting for Graph Button under "Hide Junk"
app.ui.settings.addSetting({
id: "Comfy Sidebar.Hide Junk.Graph Button",
name: "Hide floating 'Graph' (Workflow/Node Map) button",
type: "boolean",
defaultValue: false,
onChange: () => {
setTimeout(() => {
if (typeof syncClassicLayout === "function") syncClassicLayout();
}, 0);
setTimeout(() => { syncClassicLayout(); }, 0);
}
});
// 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);
if (isInitialized) {
applyClassicLayout(value, true);
}
}
});
},
@@ -75,44 +166,78 @@ app.registerExtension({
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);
applyClassicLayout(isClassicLayoutEnabled, false);
const isHistoryOverrideEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
if (isHistoryOverrideEnabled) {
syncStockHistoryAndProgressSettings(true);
}
isInitialized = true;
// Run properties panel toggle correction
setupPropertiesPanelToggleFix();
// Safely resolve circular dependencies using Dependency Injection Hooks
setSyncQueue(syncQueue);
setUIDependencies(renderDOM, updateSidebarBadge);
// 1. Initialize DOM Independent Services
injectStyles();
setupDragAndDrop();
// 2. Initialize UI Hierarchy
const sidebarContainer = setupSidebarUI();
// 3. Connect API Hooks
setupApiListeners();
// 4. Connect Storage and History Sync
await initSessionAndHistory();
// 5. Connect UI Keyboard & Override Hacks
document.addEventListener("keydown", (e) => {
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable || activeEl.tagName === "SELECT")) return;
if (e.key.toLowerCase() === "q") {
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault(); e.stopPropagation();
const ourBtn = findOurSidebarButton();
if (ourBtn) ourBtn.click();
}
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.stopPropagation();
toggleIgnoreActiveNode();
}
}, true);
setInterval(() => applySidebarOverride(), 500);
setInterval(() => {
applySidebarOverride();
if (app.graph && app.graph._nodes) {
const hasIgnoredOrColoredNodes = app.graph._nodes.some(n => n.properties?.ignoreInQueue || n.boxcolor === "#ff3333");
const hasBadgesInDOM = !!document.querySelector(".comfy-sidebar-ignore-badge");
if (hasIgnoredOrColoredNodes || hasBadgesInDOM) {
app.graph._nodes.forEach(node => {
if (!node.properties) node.properties = {};
const isIgnored = !!node.properties.ignoreInQueue;
if (isIgnored) {
if (node.boxcolor !== "#ff3333") {
node._oldBoxcolor = node.boxcolor || "";
node.boxcolor = "#ff3333";
app.graph.setDirtyCanvas(true, true);
}
} else {
if (node.boxcolor === "#ff3333") {
node.boxcolor = node._oldBoxcolor || "";
delete node._oldBoxcolor;
app.graph.setDirtyCanvas(true, true);
}
}
syncNodeVueBadge(node, isIgnored);
});
}
}
}, 500);
// 6. Final Registration
app.extensionManager.registerSidebarTab({
id: "classic-comfy-sidebar",
icon: "pi pi-images",
+5 -4
View File
@@ -1,7 +1,6 @@
export const promptStates = new Map();
export const cardElements = new Map();
// Global shared variables
export const State = {
currentSearchQuery: "",
globalOrderCounter: 0,
@@ -34,13 +33,13 @@ export function saveStatesToLocalStorage() {
const serializable = [];
for (const [pid, state] of promptStates.entries()) {
const cleanedImages = (state.images || []).map(img => {
if (img.url && img.url.startsWith("blob:")) return null; // Skip blob URLs
if (img.url && img.url.startsWith("blob:")) return null;
return img;
}).filter(Boolean);
serializable.push({
pid: state.pid, status: state.status, images: cleanedImages, texts: state.texts || [],
nodeOutputs: state.nodeOutputs, // Persist node level outputs
nodeOutputs: state.nodeOutputs,
workflow: state.workflow, progress: state.progress || 0, queueNumber: state.queueNumber,
progressText: state.progressText || "", timestamp: state.timestamp,
activeNodeName: state.activeNodeName || "", rendered: state.rendered || false,
@@ -66,4 +65,6 @@ export function loadStatesFromLocalStorage() {
} catch (e) {
console.error("Comfy Sidebar: Failed to load state from localStorage", e);
}
}
}
loadStatesFromLocalStorage();
+7 -7
View File
@@ -1,6 +1,7 @@
// styles.js
export function injectStyles() {
if (document.getElementById("comfy-sidebar-styles")) return;
const style = document.createElement("style");
style.id = "comfy-sidebar-styles";
style.textContent = `
.comfy-sidebar-card {
background: var(--comfy-input-bg, #181818);
@@ -35,24 +36,23 @@ export function injectStyles() {
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 */
background-color: #7f1d1d !important;
color: #e2e8f0 !important;
font-size: 11px !important;
border-radius: 6px !important;
cursor: pointer !important;
transition: background-color 0.15s ease, color 0.15s ease !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
}
.comfy-sidebar-queue-cancel-btn:hover {
background-color: #991b1b !important; /* Slightly brighter red on hover */
background-color: #991b1b !important;
color: #ffffff !important;
}
`;
+57 -72
View File
@@ -8,7 +8,6 @@ import { showFullscreenPreview } from "./comparison.js";
export let syncQueueFn = async () => {};
export function setSyncQueue(fn) { syncQueueFn = fn; }
// Resets any stale hover states when transitioning between submenus and the main queue
function resetAllCardHoverStates() {
for (const cardObj of cardElements.values()) {
if (cardObj.hoverPanel) cardObj.hoverPanel.style.display = "none";
@@ -16,12 +15,10 @@ function resetAllCardHoverStates() {
}
}
// Traverses node execution metadata to resolve the exact node ID that generated a specific image
function findNodeIdForImage(state, img) {
if (!state || !state.nodeOutputs || !img) return null;
for (const nodeId in state.nodeOutputs) {
const out = state.nodeOutputs[nodeId];
// Safely check both gifs, images, or videos under this specific node
for (const key in out) {
const val = out[key];
if (Array.isArray(val)) {
@@ -145,7 +142,6 @@ export function setupSidebarUI() {
titleGroup.appendChild(title);
standardHeader.appendChild(titleGroup);
// Dynamic Compact Actions Group
const actionsGroup = document.createElement("div");
Object.assign(actionsGroup.style, { display: "flex", gap: "6px", alignItems: "center" });
@@ -173,7 +169,7 @@ export function setupSidebarUI() {
if (!isPending) {
isPending = true;
Object.assign(btn.style, { color: "#fff", background: hoverColor, borderColor: hoverColor, boxShadow: `0 0 8px ${hoverColor}80` });
timeout = setTimeout(reset, 1500); // 1.5s to confirm
timeout = setTimeout(reset, 1500);
} else {
reset();
await onClickFn();
@@ -202,7 +198,7 @@ export function setupSidebarUI() {
promptStates.delete(pid);
}
}
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clear: true }) }); } catch (err) {}
try { await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ clear: true }) }); } catch (err) {}
renderDOM();
});
@@ -237,14 +233,12 @@ export function setupSidebarUI() {
Object.assign(State.cardStack.style, { flex: "1", overflowY: "auto", scrollbarWidth: "thin", display: "block" });
State.sidebarContainer.appendChild(State.cardStack);
// Global background click handler: return to main queue if clicking empty space of the entire container
State.sidebarContainer.onclick = (e) => {
if (State.activeSubmenuPromptId || State.activeSubmenuBatchImages) {
// Stop closure if we clicked on standard interactive elements (images, buttons, spans, cancels)
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;
State.activeSubmenuBatchImages = null;
resetAllCardHoverStates(); // Instantly wipe stale hover indicators on exit click
resetAllCardHoverStates();
renderDOM();
document.removeEventListener("click", handleGlobalClick, true);
globalClickRegistered = false;
@@ -258,7 +252,9 @@ export function setupSidebarUI() {
State.cardStack.style.columnGap = cols > 1 ? "12px" : "0";
}).observe(State.sidebarContainer);
// Optimized: Early-exit timer updates when no tasks are currently active
setInterval(() => {
if (State.currentlyActivePromptId === null) return;
for (const [pid, state] of promptStates.entries()) {
if (state.status === "active" && state.startTime) {
const cardObj = cardElements.get(pid);
@@ -270,7 +266,6 @@ export function setupSidebarUI() {
return State.sidebarContainer;
}
// Unified image batch pagination renderer (supports standard queue cards & both explorer submenus)
function renderCardImages(cardObj, state, keepAspect) {
cardObj.currentImageIndex = cardObj.currentImageIndex || 0;
if (cardObj.currentImageIndex >= state.images.length) {
@@ -289,7 +284,6 @@ function renderCardImages(cardObj, state, keepAspect) {
const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
const isVideo = isVideoFormat(src);
// Find or create the native wrapper
let wrapper = cardObj.grid.querySelector(".comfy-sidebar-media-wrapper");
if (!wrapper) {
cardObj.grid.innerHTML = "";
@@ -299,7 +293,6 @@ function renderCardImages(cardObj, state, keepAspect) {
cardObj.grid.appendChild(wrapper);
}
// Find or create the media tag in-place
let mediaEl = wrapper.querySelector("img, video");
const needsRebuild = !mediaEl || (isVideo !== (mediaEl.tagName.toLowerCase() === "video"));
@@ -319,13 +312,11 @@ function renderCardImages(cardObj, state, keepAspect) {
cardObj.firstImgElement = mediaEl;
// Binds interactive event listeners
mediaEl.onclick = (ev) => {
ev.stopPropagation();
showFullscreenPreview([src]);
showFullscreenPreview([src], ev.shiftKey);
};
// Shared dimensions layout compiler
const applyDimensions = (width, height) => {
if (keepAspect && width && height) {
mediaEl.style.aspectRatio = `${width} / ${height}`;
@@ -346,6 +337,9 @@ function renderCardImages(cardObj, state, keepAspect) {
mediaEl.onloadedmetadata = () => {
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
};
if (mediaEl.readyState >= 1) {
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
}
let playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
if (!playIcon) {
@@ -367,29 +361,40 @@ function renderCardImages(cardObj, state, keepAspect) {
mediaEl.onmouseenter = () => { playIcon.style.opacity = "0"; mediaEl.play().catch(()=>{}); };
mediaEl.onmouseleave = () => { playIcon.style.opacity = "1"; mediaEl.pause(); };
} else {
mediaEl.setAttribute("draggable", "true");
const isUnfinished = state.status && state.status !== "completed";
if (isUnfinished) {
mediaEl.setAttribute("draggable", "false");
mediaEl.style.cursor = "grab";
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
delete mediaEl._currentDragStart;
} else {
mediaEl.setAttribute("draggable", "true");
mediaEl.style.cursor = "zoom-in";
const dragStartHandler = (e) => {
try {
e.dataTransfer.setData("text/uri-list", src);
e.dataTransfer.setData("text/plain", src);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
e.stopPropagation();
};
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
mediaEl.addEventListener("dragstart", dragStartHandler);
mediaEl._currentDragStart = dragStartHandler;
}
mediaEl.onload = () => {
applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
};
if (mediaEl.complete && mediaEl.naturalWidth) {
applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
}
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
if (playIcon) playIcon.remove();
const dragStartHandler = (e) => {
State.currentDraggedImgData = { ...img, workflow: state.workflow };
try {
e.dataTransfer.setData("text/uri-list", src);
e.dataTransfer.setData("text/plain", src);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
e.stopPropagation();
};
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
mediaEl.addEventListener("dragstart", dragStartHandler);
mediaEl._currentDragStart = dragStartHandler;
}
// Smooth in-memory source swapping for blob live previews
const currentSrc = mediaEl.getAttribute("src") || mediaEl.src;
if (currentSrc !== src && currentSrc !== (currentSrc + "#t=0.001")) {
if (src.startsWith("blob:") && !isVideo) {
@@ -408,7 +413,6 @@ function renderCardImages(cardObj, state, keepAspect) {
mediaEl.src = isVideo ? src + "#t=0.001" : src;
}
} else {
// If the source is already correct, make sure we still force dimensions calculations
if (isVideo) {
if (mediaEl.videoWidth) applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
} else {
@@ -416,7 +420,6 @@ function renderCardImages(cardObj, state, keepAspect) {
}
}
// Sync batch pagination controller overlay
let navBar = wrapper.querySelector(".comfy-sidebar-batch-navbar");
if (state.images.length > 1) {
if (!navBar) {
@@ -445,15 +448,15 @@ function renderCardImages(cardObj, state, keepAspect) {
label.style.cursor = "pointer";
label.title = "View all images in this batch";
// Open the dynamic batch explorer explorer view when the middle counter label is clicked
label.onclick = (ev) => {
ev.stopPropagation();
State.activeSubmenuBatchImages = {
pid: state.pid || cardObj.element.id.replace("card-", ""),
images: state.images,
workflow: state.workflow
workflow: state.workflow,
nodeOutputs: state.nodeOutputs
};
resetAllCardHoverStates(); // Prevent ghost hover handles
resetAllCardHoverStates();
renderDOM();
};
@@ -479,7 +482,6 @@ function renderCardImages(cardObj, state, keepAspect) {
let globalClickRegistered = false;
// Global document mousedown-capturer to reliably trigger click-aways outside the sidebar
const handleGlobalClick = (e) => {
if (!State.activeSubmenuPromptId && !State.activeSubmenuBatchImages) {
document.removeEventListener("click", handleGlobalClick, true);
@@ -493,7 +495,7 @@ const handleGlobalClick = (e) => {
if (!clickedInsideSidebar && !clickedFullscreenOverlay) {
State.activeSubmenuPromptId = null;
State.activeSubmenuBatchImages = null;
resetAllCardHoverStates(); // Reset stale overlays when clicking away
resetAllCardHoverStates();
renderDOM();
document.removeEventListener("click", handleGlobalClick, true);
globalClickRegistered = false;
@@ -512,7 +514,6 @@ export function renderDOM() {
const headerSearchIcon = State.sidebarContainer.querySelector(".pi-search");
const headerActions = State.sidebarContainer.querySelector(".pi-eraser")?.parentNode;
// Shared action buttons styling helper
const styleActionBtn = (btn) => {
Object.assign(btn.style, {
display: "inline-flex",
@@ -526,13 +527,12 @@ export function renderDOM() {
transition: "all 0.15s ease",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.3)",
zIndex: "20",
transform: "translateZ(0)" // Forces a GPU compositor stacking context to prevent video clipping
transform: "translateZ(0)"
});
btn.onmouseenter = () => { btn.style.backgroundColor = "rgba(0, 0, 0, 0.95)"; btn.style.color = "#fff"; };
btn.onmouseleave = () => { btn.style.backgroundColor = "rgba(0, 0, 0, 0.75)"; btn.style.color = "#e2e8f0"; };
};
// --- SUBMENU 2: BATCH IMAGES EXPLORER ---
if (State.activeSubmenuBatchImages) {
const batchInfo = State.activeSubmenuBatchImages;
@@ -583,7 +583,6 @@ export function renderDOM() {
fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px", userSelect: "none"
});
// Symmetrical hover panel on the right with ONLY the Download Object button
const hoverPanel = document.createElement("div");
Object.assign(hoverPanel.style, {
position: "absolute", bottom: "4px", right: "4px", display: "none",
@@ -596,7 +595,6 @@ export function renderDOM() {
styleActionBtn(btnImg);
hoverPanel.appendChild(btnImg);
// Symmetrical left hover panel containing ONLY btnFocus
const leftHoverPanel = document.createElement("div");
Object.assign(leftHoverPanel.style, {
position: "absolute", bottom: "4px", left: "4px", display: "none",
@@ -605,7 +603,7 @@ export function renderDOM() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.title = "Focus Node";
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
leftHoverPanel.appendChild(btnFocus);
@@ -634,7 +632,6 @@ export function renderDOM() {
a.click();
};
// Focus/center target node inside Batch Explorer Submenu
const nodeId = findNodeIdForImage(batchInfo, img);
if (nodeId) {
cardObj.btnFocus.style.display = "inline-flex";
@@ -650,7 +647,7 @@ export function renderDOM() {
cardObj.btnFocus.style.display = "none";
}
renderCardImages(cardObj, { pid: batchInfo.pid, images: [img], workflow: batchInfo.workflow }, keepAspect);
renderCardImages(cardObj, { pid: batchInfo.pid, images: [img], workflow: batchInfo.workflow, nodeOutputs: batchInfo.nodeOutputs }, keepAspect);
targetElements.push(cardObj.element);
});
@@ -660,7 +657,6 @@ export function renderDOM() {
return;
}
// --- SUBMENU 1: WORKFLOW OUTPUTS EXPLORER ---
if (State.activeSubmenuPromptId) {
const st = promptStates.get(State.activeSubmenuPromptId);
if (!st) {
@@ -669,7 +665,6 @@ export function renderDOM() {
return;
}
// Sync Header UI State for the explorer view
if (headerTitle) {
headerTitle.textContent = `Outputs of #${State.activeSubmenuPromptId}`;
headerTitle.style.cursor = "pointer";
@@ -684,7 +679,7 @@ export function renderDOM() {
globalClickRegistered = true;
}
const outputs = getRunOutputs(st.nodeOutputs);
const outputs = getRunOutputs(st.nodeOutputs, st.workflow);
const targetElements = [];
outputs.forEach((out) => {
@@ -719,7 +714,6 @@ export function renderDOM() {
fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px", userSelect: "none"
});
// Action overlay on hover containing only Download Object
const hoverPanel = document.createElement("div");
Object.assign(hoverPanel.style, {
position: "absolute", bottom: "4px", right: "4px", display: "none",
@@ -732,7 +726,6 @@ export function renderDOM() {
styleActionBtn(btnImg);
hoverPanel.appendChild(btnImg);
// Symmetrical left hover panel containing ONLY btnFocus
const leftHoverPanel = document.createElement("div");
Object.assign(leftHoverPanel.style, {
position: "absolute", bottom: "4px", left: "4px", display: "none",
@@ -741,7 +734,7 @@ export function renderDOM() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.title = "Focus Node";
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
leftHoverPanel.appendChild(btnFocus);
@@ -773,7 +766,6 @@ export function renderDOM() {
});
};
// Focus/center target node inside Workflow outputs Submenu
cardObj.btnFocus.onclick = (ev) => {
ev.stopPropagation();
const node = app.graph.getNodeById(Number(out.nodeId));
@@ -783,7 +775,7 @@ export function renderDOM() {
}
};
renderCardImages(cardObj, { pid: st.pid, images: out.images, workflow: st.workflow }, keepAspect);
renderCardImages(cardObj, { pid: st.pid, images: out.images, workflow: st.workflow, nodeOutputs: st.nodeOutputs }, keepAspect);
} else {
cardObj.btnImg.style.display = "none";
cardObj.placeholder.style.display = "block";
@@ -798,7 +790,6 @@ export function renderDOM() {
return;
}
// --- STANDARD QUEUE RENDER LOGIC ---
if (headerTitle) {
headerTitle.textContent = "Queue";
headerTitle.style.cursor = "default";
@@ -846,7 +837,6 @@ export function renderDOM() {
pt.appendChild(pb);
const statusText = document.createElement("div"); Object.assign(statusText.style, { fontSize: "11px", opacity: "0.9", color: "#3b82f6", textAlign: "center", marginTop: "6px", display: "none", fontWeight: "bold" });
// Absolute Vertical Actions Panel on the right of the card
const hoverPanel = document.createElement("div");
Object.assign(hoverPanel.style, {
position: "absolute", bottom: "4px", right: "4px", display: "none",
@@ -857,7 +847,6 @@ export function renderDOM() {
const btnJson = document.createElement("span"); btnJson.className = "pi pi-file"; btnJson.title = "Download JSON"; styleActionBtn(btnJson);
const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash"; btnDel.title = "Delete Card"; styleActionBtn(btnDel);
// Absolute Vertical Left Actions Panel containing btnFocus and leftHoverBtn
const leftHoverPanel = document.createElement("div");
Object.assign(leftHoverPanel.style, {
position: "absolute", bottom: "4px", left: "4px", display: "none",
@@ -866,7 +855,7 @@ export function renderDOM() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.title = "Focus Node";
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
const leftHoverBtn = document.createElement("span");
@@ -877,7 +866,7 @@ export function renderDOM() {
leftHoverBtn.onclick = (ev) => {
ev.stopPropagation();
State.activeSubmenuPromptId = state.pid;
resetAllCardHoverStates(); // Reset standard sidebar hover states before opening submenu
resetAllCardHoverStates();
renderDOM();
};
@@ -893,7 +882,6 @@ export function renderDOM() {
cardObj.hoverPanel.style.display = "flex";
cardObj.leftHoverPanel.style.display = "flex";
// Focus/Center the exact node that generated the currently active displayed image
const currentImg = state.images[cardObj.currentImageIndex || 0];
const nodeId = findNodeIdForImage(state, currentImg);
if (nodeId) {
@@ -910,9 +898,9 @@ export function renderDOM() {
cardObj.btnFocus.style.display = "none";
}
// Show explorer view button if this run yielded multiple node outputs
const outputs = getRunOutputs(state.nodeOutputs);
if (outputs.length > 1) {
const outputs = getRunOutputs(state.nodeOutputs, state.workflow);
const isInterrupted = state.status === "cancelled" || state.status === "error";
if (outputs.length > 1 || (outputs.length > 0 && isInterrupted)) {
cardObj.leftHoverBtn.style.display = "inline-flex";
} else {
cardObj.leftHoverBtn.style.display = "none";
@@ -925,9 +913,8 @@ export function renderDOM() {
});
card.addEventListener("dragstart", (e) => {
// Custom dragstart is only used for JSON-only workflows to supply a local DownloadURL.
// For image cards, dragging is handled natively by the browser on the child <img> tag.
if (state.workflow && (!state.images || state.images.length === 0)) {
const isUnfinished = state.status && state.status !== "completed";
if (state.workflow && (!state.images || state.images.length === 0 || isUnfinished)) {
if (cardObj.firstImgElement) e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15);
const jsonStr = JSON.stringify(state.workflow, null, 2);
try {
@@ -938,15 +925,14 @@ export function renderDOM() {
e.dataTransfer.effectAllowed = "copy";
}
});
cardObj.element.id = `card-${state.pid}`; // set ID helper
cardObj.element.id = `card-${state.pid}`;
cardElements.set(state.pid, cardObj);
}
cardObj.element.className = `comfy-sidebar-card ${state.status}`;
// Native drag isolation: completely remove draggable flags on image containers.
// This exposes the child <img> tag directly to Chrome's native filesystem drag APIs.
if (state.images && state.images.length > 0) {
const isUnfinished = state.status && state.status !== "completed";
if (state.images && state.images.length > 0 && !isUnfinished) {
cardObj.element.removeAttribute("draggable");
} else {
cardObj.element.setAttribute("draggable", "true");
@@ -976,7 +962,7 @@ export function renderDOM() {
cardObj.cancelBtn.style.display = "flex";
cardObj.cancelBtn.onclick = async (ev) => {
ev.stopPropagation();
await api.interrupt(); // Natively interrupts current execution
await api.interrupt();
await syncQueueFn();
};
} else {
@@ -1010,7 +996,7 @@ export function renderDOM() {
isDeletePending = true;
Object.assign(cardObj.btnDel.style, {
color: "#fff",
backgroundColor: "#dc3545" // Changes to solid red on warning confirmation
backgroundColor: "#dc3545"
});
cardObj.btnDel.title = "Click again to confirm deletion";
deleteTimeout = setTimeout(resetDeleteBtn, 1000);
@@ -1034,7 +1020,6 @@ export function renderDOM() {
}
if (state.images.length === 0) {
// Correctly display accumulated texts (like workflow prompts) while running as requested
if (state.texts && state.texts.length > 0) {
cardObj.placeholder.textContent = state.texts.join("\n"); Object.assign(cardObj.placeholder.style, { whiteSpace: "pre-wrap", textAlign: "left" });
} else {
+21 -4
View File
@@ -1,13 +1,29 @@
import { app } from "/scripts/app.js";
export const isVideoFormat = (url) => {
const s = url.toLowerCase();
return s.includes(".mp4") || s.includes(".webm");
};
function isNodeIgnored(nodeId, workflow) {
if (!workflow || !workflow.nodes) {
// Fallback to active graph if workflow is missing (e.g. during live execution)
const liveNode = app.graph?.getNodeById(Number(nodeId));
if (liveNode) {
return !!(liveNode.properties && liveNode.properties.ignoreInQueue);
}
return false;
}
const node = workflow.nodes.find(n => String(n.id) === String(nodeId));
return !!(node && node.properties && node.properties.ignoreInQueue);
}
// Robust media output scanner supporting both arrays and single objects (gifs, mp4s, etc.)
export function findImagesInOutputs(outputs) {
export function findImagesInOutputs(outputs, workflow) {
const list = [];
if (!outputs) return list;
for (const nodeId in outputs) {
if (isNodeIgnored(nodeId, workflow)) continue;
for (const key in outputs[nodeId]) {
const val = outputs[nodeId][key];
if (Array.isArray(val)) {
@@ -22,10 +38,11 @@ export function findImagesInOutputs(outputs) {
return list;
}
export function findTextsInOutputs(outputs) {
export function findTextsInOutputs(outputs, workflow) {
const list = [];
if (!outputs) return list;
for (const nodeId in outputs) {
if (isNodeIgnored(nodeId, workflow)) continue;
for (const key in outputs[nodeId]) {
const val = outputs[nodeId][key];
if (Array.isArray(val)) {
@@ -49,11 +66,11 @@ export function findTextsInOutputs(outputs) {
}
// Organizes raw outputs dictionaries into lists of individual node-level output blocks (ignoring text-only outputs)
export function getRunOutputs(nodeOutputs) {
export function getRunOutputs(nodeOutputs, workflow) {
const list = [];
if (!nodeOutputs) return list;
for (const nodeId in nodeOutputs) {
const imgs = findImagesInOutputs({ [nodeId]: nodeOutputs[nodeId] });
const imgs = findImagesInOutputs({ [nodeId]: nodeOutputs[nodeId] }, workflow);
if (imgs.length > 0) {
list.push({ nodeId, images: imgs });
}
+13
View File
@@ -0,0 +1,13 @@
[project]
name = "comfy-sidebar"
description = "A custom queue sidebar and image comparison extension that reintroduces and improves a sidebar queue menu and unified compact topbar layout for ComfyUI."
version = "1.2.7"
license = { file = "LICENSE" }
dependencies = []
[project.urls]
Repository = "https://github.com/m0rtus59/ComfyUI-ComfySidebar"
[tool.comfy]
PublisherId = "soundslikethunder"
DisplayName = "Comfy Sidebar"