677 lines
31 KiB
JavaScript
677 lines
31 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
import { PromptStatus } from "../core/constants.js";
|
|
import { t } from "../core/i18n.js";
|
|
import { store } from "../core/store.js";
|
|
import { renderCardImages } from "./mediaPreview.js";
|
|
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage, removeImageFromNodeOutputs } from "./mediaActions.js";
|
|
import { centerAndSelectCanvasNode } from "../comfy/adapter.js";
|
|
import { getRunOutputs, extractWorkflowFromPng, isImageFormat, animateCardRemoval, downloadJson, getScrollContainer, attachDeleteConfirmation } from "../utils/utils.js";
|
|
import { showFullscreenPreview } from "../utils/comparison.js";
|
|
import { stopAllAudioPlayback } from "./audioController.js";
|
|
import { deleteHistoryItem, cancelPendingTask, interruptActive } from "../queue/queueService.js";
|
|
import { showNotification } from "../utils/nodeRenumber.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 randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
|
|
|
// 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;
|
|
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 || '')}${randParam}`;
|
|
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 || '')}${randParam}`;
|
|
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 || '')}${randParam}`;
|
|
wf = await extractWorkflowFromPng(src);
|
|
if (wf) state.workflow = wf;
|
|
}
|
|
if (!wf) {
|
|
alert(t("No workflow metadata found in this file."));
|
|
return;
|
|
}
|
|
downloadJson(wf, `workflow_${state.pid || "comfy"}.json`);
|
|
};
|
|
} else {
|
|
cardObj.btnJson.style.setProperty("display", "none", "important");
|
|
}
|
|
}
|
|
|
|
if (cardObj.btnDel) {
|
|
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
|
|
? t("Delete Card (Hold Ctrl to delete file from disk)")
|
|
: t("Delete Card");
|
|
}
|
|
|
|
if (cardObj.btnFocus) {
|
|
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) ? t("Show Stopped/Failed Node") : t("Show Node");
|
|
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
|
|
.map(o => ({
|
|
...o,
|
|
images: (o.images || []).filter(img => img && img.filename && !img.isFallback)
|
|
}))
|
|
.filter(o => o.images.length > 0);
|
|
|
|
const primarySignatures = new Set((state.images || []).map(i => `${i.subfolder || ""}/${i.filename}`));
|
|
const distinctOutputs = [];
|
|
|
|
for (const out of validOutputs) {
|
|
const distinctImages = out.images.filter(img => !primarySignatures.has(`${img.subfolder || ""}/${img.filename}`));
|
|
if (distinctImages.length > 0) {
|
|
distinctOutputs.push({ ...out, images: distinctImages });
|
|
}
|
|
}
|
|
|
|
const hasIntermediates = (validOutputs.length > 1 && distinctOutputs.length > 0) ||
|
|
(state.status !== PromptStatus.COMPLETED && validOutputs.length > 0);
|
|
|
|
if (!hasIntermediates) {
|
|
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
|
} else {
|
|
// If already verified (or freshly generated), display button immediately
|
|
if (state._hasValidatedIntermediates) {
|
|
cardObj.leftHoverBtn.style.removeProperty("display");
|
|
cardObj.leftHoverBtn.style.display = "inline-flex";
|
|
} else {
|
|
// Old restored card: keep hidden by default while stealthily testing files
|
|
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
|
|
|
if (!cardObj._isCheckingIntermediates) {
|
|
cardObj._isCheckingIntermediates = true;
|
|
|
|
const checkPromises = [];
|
|
const missingImages = [];
|
|
|
|
for (const out of distinctOutputs) {
|
|
for (const img of out.images) {
|
|
if (!img.filename || (img.url && img.url.startsWith("blob:"))) continue;
|
|
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
|
checkPromises.push(
|
|
fetch(src, { method: "HEAD" })
|
|
.then(res => {
|
|
if (res.status === 404) {
|
|
missingImages.push(img);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
missingImages.push(img);
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
const finalizeCheck = () => {
|
|
cardObj._isCheckingIntermediates = false;
|
|
const liveState = store.getPrompt(state.pid) || state;
|
|
liveState._hasValidatedIntermediates = true;
|
|
|
|
if (missingImages.length > 0 && liveState && liveState.nodeOutputs) {
|
|
for (const img of missingImages) {
|
|
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
|
}
|
|
for (const nodeId in liveState.nodeOutputs) {
|
|
const nodeVal = liveState.nodeOutputs[nodeId];
|
|
if (nodeVal && typeof nodeVal === "object") {
|
|
const hasAny = Object.values(nodeVal).some(v => Array.isArray(v) ? v.length > 0 : Boolean(v));
|
|
if (!hasAny) {
|
|
delete liveState.nodeOutputs[nodeId];
|
|
}
|
|
}
|
|
}
|
|
store.updatePrompt(liveState.pid, liveState);
|
|
}
|
|
|
|
const curOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
|
|
const curValid = curOutputs
|
|
.map(o => ({
|
|
...o,
|
|
images: (o.images || []).filter(i => i && i.filename && !i.isFallback)
|
|
}))
|
|
.filter(o => o.images.length > 0);
|
|
const curDistinct = curValid.filter(o => o.images.some(i => !primarySignatures.has(`${i.subfolder || ""}/${i.filename}`)));
|
|
const stillHas = (curValid.length > 1 && curDistinct.length > 0) ||
|
|
(liveState.status !== PromptStatus.COMPLETED && curValid.length > 0);
|
|
|
|
if (stillHas) {
|
|
cardObj.leftHoverBtn.style.removeProperty("display");
|
|
cardObj.leftHoverBtn.style.display = "inline-flex";
|
|
} else {
|
|
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
|
}
|
|
};
|
|
|
|
if (checkPromises.length > 0) {
|
|
Promise.all(checkPromises).then(finalizeCheck).catch(finalizeCheck);
|
|
} else {
|
|
finalizeCheck();
|
|
}
|
|
}
|
|
}
|
|
|
|
cardObj.leftHoverBtn.onclick = async (ev) => {
|
|
ev.stopPropagation();
|
|
|
|
const liveState = store.getPrompt(state.pid) || state;
|
|
const curOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
|
|
const curValid = curOutputs
|
|
.map(o => ({
|
|
...o,
|
|
images: (o.images || []).filter(i => i && i.filename && !i.isFallback)
|
|
}))
|
|
.filter(o => o.images.length > 0);
|
|
|
|
const primSigs = new Set((liveState.images || []).map(i => `${i.subfolder || ""}/${i.filename}`));
|
|
const curDistinct = curValid.filter(o => 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) {
|
|
if (primSigs.has(`${img.subfolder || ""}/${img.filename}`)) continue;
|
|
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
|
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) {
|
|
for (const nodeId in liveState.nodeOutputs) {
|
|
const nodeVal = liveState.nodeOutputs[nodeId];
|
|
if (nodeVal && typeof nodeVal === "object") {
|
|
const hasAny = Object.values(nodeVal).some(v => Array.isArray(v) ? v.length > 0 : Boolean(v));
|
|
if (!hasAny) {
|
|
delete liveState.nodeOutputs[nodeId];
|
|
}
|
|
}
|
|
}
|
|
store.updatePrompt(liveState.pid, liveState);
|
|
syncCardButtonVisibility(cardObj, liveState);
|
|
const remainingOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
|
|
const remaining = remainingOutputs
|
|
.map(o => ({ ...o, images: (o.images || []).filter(i => i && i.filename && !i.isFallback) }))
|
|
.filter(o => o.images.some(img => !primSigs.has(`${img.subfolder || ""}/${img.filename}`)));
|
|
if (remaining.length === 0) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const scrollEl = getScrollContainer(store.ui.cardStack);
|
|
if (scrollEl) {
|
|
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
|
|
}
|
|
store.openOutputsSubmenu(liveState.pid);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export function updateCardProgressTargeted(cardObj, progress, activeNodeName, showWorkingNode = true, state = null) {
|
|
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..." ? t("Finishing...") : `[${activeNodeName}]`) : t("Processing...");
|
|
cardObj.statusText.textContent = `${nodeText} ${progress}%`;
|
|
}
|
|
|
|
if (state && state.images && state.images.length > 0) {
|
|
const firstImg = state.images[0];
|
|
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
|
const previewUrl = firstImg.url || (firstImg.filename ? `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}${randParam}` : 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 = {}) {
|
|
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: "25"
|
|
});
|
|
|
|
const sBadge = document.createElement("div");
|
|
sBadge.className = "comfy-sidebar-status-badge";
|
|
Object.assign(sBadge.style, {
|
|
position: "absolute", top: "6px", right: "8px", fontSize: "10px", fontWeight: "bold",
|
|
padding: "3px 7px", borderRadius: "3px", textTransform: "uppercase", display: "none",
|
|
pointerEvents: "none", zIndex: "30", letterSpacing: "0.5px", boxShadow: "0 2px 6px rgba(0,0,0,0.6)"
|
|
});
|
|
|
|
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: "var(--border, #333)", borderRadius: "2px", marginTop: "8px",
|
|
overflow: "hidden", display: "none"
|
|
});
|
|
const pb = document.createElement("div");
|
|
Object.assign(pb.style, {
|
|
width: "0%", height: "100%", background: "var(--primary, var(--p-primary-color, #ea580c))", transition: "width 0.1s linear"
|
|
});
|
|
pt.appendChild(pb);
|
|
|
|
const statusText = document.createElement("div");
|
|
Object.assign(statusText.style, {
|
|
fontSize: "11px", opacity: "0.9", color: "var(--primary, var(--p-primary-color, #ea580c))", 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 = t("Download Object");
|
|
|
|
const btnCopy = document.createElement("span");
|
|
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
|
|
btnCopy.title = t("Copy to Clipboard");
|
|
btnCopy.style.display = "none";
|
|
|
|
const btnJson = document.createElement("span");
|
|
btnJson.className = "pi pi-file comfy-sidebar-card-action-btn";
|
|
btnJson.title = t("Download JSON");
|
|
|
|
const btnDel = document.createElement("span");
|
|
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
|
|
btnDel.title = t("Delete Card (Hold Ctrl to delete file from disk)");
|
|
const btnDelLabel = document.createElement("span");
|
|
btnDelLabel.className = "comfy-sidebar-del-label";
|
|
btnDelLabel.textContent = t("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 = t("Show Node");
|
|
|
|
const leftHoverBtn = document.createElement("span");
|
|
leftHoverBtn.className = "pi pi-images comfy-sidebar-card-action-btn";
|
|
leftHoverBtn.title = t("View all intermediate outputs");
|
|
|
|
hoverPanel.append(btnImg, btnJson, btnDel);
|
|
leftHoverPanel.append(btnCopy, btnFocus, leftHoverBtn);
|
|
|
|
// Appending grid FIRST ensures overlays (badges, cancel button) naturally render on top
|
|
card.append(grid, p, pt, statusText, timerEl, cancelX, dimEl, sBadge, 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,
|
|
_isCheckingIntermediates: false
|
|
};
|
|
|
|
card.addEventListener("mouseenter", () => {
|
|
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";
|
|
}
|
|
});
|
|
|
|
attachDeleteConfirmation(btnDel, {
|
|
canDiskDelete: () => cardObj.canDeleteFromDisk !== false,
|
|
idleTitle: () => cardObj.canDeleteFromDisk ? t("Delete Card (Hold Ctrl to delete file from disk)") : t("Delete Card"),
|
|
confirmTitle: t("Click again to confirm removing card"),
|
|
confirmDiskTitle: t("Ctrl+Click again to delete file from DISK / TRASH"),
|
|
onConfirm: async (shouldDeleteFromDisk) => {
|
|
animateCardRemoval(card, async () => {
|
|
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));
|
|
|
|
let deletedCount = 0;
|
|
let lastAction = "trashed";
|
|
for (const fileItem of filesToDelete) {
|
|
const action = await deleteFileOnServer(fileItem);
|
|
if (action) {
|
|
deletedCount++;
|
|
lastAction = action;
|
|
}
|
|
}
|
|
|
|
if (deletedCount > 0) {
|
|
const msg = lastAction === "trashed"
|
|
? `Moved ${deletedCount} file${deletedCount > 1 ? "s" : ""} to Recycle Bin.`
|
|
: `Permanently deleted ${deletedCount} file${deletedCount > 1 ? "s" : ""}.`;
|
|
showNotification(msg, "success", "Delete Files");
|
|
}
|
|
}
|
|
|
|
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: "#eab308",
|
|
color: "#000",
|
|
zIndex: "30"
|
|
});
|
|
cardObj.statusBadge.textContent = t("Cancelled");
|
|
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
|
} else if (state.status === PromptStatus.ERROR) {
|
|
Object.assign(cardObj.statusBadge.style, {
|
|
display: "block",
|
|
background: "#dc3545",
|
|
color: "#fff",
|
|
zIndex: "30"
|
|
});
|
|
cardObj.statusBadge.textContent = t("Error");
|
|
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
|
} else {
|
|
cardObj.statusBadge.style.display = "none";
|
|
}
|
|
|
|
if (state.status === PromptStatus.PENDING && !showPendingSummary) {
|
|
cardObj.cancelBtn.style.display = "flex";
|
|
cardObj.cancelBtn.title = t("Cancel Queued Task");
|
|
cardObj.cancelBtn.onclick = async (ev) => {
|
|
ev.stopPropagation();
|
|
cancelPendingTask(state.pid);
|
|
};
|
|
} else if (state.status === PromptStatus.ACTIVE) {
|
|
cardObj.cancelBtn.style.display = "flex";
|
|
cardObj.cancelBtn.title = t("Interrupt Execution");
|
|
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 = getScrollContainer(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 = t("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 }], false, state.pid);
|
|
};
|
|
} else {
|
|
cardObj.placeholder.textContent = state.progressText || t("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..." ? t("Finishing...") : `[${state.activeNodeName}]`) : t("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;
|
|
} |