Compare commits

...
7 Commits
12 changed files with 806 additions and 433 deletions
+24 -2
View File
@@ -1,4 +1,5 @@
name: Publish to Comfy registry
name: Publish to Comfy registry & GitHub Release
on:
workflow_dispatch:
push:
@@ -11,10 +12,31 @@ jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
permissions:
contents: write # Gives GitHub permission to create tags & releases
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 }}
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
# 1. Read version string from pyproject.toml
- name: Extract Version
id: get_version
run: |
VERSION=$(grep -m 1 'version' pyproject.toml | cut -d '"' -f 2)
echo "VERSION=$VERSION" >> $GITHUB_ENV
# 2. Automatically create Git Tag & GitHub Release
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.VERSION }}
name: v${{ env.VERSION }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -2
View File
@@ -37,6 +37,6 @@ See additional tweaks in ComfyUI settings menu > Comfy Sidebar. From there, you
## ⌨️ 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.
* **`Q`**: Toggle sidebar queue panel.
* **`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.
* **`Click`** on a card to enter preview. **`Shift+Click`** on another card in preview to enter compare mode.
+1 -5
View File
@@ -1,4 +1,3 @@
import { State } from "./state.js";
import { isVideoFormat } from "./utils.js";
let activeComparisonViewer = null;
@@ -32,7 +31,6 @@ const createMediaElement = (src, muted = false) => {
const setupVideoPlayback = (vid, container) => {
if (!vid) return () => {};
// Localized animation loop states to prevent cross-instance contamination
let videoSyncActive = true;
let syncAnimationFrameId = null;
@@ -193,7 +191,6 @@ function createComparisonViewer(baseSrc) {
});
container.appendChild(wrapper);
// 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",
@@ -206,13 +203,12 @@ function createComparisonViewer(baseSrc) {
}
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
wasDragging = false;
return;
}
destroy();
+27 -12
View File
@@ -7,7 +7,7 @@ async function uploadDroppedImageToInput(imageObj) {
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 file = new File([blob], imageObj.filename || "dropped_file", { type: blob.type });
const formData = new FormData();
formData.append("image", file);
formData.append("overwrite", "true");
@@ -17,7 +17,7 @@ async function uploadDroppedImageToInput(imageObj) {
const uploadData = await uploadRes.json();
return uploadData.name;
} catch(e) {
console.error("Comfy Sidebar: Failed to copy image to input.", e);
console.error("Comfy Sidebar: Failed to copy file to input.", e);
return null;
}
}
@@ -40,8 +40,8 @@ export function setupDragAndDrop() {
const canvas = app.canvas;
if (!canvas || !canvas.graph) return;
// 1. Check for standard workflow JSON data drop
const jsonStr = e.dataTransfer.getData("application/json") || e.dataTransfer.getData("text/plain");
// 1. Check for workflow JSON drop (text-only and unfinished cards)
const jsonStr = e.dataTransfer.getData("application/json");
if (jsonStr) {
try {
const workflow = JSON.parse(jsonStr);
@@ -59,7 +59,7 @@ export function setupDragAndDrop() {
} catch (err) {}
}
// 2. Check for native image URL drop
// 2. Check for image / video URL drop
const url = e.dataTransfer.getData("text/uri-list") || e.dataTransfer.getData("text/plain");
if (url) {
try {
@@ -77,28 +77,43 @@ export function setupDragAndDrop() {
if (canvas.convertEventToCanvasOffset) {
const pos = canvas.convertEventToCanvasOffset(e);
targetNode = canvas.graph.getNodeOnPos(pos[0], pos[1]);
} else {
} else if (canvas.canvas) {
const rect = canvas.canvas.getBoundingClientRect();
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"))) {
const widget = targetNode.widgets?.find(w => w.name === "image");
if (widget) {
// Check if dropped on a LoadImage, LoadVideo, VHS_LoadVideo, or image/video widget node
const widget = targetNode?.widgets?.find(w =>
w.name === "image" || w.name === "video" || w.name === "file" || w.name === "video_path" || w.type === "image" || w.type === "customtext"
);
const isLoadNode = targetNode && (widget || targetNode.type?.includes("LoadImage") || targetNode.type?.includes("LoadVideo") || targetNode.type?.includes("VHS_LoadVideo"));
if (isLoadNode) {
const targetWidget = widget || targetNode.widgets?.[0];
if (targetWidget) {
const newFilename = await uploadDroppedImageToInput({ filename, type, subfolder });
if (newFilename) {
widget.value = newFilename;
if (widget.callback) widget.callback(widget.value);
// Populate dropdown values list so combo widgets (like VHS_LoadVideo) accept the new video immediately!
if (targetWidget.options && Array.isArray(targetWidget.options.values)) {
if (!targetWidget.options.values.includes(newFilename)) {
targetWidget.options.values.push(newFilename);
}
}
targetWidget.value = newFilename;
if (targetWidget.callback) targetWidget.callback(targetWidget.value);
targetNode.imgs = null;
app.graph.setDirtyCanvas(true, true);
return;
}
}
} else {
// Dropped on empty canvas area: fetch file and load embedded workflow metadata
const src = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`;
const res = await fetch(src);
if (res.ok) {
const blob = await res.blob();
const file = new File([blob], filename || "workflow.png", { type: blob.type });
const file = new File([blob], filename, { type: blob.type });
if (app.handleFile) await app.handleFile(file);
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
+132 -94
View File
@@ -102,7 +102,6 @@ export function syncStockHistoryAndProgressSettings(enable) {
const dockedVal = !!enable;
const progressVal = !enable;
const QPOV2_ID = "Comfy.Queue.QPOV2";
try {
@@ -119,9 +118,7 @@ export function syncStockHistoryAndProgressSettings(enable) {
const isChecked = toggle.checked ||
toggle.getAttribute('data-p-checked') === 'true' ||
toggle.classList.contains('p-toggleswitch-checked');
if (isChecked !== dockedVal) {
toggle.click();
}
if (isChecked !== dockedVal) toggle.click();
}
}
} catch (e) {}
@@ -136,36 +133,29 @@ export function syncStockHistoryAndProgressSettings(enable) {
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();
}
if (isChecked !== progressVal) progressBarBtn.click();
}
} catch (e) {}
try {
window.dispatchEvent(new Event("storage"));
} 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 (updateSetting && app.ui && app.ui.settings) {
if (updateSetting && 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 (updateSetting && app.ui && app.ui.settings) {
if (updateSetting && app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
}
}
@@ -176,13 +166,22 @@ let domObserver = null;
let syncScheduled = false;
function findOriginalButton() {
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
if (icon) {
const icons = document.querySelectorAll('[class*="lucide--panel-right"], [class*="lucide--panel-left"], [class*="lucide--panel-bottom"]');
for (const icon of icons) {
const btn = icon.closest('button, [role="button"], .comfyui-menu-item, .p-button');
if (btn && !btn.classList.contains("comfy-sidebar-custom-properties-toggle")) {
return btn;
}
}
const buttons = document.querySelectorAll('button, [role="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("workflow overview") || title.includes("properties") || title.includes("toggle panel")) {
return btn;
}
}
return null;
}
@@ -191,13 +190,13 @@ function findTopbarContainer() {
if (container) return container;
const orig = findOriginalButton();
if (orig && orig.parentNode) return orig.parentNode;
if (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 (title.includes("run") || title.includes("manager")) {
if (btn.parentNode) return btn.parentNode;
}
}
@@ -205,10 +204,29 @@ function findTopbarContainer() {
}
function isPropertiesPanelOpen() {
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
if (!icon) return false;
const classStr = icon.getAttribute("class") || "";
return classStr.includes("lucide--panel-left");
const panel = document.querySelector('[data-testid="properties-panel"]');
if (panel && panel.offsetWidth > 0 && panel.offsetHeight > 0) {
return true;
}
const headings = document.querySelectorAll('h1, h2, h3, h4');
for (const h of headings) {
if (h.textContent.trim() === "Workflow Overview" && h.offsetWidth > 0 && h.offsetHeight > 0) {
return true;
}
}
const origBtn = findOriginalButton();
if (origBtn) {
if (origBtn.classList.contains("p-button-active") ||
origBtn.classList.contains("active") ||
origBtn.getAttribute("aria-expanded") === "true" ||
origBtn.getAttribute("aria-pressed") === "true") {
return true;
}
}
return false;
}
function findActiveQueueIndicator() {
@@ -218,7 +236,6 @@ function findActiveQueueIndicator() {
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;
@@ -236,7 +253,6 @@ function updateSidebarTabsVisibility() {
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
if (!sidebar) return;
// NodesMap removed from tabIconSelectors
const tabIconSelectors = {
"Assets": '[class*="comfy--image-ai-edit"]',
"Nodes": '[class*="comfy--node"]',
@@ -248,19 +264,13 @@ function updateSidebarTabsVisibility() {
Object.entries(tabIconSelectors).forEach(([tab, selector]) => {
const shouldHide = app.ui?.settings?.getSettingValue(`Comfy Sidebar.Hide Junk.${tab}`) ?? false;
const icon = sidebar.querySelector(selector);
if (icon) {
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");
}
if (tabBtn.style.display !== "none") tabBtn.style.setProperty("display", "none", "important");
} else {
if (tabBtn.style.display === "none") {
tabBtn.style.removeProperty("display");
}
if (tabBtn.style.display === "none") tabBtn.style.removeProperty("display");
}
}
});
@@ -270,13 +280,9 @@ function updateSidebarTabsVisibility() {
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");
}
if (historyBtn.style.display !== "none") historyBtn.style.setProperty("display", "none", "important");
} else {
if (historyBtn.style.display === "none") {
historyBtn.style.removeProperty("display");
}
if (historyBtn.style.display === "none") historyBtn.style.removeProperty("display");
}
}
}
@@ -284,17 +290,13 @@ function updateSidebarTabsVisibility() {
function findGraphButton() {
const elements = document.querySelectorAll('.bg-secondary-background.rounded-lg.items-center.inline-flex.pointer-events-auto');
for (const el of elements) {
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) {
continue;
}
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) continue;
return el;
}
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')) {
continue;
}
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) continue;
const text = (el.textContent || el.getAttribute("title") || "").toLowerCase();
if (text.includes("graph") || text.includes("workflow") || el.querySelector('[class*="sitemap"], [class*="workflow"]')) {
return el;
@@ -303,6 +305,32 @@ function findGraphButton() {
return null;
}
function syncErrorBadge(originalBtn, customBtn) {
if (!customBtn) return;
const stockDot = document.querySelector('.comfy-sidebar-hide-original-properties-btn ~ span, .comfy-sidebar-hide-original-properties-btn + span');
let customDot = customBtn.querySelector('.comfy-sidebar-error-dot');
if (stockDot) {
if (!customDot) {
customDot = document.createElement("span");
customDot.className = "comfy-sidebar-error-dot inline-flex items-center justify-center rounded-full bg-destructive-background text-white size-2 absolute -top-1 -right-1";
customBtn.appendChild(customDot);
}
} else if (customDot) {
customDot.remove();
}
}
function cleanHTML(html) {
if (!html) return "";
const tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
const tempDot = tempDiv.querySelector('[class*="destructive"]');
if (tempDot) tempDot.remove();
return tempDiv.innerHTML;
}
export function syncClassicLayout() {
if (domObserver) domObserver.disconnect();
@@ -313,13 +341,9 @@ export function syncClassicLayout() {
const graphBtn = findGraphButton();
if (graphBtn) {
if (hideGraphBtn) {
if (graphBtn.style.display !== "none") {
graphBtn.style.setProperty("display", "none", "important");
}
if (graphBtn.style.display !== "none") graphBtn.style.setProperty("display", "none", "important");
} else {
if (graphBtn.style.display === "none") {
graphBtn.style.removeProperty("display");
}
if (graphBtn.style.display === "none") graphBtn.style.removeProperty("display");
}
}
@@ -335,29 +359,23 @@ export function syncClassicLayout() {
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");
}
} 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")) {
if (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();
}
if (customBtn) customBtn.remove();
const indicator = findActiveQueueIndicator();
if (indicator && indicator.style.display === "none") {
indicator.style.removeProperty("display");
}
if (indicator && indicator.style.display === "none") indicator.style.removeProperty("display");
updateSidebarTabsVisibility();
return;
@@ -387,9 +405,8 @@ export function syncClassicLayout() {
if (!customBtn && savedButtonData && container) {
customBtn = document.createElement(savedButtonData.tagName);
customBtn.className = savedButtonData.className + " comfy-sidebar-custom-properties-toggle";
customBtn.innerHTML = savedButtonData.innerHTML;
customBtn.innerHTML = cleanHTML(savedButtonData.innerHTML);
for (const attr of savedButtonData.attributes) {
if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") {
@@ -397,28 +414,44 @@ export function syncClassicLayout() {
}
}
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();
if (nativeBtn) {
nativeBtn.click();
requestAnimationFrame(() => syncClassicLayout());
}
});
container.appendChild(customBtn);
}
if (customBtn) {
if (originalBtn) {
const cleanedHTML = cleanHTML(originalBtn.innerHTML);
if (cleanedHTML && customBtn.innerHTML !== cleanedHTML) {
const existingDot = customBtn.querySelector('.comfy-sidebar-error-dot');
customBtn.innerHTML = cleanedHTML;
if (existingDot) customBtn.appendChild(existingDot);
}
const titleVal = originalBtn.getAttribute("title") || originalBtn.getAttribute("aria-label") || "Toggle properties panel";
customBtn.setAttribute("title", titleVal);
customBtn.setAttribute("aria-label", titleVal);
for (const cls of originalBtn.classList) {
if (cls !== "comfy-sidebar-hide-original-properties-btn" && !customBtn.classList.contains(cls)) {
customBtn.classList.add(cls);
}
}
syncErrorBadge(originalBtn, customBtn);
} else if (!customBtn.getAttribute("title")) {
customBtn.setAttribute("title", "Toggle properties panel");
customBtn.setAttribute("aria-label", "Toggle properties panel");
}
customBtn.classList.toggle("comfy-panel-open", openState);
customBtn.style.display = "inline-flex";
}
@@ -427,13 +460,9 @@ export function syncClassicLayout() {
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");
}
if (indicator.style.display !== "none") indicator.style.setProperty("display", "none", "important");
} else if (indicator.style.display === "none") {
indicator.style.removeProperty("display");
}
}
@@ -459,8 +488,14 @@ export function setupPropertiesPanelToggleFix() {
.comfy-sidebar-hide-original-properties-btn {
display: none !important;
}
.comfy-sidebar-hide-original-properties-btn ~ span,
.comfy-sidebar-hide-original-properties-btn + span {
display: none !important;
}
.comfy-sidebar-custom-properties-toggle {
order: 99999 !important;
position: relative !important;
overflow: visible !important;
margin-left: -8px !important;
margin-right: 0px !important;
display: inline-flex !important;
@@ -469,30 +504,33 @@ export function setupPropertiesPanelToggleFix() {
padding: 0 !important;
}
.comfy-sidebar-custom-properties-toggle.comfy-panel-open {
order: 99999 !important;
margin-left: 0px !important;
margin-right: 0px !important;
}
.comfy-sidebar-custom-properties-toggle > span.comfy-sidebar-error-dot {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
}
`;
document.head.appendChild(style);
}
if (domObserver) {
domObserver.disconnect();
}
if (domObserver) domObserver.disconnect();
domObserver = new MutationObserver((mutations) => {
let shouldSync = false;
const isInternalSidebarMutation = (node) => {
if (!node || node.nodeType !== 1) return false;
return node.closest?.('.comfyui-sidebar, .comfy-sidebar, .comfy-sidebar-comparison-overlay') ||
node.classList?.contains('comfy-sidebar-card');
};
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
const hasElementMutation = Array.from(mutation.addedNodes).some(node => node.nodeType === 1) ||
Array.from(mutation.removedNodes).some(node => node.nodeType === 1);
const hasExternalElementMutation =
Array.from(mutation.addedNodes).some(node => node.nodeType === 1 && !isInternalSidebarMutation(node)) ||
Array.from(mutation.removedNodes).some(node => node.nodeType === 1 && !isInternalSidebarMutation(node));
if (hasElementMutation) {
if (hasExternalElementMutation) {
shouldSync = true;
break;
}
+118 -66
View File
@@ -1,7 +1,7 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { State, promptStates, pruneHistory, loadStatesFromLocalStorage, cardElements } from "./state.js";
import { findImagesInOutputs, findTextsInOutputs } from "./utils.js";
import { State, promptStates, pruneHistory, cardElements, saveStatesToLocalStorage } from "./state.js";
import { findImagesInOutputs, findTextsInOutputs, parseWorkflow, getPrimaryOutputImages } from "./utils.js";
export let renderDOMFn = () => {};
export let updateSidebarBadgeFn = () => {};
@@ -13,6 +13,7 @@ export function setUIDependencies(renderFn, badgeFn) {
export async function syncQueue() {
try {
const q = await api.getQueue();
const runningList = q.Running || q.queue_running || [];
const pendingList = q.Pending || q.queue_pending || [];
const pendingIds = new Set();
@@ -26,7 +27,7 @@ export async function syncQueue() {
pid = p.prompt_id || p.id || p.uuid;
seq = typeof p.number === 'number' ? p.number : (typeof p.prompt_number === 'number' ? p.prompt_number : idx);
}
return { pid, seq, original: p };
return { pid: pid ? String(pid) : null, seq, original: p };
});
normalizedPending.sort((a, b) => a.seq - b.seq);
@@ -65,16 +66,20 @@ export async function syncQueue() {
}
const concludeRun = async (pid, statusStr) => {
if (!pid || !promptStates.has(pid)) return;
if (State.currentlyActivePromptId === pid) State.currentlyActivePromptId = null;
const key = String(pid);
if (!key || !promptStates.has(key)) return;
if (State.currentlyActivePromptId === key) State.currentlyActivePromptId = null;
const st = promptStates.get(pid);
const st = promptStates.get(key);
// FIX: Revoke live preview blob URL to prevent browser memory leaks
if (st._previewBlobUrl) {
try { URL.revokeObjectURL(st._previewBlobUrl); } catch(e){}
delete st._previewBlobUrl;
}
if (st._oldPreviewBlobUrl) {
try { URL.revokeObjectURL(st._oldPreviewBlobUrl); } catch(e){}
delete st._oldPreviewBlobUrl;
}
st.status = statusStr;
st.progressText = "";
@@ -82,17 +87,28 @@ const concludeRun = async (pid, statusStr) => {
st.endTime = Date.now();
if (st.startTime) st.duration = (st.endTime - st.startTime) / 1000;
try {
const res = await fetch(`/history/${pid}`);
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;
if (st.images.length === 0) st.images = findImagesInOutputs(hItem[pid].outputs, st.workflow);
st.texts = findTextsInOutputs(hItem[pid].outputs, st.workflow);
}
} catch (err) {}
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(`/history/${key}`);
const hItem = await res.json();
if (hItem && hItem[key]) {
const rawWf = hItem[key].extra_data?.extra_pnginfo?.workflow || hItem[key].prompt?.[3]?.extra_pnginfo?.workflow || null;
st.workflow = parseWorkflow(rawWf) || st.workflow;
st.nodeOutputs = hItem[key].outputs;
const primaryImgs = getPrimaryOutputImages(hItem[key].outputs, st.workflow);
if (primaryImgs.length > 0) st.images = primaryImgs;
const fetchedTexts = findTextsInOutputs(hItem[key].outputs, st.workflow);
if (fetchedTexts.length > 0) st.texts = fetchedTexts;
break;
}
} catch (err) {}
await new Promise(r => setTimeout(r, 200));
}
pruneHistory(app);
saveStatesToLocalStorage();
syncQueue();
};
@@ -110,10 +126,11 @@ export function setupApiListeners() {
}
if (toDelete.length > 0) {
api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }).catch(()=>{});
saveStatesToLocalStorage();
}
}
const pid = e.detail.prompt_id;
const pid = String(e.detail.prompt_id);
State.currentlyActivePromptId = pid;
const activeWorkspaceWorkflow = app.graph.serialize();
@@ -134,10 +151,21 @@ export function setupApiListeners() {
});
api.addEventListener("progress", (e) => {
const pid = e.detail.prompt_id;
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
if (pid && promptStates.has(pid)) {
promptStates.get(pid).progress = Math.round((e.detail.value / e.detail.max) * 100);
renderDOMFn();
const st = promptStates.get(pid);
st.progress = Math.round((e.detail.value / e.detail.max) * 100);
const cardObj = cardElements.get(pid);
if (cardObj && cardObj.progressBar) {
cardObj.progressBar.style.width = `${st.progress}%`;
if (cardObj.statusText && cardObj.statusText.style.display !== "none") {
const nodeName = st.activeNodeName ? (st.activeNodeName === "Finishing..." ? "Finishing..." : `[${st.activeNodeName}]`) : "Sampling...";
cardObj.statusText.textContent = `${nodeName} ${st.progress}%`;
}
} else {
renderDOMFn();
}
}
});
@@ -161,23 +189,26 @@ export function setupApiListeners() {
const activeTasks = Array.from(promptStates.values()).filter(t => t.status === "active");
if (activeTasks.length > 0) {
const st = activeTasks[0];
if (st._previewBlobUrl) try { URL.revokeObjectURL(st._previewBlobUrl); } catch(e){}
st._previewBlobUrl = URL.createObjectURL(e.detail);
st.images = [{ url: st._previewBlobUrl }];
const newBlobUrl = URL.createObjectURL(e.detail);
st._oldPreviewBlobUrl = st._previewBlobUrl;
st._previewBlobUrl = newBlobUrl;
st.images = [{ url: newBlobUrl }];
renderDOMFn();
}
});
api.addEventListener("executed", (e) => {
if (promptStates.has(e.detail.prompt_id)) {
const st = promptStates.get(e.detail.prompt_id);
const finalImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (finalImgs.length > 0) {
st.images = finalImgs;
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
if (pid && promptStates.has(pid)) {
const st = promptStates.get(pid);
const nodeImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (nodeImgs.length > 0) {
st.images = nodeImgs;
}
const finalTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (finalTexts.length > 0) {
st.texts = finalTexts;
const nodeTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (nodeTexts.length > 0) {
st.texts = nodeTexts;
}
if (!st.nodeOutputs) st.nodeOutputs = {};
@@ -196,42 +227,63 @@ export function setupApiListeners() {
}
export async function initSessionAndHistory() {
let backendSessionId = null;
try {
const res = await fetch("/classic-sidebar/session");
const data = await res.json();
backendSessionId = data.session_id;
} catch (err) {
console.warn("Comfy Sidebar: Custom session endpoint not found. Queue persistence will fall back to cache.");
}
const historyData = await api.getHistory();
if (!historyData) return;
const storedSessionId = localStorage.getItem("comfy_sidebar_backend_session_id");
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 rawItems = [];
if (Array.isArray(historyData.History)) {
historyData.History.forEach(item => {
if (item && item.prompt_id) rawItems.push({ pid: String(item.prompt_id), data: item });
});
} else if (typeof historyData === 'object') {
Object.keys(historyData).forEach(pidKey => {
const item = historyData[pidKey];
rawItems.push({ pid: String(pidKey), data: Array.isArray(item) ? { prompt: item[0], outputs: item[1], status: item[2] } : item });
});
}
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 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,
workflow: workflow,
progressText: "", timestamp: State.globalOrderCounter, rendered: true
rawItems.sort((a, b) => {
const numA = a.data.prompt?.[0] ?? 0;
const numB = b.data.prompt?.[0] ?? 0;
return numA - numB;
});
});
pruneHistory(app);
await syncQueue();
rawItems.forEach(({ pid, data }) => {
if (promptStates.has(pid)) return;
const extraData = data.extra_data || data.prompt?.[3] || {};
const workflow = parseWorkflow(extraData.extra_pnginfo?.workflow || extraData.workflow);
const outputs = data.outputs || {};
const images = getPrimaryOutputImages(outputs, workflow);
const texts = findTextsInOutputs(outputs, workflow);
const statusStr = data.status?.status_str || "completed";
const status = (statusStr === "success" || statusStr === "completed") ? "completed" :
(statusStr === "error" ? "error" : "cancelled");
// Filter out empty records from history so dummy "No Outputs" cards don't pollute queue
if (images.length === 0 && texts.length === 0 && status === "completed") return;
State.globalOrderCounter++;
promptStates.set(pid, {
pid: pid,
status: status,
images: images,
texts: texts,
nodeOutputs: outputs,
workflow: workflow,
progressText: "",
timestamp: State.globalOrderCounter,
rendered: true
});
});
pruneHistory(app);
saveStatesToLocalStorage();
await syncQueue();
} catch (err) {
console.error("Comfy Sidebar: Failed to initialize history from server API", err);
}
}
+46 -30
View File
@@ -66,6 +66,32 @@ function syncNodeVueBadge(node, isIgnored) {
}
}
export function syncAllNodeBadges() {
applySidebarOverride();
if (!app.graph || !app.graph._nodes) return;
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";
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
} else {
if (node.boxcolor === "#ff3333") {
node.boxcolor = node._oldBoxcolor || "";
delete node._oldBoxcolor;
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
}
syncNodeVueBadge(node, isIgnored);
});
}
function toggleIgnoreActiveNode() {
const canvas = app.canvas;
if (!canvas) return;
@@ -113,7 +139,6 @@ app.registerExtension({
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 });
// NodesMap removed from sidebarTabs list
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"];
sidebarTabs.forEach(tab => {
app.ui.settings.addSetting({
@@ -190,6 +215,7 @@ app.registerExtension({
await initSessionAndHistory();
// Keyboard shortcuts listener
document.addEventListener("keydown", (e) => {
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable || activeEl.tagName === "SELECT")) return;
@@ -206,37 +232,27 @@ app.registerExtension({
}
}, true);
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");
// Event hooks for node additions and workflow loading (eliminates continuous polling loops)
if (app.graph) {
const originalConfigure = app.graph.configure;
app.graph.configure = function() {
const res = originalConfigure ? originalConfigure.apply(this, arguments) : undefined;
requestAnimationFrame(() => syncAllNodeBadges());
return res;
};
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);
});
const originalOnNodeAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function(node) {
if (originalOnNodeAdded) originalOnNodeAdded.apply(this, arguments);
if (node && node.properties?.ignoreInQueue) {
node.boxcolor = "#ff3333";
syncNodeVueBadge(node, true);
}
}
}, 500);
};
}
// Apply sidebar override when initializing
applySidebarOverride();
app.extensionManager.registerSidebarTab({
id: "classic-comfy-sidebar",
+61 -17
View File
@@ -5,12 +5,28 @@ export const State = {
currentSearchQuery: "",
globalOrderCounter: 0,
sidebarContainer: null,
cardStack: null,
cardStack: null, // Main queue scroll container
submenuStack: null, // Submenu scroll container
currentlyActivePromptId: null,
activeSubmenuPromptId: null, // Active run/outputs explorer pointer
activeSubmenuBatchImages: null // Active batch images explorer pointer
};
export function deletePromptState(pid) {
const key = String(pid);
const state = promptStates.get(key);
if (state) {
if (state._previewBlobUrl) {
try { URL.revokeObjectURL(state._previewBlobUrl); } catch (e) {}
}
if (state._oldPreviewBlobUrl) {
try { URL.revokeObjectURL(state._oldPreviewBlobUrl); } catch (e) {}
}
}
promptStates.delete(key);
cardElements.delete(key);
}
export function pruneHistory(app) {
const maxItems = app.ui.settings.getSettingValue("Comfy.Queue.MaxHistoryItems") ?? 64;
const tasks = Array.from(promptStates.entries())
@@ -22,34 +38,63 @@ export function pruneHistory(app) {
const deleteCount = tasks.length - maxItems;
for (let i = 0; i < deleteCount; i++) {
const [pid] = tasks[i];
promptStates.delete(pid);
cardElements.delete(pid);
deletePromptState(pid);
}
}
}
export function saveStatesToLocalStorage() {
try {
const serializable = [];
let serializable = [];
for (const [pid, state] of promptStates.entries()) {
const cleanedImages = (state.images || []).map(img => {
if (img.url && img.url.startsWith("blob:")) return null;
return img;
}).filter(Boolean);
// Include workflow JSON ONLY for image-less / unfinished / failed tasks
// so users can load or save their workflows even after browser reloads!
const includeWorkflow = cleanedImages.length === 0 || state.status !== "completed";
serializable.push({
pid: state.pid, status: state.status, images: cleanedImages, texts: state.texts || [],
pid: String(state.pid),
status: state.status,
images: cleanedImages,
texts: state.texts || [],
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,
startTime: state.startTime, endTime: state.endTime, duration: state.duration
workflow: includeWorkflow ? state.workflow : null,
progress: state.progress || 0,
queueNumber: state.queueNumber,
progressText: state.progressText || "",
timestamp: state.timestamp,
activeNodeName: state.activeNodeName || "",
rendered: state.rendered || false,
startTime: state.startTime,
endTime: state.endTime,
duration: state.duration
});
}
localStorage.setItem("comfy_sidebar_prompt_states", JSON.stringify(serializable));
} catch (e) {
console.error("Comfy Sidebar: Failed to save state to localStorage", e);
}
if (serializable.length === 0) {
localStorage.removeItem("comfy_sidebar_prompt_states");
return;
}
serializable.sort((a, b) => a.timestamp - b.timestamp);
while (serializable.length > 0) {
try {
localStorage.setItem("comfy_sidebar_prompt_states", JSON.stringify(serializable));
break;
} catch (e) {
if (e.name === "QuotaExceededError" || e.code === 22 || e.code === 1014) {
serializable.shift();
} else {
break;
}
}
}
} catch (e) {}
}
export function loadStatesFromLocalStorage() {
@@ -58,13 +103,12 @@ export function loadStatesFromLocalStorage() {
if (data) {
const list = JSON.parse(data);
list.forEach(state => {
promptStates.set(state.pid, state);
const key = String(state.pid);
promptStates.set(key, state);
if (state.timestamp > State.globalOrderCounter) State.globalOrderCounter = state.timestamp;
});
}
} catch (e) {
console.error("Comfy Sidebar: Failed to load state from localStorage", e);
}
} catch (e) {}
}
loadStatesFromLocalStorage();
+32 -1
View File
@@ -33,7 +33,7 @@ export function injectStyles() {
.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;
padding: 2px 4px; border-radius: 3px; pointerEvents: none; z-index: 5; color: #fff;
}
.comfy-sidebar-queue-cancel-btn {
@@ -55,6 +55,37 @@ export function injectStyles() {
background-color: #991b1b !important;
color: #ffffff !important;
}
/* Card Action Buttons */
.comfy-sidebar-card-action-btn {
display: inline-flex;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
background-color: rgba(0, 0, 0, 0.75) !important;
color: #e2e8f0 !important;
font-size: 14px !important;
border-radius: 6px !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
z-index: 20 !important;
transform: translateZ(0);
}
.comfy-sidebar-card-action-btn:hover {
background-color: rgba(0, 0, 0, 0.95) !important;
color: #ffffff !important;
}
/* Delete Confirmation Active State */
.comfy-sidebar-card-action-btn.confirm-delete,
.comfy-sidebar-card-action-btn.confirm-delete:hover {
background-color: #dc3545 !important;
color: #ffffff !important;
box-shadow: 0 0 8px rgba(220, 53, 69, 0.6) !important;
}
`;
document.head.appendChild(style);
}
+308 -178
View File
@@ -4,7 +4,6 @@ import { State, promptStates, cardElements, saveStatesToLocalStorage } from "./s
import { isVideoFormat, matchesFilter, getRunOutputs } from "./utils.js";
import { showFullscreenPreview } from "./comparison.js";
// DI Hook for cyclic imports
export let syncQueueFn = async () => {};
export function setSyncQueue(fn) { syncQueueFn = fn; }
@@ -15,6 +14,41 @@ function resetAllCardHoverStates() {
}
}
let scrollToTopBtnEl = null;
function getScrollContainer() {
if (!State.cardStack) return null;
return State.cardStack.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| State.cardStack.parentElement
|| State.cardStack;
}
function updateScrollTopBtnVisibility() {
if (!scrollToTopBtnEl || !State.sidebarContainer || !State.sidebarContainer.isConnected) return;
const scrollEl = getScrollContainer();
if (scrollEl) {
const viewportEl = scrollEl.parentElement || State.sidebarContainer;
if (viewportEl && scrollToTopBtnEl.parentNode !== viewportEl) {
if (window.getComputedStyle(viewportEl).position === "static") {
viewportEl.style.position = "relative";
}
viewportEl.appendChild(scrollToTopBtnEl);
}
if (scrollEl.scrollTop > 200) {
scrollToTopBtnEl.style.display = "flex";
} else {
scrollToTopBtnEl.style.display = "none";
}
}
}
document.addEventListener("scroll", () => {
if (State.sidebarContainer && State.sidebarContainer.isConnected) {
updateScrollTopBtnVisibility();
}
}, { capture: true, passive: true });
function findNodeIdForImage(state, img) {
if (!state || !state.nodeOutputs || !img) return null;
for (const nodeId in state.nodeOutputs) {
@@ -115,7 +149,7 @@ export function setupSidebarUI() {
State.sidebarContainer = document.createElement("div");
Object.assign(State.sidebarContainer.style, {
display: "flex", flexDirection: "column", height: "100%", padding: "14px", boxSizing: "border-box",
background: "var(--comfy-menu-bg, #121212)", color: "var(--fg-color, #eee)"
background: "var(--comfy-menu-bg, #121212)", color: "var(--fg-color, #eee)", position: "relative"
});
const header = document.createElement("div");
@@ -188,6 +222,7 @@ export function setupSidebarUI() {
}
if (toDelete.length > 0) {
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }); } catch (err) {}
saveStatesToLocalStorage();
renderDOM();
}
});
@@ -199,6 +234,7 @@ export function setupSidebarUI() {
}
}
try { await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ clear: true }) }); } catch (err) {}
saveStatesToLocalStorage();
renderDOM();
});
@@ -230,9 +266,43 @@ export function setupSidebarUI() {
searchInput.oninput = () => { State.currentSearchQuery = searchInput.value.trim(); renderDOM(); };
State.cardStack = document.createElement("div");
Object.assign(State.cardStack.style, { flex: "1", overflowY: "auto", scrollbarWidth: "thin", display: "block" });
Object.assign(State.cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" });
State.sidebarContainer.appendChild(State.cardStack);
scrollToTopBtnEl = document.createElement("button");
scrollToTopBtnEl.className = "pi pi-chevron-up";
scrollToTopBtnEl.title = "Scroll to Top";
Object.assign(scrollToTopBtnEl.style, {
position: "absolute", bottom: "12px", right: "12px", width: "32px", height: "32px",
borderRadius: "50%", background: "rgba(30, 30, 30, 0.9)", color: "#eee",
border: "1px solid var(--border-color, #555)", cursor: "pointer", fontSize: "13px",
display: "none", alignItems: "center", justifyContent: "center", zIndex: "999",
boxShadow: "0 4px 12px rgba(0,0,0,0.6)", transition: "all 0.15s ease-in-out", opacity: "0.85"
});
scrollToTopBtnEl.onmouseenter = () => {
scrollToTopBtnEl.style.opacity = "1";
scrollToTopBtnEl.style.background = "var(--p-primary-color, #3b82f6)";
scrollToTopBtnEl.style.color = "#fff";
scrollToTopBtnEl.style.borderColor = "var(--p-primary-color, #3b82f6)";
};
scrollToTopBtnEl.onmouseleave = () => {
scrollToTopBtnEl.style.opacity = "0.85";
scrollToTopBtnEl.style.background = "rgba(30, 30, 30, 0.9)";
scrollToTopBtnEl.style.color = "#eee";
scrollToTopBtnEl.style.borderColor = "var(--border-color, #555)";
};
scrollToTopBtnEl.onclick = (e) => {
e.stopPropagation();
const scrollEl = getScrollContainer();
if (scrollEl) {
scrollEl.scrollTo({ top: 0, behavior: "smooth" });
}
};
State.sidebarContainer.appendChild(scrollToTopBtnEl);
State.sidebarContainer.onclick = (e) => {
if (State.activeSubmenuPromptId || State.activeSubmenuBatchImages) {
if (e.target.closest('img, video, .comfy-sidebar-card-timer, .pi-times, .comfy-sidebar-left-hover-btn, .comfy-sidebar-queue-cancel-btn, button, span')) return;
@@ -248,11 +318,12 @@ export function setupSidebarUI() {
new ResizeObserver((entries) => {
const threshold = app.ui.settings.getSettingValue("Comfy Sidebar.Grid Columns Threshold") ?? 350;
const cols = Math.max(1, Math.floor(entries[0].contentRect.width / (threshold / 2)));
State.cardStack.style.columnCount = cols.toString();
State.cardStack.style.columnGap = cols > 1 ? "12px" : "0";
if (State.cardStack) {
State.cardStack.style.columnCount = cols.toString();
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()) {
@@ -261,11 +332,125 @@ export function setupSidebarUI() {
if (cardObj && cardObj.timerEl) cardObj.timerEl.textContent = ((Date.now() - state.startTime) / 1000).toFixed(2) + "s";
}
}
}, 100);
}, 250);
return State.sidebarContainer;
}
function syncCardButtonVisibility(cardObj, state) {
if (!cardObj) return;
if (state.status === "active" || state.status === "pending") {
if (cardObj.hoverPanel) cardObj.hoverPanel.style.setProperty("display", "none", "important");
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.setProperty("display", "none", "important");
return;
}
const isCompleted = state.status === "completed";
const hasRealImages = isCompleted && state.images && state.images.length > 0 && !state.images.some(img => img.url && img.url.startsWith("blob:"));
if (cardObj.btnImg) {
if (hasRealImages) {
cardObj.btnImg.style.removeProperty("display");
cardObj.btnImg.style.display = "inline-flex";
cardObj.btnImg.onclick = (ev) => {
ev.stopPropagation();
state.images.forEach(img => {
const a = document.createElement("a");
a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
a.download = img.filename || "output";
a.click();
});
};
} else {
cardObj.btnImg.style.setProperty("display", "none", "important");
}
}
if (cardObj.btnJson) {
if (state.workflow) {
cardObj.btnJson.style.removeProperty("display");
cardObj.btnJson.style.display = "inline-flex";
cardObj.btnJson.onclick = (ev) => {
ev.stopPropagation();
const blob = new Blob([JSON.stringify(state.workflow, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `workflow_${state.pid}.json`;
a.click();
URL.revokeObjectURL(url);
};
} else {
cardObj.btnJson.style.setProperty("display", "none", "important");
}
}
if (cardObj.btnFocus) {
const currentImg = hasRealImages ? state.images[cardObj.currentImageIndex || 0] : null;
const nodeId = currentImg ? findNodeIdForImage(state, currentImg) : null;
if (nodeId) {
cardObj.btnFocus.style.removeProperty("display");
cardObj.btnFocus.style.display = "inline-flex";
cardObj.btnFocus.onclick = (ev) => {
ev.stopPropagation();
const node = app.graph.getNodeById(Number(nodeId));
if (node) {
app.canvas.centerOnNode(node);
app.canvas.selectNode(node);
}
};
} else {
cardObj.btnFocus.style.setProperty("display", "none", "important");
}
}
if (cardObj.leftHoverBtn) {
const outputs = getRunOutputs(state.nodeOutputs, state.workflow);
if (outputs.length > 1) {
cardObj.leftHoverBtn.style.removeProperty("display");
cardObj.leftHoverBtn.style.display = "inline-flex";
cardObj.leftHoverBtn.onclick = (ev) => {
ev.stopPropagation();
if (!State.activeSubmenuBatchImages && !State.activeSubmenuPromptId) {
const scrollEl = getScrollContainer();
if (scrollEl) State.mainQueueScrollTop = scrollEl.scrollTop;
}
State.activeSubmenuPromptId = state.pid;
resetAllCardHoverStates();
renderDOM();
};
} else {
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
}
}
const hasRightButtons = (cardObj.btnImg && cardObj.btnImg.style.display !== "none") ||
(cardObj.btnJson && cardObj.btnJson.style.display !== "none") ||
(cardObj.btnDel && cardObj.btnDel.style.display !== "none");
const hasLeftButtons = (cardObj.btnFocus && cardObj.btnFocus.style.display !== "none") ||
(cardObj.leftHoverBtn && cardObj.leftHoverBtn.style.display !== "none");
if (cardObj.hoverPanel) {
if (hasRightButtons) {
cardObj.hoverPanel.style.removeProperty("display");
cardObj.hoverPanel.style.display = "flex";
} else {
cardObj.hoverPanel.style.setProperty("display", "none", "important");
}
}
if (cardObj.leftHoverPanel) {
if (hasLeftButtons) {
cardObj.leftHoverPanel.style.removeProperty("display");
cardObj.leftHoverPanel.style.display = "flex";
} else {
cardObj.leftHoverPanel.style.setProperty("display", "none", "important");
}
}
}
function renderCardImages(cardObj, state, keepAspect) {
cardObj.currentImageIndex = cardObj.currentImageIndex || 0;
if (cardObj.currentImageIndex >= state.images.length) {
@@ -327,12 +512,51 @@ function renderCardImages(cardObj, state, keepAspect) {
}
};
const isUnfinished = state.status && state.status !== "completed";
if (isUnfinished) {
mediaEl.setAttribute("draggable", "false");
mediaEl.style.cursor = "grab";
if (mediaEl._currentDragStart) {
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
delete mediaEl._currentDragStart;
}
} else {
mediaEl.setAttribute("draggable", "true");
mediaEl.style.cursor = "zoom-in";
if (isVideo) {
const fullSrc = src.startsWith("http") ? src : window.location.origin + src;
const filename = img.filename || "output.mp4";
const mimeType = src.includes(".webm") ? "video/webm" : "video/mp4";
const videoDragHandler = (e) => {
try {
e.dataTransfer.setData("text/uri-list", fullSrc);
e.dataTransfer.setData("text/plain", fullSrc);
e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullSrc}`);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
e.stopPropagation();
};
if (mediaEl._currentDragStart) {
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
}
mediaEl.addEventListener("dragstart", videoDragHandler);
mediaEl._currentDragStart = videoDragHandler;
} else {
if (mediaEl._currentDragStart) {
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
delete mediaEl._currentDragStart;
}
}
}
if (isVideo) {
mediaEl.muted = true;
mediaEl.playsInline = true;
mediaEl.preload = "metadata";
mediaEl.loop = true;
mediaEl.setAttribute("draggable", "false");
mediaEl.onloadedmetadata = () => {
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
@@ -361,31 +585,12 @@ function renderCardImages(cardObj, state, keepAspect) {
mediaEl.onmouseenter = () => { playIcon.style.opacity = "0"; mediaEl.play().catch(()=>{}); };
mediaEl.onmouseleave = () => { playIcon.style.opacity = "1"; mediaEl.pause(); };
} else {
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 (state._oldPreviewBlobUrl) {
try { URL.revokeObjectURL(state._oldPreviewBlobUrl); } catch(e){}
delete state._oldPreviewBlobUrl;
}
};
if (mediaEl.complete && mediaEl.naturalWidth) {
applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
@@ -404,7 +609,7 @@ function renderCardImages(cardObj, state, keepAspect) {
mediaEl.src = src;
mediaEl._lastBlob = src;
applyDimensions(tempImg.naturalWidth, tempImg.naturalHeight);
if (oldBlob && oldBlob !== src) {
if (oldBlob && oldBlob !== src && oldBlob.startsWith("blob:")) {
try { URL.revokeObjectURL(oldBlob); } catch(e){}
}
};
@@ -450,6 +655,10 @@ function renderCardImages(cardObj, state, keepAspect) {
label.onclick = (ev) => {
ev.stopPropagation();
if (!State.activeSubmenuBatchImages && !State.activeSubmenuPromptId) {
const scrollEl = getScrollContainer();
if (scrollEl) State.mainQueueScrollTop = scrollEl.scrollTop;
}
State.activeSubmenuBatchImages = {
pid: state.pid || cardObj.element.id.replace("card-", ""),
images: state.images,
@@ -514,25 +723,7 @@ export function renderDOM() {
const headerSearchIcon = State.sidebarContainer.querySelector(".pi-search");
const headerActions = State.sidebarContainer.querySelector(".pi-eraser")?.parentNode;
const styleActionBtn = (btn) => {
Object.assign(btn.style, {
display: "inline-flex",
alignItems: "center", justifyContent: "center",
width: "32px", height: "32px",
backgroundColor: "rgba(0, 0, 0, 0.75)",
color: "#e2e8f0",
fontSize: "14px",
borderRadius: "6px",
cursor: "pointer",
transition: "all 0.15s ease",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.3)",
zIndex: "20",
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 1: Batch Submenu View
if (State.activeSubmenuBatchImages) {
const batchInfo = State.activeSubmenuBatchImages;
@@ -540,7 +731,11 @@ export function renderDOM() {
headerTitle.textContent = `Batch of #${batchInfo.pid}`;
headerTitle.style.cursor = "pointer";
headerTitle.title = "Go Back to Queue";
headerTitle.onclick = () => { State.activeSubmenuBatchImages = null; resetAllCardHoverStates(); renderDOM(); };
headerTitle.onclick = () => {
State.activeSubmenuBatchImages = null;
resetAllCardHoverStates();
renderDOM();
};
}
if (headerSearchIcon) headerSearchIcon.style.display = "none";
if (headerActions) headerActions.style.display = "none";
@@ -590,9 +785,8 @@ export function renderDOM() {
});
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image";
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
styleActionBtn(btnImg);
hoverPanel.appendChild(btnImg);
const leftHoverPanel = document.createElement("div");
@@ -602,9 +796,8 @@ export function renderDOM() {
});
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
leftHoverPanel.appendChild(btnFocus);
card.append(timerEl, dimEl, grid, p, hoverPanel, leftHoverPanel);
@@ -612,8 +805,7 @@ export function renderDOM() {
cardElements.set(cardId, cardObj);
card.addEventListener("mouseenter", () => {
cardObj.hoverPanel.style.display = "flex";
cardObj.leftHoverPanel.style.display = "flex";
syncCardButtonVisibility(cardObj, { status: "completed", images: [img], workflow: batchInfo.workflow, nodeOutputs: batchInfo.nodeOutputs });
});
card.addEventListener("mouseleave", () => {
cardObj.hoverPanel.style.display = "none";
@@ -623,30 +815,6 @@ export function renderDOM() {
cardObj.placeholder.style.display = "none";
cardObj.btnImg.style.display = "inline-flex";
cardObj.btnImg.onclick = (ev) => {
ev.stopPropagation();
const a = document.createElement("a");
a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`;
a.download = img.filename || "output";
a.click();
};
const nodeId = findNodeIdForImage(batchInfo, img);
if (nodeId) {
cardObj.btnFocus.style.display = "inline-flex";
cardObj.btnFocus.onclick = (ev) => {
ev.stopPropagation();
const node = app.graph.getNodeById(Number(nodeId));
if (node) {
app.canvas.centerOnNode(node);
app.canvas.selectNode(node);
}
};
} else {
cardObj.btnFocus.style.display = "none";
}
renderCardImages(cardObj, { pid: batchInfo.pid, images: [img], workflow: batchInfo.workflow, nodeOutputs: batchInfo.nodeOutputs }, keepAspect);
targetElements.push(cardObj.element);
@@ -654,9 +822,14 @@ export function renderDOM() {
targetElements.forEach((el, index) => { if (State.cardStack.children[index] !== el) State.cardStack.insertBefore(el, State.cardStack.children[index] || null); });
while (State.cardStack.children.length > targetElements.length) State.cardStack.removeChild(State.cardStack.lastChild);
const scrollEl = getScrollContainer();
if (scrollEl) scrollEl.scrollTop = 0;
updateScrollTopBtnVisibility();
return;
}
// Submenu 2: Intermediate Outputs View
if (State.activeSubmenuPromptId) {
const st = promptStates.get(State.activeSubmenuPromptId);
if (!st) {
@@ -669,7 +842,11 @@ export function renderDOM() {
headerTitle.textContent = `Outputs of #${State.activeSubmenuPromptId}`;
headerTitle.style.cursor = "pointer";
headerTitle.title = "Go Back to Queue";
headerTitle.onclick = () => { State.activeSubmenuPromptId = null; resetAllCardHoverStates(); renderDOM(); };
headerTitle.onclick = () => {
State.activeSubmenuPromptId = null;
resetAllCardHoverStates();
renderDOM();
};
}
if (headerSearchIcon) headerSearchIcon.style.display = "none";
if (headerActions) headerActions.style.display = "none";
@@ -721,9 +898,8 @@ export function renderDOM() {
});
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image";
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
styleActionBtn(btnImg);
hoverPanel.appendChild(btnImg);
const leftHoverPanel = document.createElement("div");
@@ -733,9 +909,8 @@ export function renderDOM() {
});
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
leftHoverPanel.appendChild(btnFocus);
card.append(timerEl, dimEl, grid, p, hoverPanel, leftHoverPanel);
@@ -743,8 +918,7 @@ export function renderDOM() {
cardElements.set(cardId, cardObj);
card.addEventListener("mouseenter", () => {
cardObj.hoverPanel.style.display = "flex";
cardObj.leftHoverPanel.style.display = "flex";
syncCardButtonVisibility(cardObj, { status: "completed", images: out.images, workflow: st.workflow, nodeOutputs: st.nodeOutputs });
});
card.addEventListener("mouseleave", () => {
cardObj.hoverPanel.style.display = "none";
@@ -754,30 +928,8 @@ export function renderDOM() {
if (out.images && out.images.length > 0) {
cardObj.placeholder.style.display = "none";
cardObj.btnImg.style.display = "inline-flex";
cardObj.btnImg.onclick = (ev) => {
ev.stopPropagation();
out.images.forEach(img => {
const a = document.createElement("a");
a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`;
a.download = img.filename || "output";
a.click();
});
};
cardObj.btnFocus.onclick = (ev) => {
ev.stopPropagation();
const node = app.graph.getNodeById(Number(out.nodeId));
if (node) {
app.canvas.centerOnNode(node);
app.canvas.selectNode(node);
}
};
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";
cardObj.placeholder.textContent = "No Outputs";
}
@@ -787,9 +939,14 @@ export function renderDOM() {
targetElements.forEach((el, index) => { if (State.cardStack.children[index] !== el) State.cardStack.insertBefore(el, State.cardStack.children[index] || null); });
while (State.cardStack.children.length > targetElements.length) State.cardStack.removeChild(State.cardStack.lastChild);
const scrollEl = getScrollContainer();
if (scrollEl) scrollEl.scrollTop = 0;
updateScrollTopBtnVisibility();
return;
}
// Standard Queue Main View
if (headerTitle) {
headerTitle.textContent = "Queue";
headerTitle.style.cursor = "default";
@@ -810,7 +967,10 @@ export function renderDOM() {
const syncCardElement = (state) => {
let cardObj = cardElements.get(state.pid);
const isFinalStatus = state.status === "completed" || state.status === "cancelled" || state.status === "error";
if (cardObj && isFinalStatus && state.rendered) return cardObj.element;
if (cardObj && isFinalStatus && state.rendered) {
syncCardButtonVisibility(cardObj, state);
return cardObj.element;
}
if (!cardObj) {
const card = document.createElement("div");
@@ -843,9 +1003,9 @@ export function renderDOM() {
flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnImg = document.createElement("span"); btnImg.className = "pi pi-image"; btnImg.title = "Download Object"; styleActionBtn(btnImg);
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);
const btnImg = document.createElement("span"); btnImg.className = "pi pi-image comfy-sidebar-card-action-btn"; btnImg.title = "Download Object";
const btnJson = document.createElement("span"); btnJson.className = "pi pi-file comfy-sidebar-card-action-btn"; btnJson.title = "Download JSON";
const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn"; btnDel.title = "Delete Card";
const leftHoverPanel = document.createElement("div");
Object.assign(leftHoverPanel.style, {
@@ -854,21 +1014,12 @@ export function renderDOM() {
});
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
const leftHoverBtn = document.createElement("span");
leftHoverBtn.className = "pi pi-images";
leftHoverBtn.className = "pi pi-images comfy-sidebar-card-action-btn";
leftHoverBtn.title = "View all intermediate outputs";
styleActionBtn(leftHoverBtn);
leftHoverBtn.onclick = (ev) => {
ev.stopPropagation();
State.activeSubmenuPromptId = state.pid;
resetAllCardHoverStates();
renderDOM();
};
hoverPanel.append(btnImg, btnJson, btnDel);
leftHoverPanel.append(btnFocus, leftHoverBtn);
@@ -878,34 +1029,7 @@ export function renderDOM() {
cardObj = { element: card, timerEl, statusBadge: sBadge, grid, placeholder: p, progressContainer: pt, progressBar: pb, cancelBtn: cancelX, hoverPanel, leftHoverPanel, btnFocus, leftHoverBtn, btnImg, btnJson, btnDel, statusText, firstImgElement: null, lastImagesSignature: "" };
card.addEventListener("mouseenter", () => {
if (state.status !== "active" && state.status !== "pending") {
cardObj.hoverPanel.style.display = "flex";
cardObj.leftHoverPanel.style.display = "flex";
const currentImg = state.images[cardObj.currentImageIndex || 0];
const nodeId = findNodeIdForImage(state, currentImg);
if (nodeId) {
cardObj.btnFocus.style.display = "inline-flex";
cardObj.btnFocus.onclick = (ev) => {
ev.stopPropagation();
const node = app.graph.getNodeById(Number(nodeId));
if (node) {
app.canvas.centerOnNode(node);
app.canvas.selectNode(node);
}
};
} else {
cardObj.btnFocus.style.display = "none";
}
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";
}
}
syncCardButtonVisibility(cardObj, state);
});
card.addEventListener("mouseleave", () => {
cardObj.hoverPanel.style.display = "none";
@@ -914,12 +1038,18 @@ export function renderDOM() {
card.addEventListener("dragstart", (e) => {
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 hasNoImages = !state.images || state.images.length === 0;
if (state.workflow && (hasNoImages || isUnfinished)) {
if (cardObj.firstImgElement) {
try { e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15); } catch(err){}
}
const jsonStr = JSON.stringify(state.workflow, null, 2);
const filename = `workflow_${state.pid}.json`;
const base64Data = btoa(unescape(encodeURIComponent(jsonStr)));
try {
e.dataTransfer.setData("DownloadURL", `application/json:workflow_${state.pid}.json:data:application/json;base64,` + btoa(unescape(encodeURIComponent(jsonStr))));
e.dataTransfer.setData("text/plain", jsonStr);
e.dataTransfer.setData("DownloadURL", `application/json:${filename}:data:application/json;base64,${base64Data}`);
e.dataTransfer.setData("application/json", jsonStr);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
@@ -929,6 +1059,8 @@ export function renderDOM() {
cardElements.set(state.pid, cardObj);
}
syncCardButtonVisibility(cardObj, state);
cardObj.element.className = `comfy-sidebar-card ${state.status}`;
const isUnfinished = state.status && state.status !== "completed";
@@ -969,23 +1101,10 @@ export function renderDOM() {
cardObj.cancelBtn.style.display = "none";
}
if (state.images && state.images.length > 0) {
cardObj.btnImg.style.display = "inline-flex";
cardObj.btnImg.onclick = (ev) => { ev.stopPropagation(); state.images.forEach(img => { const a = document.createElement("a"); a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`; a.download = img.filename || "output"; a.click(); }); };
} else cardObj.btnImg.style.display = "none";
if (state.workflow) {
cardObj.btnJson.style.display = "inline-flex";
cardObj.btnJson.onclick = (ev) => { ev.stopPropagation(); const blob = new Blob([JSON.stringify(state.workflow, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `workflow_${state.pid}.json`; a.click(); URL.revokeObjectURL(url); };
} else cardObj.btnJson.style.display = "none";
let deleteTimeout = null, isDeletePending = false;
const resetDeleteBtn = () => {
isDeletePending = false;
Object.assign(cardObj.btnDel.style, {
color: "#e2e8f0",
backgroundColor: "rgba(0, 0, 0, 0.75)"
});
cardObj.btnDel.classList.remove("confirm-delete");
cardObj.btnDel.title = "Delete Card";
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
@@ -994,16 +1113,14 @@ export function renderDOM() {
ev.stopPropagation();
if (!isDeletePending) {
isDeletePending = true;
Object.assign(cardObj.btnDel.style, {
color: "#fff",
backgroundColor: "#dc3545"
});
cardObj.btnDel.classList.add("confirm-delete");
cardObj.btnDel.title = "Click again to confirm deletion";
deleteTimeout = setTimeout(resetDeleteBtn, 1000);
deleteTimeout = setTimeout(resetDeleteBtn, 1500);
} else {
resetDeleteBtn();
promptStates.delete(state.pid);
await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ delete: [state.pid] }) });
saveStatesToLocalStorage();
renderDOM();
}
};
@@ -1073,6 +1190,19 @@ export function renderDOM() {
targetElements.forEach((el, index) => { if (State.cardStack.children[index] !== el) State.cardStack.insertBefore(el, State.cardStack.children[index] || null); });
while (State.cardStack.children.length > targetElements.length) State.cardStack.removeChild(State.cardStack.lastChild);
saveStatesToLocalStorage();
if (State.mainQueueScrollTop !== null) {
const restorePos = State.mainQueueScrollTop;
State.mainQueueScrollTop = null;
const scrollEl = getScrollContainer();
if (scrollEl) {
scrollEl.scrollTop = restorePos;
requestAnimationFrame(() => {
scrollEl.scrollTop = restorePos;
});
}
}
updateScrollTopBtnVisibility();
});
}
+54 -25
View File
@@ -1,44 +1,65 @@
import { app } from "/scripts/app.js";
export const isVideoFormat = (url) => {
const s = url.toLowerCase();
if (!url) return false;
const s = String(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);
}
export function parseWorkflow(workflow) {
if (!workflow) return null;
if (typeof workflow === "object") return workflow;
if (typeof workflow === "string") {
try { return JSON.parse(workflow); } catch (e) { return null; }
}
return null;
}
function isNodeIgnored(nodeId, rawWorkflow) {
const workflow = parseWorkflow(rawWorkflow);
if (!workflow || !Array.isArray(workflow.nodes)) {
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, workflow) {
export function findImagesInOutputs(outputs, rawWorkflow) {
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)) {
val.forEach(item => {
if (item && typeof item === 'object' && item.filename) list.push(item);
});
} else if (val && typeof val === 'object' && val.filename) {
list.push(val);
const scan = (obj) => {
if (!obj) return;
if (Array.isArray(obj)) {
obj.forEach(item => scan(item));
} else if (typeof obj === "object") {
if (obj.filename || obj.name) {
const fname = obj.filename || obj.name;
if (typeof fname === "string" && (fname.includes(".") || obj.type)) {
list.push({
filename: fname,
subfolder: obj.subfolder || "",
type: obj.type || "output"
});
return;
}
}
for (const key in obj) {
scan(obj[key]);
}
}
};
const workflow = parseWorkflow(rawWorkflow);
for (const nodeId in outputs) {
if (isNodeIgnored(nodeId, workflow)) continue;
scan(outputs[nodeId]);
}
return list;
}
export function findTextsInOutputs(outputs, workflow) {
export function findTextsInOutputs(outputs, rawWorkflow) {
const workflow = parseWorkflow(rawWorkflow);
const list = [];
if (!outputs) return list;
for (const nodeId in outputs) {
@@ -65,8 +86,8 @@ export function findTextsInOutputs(outputs, workflow) {
return list;
}
// Organizes raw outputs dictionaries into lists of individual node-level output blocks (ignoring text-only outputs)
export function getRunOutputs(nodeOutputs, workflow) {
export function getRunOutputs(nodeOutputs, rawWorkflow) {
const workflow = parseWorkflow(rawWorkflow);
const list = [];
if (!nodeOutputs) return list;
for (const nodeId in nodeOutputs) {
@@ -78,6 +99,13 @@ export function getRunOutputs(nodeOutputs, workflow) {
return list;
}
export function getPrimaryOutputImages(nodeOutputs, rawWorkflow) {
if (!nodeOutputs) return [];
const runOutputs = getRunOutputs(nodeOutputs, rawWorkflow);
if (runOutputs.length === 0) return [];
return runOutputs[runOutputs.length - 1].images || [];
}
export function matchesFilter(state, query) {
if (!query) return true;
const q = query.toLowerCase();
@@ -87,8 +115,9 @@ export function matchesFilter(state, query) {
if (state.images && state.images.some(img => (img.filename || "").toLowerCase().includes(q))) return true;
if (state.activeNodeName && state.activeNodeName.toLowerCase().includes(q)) return true;
if (state.workflow && Array.isArray(state.workflow.nodes)) {
for (const node of state.workflow.nodes) {
const workflow = parseWorkflow(state.workflow);
if (workflow && Array.isArray(workflow.nodes)) {
for (const node of workflow.nodes) {
if (node.title && node.title.toLowerCase().includes(q)) return true;
if (node.type && node.type.toLowerCase().includes(q)) return true;
if (Array.isArray(node.widgets)) {
+1 -1
View File
@@ -1,7 +1,7 @@
[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"
version = "1.2.10"
license = { file = "LICENSE" }
dependencies = []