Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f09e2c7921 | ||
|
|
9adfb2a53f | ||
|
|
a17314673e |
@@ -5,6 +5,7 @@ import { PromptStatus } from "../core/constants.js";
|
||||
import { createPromptState, activatePrompt, updatePromptProgress } from "../core/promptState.js";
|
||||
import { syncQueue, concludeRun, initSessionAndHistory, clearCancelledOrFailed } from "./queueService.js";
|
||||
import { findImagesInOutputs, findTextsInOutputs } from "../utils/utils.js";
|
||||
import { updateActiveComparisonPreview } from "../utils/comparison.js";
|
||||
|
||||
export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
const onStatus = () => syncQueue();
|
||||
@@ -68,9 +69,10 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
const showWorkingNode = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Show Working Node Name") ?? true;
|
||||
const activePid = store.ui.currentlyActivePromptId;
|
||||
|
||||
if (showWorkingNode && activePid && store.hasPrompt(activePid)) {
|
||||
if (activePid && store.hasPrompt(activePid)) {
|
||||
const prompt = store.getPrompt(activePid);
|
||||
if (nodeId) {
|
||||
prompt.activeNodeId = String(nodeId);
|
||||
const node = app.graph?.getNodeById ? app.graph.getNodeById(nodeId) : null;
|
||||
prompt.activeNodeName = node ? (node.title || node.type) : `Node #${nodeId}`;
|
||||
} else {
|
||||
@@ -78,7 +80,7 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
}
|
||||
store.updatePrompt(activePid, prompt);
|
||||
|
||||
if (typeof onTargetedProgressUpdate === "function") {
|
||||
if (showWorkingNode && typeof onTargetedProgressUpdate === "function") {
|
||||
onTargetedProgressUpdate(activePid, prompt.progress, prompt.activeNodeName);
|
||||
}
|
||||
}
|
||||
@@ -94,6 +96,14 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
prompt._previewBlobUrl = newBlobUrl;
|
||||
prompt.images = [{ url: newBlobUrl }];
|
||||
store.updatePrompt(prompt.pid, prompt);
|
||||
|
||||
// Update live fullscreen preview if currently open
|
||||
updateActiveComparisonPreview(prompt.pid, newBlobUrl);
|
||||
|
||||
// Update the active card in the sidebar
|
||||
if (typeof onTargetedProgressUpdate === "function") {
|
||||
onTargetedProgressUpdate(prompt.pid, prompt.progress, prompt.activeNodeName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,8 +112,14 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
if (pid && store.hasPrompt(pid)) {
|
||||
const prompt = store.getPrompt(pid);
|
||||
const nodeImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow);
|
||||
if (nodeImgs.length > 0) {
|
||||
// Only update prompt.images if this node actually produced real output images
|
||||
const hasRealNewImgs = nodeImgs.some(img => !img.isFallback);
|
||||
if (hasRealNewImgs) {
|
||||
prompt.images = nodeImgs;
|
||||
// If fullscreen is open, smoothly transition to the final saved image
|
||||
const finalImg = nodeImgs[0];
|
||||
const finalSrc = finalImg.url || (window.location.origin + `/view?filename=${encodeURIComponent(finalImg.filename)}&type=${finalImg.type || 'output'}&subfolder=${encodeURIComponent(finalImg.subfolder || '')}`);
|
||||
updateActiveComparisonPreview(pid, finalSrc);
|
||||
}
|
||||
const nodeTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow);
|
||||
if (nodeTexts.length > 0) {
|
||||
@@ -118,7 +134,16 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
};
|
||||
|
||||
const onExecutionSuccess = (e) => concludeRun(e.detail.prompt_id, PromptStatus.COMPLETED);
|
||||
const onExecutionError = (e) => concludeRun(e.detail.prompt_id, PromptStatus.ERROR);
|
||||
const onExecutionError = (e) => {
|
||||
const pid = e.detail?.prompt_id;
|
||||
const nodeId = e.detail?.node_id;
|
||||
if (pid && nodeId && store.hasPrompt(String(pid))) {
|
||||
const prompt = store.getPrompt(String(pid));
|
||||
prompt.activeNodeId = String(nodeId);
|
||||
store.updatePrompt(String(pid), prompt);
|
||||
}
|
||||
concludeRun(e.detail.prompt_id, PromptStatus.ERROR);
|
||||
};
|
||||
const onExecutionInterrupted = () => {
|
||||
store.getAllPrompts()
|
||||
.filter(t => t.status === PromptStatus.ACTIVE)
|
||||
|
||||
@@ -128,6 +128,10 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
|
||||
const state = store.getPrompt(key);
|
||||
finalizePrompt(state, fallbackStatus);
|
||||
|
||||
// If cancelled while actively sampling, preserve the exact step preview where it was cancelled
|
||||
const isCancelled = fallbackStatus === PromptStatus.CANCELLED || state.status === PromptStatus.CANCELLED;
|
||||
const hasActivePreviewBlob = Boolean(state._previewBlobUrl || state.images?.some(img => img.url?.startsWith("blob:")));
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const res = await fetch(`/history/${key}`);
|
||||
@@ -144,7 +148,14 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
|
||||
Object.assign(state.nodeTitles, titles);
|
||||
|
||||
const primaryImgs = getPrimaryOutputImages(norm.outputs, state.workflow);
|
||||
if (primaryImgs.length > 0) state.images = primaryImgs;
|
||||
const hasActualSavedOutputs = primaryImgs.some(img => !img.isFallback);
|
||||
|
||||
// If cancelled while sampling, do NOT overwrite with older nodes; keep the exact step preview!
|
||||
if (!isCancelled || !hasActivePreviewBlob) {
|
||||
if (hasActualSavedOutputs || (state.status === PromptStatus.COMPLETED && primaryImgs.length > 0)) {
|
||||
state.images = primaryImgs;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchedTexts = findTextsInOutputs(norm.outputs, state.workflow);
|
||||
if (fetchedTexts.length > 0) state.texts = fetchedTexts;
|
||||
@@ -154,6 +165,13 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Ensure cancelled prompt keeps its preview blob if it had one
|
||||
if (isCancelled && hasActivePreviewBlob) {
|
||||
if (state._previewBlobUrl && (!state.images || state.images.length === 0 || !state.images[0].url?.startsWith("blob:"))) {
|
||||
state.images = [{ url: state._previewBlobUrl }];
|
||||
}
|
||||
}
|
||||
|
||||
const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64;
|
||||
store.pruneHistory(maxItems);
|
||||
store.updatePrompt(key, state);
|
||||
|
||||
+4
-2
@@ -9,6 +9,7 @@ import { initSessionAndHistory, syncQueue } from "./queue/queueService.js";
|
||||
import { setupExecutionTracker } from "./queue/executionTracker.js";
|
||||
import { setupSidebarView, renderSidebar, teardownSidebarView } from "./ui/sidebarView.js";
|
||||
import { updateCardProgressTargeted, cardPool } from "./ui/cardRenderer.js";
|
||||
import { store } from "./core/store.js";
|
||||
|
||||
let isInitialized = false;
|
||||
let activeKeydownHandler = null;
|
||||
@@ -178,9 +179,10 @@ app.registerExtension({
|
||||
// Setup execution tracker with targeted fast-path update for active card
|
||||
cleanupFns.push(setupExecutionTracker((pid, progress, nodeName) => {
|
||||
const cardObj = cardPool.get(pid);
|
||||
if (cardObj) {
|
||||
const state = store.getPrompt(pid);
|
||||
if (cardObj && state) {
|
||||
const showWorkingNode = app.ui.settings.getSettingValue(SettingIds.SHOW_WORKING_NODE_NAME) ?? true;
|
||||
updateCardProgressTargeted(cardObj, progress, nodeName, showWorkingNode);
|
||||
updateCardProgressTargeted(cardObj, progress, nodeName, showWorkingNode, state);
|
||||
} else {
|
||||
renderSidebar();
|
||||
}
|
||||
|
||||
+95
-21
@@ -2,7 +2,7 @@ import { app } from "/scripts/app.js";
|
||||
import { PromptStatus } from "../core/constants.js";
|
||||
import { store } from "../core/store.js";
|
||||
import { renderCardImages } from "./mediaPreview.js";
|
||||
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage } from "./mediaActions.js";
|
||||
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage, removeImageFromNodeOutputs } from "./mediaActions.js";
|
||||
import { centerAndSelectCanvasNode } from "../comfy/adapter.js";
|
||||
import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/utils.js";
|
||||
import { showFullscreenPreview } from "../utils/comparison.js";
|
||||
@@ -31,9 +31,9 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.removeProperty("display");
|
||||
}
|
||||
|
||||
const isCompleted = state.status === PromptStatus.COMPLETED;
|
||||
const hasRealDiskFiles = isCompleted && state.images && state.images.length > 0 &&
|
||||
!state.images.every(img => img.isFallback || (img.url && img.url.startsWith("blob:")));
|
||||
// Check if the card has an actual verified file saved on disk
|
||||
const hasRealDiskFiles = state.images && state.images.length > 0 &&
|
||||
state.images.some(img => img.filename && !img.isFallback && (!img.url || !img.url.startsWith("blob:")));
|
||||
|
||||
const hasRealImages = hasRealDiskFiles && !state.images.some(img => img.url && img.url.startsWith("blob:"));
|
||||
const currentImg = hasRealImages ? state.images[cardObj.currentImageIndex || 0] : null;
|
||||
@@ -110,26 +110,26 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
}
|
||||
|
||||
if (cardObj.btnDel) {
|
||||
// Only show delete button if we have actual verified output files on disk to delete
|
||||
if (hasRealDiskFiles) {
|
||||
cardObj.btnDel.style.removeProperty("display");
|
||||
cardObj.btnDel.style.display = "inline-flex";
|
||||
cardObj.canDeleteFromDisk = true;
|
||||
if (cardObj.btnDelLabel) {
|
||||
cardObj.btnDelLabel.style.display = "";
|
||||
}
|
||||
cardObj.btnDel.title = "Delete Card (Hold Ctrl to delete file from disk)";
|
||||
} else {
|
||||
cardObj.btnDel.style.setProperty("display", "none", "important");
|
||||
cardObj.canDeleteFromDisk = false;
|
||||
// Always show the delete button so cards can be cleared from history
|
||||
cardObj.btnDel.style.removeProperty("display");
|
||||
cardObj.btnDel.style.display = "inline-flex";
|
||||
|
||||
cardObj.canDeleteFromDisk = hasRealDiskFiles;
|
||||
if (cardObj.btnDelLabel) {
|
||||
cardObj.btnDelLabel.style.display = hasRealDiskFiles ? "" : "none";
|
||||
}
|
||||
cardObj.btnDel.title = hasRealDiskFiles
|
||||
? "Delete Card (Hold Ctrl to delete file from disk)"
|
||||
: "Delete Card";
|
||||
}
|
||||
|
||||
if (cardObj.btnFocus) {
|
||||
const nodeId = currentImg ? findNodeIdForImage(state, currentImg) : null;
|
||||
// Focus the image's node, or the node where the workflow stopped/errored
|
||||
const nodeId = (currentImg ? findNodeIdForImage(state, currentImg) : null) || state.activeNodeId;
|
||||
if (nodeId) {
|
||||
cardObj.btnFocus.style.removeProperty("display");
|
||||
cardObj.btnFocus.style.display = "inline-flex";
|
||||
cardObj.btnFocus.title = (!currentImg && state.activeNodeId) ? "Show Stopped/Failed Node" : "Show Node";
|
||||
cardObj.btnFocus.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
centerAndSelectCanvasNode(nodeId);
|
||||
@@ -153,20 +153,80 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
}
|
||||
}
|
||||
|
||||
const hasIntermediates = validOutputs.length > 1 && distinctOutputs.length > 0;
|
||||
// Show intermediate button if run completed with multiple outputs, or if stopped with saved outputs
|
||||
const hasIntermediates = (validOutputs.length > 1 && distinctOutputs.length > 0) ||
|
||||
(state.status !== PromptStatus.COMPLETED && validOutputs.length > 0);
|
||||
|
||||
if (hasIntermediates) {
|
||||
// Check if intermediate files still exist on disk
|
||||
for (const out of distinctOutputs) {
|
||||
for (const img of out.images) {
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
if (!src.startsWith("blob:")) {
|
||||
fetch(src, { method: "HEAD" }).then(res => {
|
||||
if (res.status === 404) {
|
||||
const liveState = store.getPrompt(state.pid);
|
||||
if (liveState && liveState.nodeOutputs) {
|
||||
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
syncCardButtonVisibility(cardObj, liveState);
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cardObj.leftHoverBtn.style.removeProperty("display");
|
||||
cardObj.leftHoverBtn.style.display = "inline-flex";
|
||||
cardObj.leftHoverBtn.onclick = (ev) => {
|
||||
|
||||
cardObj.leftHoverBtn.onclick = async (ev) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
// On click: verify distinct outputs still exist before opening
|
||||
const liveState = store.getPrompt(state.pid) || state;
|
||||
const curOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
|
||||
const primSigs = new Set((liveState.images || []).map(i => `${i.subfolder || ""}/${i.filename}`));
|
||||
const curDistinct = curOutputs.filter(o => o.images && o.images.some(img => !primSigs.has(`${img.subfolder || ""}/${img.filename}`)));
|
||||
|
||||
if (curDistinct.length === 0) {
|
||||
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
||||
return;
|
||||
}
|
||||
|
||||
let anyMissing = false;
|
||||
for (const out of curDistinct) {
|
||||
for (const img of out.images) {
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
if (!src.startsWith("blob:")) {
|
||||
try {
|
||||
const res = await fetch(src, { method: "HEAD" });
|
||||
if (res.status === 404) {
|
||||
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
||||
anyMissing = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyMissing) {
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
syncCardButtonVisibility(cardObj, liveState);
|
||||
const remaining = getRunOutputs(liveState.nodeOutputs, liveState.workflow)
|
||||
.filter(o => o.images && o.images.some(img => !primSigs.has(`${img.subfolder || ""}/${img.filename}`)));
|
||||
if (remaining.length === 0) {
|
||||
return; // All intermediate outputs were wiped; button is now hidden
|
||||
}
|
||||
}
|
||||
|
||||
const scrollEl = document.querySelector('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|
||||
|| store.ui.cardStack?.parentElement
|
||||
|| store.ui.cardStack;
|
||||
if (scrollEl) {
|
||||
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
|
||||
}
|
||||
store.openOutputsSubmenu(state.pid);
|
||||
store.openOutputsSubmenu(liveState.pid);
|
||||
};
|
||||
} else {
|
||||
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
||||
@@ -174,7 +234,7 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateCardProgressTargeted(cardObj, progress, activeNodeName, showWorkingNode = true) {
|
||||
export function updateCardProgressTargeted(cardObj, progress, activeNodeName, showWorkingNode = true, state = null) {
|
||||
if (!cardObj) return;
|
||||
if (cardObj.progressBar) {
|
||||
cardObj.progressBar.style.width = `${progress}%`;
|
||||
@@ -184,6 +244,20 @@ export function updateCardProgressTargeted(cardObj, progress, activeNodeName, sh
|
||||
const nodeText = activeNodeName ? (activeNodeName === "Finishing..." ? "Finishing..." : `[${activeNodeName}]`) : "Processing...";
|
||||
cardObj.statusText.textContent = `${nodeText} ${progress}%`;
|
||||
}
|
||||
|
||||
// Fast-path: update live preview image directly on active card
|
||||
if (state && state.images && state.images.length > 0) {
|
||||
const firstImg = state.images[0];
|
||||
const previewUrl = firstImg.url || (firstImg.filename ? `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}` : null);
|
||||
if (previewUrl && cardObj.firstImgElement && cardObj.firstImgElement.tagName === "IMG") {
|
||||
const currentSrc = cardObj.firstImgElement.getAttribute("src") || cardObj.firstImgElement.src;
|
||||
if (currentSrc !== previewUrl && !cardObj.firstImgElement.src.endsWith(previewUrl)) {
|
||||
cardObj.firstImgElement.src = previewUrl;
|
||||
}
|
||||
} else {
|
||||
updateCardDOM(cardObj, state, true, showWorkingNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getOrCreateCard(state, callbacks = {}) {
|
||||
|
||||
+101
-12
@@ -1,8 +1,9 @@
|
||||
import { isImageFormat, isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "../utils/utils.js";
|
||||
import { openFileOrFolder } from "./mediaActions.js";
|
||||
import { openFileOrFolder, removeImageFromNodeOutputs } from "./mediaActions.js";
|
||||
import { stopAllAudioPlayback, isAudioViewerOpen, setCurrentlyPlayingAudio, getCurrentlyPlayingAudio } from "./audioController.js";
|
||||
import { showFullscreenPreview } from "../utils/comparison.js";
|
||||
import { store } from "../core/store.js";
|
||||
import { PromptStatus } from "../core/constants.js";
|
||||
|
||||
export function render3DCardPreview(cardObj, wrapper, src, img, state) {
|
||||
let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper");
|
||||
@@ -272,6 +273,9 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
}
|
||||
|
||||
export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
|
||||
if (playIcon) playIcon.remove();
|
||||
let previewFile = wrapper.querySelector(".comfy-sidebar-file-wrapper");
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const filename = img.filename || getFilenameFromUrl(src) || "output_file";
|
||||
@@ -427,25 +431,110 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
openFileOrFolder(img);
|
||||
return;
|
||||
}
|
||||
showFullscreenPreview([src], ev.shiftKey);
|
||||
let activeSrc = mediaEl.currentSrc || mediaEl.src || src;
|
||||
// If it's a preview blob, snapshot the rendered pixels from the card so revoked blobs don't 404 in fullscreen
|
||||
if (activeSrc && activeSrc.startsWith("blob:") && mediaEl.naturalWidth > 0) {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = mediaEl.naturalWidth;
|
||||
canvas.height = mediaEl.naturalHeight;
|
||||
canvas.getContext("2d").drawImage(mediaEl, 0, 0);
|
||||
activeSrc = canvas.toDataURL("image/png");
|
||||
} catch (_) {}
|
||||
}
|
||||
showFullscreenPreview([activeSrc], ev.shiftKey, state?.pid);
|
||||
};
|
||||
|
||||
mediaEl.onerror = async () => {
|
||||
if (src && !src.startsWith("blob:") && !is3D && !isAudio) {
|
||||
try {
|
||||
const res = await fetch(src, { method: "HEAD" });
|
||||
if (res.status === 404) {
|
||||
store.deletePrompt(state.pid);
|
||||
if (cardObj.element) cardObj.element.remove();
|
||||
return;
|
||||
mediaEl.onerror = () => {
|
||||
mediaEl.onerror = null;
|
||||
if ((src && src.startsWith("blob:")) || is3D || isAudio) return;
|
||||
|
||||
const cardId = cardObj.element?.id || "";
|
||||
|
||||
// 1. If an intermediate output item is missing, remove only this item
|
||||
if (cardId.startsWith("card-submenu-")) {
|
||||
const parentState = store.getPrompt(state.pid);
|
||||
if (parentState && parentState.nodeOutputs) {
|
||||
if (state.nodeId && parentState.nodeOutputs[state.nodeId]) {
|
||||
delete parentState.nodeOutputs[state.nodeId];
|
||||
} else {
|
||||
removeImageFromNodeOutputs(parentState.nodeOutputs, img);
|
||||
}
|
||||
} catch (e) {}
|
||||
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
|
||||
store.updatePrompt(parentState.pid, parentState);
|
||||
}
|
||||
cardObj.element?.remove();
|
||||
const remaining = store.ui.cardStack?.querySelectorAll('.comfy-sidebar-card');
|
||||
if (!remaining || remaining.length === 0) {
|
||||
store.closeSubmenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If a batch item is missing, remove only this item from the batch
|
||||
if (cardId.startsWith("card-batch-")) {
|
||||
const parentPid = state.parentPromptId || state.pid;
|
||||
const parentState = store.getPrompt(parentPid);
|
||||
if (parentState) {
|
||||
if (Array.isArray(parentState.images)) {
|
||||
const idx = parentState.images.findIndex(i => i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""));
|
||||
if (idx > -1) parentState.images.splice(idx, 1);
|
||||
}
|
||||
if (parentState.nodeOutputs) {
|
||||
removeImageFromNodeOutputs(parentState.nodeOutputs, img);
|
||||
}
|
||||
store.updatePrompt(parentPid, parentState);
|
||||
}
|
||||
cardObj.element?.remove();
|
||||
const remaining = store.ui.cardStack?.querySelectorAll('.comfy-sidebar-card');
|
||||
if (!remaining || remaining.length === 0) {
|
||||
store.closeSubmenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Main card: if this file is gone, clean it up
|
||||
const liveState = store.getPrompt(state.pid);
|
||||
if (liveState) {
|
||||
if (Array.isArray(liveState.images)) {
|
||||
liveState.images = liveState.images.filter(i =>
|
||||
!(i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""))
|
||||
);
|
||||
}
|
||||
if (liveState.nodeOutputs) {
|
||||
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
||||
}
|
||||
|
||||
// If the card still has other images (e.g. from a batch), persist and switch to next image
|
||||
if (liveState.images && liveState.images.length > 0) {
|
||||
cardObj.currentImageIndex = 0;
|
||||
cardObj.lastImagesSignature = "";
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the card has text outputs or is still in progress, persist
|
||||
if ((liveState.texts && liveState.texts.length > 0) || (liveState.status && liveState.status !== PromptStatus.COMPLETED)) {
|
||||
cardObj.lastImagesSignature = "";
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only delete the main card when all items belonging to it are gone
|
||||
store.deletePrompt(liveState.pid);
|
||||
cardObj.element?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
|
||||
};
|
||||
|
||||
const applyDimensions = (width, height) => {
|
||||
if (cardObj.dimEl && width && height) {
|
||||
// Don't show the dimension badge for active/running preview frames
|
||||
if (state.status === "active") {
|
||||
cardObj.dimEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
cardObj.dimEl.textContent = `${width}x${height}`;
|
||||
cardObj.dimEl.style.display = "block";
|
||||
}
|
||||
|
||||
+108
-36
@@ -12,6 +12,80 @@ let scrollToTopBtnEl = null;
|
||||
let globalClickRegistered = false;
|
||||
let renderAnimationFrameId = null;
|
||||
let activeTimerInterval = null;
|
||||
let currentColumnCount = 1;
|
||||
|
||||
function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
|
||||
if (!cardStack) return;
|
||||
|
||||
// 1. Full-width container (for banners like Pending Queue summary)
|
||||
let bannerContainer = cardStack.querySelector('.comfy-sidebar-banners');
|
||||
if (!bannerContainer) {
|
||||
bannerContainer = document.createElement('div');
|
||||
bannerContainer.className = 'comfy-sidebar-banners';
|
||||
Object.assign(bannerContainer.style, { display: 'flex', flexDirection: 'column', gap: '12px', width: '100%' });
|
||||
cardStack.insertBefore(bannerContainer, cardStack.firstChild);
|
||||
}
|
||||
fullWidthEls.forEach((el, i) => {
|
||||
if (bannerContainer.children[i] !== el) {
|
||||
bannerContainer.insertBefore(el, bannerContainer.children[i] || null);
|
||||
}
|
||||
});
|
||||
while (bannerContainer.children.length > fullWidthEls.length) {
|
||||
bannerContainer.removeChild(bannerContainer.lastChild);
|
||||
}
|
||||
bannerContainer.style.display = fullWidthEls.length > 0 ? 'flex' : 'none';
|
||||
bannerContainer.style.marginBottom = fullWidthEls.length > 0 ? '12px' : '0';
|
||||
|
||||
// 2. Masonry Columns container
|
||||
let columnsWrapper = cardStack.querySelector('.comfy-sidebar-masonry-wrapper');
|
||||
if (!columnsWrapper) {
|
||||
columnsWrapper = document.createElement('div');
|
||||
columnsWrapper.className = 'comfy-sidebar-masonry-wrapper';
|
||||
Object.assign(columnsWrapper.style, { display: 'flex', gap: '12px', alignItems: 'flex-start', width: '100%' });
|
||||
cardStack.appendChild(columnsWrapper);
|
||||
}
|
||||
|
||||
while (columnsWrapper.children.length < cols) {
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'comfy-sidebar-masonry-col';
|
||||
Object.assign(colDiv.style, { flex: '1', display: 'flex', flexDirection: 'column', gap: '12px', minWidth: '0' });
|
||||
columnsWrapper.appendChild(colDiv);
|
||||
}
|
||||
while (columnsWrapper.children.length > cols) {
|
||||
columnsWrapper.removeChild(columnsWrapper.lastChild);
|
||||
}
|
||||
|
||||
// Distribute cards into the shortest column so short cards stack next to tall cards
|
||||
const colBuckets = Array.from({ length: cols }, () => []);
|
||||
const colHeights = new Array(cols).fill(0);
|
||||
|
||||
cardEls.forEach((cardEl) => {
|
||||
let shortestCol = 0;
|
||||
for (let c = 1; c < cols; c++) {
|
||||
if (colHeights[c] < colHeights[shortestCol]) {
|
||||
shortestCol = c;
|
||||
}
|
||||
}
|
||||
colBuckets[shortestCol].push(cardEl);
|
||||
|
||||
const cardH = cardEl.offsetHeight > 0 ? cardEl.offsetHeight : (cardEl._cachedHeight || 160);
|
||||
cardEl._cachedHeight = cardH;
|
||||
colHeights[shortestCol] += cardH + 12;
|
||||
});
|
||||
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const colDiv = columnsWrapper.children[c];
|
||||
const bucket = colBuckets[c];
|
||||
bucket.forEach((el, i) => {
|
||||
if (colDiv.children[i] !== el) {
|
||||
colDiv.insertBefore(el, colDiv.children[i] || null);
|
||||
}
|
||||
});
|
||||
while (colDiv.children.length > bucket.length) {
|
||||
colDiv.removeChild(colDiv.lastChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getScrollContainer() {
|
||||
if (!store.ui.cardStack) return null;
|
||||
@@ -213,7 +287,13 @@ export function setupSidebarView() {
|
||||
|
||||
const cardStack = document.createElement("div");
|
||||
store.ui.cardStack = cardStack;
|
||||
Object.assign(cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" });
|
||||
Object.assign(cardStack.style, {
|
||||
flex: "1",
|
||||
overflowY: "visible",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
paddingBottom: "28px"
|
||||
});
|
||||
container.appendChild(cardStack);
|
||||
|
||||
scrollToTopBtnEl = document.createElement("button");
|
||||
@@ -233,9 +313,9 @@ export function setupSidebarView() {
|
||||
new ResizeObserver((entries) => {
|
||||
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
|
||||
const cols = Math.max(1, Math.floor(entries[0].contentRect.width / (threshold / 2)));
|
||||
if (store.ui.cardStack) {
|
||||
store.ui.cardStack.style.columnCount = cols.toString();
|
||||
store.ui.cardStack.style.columnGap = cols > 1 ? "12px" : "0";
|
||||
if (cols !== currentColumnCount) {
|
||||
currentColumnCount = cols;
|
||||
renderSidebar();
|
||||
}
|
||||
}).observe(container);
|
||||
|
||||
@@ -504,14 +584,7 @@ export function renderSidebar() {
|
||||
targetElements.push(cardObj.element);
|
||||
});
|
||||
|
||||
targetElements.forEach((el, index) => {
|
||||
if (store.ui.cardStack.children[index] !== el) {
|
||||
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
|
||||
}
|
||||
});
|
||||
while (store.ui.cardStack.children.length > targetElements.length) {
|
||||
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
|
||||
}
|
||||
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
|
||||
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) scrollEl.scrollTop = 0;
|
||||
@@ -568,7 +641,8 @@ export function renderSidebar() {
|
||||
const emptyCard = document.createElement("div");
|
||||
emptyCard.className = "comfy-sidebar-card completed";
|
||||
Object.assign(emptyCard.style, {
|
||||
padding: "16px", textAlign: "center", color: "var(--desc-color, #aaa)", fontSize: "12px"
|
||||
padding: "16px", textAlign: "center", color: "var(--desc-color, #aaa)", fontSize: "12px",
|
||||
gridColumn: "1 / -1"
|
||||
});
|
||||
emptyCard.textContent = "No separate intermediate outputs found.";
|
||||
targetElements.push(emptyCard);
|
||||
@@ -749,6 +823,7 @@ export function renderSidebar() {
|
||||
cardObj.placeholder.style.display = "none";
|
||||
renderCardImages(cardObj, {
|
||||
pid: st.pid,
|
||||
nodeId: out.nodeId,
|
||||
status: PromptStatus.COMPLETED,
|
||||
images: out.images,
|
||||
workflow: st.workflow,
|
||||
@@ -777,13 +852,10 @@ export function renderSidebar() {
|
||||
});
|
||||
}
|
||||
|
||||
targetElements.forEach((el, index) => {
|
||||
if (store.ui.cardStack.children[index] !== el) {
|
||||
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
|
||||
}
|
||||
});
|
||||
while (store.ui.cardStack.children.length > targetElements.length) {
|
||||
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
|
||||
if (outputs.length === 0) {
|
||||
mountCards(store.ui.cardStack, targetElements, [], currentColumnCount);
|
||||
} else {
|
||||
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
|
||||
}
|
||||
|
||||
updateScrollTopBtnVisibility();
|
||||
@@ -817,7 +889,8 @@ export function renderSidebar() {
|
||||
return updateCardDOM(cardObj, state, showPendingSummary, showWorkingNode);
|
||||
};
|
||||
|
||||
const targetElements = [];
|
||||
const bannerElements = [];
|
||||
const cardElements = [];
|
||||
const pendingCount = store.getAllPrompts().filter(t => t.status === PromptStatus.PENDING).length;
|
||||
|
||||
if (pendingCount > 0) {
|
||||
@@ -827,8 +900,8 @@ export function renderSidebar() {
|
||||
const el = document.createElement("div");
|
||||
Object.assign(el.style, {
|
||||
background: "var(--comfy-input-bg, #181818)", border: "2px solid #6c757d", borderRadius: "4px", padding: "10px",
|
||||
marginBottom: "12px", textAlign: "center", fontSize: "12px", fontWeight: "bold",
|
||||
color: "var(--desc-color, #aaa)", breakInside: "avoid", display: "flex", flexDirection: "column", gap: "8px"
|
||||
textAlign: "center", fontSize: "12px", fontWeight: "bold",
|
||||
color: "var(--desc-color, #aaa)", display: "flex", flexDirection: "column", gap: "8px"
|
||||
});
|
||||
const textDiv = document.createElement("div");
|
||||
el.appendChild(textDiv);
|
||||
@@ -846,7 +919,7 @@ export function renderSidebar() {
|
||||
cardPool.set("pending-summary-card", pCard);
|
||||
}
|
||||
pCard.textDiv.textContent = `Pending Queue: ${pendingCount} tasks`;
|
||||
targetElements.push(pCard.element);
|
||||
bannerElements.push(pCard.element);
|
||||
} else {
|
||||
let btnCard = cardPool.get("pending-cancel-all-standalone");
|
||||
if (!btnCard) {
|
||||
@@ -854,7 +927,7 @@ export function renderSidebar() {
|
||||
btn.textContent = "Cancel All Pending";
|
||||
Object.assign(btn.style, {
|
||||
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "6px",
|
||||
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%", marginBottom: "12px", breakInside: "avoid"
|
||||
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%"
|
||||
});
|
||||
btn.onclick = async () => {
|
||||
await cancelAllPending();
|
||||
@@ -862,23 +935,22 @@ export function renderSidebar() {
|
||||
btnCard = { element: btn };
|
||||
cardPool.set("pending-cancel-all-standalone", btnCard);
|
||||
}
|
||||
targetElements.push(btnCard.element);
|
||||
pendingTasks.forEach(st => targetElements.push(syncTaskCard(st)));
|
||||
bannerElements.push(btnCard.element);
|
||||
pendingTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
||||
}
|
||||
}
|
||||
|
||||
activeTasks.forEach(st => targetElements.push(syncTaskCard(st)));
|
||||
completedTasks.forEach(st => targetElements.push(syncTaskCard(st)));
|
||||
activeTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
||||
completedTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
||||
|
||||
targetElements.forEach((el, index) => {
|
||||
if (store.ui.cardStack.children[index] !== el) {
|
||||
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
|
||||
}
|
||||
});
|
||||
while (store.ui.cardStack.children.length > targetElements.length) {
|
||||
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
|
||||
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
|
||||
const containerWidth = store.ui.sidebarContainer?.clientWidth || 0;
|
||||
if (containerWidth > 0) {
|
||||
currentColumnCount = Math.max(1, Math.floor(containerWidth / (threshold / 2)));
|
||||
}
|
||||
|
||||
mountCards(store.ui.cardStack, bannerElements, cardElements, currentColumnCount);
|
||||
|
||||
if (store.ui.mainQueueScrollTop !== null) {
|
||||
const restorePos = store.ui.mainQueueScrollTop;
|
||||
store.ui.mainQueueScrollTop = null;
|
||||
|
||||
+10
-1
@@ -10,6 +10,12 @@ export function isAudioViewerOpen() {
|
||||
return !!(activeComparisonViewer && activeComparisonViewer.isAudio);
|
||||
}
|
||||
|
||||
export function updateActiveComparisonPreview(pid, newSrc) {
|
||||
if (activeComparisonViewer && activeComparisonViewer.targetPid === String(pid)) {
|
||||
activeComparisonViewer.loadTarget(newSrc);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -675,7 +681,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function showFullscreenPreview(imgSrcs, isShiftClick = false) {
|
||||
export function showFullscreenPreview(imgSrcs, isShiftClick = false, pid = null) {
|
||||
if (!imgSrcs || imgSrcs.length === 0) return;
|
||||
|
||||
const item = imgSrcs[0];
|
||||
@@ -751,4 +757,7 @@ export function showFullscreenPreview(imgSrcs, isShiftClick = false) {
|
||||
src,
|
||||
() => { activeComparisonViewer = null; }
|
||||
);
|
||||
if (activeComparisonViewer) {
|
||||
activeComparisonViewer.targetPid = pid ? String(pid) : null;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ export function injectStyles() {
|
||||
.comfy-sidebar-card {
|
||||
background: var(--comfy-input-bg, #181818);
|
||||
border-radius: 4px; padding: 8px; position: relative;
|
||||
min-height: 80px; margin-bottom: 12px; break-inside: avoid;
|
||||
min-height: 80px; margin-bottom: 0px; break-inside: avoid;
|
||||
user-select: none; -webkit-user-select: none;
|
||||
border: 2px solid var(--border-color, #333);
|
||||
color: var(--comfy-input-color, var(--fg-color, #eee));
|
||||
|
||||
@@ -339,6 +339,13 @@ export function getPrimaryOutputImages(nodeOutputs, rawWorkflow) {
|
||||
if (!nodeOutputs) return findImagesInOutputs(null, rawWorkflow);
|
||||
const runOutputs = getRunOutputs(nodeOutputs, rawWorkflow);
|
||||
if (runOutputs.length === 0) return findImagesInOutputs(nodeOutputs, rawWorkflow);
|
||||
// Find the latest executed node that actually produced real output images
|
||||
for (let i = runOutputs.length - 1; i >= 0; i--) {
|
||||
const imgs = runOutputs[i].images || [];
|
||||
if (imgs.some(img => !img.isFallback)) {
|
||||
return imgs;
|
||||
}
|
||||
}
|
||||
return runOutputs[runOutputs.length - 1].images || [];
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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.5.0"
|
||||
version = "1.5.3"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user