1067 lines
48 KiB
JavaScript
1067 lines
48 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
import { PromptStatus, StoreEvents, SettingIds } from "../core/constants.js";
|
|
import { store } from "../core/store.js";
|
|
import { t } from "../core/i18n.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, animateCardRemoval, getScrollContainer, calculateGridColumns, setupSpotlightTracker, attachDeleteConfirmation } from "../utils/utils.js";
|
|
import { getSetting } from "../core/settings.js";
|
|
import { renderCardImages } from "./mediaPreview.js";
|
|
import { deleteFileOnServer, removeImageFromNodeOutputs, copyImageToClipboard, findNodeIdForImage } from "./mediaActions.js";
|
|
import { toggleIgnoreActiveNode } from "../comfy/nodes.js";
|
|
import { renumberNodesTopologically, showNotification } from "../utils/nodeRenumber.js";
|
|
|
|
let scrollToTopBtnEl = null;
|
|
let globalClickRegistered = false;
|
|
let renderAnimationFrameId = null;
|
|
let activeTimerInterval = null;
|
|
let currentColumnCount = 1;
|
|
let stickyHeaderGroupEl = null;
|
|
let lastScrollTop = 0;
|
|
let scrollDeltaAccumulator = 0;
|
|
let isHeaderHidden = false;
|
|
|
|
function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
|
|
if (!cardStack) return;
|
|
|
|
// 1. Full-width container (for banners like Pending Queue summary)
|
|
let bannerContainer = cardStack.querySelector('.comfy-sidebar-banners');
|
|
if (!bannerContainer) {
|
|
bannerContainer = document.createElement('div');
|
|
bannerContainer.className = 'comfy-sidebar-banners';
|
|
Object.assign(bannerContainer.style, { display: 'flex', flexDirection: 'column', gap: '12px', width: '100%' });
|
|
cardStack.insertBefore(bannerContainer, cardStack.firstChild);
|
|
}
|
|
fullWidthEls.forEach((el, i) => {
|
|
if (bannerContainer.children[i] !== el) {
|
|
bannerContainer.insertBefore(el, bannerContainer.children[i] || null);
|
|
}
|
|
});
|
|
while (bannerContainer.children.length > fullWidthEls.length) {
|
|
bannerContainer.removeChild(bannerContainer.lastChild);
|
|
}
|
|
bannerContainer.style.display = fullWidthEls.length > 0 ? 'flex' : 'none';
|
|
bannerContainer.style.marginBottom = fullWidthEls.length > 0 ? '12px' : '0';
|
|
|
|
// 2. Masonry Columns container
|
|
let columnsWrapper = cardStack.querySelector('.comfy-sidebar-masonry-wrapper');
|
|
if (!columnsWrapper) {
|
|
columnsWrapper = document.createElement('div');
|
|
columnsWrapper.className = 'comfy-sidebar-masonry-wrapper';
|
|
Object.assign(columnsWrapper.style, { display: 'flex', gap: '12px', alignItems: 'flex-start', width: '100%' });
|
|
cardStack.appendChild(columnsWrapper);
|
|
}
|
|
|
|
while (columnsWrapper.children.length < cols) {
|
|
const colDiv = document.createElement('div');
|
|
colDiv.className = 'comfy-sidebar-masonry-col';
|
|
Object.assign(colDiv.style, { flex: '1', display: 'flex', flexDirection: 'column', gap: '12px', minWidth: '0' });
|
|
columnsWrapper.appendChild(colDiv);
|
|
}
|
|
while (columnsWrapper.children.length > cols) {
|
|
columnsWrapper.removeChild(columnsWrapper.lastChild);
|
|
}
|
|
|
|
// Distribute cards into the shortest column so short cards stack next to tall cards
|
|
const colBuckets = Array.from({ length: cols }, () => []);
|
|
const colHeights = new Array(cols).fill(0);
|
|
|
|
cardEls.forEach((cardEl) => {
|
|
let shortestCol = 0;
|
|
for (let c = 1; c < cols; c++) {
|
|
if (colHeights[c] < colHeights[shortestCol]) {
|
|
shortestCol = c;
|
|
}
|
|
}
|
|
colBuckets[shortestCol].push(cardEl);
|
|
|
|
const cardH = cardEl.offsetHeight > 0 ? cardEl.offsetHeight : (cardEl._cachedHeight || 160);
|
|
cardEl._cachedHeight = cardH;
|
|
colHeights[shortestCol] += cardH + 12;
|
|
});
|
|
|
|
for (let c = 0; c < cols; c++) {
|
|
const colDiv = columnsWrapper.children[c];
|
|
const bucket = colBuckets[c];
|
|
bucket.forEach((el, i) => {
|
|
if (colDiv.children[i] !== el) {
|
|
colDiv.insertBefore(el, colDiv.children[i] || null);
|
|
}
|
|
});
|
|
while (colDiv.children.length > bucket.length) {
|
|
colDiv.removeChild(colDiv.lastChild);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleQueueHeaderScroll(scrollEl) {
|
|
if (!stickyHeaderGroupEl) return;
|
|
|
|
const currentScroll = scrollEl.scrollTop;
|
|
const searchContainer = stickyHeaderGroupEl.querySelector(".comfy-sidebar-search-container");
|
|
const searchIsOpen = searchContainer && searchContainer.style.display !== "none";
|
|
|
|
// Keep the frosted surface visible while search is open, matching Gallery.
|
|
stickyHeaderGroupEl.classList.toggle("is-scrolled", currentScroll > 35);
|
|
|
|
if (searchIsOpen) {
|
|
lastScrollTop = currentScroll;
|
|
scrollDeltaAccumulator = 0;
|
|
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
|
isHeaderHidden = false;
|
|
return;
|
|
}
|
|
|
|
const delta = currentScroll - lastScrollTop;
|
|
lastScrollTop = currentScroll;
|
|
|
|
if (currentScroll <= 35) {
|
|
stickyHeaderGroupEl.classList.remove("is-scrolled");
|
|
if (isHeaderHidden) {
|
|
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
|
isHeaderHidden = false;
|
|
}
|
|
scrollDeltaAccumulator = 0;
|
|
return;
|
|
}
|
|
|
|
stickyHeaderGroupEl.classList.add("is-scrolled");
|
|
|
|
if (delta > 0) {
|
|
if (scrollDeltaAccumulator < 0) scrollDeltaAccumulator = 0;
|
|
scrollDeltaAccumulator += delta;
|
|
|
|
if (scrollDeltaAccumulator > 25 && !isHeaderHidden) {
|
|
stickyHeaderGroupEl.style.transform = "translateY(-100%)";
|
|
isHeaderHidden = true;
|
|
}
|
|
} else if (delta < 0) {
|
|
if (scrollDeltaAccumulator > 0) scrollDeltaAccumulator = 0;
|
|
scrollDeltaAccumulator += delta;
|
|
|
|
if (scrollDeltaAccumulator < -15 && isHeaderHidden) {
|
|
stickyHeaderGroupEl.style.transform = "translateY(0)";
|
|
isHeaderHidden = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateScrollTopBtnVisibility() {
|
|
if (!scrollToTopBtnEl) return;
|
|
if (!store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) {
|
|
scrollToTopBtnEl.style.display = "none";
|
|
return;
|
|
}
|
|
const scrollEl = getScrollContainer(store.ui.cardStack);
|
|
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";
|
|
}
|
|
|
|
handleQueueHeaderScroll(scrollEl);
|
|
}
|
|
}
|
|
|
|
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: "transparent",
|
|
color: "var(--fg-color, var(--p-text-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 = t("Search History");
|
|
|
|
const title = document.createElement("h3");
|
|
title.textContent = t("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: "5px", alignItems: "center" });
|
|
|
|
const createInstantBtn = (iconClass, tooltip, onClickFn) => {
|
|
const btn = document.createElement("button");
|
|
btn.className = `${iconClass} comfy-sidebar-header-btn`;
|
|
btn.title = tooltip;
|
|
btn.addEventListener("click", async (ev) => {
|
|
ev.stopPropagation();
|
|
btn.style.transform = "scale(0.88)";
|
|
setTimeout(() => { btn.style.transform = ""; }, 120);
|
|
await onClickFn();
|
|
});
|
|
return btn;
|
|
};
|
|
|
|
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: "", background: "",
|
|
borderColor: "", boxShadow: ""
|
|
});
|
|
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 btnToggleIgnore = createInstantBtn("pi pi-eye-slash", t("Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)"), () => {
|
|
const raw = app.canvas?.selected_nodes;
|
|
const selected = raw instanceof Map || raw instanceof Set
|
|
? Array.from(raw.values())
|
|
: (Array.isArray(raw) ? raw : (raw && typeof raw === "object" ? Object.values(raw) : []));
|
|
const selectedNodes = selected.length > 0 ? selected : (app.canvas?.selected_node ? [app.canvas.selected_node] : []);
|
|
|
|
if (selectedNodes.length === 0) {
|
|
showNotification(t("No nodes selected. Click a node on the canvas first to toggle ignore."), "warn");
|
|
return;
|
|
}
|
|
|
|
toggleIgnoreActiveNode(() => renderSidebar());
|
|
});
|
|
|
|
const btnRenumber = createInstantBtn("pi pi-sort-numeric-down", t("Renumber All Nodes by Execution Order (Alt+R)"), async () => {
|
|
await renumberNodesTopologically();
|
|
});
|
|
|
|
const divider = document.createElement("div");
|
|
Object.assign(divider.style, {
|
|
width: "1px", height: "14px", background: "var(--border, var(--border-color, #444))",
|
|
margin: "0 2px", opacity: "0.6"
|
|
});
|
|
|
|
const btnClearInterrupted = createActionBtn("pi pi-eraser", t("Clear Cancelled & Failed"), "#ffc107", async () => {
|
|
await clearCancelledOrFailed();
|
|
renderSidebar();
|
|
});
|
|
|
|
const btnClearAll = createActionBtn("pi pi-trash", t("Clear All History"), "#dc3545", async () => {
|
|
await clearAllHistory();
|
|
renderSidebar();
|
|
});
|
|
|
|
actionsGroup.appendChild(btnToggleIgnore);
|
|
actionsGroup.appendChild(btnRenumber);
|
|
actionsGroup.appendChild(divider);
|
|
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(--card, var(--secondary, var(--comfy-input-bg, #181818)))",
|
|
border: "1px solid var(--border, 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: t("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 = t("Clear & Close Search");
|
|
Object.assign(clearSearchBtn.style, { marginLeft: "6px" });
|
|
|
|
searchContainer.appendChild(searchInputIcon);
|
|
searchContainer.appendChild(searchInput);
|
|
searchContainer.appendChild(clearSearchBtn);
|
|
|
|
header.appendChild(standardHeader);
|
|
header.appendChild(searchContainer);
|
|
|
|
const stickyHeaderGroup = document.createElement("div");
|
|
stickyHeaderGroup.className = "comfy-sidebar-sticky-header-group";
|
|
stickyHeaderGroupEl = stickyHeaderGroup;
|
|
stickyHeaderGroup.appendChild(header);
|
|
container.appendChild(stickyHeaderGroup);
|
|
|
|
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: "flex",
|
|
flexDirection: "column",
|
|
paddingBottom: "46px"
|
|
});
|
|
container.appendChild(cardStack);
|
|
|
|
scrollToTopBtnEl = document.createElement("button");
|
|
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
|
|
scrollToTopBtnEl.title = t("Scroll to Top");
|
|
|
|
scrollToTopBtnEl.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const scrollEl = getScrollContainer(store.ui.cardStack);
|
|
if (scrollEl) scrollEl.scrollTo({ top: 0, behavior: "smooth" });
|
|
});
|
|
|
|
container.appendChild(scrollToTopBtnEl);
|
|
|
|
// Reveal Spotlight: track cursor position over Queue card borders
|
|
setupSpotlightTracker(container, ".comfy-sidebar-card");
|
|
|
|
document.addEventListener("scroll", updateScrollTopBtnVisibility, { capture: true, passive: true });
|
|
|
|
new ResizeObserver((entries) => {
|
|
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
|
|
const cols = calculateGridColumns(entries[0].contentRect.width, threshold);
|
|
if (cols !== currentColumnCount) {
|
|
currentColumnCount = cols;
|
|
renderSidebar();
|
|
}
|
|
}).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 = getSetting(SettingIds.SHOW_PENDING_COUNT_ONLY, true);
|
|
const showWorkingNode = getSetting(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 = `${t("Batch of #")}${batchInfo.pid}`;
|
|
headerTitle.style.cursor = "pointer";
|
|
headerTitle.title = t("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 = t("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";
|
|
const btnDelLabel = document.createElement("span");
|
|
btnDelLabel.className = "comfy-sidebar-del-label";
|
|
btnDelLabel.textContent = t("Delete File");
|
|
btnDel.appendChild(btnDelLabel);
|
|
|
|
attachDeleteConfirmation(btnDel, {
|
|
idleTitle: t("Remove from batch (Hold Ctrl to delete file from disk)"),
|
|
confirmTitle: t("Click again to confirm removing from batch"),
|
|
confirmDiskTitle: t("Ctrl+Click again to delete file from DISK / TRASH"),
|
|
onConfirm: async (shouldDeleteFromDisk) => {
|
|
animateCardRemoval(card, async () => {
|
|
if (shouldDeleteFromDisk && img && img.filename) {
|
|
const action = await deleteFileOnServer(img);
|
|
if (action) {
|
|
const msg = action === "trashed" ? t("Moved file to Recycle Bin.") : t("Permanently deleted file.");
|
|
showNotification(msg, "success", t("Delete File"));
|
|
}
|
|
}
|
|
|
|
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 = t("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 = t("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);
|
|
});
|
|
|
|
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
|
|
|
|
const scrollEl = getScrollContainer(store.ui.cardStack);
|
|
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 = `${t("Outputs of #")}${store.ui.activeSubmenuPromptId}`;
|
|
headerTitle.style.cursor = "pointer";
|
|
headerTitle.title = t("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",
|
|
gridColumn: "1 / -1"
|
|
});
|
|
emptyCard.textContent = t("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 = t("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";
|
|
const btnDelLabel = document.createElement("span");
|
|
btnDelLabel.className = "comfy-sidebar-del-label";
|
|
btnDelLabel.textContent = t("Delete File");
|
|
btnDel.appendChild(btnDelLabel);
|
|
|
|
attachDeleteConfirmation(btnDel, {
|
|
idleTitle: t("Delete node output (Hold Ctrl to delete file from disk)"),
|
|
confirmTitle: t("Click again to confirm removing output"),
|
|
confirmDiskTitle: t("Ctrl+Click again to delete this node's files from DISK / TRASH"),
|
|
onConfirm: async (shouldDeleteFromDisk) => {
|
|
animateCardRemoval(card, async () => {
|
|
if (shouldDeleteFromDisk && out.images) {
|
|
let count = 0;
|
|
let lastAct = "trashed";
|
|
for (const imgItem of out.images) {
|
|
const action = await deleteFileOnServer(imgItem);
|
|
if (action) { count++; lastAct = action; }
|
|
}
|
|
if (count > 0) {
|
|
const msg = lastAct === "trashed"
|
|
? `Moved ${count} file${count > 1 ? "s" : ""} to Recycle Bin.`
|
|
: `Permanently deleted ${count} file${count > 1 ? "s" : ""}.`;
|
|
showNotification(msg, "success", "Delete Files");
|
|
}
|
|
}
|
|
|
|
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 = t("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 = t("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,
|
|
nodeId: out.nodeId,
|
|
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(store.ui.cardStack);
|
|
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 = t("No Outputs");
|
|
}
|
|
|
|
targetElements.push(cardObj.element);
|
|
});
|
|
}
|
|
|
|
if (outputs.length === 0) {
|
|
mountCards(store.ui.cardStack, targetElements, [], currentColumnCount);
|
|
} else {
|
|
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
|
|
}
|
|
|
|
updateScrollTopBtnVisibility();
|
|
return;
|
|
}
|
|
|
|
// Main Queue View
|
|
if (headerTitle) {
|
|
headerTitle.textContent = t("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 bannerElements = [];
|
|
const cardElements = [];
|
|
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(--card, var(--secondary, var(--comfy-input-bg, #181818)))", border: "2px solid var(--border, #6c757d)", borderRadius: "4px", padding: "10px",
|
|
textAlign: "center", fontSize: "12px", fontWeight: "bold",
|
|
color: "var(--muted-foreground, var(--desc-color, #aaa))", display: "flex", flexDirection: "column", gap: "8px"
|
|
});
|
|
const textDiv = document.createElement("div");
|
|
el.appendChild(textDiv);
|
|
const cancelBtn = document.createElement("button");
|
|
cancelBtn.className = "comfy-sidebar-cancel-all-btn";
|
|
cancelBtn.textContent = t("Cancel All Pending");
|
|
cancelBtn.onclick = async () => {
|
|
await cancelAllPending();
|
|
};
|
|
el.appendChild(cancelBtn);
|
|
pCard = { element: el, textDiv };
|
|
cardPool.set("pending-summary-card", pCard);
|
|
}
|
|
pCard.textDiv.textContent = `${t("Pending Queue:")} ${pendingCount} ${t("tasks")}`;
|
|
bannerElements.push(pCard.element);
|
|
} else {
|
|
let btnCard = cardPool.get("pending-cancel-all-standalone");
|
|
if (!btnCard) {
|
|
const btn = document.createElement("button");
|
|
btn.className = "comfy-sidebar-cancel-all-btn";
|
|
btn.textContent = t("Cancel All Pending");
|
|
btn.onclick = async () => {
|
|
await cancelAllPending();
|
|
};
|
|
btnCard = { element: btn };
|
|
cardPool.set("pending-cancel-all-standalone", btnCard);
|
|
}
|
|
bannerElements.push(btnCard.element);
|
|
pendingTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
|
}
|
|
}
|
|
|
|
activeTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
|
completedTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
|
|
|
const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350);
|
|
const containerWidth = store.ui.sidebarContainer?.clientWidth || 0;
|
|
if (containerWidth > 0) {
|
|
currentColumnCount = calculateGridColumns(containerWidth, threshold);
|
|
}
|
|
|
|
mountCards(store.ui.cardStack, bannerElements, cardElements, currentColumnCount);
|
|
|
|
if (store.ui.mainQueueScrollTop !== null) {
|
|
const restorePos = store.ui.mainQueueScrollTop;
|
|
store.ui.mainQueueScrollTop = null;
|
|
const scrollEl = getScrollContainer(store.ui.cardStack);
|
|
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();
|
|
if (scrollToTopBtnEl && scrollToTopBtnEl.parentNode) {
|
|
scrollToTopBtnEl.parentNode.removeChild(scrollToTopBtnEl);
|
|
}
|
|
scrollToTopBtnEl = null;
|
|
} |