Refactor. Improve light theme compatibility. Add offline 3D viewer & CSP support. Improve file deletion logic

This commit is contained in:
m0rtus59
2026-09-10 21:57:14 +05:00
parent 3e7448dd80
commit 8343f216fc
29 changed files with 4086 additions and 3122 deletions
+34
View File
@@ -0,0 +1,34 @@
let currentlyPlayingAudio = null;
let activeComparisonViewer = null;
export function stopAllAudioPlayback() {
if (currentlyPlayingAudio) {
currentlyPlayingAudio.pause();
currentlyPlayingAudio.currentTime = 0;
if (typeof currentlyPlayingAudio._onResetUI === "function") {
currentlyPlayingAudio._onResetUI();
}
currentlyPlayingAudio = null;
}
}
export function setCurrentlyPlayingAudio(audio) {
stopAllAudioPlayback();
currentlyPlayingAudio = audio;
}
export function getCurrentlyPlayingAudio() {
return currentlyPlayingAudio;
}
export function setActiveComparisonViewer(viewer) {
activeComparisonViewer = viewer;
}
export function getActiveComparisonViewer() {
return activeComparisonViewer;
}
export function isAudioViewerOpen() {
return !!(activeComparisonViewer && activeComparisonViewer.isAudio);
}
+519
View File
@@ -0,0 +1,519 @@
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 { centerAndSelectCanvasNode } from "../comfy/adapter.js";
import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/utils.js";
import { showFullscreenPreview } from "../utils/comparison.js";
import { stopAllAudioPlayback } from "./audioController.js";
import { deleteHistoryItem, cancelPendingTask, interruptActive } from "../queue/queueService.js";
export const cardPool = new Map();
export function resetAllCardHoverStates() {
for (const cardObj of cardPool.values()) {
if (cardObj.hoverPanel) cardObj.hoverPanel.style.display = "none";
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.display = "none";
}
}
export function syncCardButtonVisibility(cardObj, state) {
if (!cardObj) return;
const isPendingOrActive = state.status === PromptStatus.PENDING || state.status === PromptStatus.ACTIVE;
if (isPendingOrActive) {
if (cardObj.hoverPanel) cardObj.hoverPanel.style.setProperty("display", "none", "important");
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.setProperty("display", "none", "important");
return;
} else {
if (cardObj.hoverPanel) cardObj.hoverPanel.style.removeProperty("display");
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:")));
const hasRealImages = hasRealDiskFiles && !state.images.some(img => img.url && img.url.startsWith("blob:"));
const currentImg = hasRealImages ? state.images[cardObj.currentImageIndex || 0] : null;
const isPng = currentImg && (currentImg.filename || "").toLowerCase().endsWith(".png");
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.btnCopy) {
const isCopyableImage = hasRealImages && currentImg && isImageFormat(currentImg.filename || currentImg.url);
if (isCopyableImage) {
cardObj.btnCopy.style.removeProperty("display");
cardObj.btnCopy.style.display = "inline-flex";
cardObj.btnCopy.onclick = async (ev) => {
ev.stopPropagation();
const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}`;
const success = await copyImageToClipboard(src);
if (success) {
cardObj.btnCopy.className = "pi pi-check comfy-sidebar-card-action-btn";
cardObj.btnCopy.style.color = "#4ade80";
setTimeout(() => {
cardObj.btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
cardObj.btnCopy.style.color = "";
}, 1500);
}
};
} else {
cardObj.btnCopy.style.setProperty("display", "none", "important");
}
}
if (cardObj.btnJson) {
if (state.workflow || isPng) {
cardObj.btnJson.style.removeProperty("display");
cardObj.btnJson.style.display = "inline-flex";
cardObj.btnJson.onclick = async (ev) => {
ev.stopPropagation();
let wf = state.workflow;
if (!wf && currentImg) {
const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}`;
wf = await extractWorkflowFromPng(src);
if (wf) state.workflow = wf;
}
if (!wf) {
alert("No workflow metadata found in this file.");
return;
}
const blob = new Blob([JSON.stringify(wf, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `workflow_${state.pid || "comfy"}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1500);
};
} else {
cardObj.btnJson.style.setProperty("display", "none", "important");
}
}
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;
}
}
if (cardObj.btnFocus) {
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();
centerAndSelectCanvasNode(nodeId);
};
} else {
cardObj.btnFocus.style.setProperty("display", "none", "important");
}
}
if (cardObj.leftHoverBtn) {
const outputs = getRunOutputs(state.nodeOutputs, state.workflow);
const validOutputs = outputs.filter(o => o.images && o.images.length > 0);
const primarySignatures = new Set((state.images || []).map(i => `${i.subfolder || ""}/${i.filename}`));
const distinctOutputs = [];
for (const out of validOutputs) {
const hasNewImage = out.images.some(img => !primarySignatures.has(`${img.subfolder || ""}/${img.filename}`));
if (hasNewImage) {
distinctOutputs.push(out);
}
}
const hasIntermediates = validOutputs.length > 1 && distinctOutputs.length > 0;
if (hasIntermediates) {
cardObj.leftHoverBtn.style.removeProperty("display");
cardObj.leftHoverBtn.style.display = "inline-flex";
cardObj.leftHoverBtn.onclick = (ev) => {
ev.stopPropagation();
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);
};
} else {
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
}
}
}
export function updateCardProgressTargeted(cardObj, progress, activeNodeName, showWorkingNode = true) {
if (!cardObj) return;
if (cardObj.progressBar) {
cardObj.progressBar.style.width = `${progress}%`;
}
if (cardObj.statusText && showWorkingNode) {
cardObj.statusText.style.display = "block";
const nodeText = activeNodeName ? (activeNodeName === "Finishing..." ? "Finishing..." : `[${activeNodeName}]`) : "Processing...";
cardObj.statusText.textContent = `${nodeText} ${progress}%`;
}
}
export function getOrCreateCard(state, callbacks = {}) {
let cardObj = cardPool.get(state.pid);
if (cardObj) return cardObj;
const card = document.createElement("div");
card.id = `card-${state.pid}`;
card.className = `comfy-sidebar-card ${state.status || "completed"}`;
card.style.position = "relative";
const timerEl = document.createElement("div");
timerEl.className = "comfy-sidebar-card-timer";
const cancelX = document.createElement("span");
cancelX.className = "pi pi-times comfy-sidebar-queue-cancel-btn";
Object.assign(cancelX.style, {
position: "absolute", top: "4px", right: "4px", display: "none", zIndex: "10"
});
const sBadge = document.createElement("div");
Object.assign(sBadge.style, {
position: "absolute", top: "6px", right: "8px", fontSize: "9px", fontWeight: "bold",
padding: "2px 6px", borderRadius: "2px", textTransform: "uppercase", display: "none",
pointerEvents: "none", zIndex: "10"
});
const dimEl = document.createElement("div");
Object.assign(dimEl.style, {
position: "absolute", top: "6px", right: "8px", fontSize: "10px",
fontFamily: "monospace", opacity: "0.7", background: "rgba(0, 0, 0, 0.6)",
padding: "2px 4px", borderRadius: "3px", pointerEvents: "none", zIndex: "5", color: "#fff",
display: "none", transform: "translateZ(0)"
});
const grid = document.createElement("div");
grid.style.display = "flex";
grid.style.flexDirection = "column";
grid.style.gap = "6px";
const p = document.createElement("div");
p.className = "comfy-sidebar-text-clamp";
const pt = document.createElement("div");
Object.assign(pt.style, {
width: "100%", height: "4px", background: "#333", borderRadius: "2px", marginTop: "8px",
overflow: "hidden", display: "none"
});
const pb = document.createElement("div");
Object.assign(pb.style, {
width: "0%", height: "100%", background: "#3b82f6", transition: "width 0.1s linear"
});
pt.appendChild(pb);
const statusText = document.createElement("div");
Object.assign(statusText.style, {
fontSize: "11px", opacity: "0.9", color: "#3b82f6", textAlign: "center", marginTop: "6px",
display: "none", fontWeight: "bold"
});
const hoverPanel = document.createElement("div");
hoverPanel.className = "comfy-sidebar-hover-panel";
Object.assign(hoverPanel.style, {
position: "absolute", bottom: "4px", right: "4px", flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
const btnCopy = document.createElement("span");
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.title = "Copy to Clipboard";
btnCopy.style.display = "none";
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 comfy-sidebar-btn-del";
btnDel.title = "Delete Card (Hold Ctrl to delete file from disk)";
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = "Delete File";
btnDel.appendChild(btnDelLabel);
const leftHoverPanel = document.createElement("div");
leftHoverPanel.className = "comfy-sidebar-left-hover-panel";
Object.assign(leftHoverPanel.style, {
position: "absolute", bottom: "4px", left: "4px", flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
const leftHoverBtn = document.createElement("span");
leftHoverBtn.className = "pi pi-images comfy-sidebar-card-action-btn";
leftHoverBtn.title = "View all intermediate outputs";
hoverPanel.append(btnImg, btnJson, btnDel);
leftHoverPanel.append(btnCopy, btnFocus, leftHoverBtn);
card.append(timerEl, cancelX, sBadge, dimEl, grid, p, pt, statusText, hoverPanel, leftHoverPanel);
cardObj = {
element: card,
timerEl,
statusBadge: sBadge,
dimEl,
grid,
placeholder: p,
progressContainer: pt,
progressBar: pb,
cancelBtn: cancelX,
hoverPanel,
leftHoverPanel,
btnFocus,
leftHoverBtn,
btnImg,
btnCopy,
btnJson,
btnDel,
btnDelLabel,
canDeleteFromDisk: true,
statusText,
firstImgElement: null,
lastImagesSignature: "",
currentImageIndex: 0
};
card.addEventListener("mouseenter", () => {
// Only run automatic button visibility sync on main queue cards (avoid synthetic submenu IDs)
if (store.hasPrompt(state.pid)) {
const liveState = store.getPrompt(state.pid);
syncCardButtonVisibility(cardObj, liveState);
}
});
card.addEventListener("dragstart", (e) => {
const liveState = store.getPrompt(state.pid) || state;
const isUnfinished = liveState.status && liveState.status !== PromptStatus.COMPLETED;
const hasNoImages = !liveState.images || liveState.images.length === 0;
if (liveState.workflow && (hasNoImages || isUnfinished)) {
if (cardObj.firstImgElement) {
try { e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15); } catch(err){}
}
const jsonStr = JSON.stringify(liveState.workflow, null, 2);
const filename = `workflow_${liveState.pid}.json`;
const base64Data = btoa(unescape(encodeURIComponent(jsonStr)));
try {
e.dataTransfer.setData("DownloadURL", `application/json:${filename}:data:application/json;base64,${base64Data}`);
e.dataTransfer.setData("application/json", jsonStr);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
}
});
let deleteTimeout = null, isDeletePending = false, isDiskDelete = false;
const resetDeleteBtn = () => {
isDeletePending = false;
isDiskDelete = false;
cardObj.btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
cardObj.btnDel.title = "Delete Card (Ctrl+Click to delete from disk)";
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
btnDel.onclick = async (ev) => {
ev.stopPropagation();
const wantsDiskDelete = (ev.ctrlKey || ev.metaKey) && cardObj.canDeleteFromDisk !== false;
if (!isDeletePending) {
isDeletePending = true;
isDiskDelete = wantsDiskDelete;
cardObj.btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
cardObj.btnDel.title = wantsDiskDelete
? "Ctrl+Click again to delete file from DISK / TRASH"
: "Click again to confirm removing card";
deleteTimeout = setTimeout(resetDeleteBtn, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
resetDeleteBtn();
const liveState = store.getPrompt(state.pid) || state;
if (shouldDeleteFromDisk) {
const filesToDelete = [];
const seen = new Set();
const addFile = (item) => {
if (item && item.filename) {
const key = `${item.type || ""}/${item.subfolder || ""}/${item.filename}`;
if (!seen.has(key)) {
seen.add(key);
filesToDelete.push(item);
}
}
};
(liveState.images || []).forEach(addFile);
const allOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
allOutputs.forEach(out => (out.images || []).forEach(addFile));
for (const fileItem of filesToDelete) {
await deleteFileOnServer(fileItem);
}
}
deleteHistoryItem(state.pid);
}
};
cardPool.set(state.pid, cardObj);
return cardObj;
}
export function updateCardDOM(cardObj, state, showPendingSummary = true, showWorkingNode = true) {
cardObj.element.className = `comfy-sidebar-card ${state.status}`;
cardObj.element.style.position = "relative";
const isUnfinished = state.status && state.status !== PromptStatus.COMPLETED;
if (state.images && state.images.length > 0 && !isUnfinished) {
cardObj.element.removeAttribute("draggable");
} else {
cardObj.element.setAttribute("draggable", "true");
}
if (state.status === PromptStatus.ACTIVE) {
cardObj.timerEl.textContent = state.startTime ? ((Date.now() - state.startTime) / 1000).toFixed(2) + "s" : "...";
cardObj.timerEl.style.display = "block";
} else if (state.duration !== undefined && state.duration !== null) {
cardObj.timerEl.textContent = state.duration.toFixed(2) + "s";
cardObj.timerEl.style.display = "block";
} else {
cardObj.timerEl.style.display = "none";
}
if (state.status === PromptStatus.CANCELLED) {
Object.assign(cardObj.statusBadge.style, { display: "block", background: "#ffc107", color: "#000" });
cardObj.statusBadge.textContent = "Cancelled";
} else if (state.status === PromptStatus.ERROR) {
Object.assign(cardObj.statusBadge.style, { display: "block", background: "#dc3545", color: "#fff" });
cardObj.statusBadge.textContent = "Error";
} else {
cardObj.statusBadge.style.display = "none";
}
if (state.status === PromptStatus.PENDING && !showPendingSummary) {
cardObj.cancelBtn.style.display = "flex";
cardObj.cancelBtn.onclick = async (ev) => {
ev.stopPropagation();
cancelPendingTask(state.pid);
};
} else if (state.status === PromptStatus.ACTIVE) {
cardObj.cancelBtn.style.display = "flex";
cardObj.cancelBtn.onclick = async (ev) => {
ev.stopPropagation();
interruptActive();
};
} else {
cardObj.cancelBtn.style.display = "none";
}
const currentImagesSignature = `${state.status || ""}:${state.images ? state.images.map(img => img.url || img.filename).join("|") : ""}`;
if (cardObj.lastImagesSignature !== currentImagesSignature) {
if (!state.images || state.images.length === 0) {
cardObj.grid.innerHTML = "";
cardObj.firstImgElement = null;
cardObj.placeholder.style.display = "block";
} else {
cardObj.placeholder.style.display = "none";
renderCardImages(cardObj, state, (st) => {
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.openBatchSubmenu({
pid: st.pid,
images: st.images,
workflow: st.workflow,
nodeOutputs: st.nodeOutputs
});
});
}
cardObj.lastImagesSignature = currentImagesSignature;
}
if (state.images.length === 0) {
if (state.texts && state.texts.length > 0) {
const fullText = state.texts.join("\n\n");
cardObj.placeholder.textContent = fullText;
cardObj.placeholder.title = "Click to read full text";
cardObj.placeholder.className = "comfy-sidebar-text-clamp";
cardObj.placeholder.style.cssText = "";
cardObj.placeholder.style.display = "-webkit-box";
cardObj.placeholder.onclick = (e) => {
e.stopPropagation();
stopAllAudioPlayback();
showFullscreenPreview([{ text: fullText, pid: state.pid }]);
};
} else {
cardObj.placeholder.textContent = state.progressText || "No Outputs";
cardObj.placeholder.className = "";
Object.assign(cardObj.placeholder.style, {
fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px",
userSelect: "none", whiteSpace: "normal", maxHeight: "none", display: "block", cursor: "default"
});
cardObj.placeholder.onclick = null;
}
}
if (state.status === PromptStatus.ACTIVE) {
if (showWorkingNode) {
cardObj.statusText.style.display = "block";
const nodeText = state.activeNodeName ? (state.activeNodeName === "Finishing..." ? "Finishing..." : `[${state.activeNodeName}]`) : "Processing...";
cardObj.statusText.textContent = `${nodeText}${state.progress ? ` ${state.progress}%` : ""}`;
} else {
cardObj.statusText.style.display = "none";
}
cardObj.progressContainer.style.display = "block";
cardObj.progressBar.style.width = `${state.progress || 0}%`;
} else {
cardObj.statusText.style.display = "none";
cardObj.progressContainer.style.display = "none";
}
syncCardButtonVisibility(cardObj, state);
return cardObj.element;
}
+119
View File
@@ -0,0 +1,119 @@
export async function copyImageToClipboard(src) {
try {
const res = await fetch(src);
let blob = await res.blob();
if (blob.type !== "image/png") {
const img = new Image();
img.crossOrigin = "anonymous";
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = src;
});
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
canvas.getContext("2d").drawImage(img, 0, 0);
blob = await new Promise(resolve => canvas.toBlob(resolve, "image/png"));
}
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
return true;
} catch (err) {
console.error("Comfy Sidebar: Failed to copy image to clipboard", err);
return false;
}
}
export async function openFileOrFolder(img) {
if (!img) return;
try {
const res = await fetch("/comfy-sidebar/open-folder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: img.filename,
subfolder: img.subfolder || "",
type: img.type || "output"
})
});
if (res.ok) return;
} catch (e) {}
const src = img.url || `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
const a = document.createElement("a");
a.href = src;
a.target = "_blank";
a.download = img.filename || "file";
document.body.appendChild(a);
a.click();
a.remove();
}
export async function deleteFileOnServer(fileItem) {
if (!fileItem || !fileItem.filename) return false;
try {
const res = await fetch("/comfy-sidebar/delete-file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: fileItem.filename,
subfolder: fileItem.subfolder || "",
type: fileItem.type || "output"
})
});
return res.ok;
} catch (e) {
console.error("Comfy Sidebar: Failed to delete file on server", e);
return false;
}
}
export function removeImageFromNodeOutputs(nodeOutputs, targetImg) {
if (!nodeOutputs || !targetImg || !targetImg.filename) return;
const scanAndRemove = (obj) => {
if (!obj || typeof obj !== "object") return;
for (const key in obj) {
const val = obj[key];
if (Array.isArray(val)) {
for (let i = val.length - 1; i >= 0; i--) {
const item = val[i];
if (item && typeof item === "object") {
if (item.filename === targetImg.filename && (item.subfolder || "") === (targetImg.subfolder || "")) {
val.splice(i, 1);
} else {
scanAndRemove(item);
}
} else if (typeof item === "string" && (item === targetImg.filename || item.endsWith("/" + targetImg.filename))) {
val.splice(i, 1);
}
}
} else if (typeof val === "object") {
scanAndRemove(val);
}
}
};
for (const nodeId in nodeOutputs) {
scanAndRemove(nodeOutputs[nodeId]);
}
}
export function findNodeIdForImage(state, img) {
if (!state || !state.nodeOutputs || !img) return null;
for (const nodeId in state.nodeOutputs) {
const out = state.nodeOutputs[nodeId];
for (const key in out) {
const val = out[key];
if (Array.isArray(val)) {
if (val.some(i => i && typeof i === "object" && i.filename === img.filename)) {
return nodeId;
}
} else if (val && typeof val === "object" && val.filename === img.filename) {
return nodeId;
}
}
}
return null;
}
+623
View File
@@ -0,0 +1,623 @@
import { isImageFormat, isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "../utils/utils.js";
import { openFileOrFolder } from "./mediaActions.js";
import { stopAllAudioPlayback, isAudioViewerOpen, setCurrentlyPlayingAudio, getCurrentlyPlayingAudio } from "./audioController.js";
import { showFullscreenPreview } from "../utils/comparison.js";
import { store } from "../core/store.js";
export function render3DCardPreview(cardObj, wrapper, src, img, state) {
let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper");
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
const imageAssets = state?.images?.filter(i => isImageFormat(i.filename || i.url)) || [];
const lastImageAsset = imageAssets.length > 0 ? imageAssets[imageAssets.length - 1] : null;
const bgImgSrc = lastImageAsset
? (lastImageAsset.url || window.location.origin + `/view?filename=${encodeURIComponent(lastImageAsset.filename)}&type=${lastImageAsset.type || 'output'}&subfolder=${encodeURIComponent(lastImageAsset.subfolder || '')}`)
: null;
if (!preview3D) {
wrapper.innerHTML = "";
preview3D = document.createElement("div");
preview3D.className = "comfy-sidebar-3d-wrapper";
if (bgImgSrc) {
const bgImg = document.createElement("img");
bgImg.className = "comfy-sidebar-3d-preview-img";
bgImg.src = bgImgSrc;
bgImg.alt = "3D Thumbnail Preview";
preview3D.appendChild(bgImg);
}
const overlay = document.createElement("div");
overlay.className = "comfy-sidebar-3d-overlay";
const badge = document.createElement("span");
badge.className = "comfy-sidebar-3d-badge";
const ext = (img.filename || src).split('.').pop().toUpperCase();
badge.textContent = ext || "3D";
const icon = document.createElement("span");
icon.className = "pi pi-box comfy-sidebar-3d-icon";
const title = document.createElement("span");
title.className = "comfy-sidebar-3d-title";
title.textContent = img.filename || "3D Model";
overlay.append(badge, icon, title);
preview3D.appendChild(overlay);
wrapper.appendChild(preview3D);
} else {
const bgImg = preview3D.querySelector(".comfy-sidebar-3d-preview-img");
if (bgImg && bgImgSrc && bgImg.src !== bgImgSrc) {
bgImg.src = bgImgSrc;
} else if (!bgImg && bgImgSrc) {
const newBgImg = document.createElement("img");
newBgImg.className = "comfy-sidebar-3d-preview-img";
newBgImg.src = bgImgSrc;
newBgImg.alt = "3D Thumbnail Preview";
preview3D.insertBefore(newBgImg, preview3D.firstChild);
}
const title = preview3D.querySelector(".comfy-sidebar-3d-title");
if (title) title.textContent = img.filename || "3D Model";
}
cardObj.firstImgElement = preview3D;
preview3D.onclick = (ev) => {
ev.stopPropagation();
if (ev.ctrlKey || ev.metaKey) {
openFileOrFolder(img);
return;
}
showFullscreenPreview([fullUrl], ev.shiftKey);
};
preview3D.setAttribute("draggable", "true");
preview3D.ondragstart = (e) => {
e.stopPropagation();
const filename = img.filename || "model.glb";
const mimeType = "application/octet-stream";
try {
e.dataTransfer.setData("text/uri-list", fullUrl);
e.dataTransfer.setData("text/plain", fullUrl);
e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullUrl}`);
if (state && state.workflow) {
e.dataTransfer.setData("application/json", JSON.stringify(state.workflow));
}
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
};
}
export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
let previewAudio = wrapper.querySelector(".comfy-sidebar-audio-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) || "audio.wav";
const ext = filename.split('.').pop().toUpperCase();
const playIconSvg = `<svg viewBox="0 0 24 24" width="14" height="14" style="margin-left: 2px; pointer-events: none;"><polygon points="6,4 20,12 6,20" fill="#ffffff"/></svg>`;
const stopIconSvg = `<svg viewBox="0 0 24 24" width="12" height="12" style="pointer-events: none;"><rect x="5" y="5" width="14" height="14" rx="2" fill="#ffffff"/></svg>`;
if (!previewAudio) {
wrapper.innerHTML = "";
previewAudio = document.createElement("div");
previewAudio.className = "comfy-sidebar-audio-wrapper";
const topRow = document.createElement("div");
Object.assign(topRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", paddingLeft: "42px" });
const title = document.createElement("span");
title.className = "comfy-sidebar-audio-title";
title.textContent = filename;
const badge = document.createElement("span");
badge.className = "comfy-sidebar-audio-badge";
badge.textContent = ext || "AUDIO";
topRow.append(title, badge);
const centerArea = document.createElement("div");
Object.assign(centerArea.style, { position: "relative", width: "100%", display: "flex", alignItems: "center", justifyContent: "center", margin: "6px 0" });
const soundwave = document.createElement("div");
soundwave.className = "comfy-sidebar-soundwave-container";
soundwave.style.width = "100%";
const barHeights = [10, 16, 24, 18, 30, 22, 14, 28, 34, 20, 12, 26, 32, 18, 14, 22, 30, 16, 12];
barHeights.forEach((h, i) => {
const bar = document.createElement("div");
bar.className = "comfy-sidebar-soundwave-bar";
bar.style.height = `${h}px`;
bar.style.animationDelay = `${(i * 0.08).toFixed(2)}s`;
soundwave.appendChild(bar);
});
const playBtn = document.createElement("button");
Object.assign(playBtn.style, {
position: "absolute", width: "36px", height: "36px", borderRadius: "50%",
background: "rgba(22, 22, 30, 0.88)", border: "1px solid rgba(255, 255, 255, 0.25)",
color: "#ffffff", cursor: "pointer", display: "flex", alignItems: "center",
justifyContent: "center", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.65)", zIndex: "5",
transition: "all 0.15s ease", outline: "none"
});
playBtn.innerHTML = playIconSvg;
centerArea.append(soundwave, playBtn);
const bottomRow = document.createElement("div");
Object.assign(bottomRow.style, { display: "flex", flexDirection: "column", gap: "4px", width: "100%", padding: "0 34px", boxSizing: "border-box" });
const scrubber = document.createElement("div");
Object.assign(scrubber.style, {
width: "100%", height: "4px", background: "#334155", borderRadius: "2px",
position: "relative", cursor: "pointer"
});
const scrubberFill = document.createElement("div");
Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#c084fc", borderRadius: "2px" });
scrubber.appendChild(scrubberFill);
const timeLabel = document.createElement("div");
Object.assign(timeLabel.style, { fontSize: "9px", fontFamily: "monospace", color: "#94a3b8", textAlign: "center" });
timeLabel.textContent = "0:00 / 0:00";
bottomRow.append(scrubber, timeLabel);
const audioEl = document.createElement("audio");
audioEl.src = fullUrl;
audioEl.preload = "metadata";
const formatTime = (t) => {
const m = Math.floor(t / 60);
const s = Math.floor(t % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
};
const resetAudioUI = () => {
playBtn.innerHTML = playIconSvg;
previewAudio.classList.remove("playing");
scrubberFill.style.width = "0%";
timeLabel.textContent = `0:00 / ${formatTime(audioEl.duration || 0)}`;
};
audioEl._onResetUI = resetAudioUI;
audioEl.onloadedmetadata = () => {
timeLabel.textContent = `0:00 / ${formatTime(audioEl.duration || 0)}`;
};
audioEl.ontimeupdate = () => {
if (audioEl.duration > 0) {
const percent = (audioEl.currentTime / audioEl.duration) * 100;
scrubberFill.style.width = `${percent}%`;
timeLabel.textContent = `${formatTime(audioEl.currentTime)} / ${formatTime(audioEl.duration)}`;
}
};
audioEl.onended = () => {
resetAudioUI();
if (getCurrentlyPlayingAudio() === audioEl) stopAllAudioPlayback();
};
const togglePlay = (e) => {
e.stopPropagation();
if (isAudioViewerOpen()) {
stopAllAudioPlayback();
showFullscreenPreview([fullUrl]);
return;
}
if (audioEl.paused) {
setCurrentlyPlayingAudio(audioEl);
audioEl.play().catch(()=>{});
playBtn.innerHTML = stopIconSvg;
previewAudio.classList.add("playing");
} else {
audioEl.pause();
audioEl.currentTime = 0;
resetAudioUI();
stopAllAudioPlayback();
}
};
playBtn.onclick = togglePlay;
playBtn.onmouseenter = () => {
playBtn.style.background = "#9333ea";
playBtn.style.borderColor = "#c084fc";
playBtn.style.transform = "scale(1.08)";
};
playBtn.onmouseleave = () => {
playBtn.style.background = previewAudio.classList.contains("playing") ? "rgba(147, 51, 234, 0.85)" : "rgba(22, 22, 30, 0.88)";
playBtn.style.borderColor = "rgba(255, 255, 255, 0.25)";
playBtn.style.transform = "scale(1.0)";
};
scrubber.onclick = (e) => {
e.stopPropagation();
const rect = scrubber.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
if (audioEl.duration > 0) audioEl.currentTime = pos * audioEl.duration;
};
previewAudio.append(topRow, centerArea, bottomRow, audioEl);
wrapper.appendChild(previewAudio);
previewAudio.onclick = (e) => {
if (e.target.closest('button, input') || e.target === scrubber || e.target === scrubberFill) return;
stopAllAudioPlayback();
if (e.ctrlKey || e.metaKey) {
openFileOrFolder(img);
return;
}
showFullscreenPreview([fullUrl]);
};
}
cardObj.firstImgElement = previewAudio;
previewAudio.setAttribute("draggable", "true");
previewAudio.ondragstart = (e) => {
e.stopPropagation();
const mimeType = "audio/wav";
try {
e.dataTransfer.setData("text/uri-list", fullUrl);
e.dataTransfer.setData("text/plain", fullUrl);
e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullUrl}`);
if (state && state.workflow) {
e.dataTransfer.setData("application/json", JSON.stringify(state.workflow));
}
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
};
}
export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state) {
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";
const ext = (filename.split('.').pop() || "FILE").toUpperCase();
const folderName = img.subfolder ? `${img.subfolder}/` : (img.type || "output");
if (!previewFile) {
wrapper.innerHTML = "";
previewFile = document.createElement("div");
previewFile.className = "comfy-sidebar-file-wrapper";
const topRow = document.createElement("div");
Object.assign(topRow.style, { display: "flex", justifyContent: "flex-end", width: "100%", minHeight: "18px" });
const badge = document.createElement("span");
badge.className = "comfy-sidebar-file-badge";
badge.textContent = ext;
topRow.appendChild(badge);
const centerArea = document.createElement("div");
Object.assign(centerArea.style, { display: "flex", alignItems: "center", gap: "8px", width: "100%", margin: "2px 0 6px 0" });
const icon = document.createElement("span");
icon.className = "pi pi-file comfy-sidebar-file-icon";
const title = document.createElement("span");
title.className = "comfy-sidebar-file-title";
title.textContent = filename;
title.title = filename;
centerArea.append(icon, title);
const bottomRow = document.createElement("div");
Object.assign(bottomRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" });
const folderInfo = document.createElement("div");
folderInfo.className = "comfy-sidebar-file-subtext";
folderInfo.innerHTML = `<i class="pi pi-folder" style="font-size: 10px;"></i><span>${folderName}</span>`;
const openHint = document.createElement("div");
openHint.className = "comfy-sidebar-file-subtext";
openHint.innerHTML = `<i class="pi pi-external-link" style="font-size: 9px;"></i><span>Open Folder</span>`;
bottomRow.append(folderInfo, openHint);
previewFile.append(topRow, centerArea, bottomRow);
wrapper.appendChild(previewFile);
} else {
const title = previewFile.querySelector(".comfy-sidebar-file-title");
if (title) { title.textContent = filename; title.title = filename; }
const badge = previewFile.querySelector(".comfy-sidebar-file-badge");
if (badge) badge.textContent = ext;
}
cardObj.firstImgElement = previewFile;
previewFile.onclick = (ev) => {
ev.stopPropagation();
openFileOrFolder(img);
};
previewFile.setAttribute("draggable", "true");
previewFile.ondragstart = (e) => {
e.stopPropagation();
const mimeType = "application/octet-stream";
try {
e.dataTransfer.setData("text/uri-list", fullUrl);
e.dataTransfer.setData("text/plain", fullUrl);
e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullUrl}`);
if (state && state.workflow) {
e.dataTransfer.setData("application/json", JSON.stringify(state.workflow));
}
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
};
}
export function renderCardImages(cardObj, state, onNavigateBatch) {
cardObj.currentImageIndex = cardObj.currentImageIndex || 0;
if (cardObj.currentImageIndex >= state.images.length) {
cardObj.currentImageIndex = 0;
}
const idx = cardObj.currentImageIndex;
const img = state.images[idx];
if (!img) {
cardObj.grid.innerHTML = "";
cardObj.firstImgElement = null;
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
return;
}
const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
const isVideo = isVideoFormat(src) || isVideoFormat(img.filename);
const is3D = is3DFormat(src) || is3DFormat(img.filename);
const isAudio = isAudioFormat(src) || isAudioFormat(img.filename);
const isImage = isImageFormat(src) || isImageFormat(img.filename) || (!isVideo && !is3D && !isAudio && !img.filename?.includes("."));
let wrapper = cardObj.grid.querySelector(".comfy-sidebar-media-wrapper");
if (!wrapper) {
cardObj.grid.innerHTML = "";
wrapper = document.createElement("div");
wrapper.className = "comfy-sidebar-media-wrapper";
Object.assign(wrapper.style, { position: "relative", width: "100%", display: "block" });
cardObj.grid.appendChild(wrapper);
}
let ctrlOverlay = wrapper.querySelector(".comfy-sidebar-ctrl-overlay");
if (!ctrlOverlay) {
ctrlOverlay = document.createElement("div");
ctrlOverlay.className = "comfy-sidebar-ctrl-overlay";
ctrlOverlay.innerHTML = `<i class="pi pi-folder-open"></i><span>Open Location</span>`;
wrapper.appendChild(ctrlOverlay);
}
if (is3D) {
render3DCardPreview(cardObj, wrapper, src, img, state);
return;
}
if (isAudio) {
renderAudioCardPreview(cardObj, wrapper, src, img, state);
return;
}
if (!isImage && !isVideo) {
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
return;
}
let mediaEl = wrapper.querySelector("img, video");
const needsRebuild = !mediaEl || (isVideo !== (mediaEl.tagName.toLowerCase() === "video"));
if (needsRebuild) {
if (mediaEl) mediaEl.remove();
mediaEl = isVideo ? document.createElement("video") : document.createElement("img");
Object.assign(mediaEl.style, {
width: "100%",
borderRadius: "2px",
display: "block",
cursor: "zoom-in",
webkitUserDrag: "element",
zIndex: "1"
});
wrapper.insertBefore(mediaEl, wrapper.firstChild);
}
cardObj.firstImgElement = mediaEl;
mediaEl.onclick = (ev) => {
ev.stopPropagation();
if (ev.ctrlKey || ev.metaKey) {
openFileOrFolder(img);
return;
}
showFullscreenPreview([src], ev.shiftKey);
};
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;
}
} catch (e) {}
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
}
};
const applyDimensions = (width, height) => {
if (cardObj.dimEl && width && height) {
cardObj.dimEl.textContent = `${width}x${height}`;
cardObj.dimEl.style.display = "block";
}
};
const isUnfinished = state.status ? (state.status !== "completed") : false;
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}`);
if (state && state.workflow) {
e.dataTransfer.setData("application/json", JSON.stringify(state.workflow));
}
} 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.onloadedmetadata = () => {
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
};
if (mediaEl.readyState >= 1) {
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
}
let playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
if (!playIcon) {
playIcon = document.createElement("div");
playIcon.className = "comfy-sidebar-play-icon";
Object.assign(playIcon.style, {
position: "absolute", top: "0", left: "0", width: "100%", height: "100%",
pointerEvents: "none", zIndex: "2", transition: "opacity 0.2s ease"
});
playIcon.innerHTML = `
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" style="width: 100%; height: 100%; filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));">
<circle cx="50" cy="50" r="16.65" fill="rgba(0, 0, 0, 0.55)" />
<polygon points="45.7,42.5 45.7,57.5 58.7,50" fill="rgba(255, 255, 255, 0.9)" />
</svg>
`;
wrapper.appendChild(playIcon);
}
mediaEl.onmouseenter = () => { playIcon.style.opacity = "0"; mediaEl.play().catch(()=>{}); };
mediaEl.onmouseleave = () => { playIcon.style.opacity = "1"; mediaEl.pause(); };
} else {
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);
}
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
if (playIcon) playIcon.remove();
}
const currentSrc = mediaEl.getAttribute("src") || mediaEl.src;
if (currentSrc !== src && currentSrc !== (currentSrc + "#t=0.001")) {
if (src.startsWith("blob:") && !isVideo) {
const tempImg = new Image();
tempImg.onload = () => {
const oldBlob = mediaEl._lastBlob;
mediaEl.src = src;
mediaEl._lastBlob = src;
applyDimensions(tempImg.naturalWidth, tempImg.naturalHeight);
if (oldBlob && oldBlob !== src && oldBlob.startsWith("blob:")) {
try { URL.revokeObjectURL(oldBlob); } catch(e){}
}
};
tempImg.src = src;
} else {
mediaEl.src = isVideo ? src + "#t=0.001" : src;
}
} else {
if (isVideo) {
if (mediaEl.videoWidth) applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
} else {
if (mediaEl.naturalWidth) applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
}
}
let navBar = wrapper.querySelector(".comfy-sidebar-batch-navbar");
if (state.images.length > 1) {
if (!navBar) {
navBar = document.createElement("div");
navBar.className = "comfy-sidebar-batch-navbar";
Object.assign(navBar.style, {
position: "absolute", bottom: "6px", left: "50%",
display: "flex", alignItems: "center", gap: "8px", background: "rgba(0,0,0,0.75)",
padding: "4px 10px", borderRadius: "12px", zIndex: "15", fontSize: "10px",
fontFamily: "monospace", color: "#eee", userSelect: "none", pointerEvents: "auto",
boxShadow: "0 1px 3px rgba(0,0,0,0.4)", transform: "translate3d(-50%, 0, 0)"
});
const prevBtn = document.createElement("span");
prevBtn.className = "pi pi-chevron-left";
prevBtn.style.cursor = "pointer";
prevBtn.onclick = (ev) => {
ev.stopPropagation();
cardObj.currentImageIndex = (cardObj.currentImageIndex - 1 + state.images.length) % state.images.length;
renderCardImages(cardObj, state, onNavigateBatch);
};
const label = document.createElement("span");
label.className = "comfy-sidebar-batch-label";
label.textContent = `${idx + 1}/${state.images.length}`;
label.style.cursor = "pointer";
label.title = "View all images in this batch";
label.onclick = (ev) => {
ev.stopPropagation();
if (typeof onNavigateBatch === "function") {
onNavigateBatch(state);
}
};
const nextBtn = document.createElement("span");
nextBtn.className = "pi pi-chevron-right";
nextBtn.style.cursor = "pointer";
nextBtn.onclick = (ev) => {
ev.stopPropagation();
cardObj.currentImageIndex = (cardObj.currentImageIndex + 1) % state.images.length;
renderCardImages(cardObj, state, onNavigateBatch);
};
navBar.append(prevBtn, label, nextBtn);
wrapper.appendChild(navBar);
}
const label = navBar.querySelector(".comfy-sidebar-batch-label");
if (label) label.textContent = `${idx + 1}/${state.images.length}`;
} else {
if (navBar) navBar.remove();
}
}
+912
View File
@@ -0,0 +1,912 @@
import { app } from "/scripts/app.js";
import { PromptStatus, StoreEvents, SettingIds } from "../core/constants.js";
import { store } from "../core/store.js";
import { getOrCreateCard, updateCardDOM, resetAllCardHoverStates, cardPool } from "./cardRenderer.js";
import { clearCancelledOrFailed, clearAllHistory, cancelAllPending } from "../queue/queueService.js";
import { updateSidebarBadge, centerAndSelectCanvasNode } from "../comfy/adapter.js";
import { matchesFilter, getRunOutputs, isImageFormat } from "../utils/utils.js";
import { renderCardImages } from "./mediaPreview.js";
import { deleteFileOnServer, removeImageFromNodeOutputs, copyImageToClipboard, findNodeIdForImage } from "./mediaActions.js";
let scrollToTopBtnEl = null;
let globalClickRegistered = false;
let renderAnimationFrameId = null;
let activeTimerInterval = null;
function getScrollContainer() {
if (!store.ui.cardStack) return null;
return store.ui.cardStack.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| store.ui.cardStack.parentElement
|| store.ui.cardStack;
}
function updateScrollTopBtnVisibility() {
if (!scrollToTopBtnEl || !store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) return;
const scrollEl = getScrollContainer();
if (scrollEl) {
const viewportEl = scrollEl.parentElement || store.ui.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";
}
}
}
const handleGlobalKeyDown = (e) => {
if (e.key === "Control" || e.key === "Meta") {
document.body.classList.add("comfy-sidebar-ctrl-active");
}
};
const handleGlobalKeyUp = (e) => {
if (e.key === "Control" || e.key === "Meta") {
document.body.classList.remove("comfy-sidebar-ctrl-active");
}
};
const handleGlobalBlur = () => {
document.body.classList.remove("comfy-sidebar-ctrl-active");
};
const handleGlobalClick = (e) => {
if (!store.ui.activeSubmenuPromptId && !store.ui.activeSubmenuBatchImages) {
document.removeEventListener("click", handleGlobalClick, true);
globalClickRegistered = false;
return;
}
const sidebar = store.ui.sidebarContainer;
const clickedInsideSidebar = sidebar && sidebar.contains(e.target);
const clickedFullscreenOverlay = e.target.closest('div[style*="zIndex: 999"], .comfy-sidebar-comparison-overlay');
if (!clickedInsideSidebar && !clickedFullscreenOverlay) {
store.closeSubmenu();
resetAllCardHoverStates();
renderSidebar();
document.removeEventListener("click", handleGlobalClick, true);
globalClickRegistered = false;
}
};
export function setupSidebarView() {
window.addEventListener("keydown", handleGlobalKeyDown);
window.addEventListener("keyup", handleGlobalKeyUp);
window.addEventListener("blur", handleGlobalBlur);
const container = document.createElement("div");
store.ui.sidebarContainer = container;
Object.assign(container.style, {
display: "flex", flexDirection: "column", height: "100%", padding: "14px", boxSizing: "border-box",
background: "var(--comfy-menu-bg, #121212)", color: "var(--fg-color, #eee)", position: "relative"
});
const header = document.createElement("div");
header.className = "comfy-sidebar-header-root";
Object.assign(header.style, { position: "relative", marginBottom: "12px", height: "26px", display: "flex", alignItems: "center" });
const standardHeader = document.createElement("div");
standardHeader.className = "comfy-sidebar-standard-header";
Object.assign(standardHeader.style, { display: "flex", width: "100%", alignItems: "center", justifyContent: "space-between" });
const titleGroup = document.createElement("div");
Object.assign(titleGroup.style, { display: "flex", alignItems: "center", gap: "8px" });
const searchIcon = document.createElement("span");
searchIcon.className = "pi pi-search comfy-sidebar-icon-btn";
searchIcon.title = "Search History";
const title = document.createElement("h3");
title.textContent = "Queue";
Object.assign(title.style, { margin: "0", fontSize: "14px", fontWeight: "bold", opacity: "0.9", color: "var(--fg-color, #eee)" });
titleGroup.appendChild(searchIcon);
titleGroup.appendChild(title);
standardHeader.appendChild(titleGroup);
const actionsGroup = document.createElement("div");
Object.assign(actionsGroup.style, { display: "flex", gap: "6px", alignItems: "center" });
const createActionBtn = (iconClass, tooltip, hoverColor, onClickFn) => {
const btn = document.createElement("button");
btn.className = `${iconClass} comfy-sidebar-header-btn`;
btn.title = tooltip;
let timeout = null, isPending = false;
const reset = () => {
isPending = false;
Object.assign(btn.style, {
color: "var(--desc-color, #aaa)", background: "transparent",
borderColor: "var(--border-color, #555)", boxShadow: "none"
});
if (timeout) { clearTimeout(timeout); timeout = null; }
};
btn.addEventListener("click", async (ev) => {
ev.stopPropagation();
if (!isPending) {
isPending = true;
Object.assign(btn.style, {
color: "#fff", background: hoverColor, borderColor: hoverColor,
boxShadow: `0 0 8px ${hoverColor}80`
});
timeout = setTimeout(reset, 1500);
} else {
reset();
await onClickFn();
}
});
return btn;
};
const btnClearInterrupted = createActionBtn("pi pi-eraser", "Clear Cancelled & Failed", "#ffc107", async () => {
await clearCancelledOrFailed();
renderSidebar();
});
const btnClearAll = createActionBtn("pi pi-trash", "Clear All History", "#dc3545", async () => {
await clearAllHistory();
renderSidebar();
});
actionsGroup.appendChild(btnClearInterrupted);
actionsGroup.appendChild(btnClearAll);
standardHeader.appendChild(actionsGroup);
const searchContainer = document.createElement("div");
searchContainer.className = "comfy-sidebar-search-container";
Object.assign(searchContainer.style, {
display: "none", width: "100%", alignItems: "center", background: "var(--comfy-input-bg, #181818)",
border: "1px solid var(--border-color, #555)", borderRadius: "4px", padding: "2px 8px",
boxSizing: "border-box", height: "26px"
});
const searchInputIcon = document.createElement("span");
searchInputIcon.className = "pi pi-search";
Object.assign(searchInputIcon.style, { fontSize: "11px", opacity: "0.5", marginRight: "6px" });
const searchInput = document.createElement("input");
Object.assign(searchInput, { type: "text", placeholder: "Filter by text, images, nodes..." });
Object.assign(searchInput.style, {
flex: "1", background: "transparent", border: "none", outline: "none",
color: "var(--comfy-input-color, var(--fg-color, #eee))", fontSize: "11px", padding: "0"
});
const clearSearchBtn = document.createElement("span");
clearSearchBtn.className = "pi pi-times comfy-sidebar-icon-btn";
clearSearchBtn.title = "Clear & Close Search";
Object.assign(clearSearchBtn.style, { marginLeft: "6px" });
searchContainer.appendChild(searchInputIcon);
searchContainer.appendChild(searchInput);
searchContainer.appendChild(clearSearchBtn);
header.appendChild(standardHeader);
header.appendChild(searchContainer);
container.appendChild(header);
searchIcon.addEventListener("click", (e) => {
e.stopPropagation();
standardHeader.style.display = "none";
searchContainer.style.display = "flex";
searchInput.focus();
});
const closeSearch = () => {
searchInput.value = "";
store.setSearchQuery("");
searchContainer.style.display = "none";
standardHeader.style.display = "flex";
renderSidebar();
};
clearSearchBtn.addEventListener("click", (e) => { e.stopPropagation(); closeSearch(); });
searchInput.addEventListener("keydown", (e) => { if (e.key === "Escape") closeSearch(); });
searchInput.addEventListener("input", () => {
store.setSearchQuery(searchInput.value.trim());
renderSidebar();
});
const cardStack = document.createElement("div");
store.ui.cardStack = cardStack;
Object.assign(cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" });
container.appendChild(cardStack);
scrollToTopBtnEl = document.createElement("button");
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
scrollToTopBtnEl.title = "Scroll to Top";
scrollToTopBtnEl.addEventListener("click", (e) => {
e.stopPropagation();
const scrollEl = getScrollContainer();
if (scrollEl) scrollEl.scrollTo({ top: 0, behavior: "smooth" });
});
container.appendChild(scrollToTopBtnEl);
document.addEventListener("scroll", updateScrollTopBtnVisibility, { capture: true, passive: true });
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";
}
}).observe(container);
activeTimerInterval = setInterval(() => {
const activePid = store.ui.currentlyActivePromptId;
if (!activePid) return;
const prompt = store.getPrompt(activePid);
if (prompt && prompt.status === PromptStatus.ACTIVE && prompt.startTime) {
const cardObj = cardPool.get(activePid);
if (cardObj && cardObj.timerEl) {
cardObj.timerEl.textContent = ((Date.now() - prompt.startTime) / 1000).toFixed(2) + "s";
}
}
}, 250);
store.on(StoreEvents.QUEUE_SYNCED, () => {
const pendingCount = store.getAllPrompts().filter(t => t.status === PromptStatus.PENDING).length;
const runningCount = store.getAllPrompts().filter(t => t.status === PromptStatus.ACTIVE).length;
updateSidebarBadge(pendingCount + (runningCount > 0 ? 1 : 0));
renderSidebar();
});
store.on(StoreEvents.PROMPT_ADDED, () => renderSidebar());
store.on(StoreEvents.PROMPT_DELETED, () => renderSidebar());
store.on(StoreEvents.PROMPTS_CLEARED, () => renderSidebar());
store.on(StoreEvents.SUBMENU_CHANGED, () => renderSidebar());
return container;
}
export function renderSidebar() {
if (renderAnimationFrameId) cancelAnimationFrame(renderAnimationFrameId);
renderAnimationFrameId = requestAnimationFrame(() => {
if (!store.ui.sidebarContainer || !store.ui.cardStack) return;
const showPendingSummary = app.ui?.settings?.getSettingValue?.(SettingIds.SHOW_PENDING_COUNT_ONLY) ?? true;
const showWorkingNode = app.ui?.settings?.getSettingValue?.(SettingIds.SHOW_WORKING_NODE_NAME) ?? true;
const headerTitle = store.ui.sidebarContainer.querySelector("h3");
const headerSearchIcon = store.ui.sidebarContainer.querySelector(".comfy-sidebar-header-root .pi-search");
const headerActions = store.ui.sidebarContainer.querySelector(".comfy-sidebar-header-btn")?.parentNode;
// Submenu 1: Batch Images View
if (store.ui.activeSubmenuBatchImages) {
const batchInfo = store.ui.activeSubmenuBatchImages;
if (headerTitle) {
headerTitle.textContent = `Batch of #${batchInfo.pid}`;
headerTitle.style.cursor = "pointer";
headerTitle.title = "Go Back";
headerTitle.onclick = () => {
if (batchInfo.parentPromptId) {
store.openOutputsSubmenu(batchInfo.parentPromptId);
} else {
store.closeSubmenu();
}
resetAllCardHoverStates();
renderSidebar();
};
}
if (headerSearchIcon) headerSearchIcon.style.display = "none";
if (headerActions) headerActions.style.display = "none";
if (!globalClickRegistered) {
document.addEventListener("click", handleGlobalClick, true);
globalClickRegistered = true;
}
const targetElements = [];
batchInfo.images.forEach((img, index) => {
const cardId = `batch-${batchInfo.pid}-${index}`;
let cardObj = cardPool.get(cardId);
if (!cardObj) {
const card = document.createElement("div");
card.id = `card-${cardId}`;
card.className = "comfy-sidebar-card completed";
card.style.position = "relative";
const timerEl = document.createElement("div");
timerEl.className = "comfy-sidebar-card-timer";
timerEl.textContent = `Image ${index + 1}/${batchInfo.images.length}`;
timerEl.style.display = "block";
const dimEl = document.createElement("div");
Object.assign(dimEl.style, {
position: "absolute", top: "6px", right: "8px", fontSize: "10px",
fontFamily: "monospace", opacity: "0.7", background: "rgba(0, 0, 0, 0.6)",
padding: "2px 4px", borderRadius: "3px", pointerEvents: "none", zIndex: "5", color: "#fff",
display: "none", transform: "translateZ(0)"
});
const grid = document.createElement("div");
grid.style.display = "flex";
grid.style.flexDirection = "column";
grid.style.gap = "6px";
const p = document.createElement("div");
p.className = "comfy-sidebar-text-clamp";
const hoverPanel = document.createElement("div");
hoverPanel.className = "comfy-sidebar-hover-panel";
Object.assign(hoverPanel.style, {
position: "absolute", bottom: "4px", right: "4px",
flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
btnImg.onclick = (ev) => {
ev.stopPropagation();
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();
};
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
btnDel.title = "Remove from batch (Hold Ctrl to delete file from disk)";
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = "Delete File";
btnDel.appendChild(btnDelLabel);
let isDeletePending = false, isDiskDelete = false, deleteTimeout = null;
const resetDelete = () => {
isDeletePending = false;
isDiskDelete = false;
btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
btnDel.title = "Remove from batch (Hold Ctrl to delete file from disk)";
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
btnDel.onclick = async (ev) => {
ev.stopPropagation();
const wantsDiskDelete = ev.ctrlKey || ev.metaKey;
if (!isDeletePending) {
isDeletePending = true;
isDiskDelete = wantsDiskDelete;
cardObj.btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
cardObj.btnDel.title = wantsDiskDelete
? "Ctrl+Click again to delete file from DISK / TRASH"
: "Click again to confirm removing from batch";
deleteTimeout = setTimeout(resetDelete, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
resetDelete();
if (shouldDeleteFromDisk && img && img.filename) {
await deleteFileOnServer(img);
}
const pid = batchInfo.parentPromptId || batchInfo.pid;
const state = store.getPrompt(pid);
const imgIdx = batchInfo.images.indexOf(img);
if (imgIdx > -1) batchInfo.images.splice(imgIdx, 1);
if (state) {
if (Array.isArray(state.images)) {
const stIdx = state.images.findIndex(i => i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""));
if (stIdx > -1) state.images.splice(stIdx, 1);
}
if (state.nodeOutputs) {
removeImageFromNodeOutputs(state.nodeOutputs, img);
}
state.rendered = false;
store.updatePrompt(pid, state);
}
const mainCardObj = cardPool.get(pid);
if (mainCardObj) {
mainCardObj.currentImageIndex = 0;
mainCardObj.lastImagesSignature = "";
}
if (batchInfo.images.length === 0) {
if (batchInfo.parentPromptId) {
store.openOutputsSubmenu(batchInfo.parentPromptId);
} else {
store.closeSubmenu();
}
}
renderSidebar();
}
};
hoverPanel.append(btnImg, btnDel);
const leftHoverPanel = document.createElement("div");
leftHoverPanel.className = "comfy-sidebar-left-hover-panel";
Object.assign(leftHoverPanel.style, {
position: "absolute", bottom: "4px", left: "4px",
flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnCopy = document.createElement("span");
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.title = "Copy to Clipboard";
const isCopyable = isImageFormat(img.filename || img.url);
if (!isCopyable) {
btnCopy.style.display = "none";
} else {
btnCopy.onclick = async (ev) => {
ev.stopPropagation();
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
const success = await copyImageToClipboard(src);
if (success) {
btnCopy.className = "pi pi-check comfy-sidebar-card-action-btn";
btnCopy.style.color = "#4ade80";
setTimeout(() => {
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.style.color = "";
}, 1500);
}
};
}
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
const nodeId = findNodeIdForImage({ nodeOutputs: batchInfo.nodeOutputs }, img);
if (nodeId) {
btnFocus.onclick = (ev) => {
ev.stopPropagation();
centerAndSelectCanvasNode(nodeId);
};
} else {
btnFocus.style.display = "none";
}
leftHoverPanel.append(btnCopy, btnFocus);
card.append(timerEl, dimEl, grid, p, hoverPanel, leftHoverPanel);
cardObj = {
element: card,
timerEl,
dimEl,
grid,
placeholder: p,
hoverPanel,
leftHoverPanel,
btnImg,
btnCopy,
btnFocus,
btnDel,
firstImgElement: null
};
cardPool.set(cardId, cardObj);
}
cardObj.placeholder.style.display = "none";
renderCardImages(cardObj, {
pid: batchInfo.pid,
status: PromptStatus.COMPLETED,
images: [img],
workflow: batchInfo.workflow,
nodeOutputs: batchInfo.nodeOutputs
});
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);
}
const scrollEl = getScrollContainer();
if (scrollEl) scrollEl.scrollTop = 0;
updateScrollTopBtnVisibility();
return;
}
// Submenu 2: Intermediate Outputs View
if (store.ui.activeSubmenuPromptId) {
const st = store.getPrompt(store.ui.activeSubmenuPromptId);
if (!st) {
store.closeSubmenu();
renderSidebar();
return;
}
if (headerTitle) {
headerTitle.textContent = `Outputs of #${store.ui.activeSubmenuPromptId}`;
headerTitle.style.cursor = "pointer";
headerTitle.title = "Go Back to Queue";
headerTitle.onclick = () => {
store.closeSubmenu();
resetAllCardHoverStates();
renderSidebar();
};
}
if (headerSearchIcon) headerSearchIcon.style.display = "none";
if (headerActions) headerActions.style.display = "none";
if (!globalClickRegistered) {
document.addEventListener("click", handleGlobalClick, true);
globalClickRegistered = true;
}
const rawOutputs = getRunOutputs(st.nodeOutputs, st.workflow);
const seenFiles = new Set();
const outputs = [];
rawOutputs.forEach(out => {
const uniqueImgs = (out.images || []).filter(img => {
const key = `${img.subfolder || ""}/${img.filename}`;
if (seenFiles.has(key)) return false;
seenFiles.add(key);
return true;
});
if (uniqueImgs.length > 0) {
outputs.push({ nodeId: out.nodeId, images: uniqueImgs, nodeTitle: out.nodeTitle });
}
});
const targetElements = [];
if (outputs.length === 0) {
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"
});
emptyCard.textContent = "No separate intermediate outputs found.";
targetElements.push(emptyCard);
} else {
outputs.forEach((out) => {
const cardId = `submenu-${st.pid}-${out.nodeId}`;
let cardObj = cardPool.get(cardId);
if (!cardObj) {
const card = document.createElement("div");
card.id = `card-${cardId}`;
card.className = "comfy-sidebar-card completed";
card.style.position = "relative";
const titleFromState = st.nodeTitles?.[out.nodeId];
const node = st.workflow?.nodes?.find(n => String(n.id) === String(out.nodeId));
const resolvedTitle = titleFromState || (node ? (node.title || node.type) : out.nodeTitle) || "Node";
const timerEl = document.createElement("div");
timerEl.className = "comfy-sidebar-card-timer";
timerEl.textContent = `${resolvedTitle} (#${out.nodeId})`;
timerEl.style.display = "block";
const dimEl = document.createElement("div");
Object.assign(dimEl.style, {
position: "absolute", top: "6px", right: "8px", fontSize: "10px",
fontFamily: "monospace", opacity: "0.7", background: "rgba(0, 0, 0, 0.6)",
padding: "2px 4px", borderRadius: "3px", pointerEvents: "none", zIndex: "5", color: "#fff",
display: "none", transform: "translateZ(0)"
});
const grid = document.createElement("div");
grid.style.display = "flex";
grid.style.flexDirection = "column";
grid.style.gap = "6px";
const p = document.createElement("div");
p.className = "comfy-sidebar-text-clamp";
const hoverPanel = document.createElement("div");
hoverPanel.className = "comfy-sidebar-hover-panel";
Object.assign(hoverPanel.style, {
position: "absolute", bottom: "4px", right: "4px",
flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
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 || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
a.download = img.filename || "output";
a.click();
});
};
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
btnDel.title = "Delete node output (Hold Ctrl to delete file from disk)";
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = "Delete File";
btnDel.appendChild(btnDelLabel);
let isDeletePending = false, isDiskDelete = false, deleteTimeout = null;
const resetDelete = () => {
isDeletePending = false;
isDiskDelete = false;
btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
btnDel.title = "Delete node output (Hold Ctrl to delete file from disk)";
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
btnDel.onclick = async (ev) => {
ev.stopPropagation();
const wantsDiskDelete = ev.ctrlKey || ev.metaKey;
if (!isDeletePending) {
isDeletePending = true;
isDiskDelete = wantsDiskDelete;
btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
btnDel.title = wantsDiskDelete
? "Ctrl+Click again to delete this node's files from DISK / TRASH"
: "Click again to confirm removing output";
deleteTimeout = setTimeout(resetDelete, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
resetDelete();
if (shouldDeleteFromDisk && out.images) {
for (const imgItem of out.images) {
await deleteFileOnServer(imgItem);
}
}
if (st.nodeOutputs && st.nodeOutputs[out.nodeId]) {
delete st.nodeOutputs[out.nodeId];
}
const remainingOutputs = getRunOutputs(st.nodeOutputs, st.workflow);
if (remainingOutputs.length > 0) {
st.images = remainingOutputs[remainingOutputs.length - 1].images || [];
} else {
st.images = [];
store.closeSubmenu();
}
store.updatePrompt(st.pid, st);
renderSidebar();
}
};
hoverPanel.append(btnImg, btnDel);
const leftHoverPanel = document.createElement("div");
leftHoverPanel.className = "comfy-sidebar-left-hover-panel";
Object.assign(leftHoverPanel.style, {
position: "absolute", bottom: "4px", left: "4px",
flexDirection: "column", gap: "4px", zIndex: "20"
});
const btnCopy = document.createElement("span");
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.title = "Copy to Clipboard";
const firstImg = out.images[0];
const isCopyable = firstImg && isImageFormat(firstImg.filename || firstImg.url);
if (!isCopyable) {
btnCopy.style.display = "none";
} else {
btnCopy.onclick = async (ev) => {
ev.stopPropagation();
const src = firstImg.url ? firstImg.url : `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}`;
const success = await copyImageToClipboard(src);
if (success) {
btnCopy.className = "pi pi-check comfy-sidebar-card-action-btn";
btnCopy.style.color = "#4ade80";
setTimeout(() => {
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.style.color = "";
}, 1500);
}
};
}
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
btnFocus.onclick = (ev) => {
ev.stopPropagation();
centerAndSelectCanvasNode(out.nodeId);
};
leftHoverPanel.append(btnCopy, btnFocus);
card.append(timerEl, dimEl, grid, p, hoverPanel, leftHoverPanel);
cardObj = {
element: card,
timerEl,
dimEl,
grid,
placeholder: p,
hoverPanel,
leftHoverPanel,
btnImg,
btnCopy,
btnFocus,
btnDel,
firstImgElement: null
};
cardPool.set(cardId, cardObj);
}
if (out.images && out.images.length > 0) {
cardObj.placeholder.style.display = "none";
renderCardImages(cardObj, {
pid: st.pid,
status: PromptStatus.COMPLETED,
images: out.images,
workflow: st.workflow,
nodeOutputs: st.nodeOutputs
}, () => {
// Clicking (1/N) opens batch drill-down for this specific intermediate node
const scrollEl = getScrollContainer();
if (scrollEl) {
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
}
const nodeTitle = st.nodeTitles?.[out.nodeId] || out.nodeTitle || "Node";
store.openBatchSubmenu({
pid: `${st.pid} - ${nodeTitle} (#${out.nodeId})`,
parentPromptId: st.pid,
images: out.images,
workflow: st.workflow,
nodeOutputs: { [out.nodeId]: st.nodeOutputs?.[out.nodeId] }
});
});
} else {
cardObj.placeholder.style.display = "block";
cardObj.placeholder.textContent = "No Outputs";
}
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);
}
updateScrollTopBtnVisibility();
return;
}
// Main Queue View
if (headerTitle) {
headerTitle.textContent = "Queue";
headerTitle.style.cursor = "default";
headerTitle.onclick = null;
}
if (headerSearchIcon) headerSearchIcon.style.display = "inline";
if (headerActions) headerActions.style.display = "flex";
let tasksArray = store.getAllPrompts();
if (showPendingSummary) {
tasksArray = tasksArray.filter(t => t.status !== PromptStatus.PENDING);
}
if (store.ui.searchQuery) {
tasksArray = tasksArray.filter(t => matchesFilter(t, store.ui.searchQuery));
}
tasksArray.sort((a, b) => b.timestamp - a.timestamp);
const activeTasks = tasksArray.filter(t => t.status === PromptStatus.ACTIVE);
const completedTasks = tasksArray.filter(t => t.status === PromptStatus.COMPLETED || t.status === PromptStatus.CANCELLED || t.status === PromptStatus.ERROR);
const pendingTasks = tasksArray.filter(t => t.status === PromptStatus.PENDING).sort((a, b) => (b.queueNumber || 0) - (a.queueNumber || 0));
const syncTaskCard = (state) => {
const cardObj = getOrCreateCard(state);
return updateCardDOM(cardObj, state, showPendingSummary, showWorkingNode);
};
const targetElements = [];
const pendingCount = store.getAllPrompts().filter(t => t.status === PromptStatus.PENDING).length;
if (pendingCount > 0) {
if (showPendingSummary) {
let pCard = cardPool.get("pending-summary-card");
if (!pCard) {
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"
});
const textDiv = document.createElement("div");
el.appendChild(textDiv);
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancel All Pending";
Object.assign(cancelBtn.style, {
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "4px",
cursor: "pointer", fontSize: "11px", fontWeight: "bold"
});
cancelBtn.onclick = async () => {
await cancelAllPending();
};
el.appendChild(cancelBtn);
pCard = { element: el, textDiv };
cardPool.set("pending-summary-card", pCard);
}
pCard.textDiv.textContent = `Pending Queue: ${pendingCount} tasks`;
targetElements.push(pCard.element);
} else {
let btnCard = cardPool.get("pending-cancel-all-standalone");
if (!btnCard) {
const btn = document.createElement("button");
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"
});
btn.onclick = async () => {
await cancelAllPending();
};
btnCard = { element: btn };
cardPool.set("pending-cancel-all-standalone", btnCard);
}
targetElements.push(btnCard.element);
pendingTasks.forEach(st => targetElements.push(syncTaskCard(st)));
}
}
activeTasks.forEach(st => targetElements.push(syncTaskCard(st)));
completedTasks.forEach(st => targetElements.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);
}
if (store.ui.mainQueueScrollTop !== null) {
const restorePos = store.ui.mainQueueScrollTop;
store.ui.mainQueueScrollTop = null;
const scrollEl = getScrollContainer();
if (scrollEl) {
scrollEl.scrollTop = restorePos;
requestAnimationFrame(() => { scrollEl.scrollTop = restorePos; });
}
}
updateScrollTopBtnVisibility();
});
}
export function teardownSidebarView() {
window.removeEventListener("keydown", handleGlobalKeyDown);
window.removeEventListener("keyup", handleGlobalKeyUp);
window.removeEventListener("blur", handleGlobalBlur);
document.removeEventListener("click", handleGlobalClick, true);
document.removeEventListener("scroll", updateScrollTopBtnVisibility, { capture: true, passive: true });
if (activeTimerInterval) {
clearInterval(activeTimerInterval);
activeTimerInterval = null;
}
if (renderAnimationFrameId) {
cancelAnimationFrame(renderAnimationFrameId);
renderAnimationFrameId = null;
}
cardPool.clear();
}