From 700b85408cc69e72613324c7775b01b9e68221df Mon Sep 17 00:00:00 2001 From: m0rtus59 Date: Sun, 8 Nov 2026 20:24:09 +0500 Subject: [PATCH] DRYing --- js/comfy/layout.js | 3 +- js/queue/queueService.js | 30 ++++---- js/sidebar.js | 16 ++-- js/ui/cardRenderer.js | 51 +++---------- js/ui/galleryView.js | 76 +++++-------------- js/ui/mediaActions.js | 8 +- js/ui/sidebarView.js | 117 +++++++--------------------- js/utils/styles.js | 52 ++++++++++--- js/utils/utils.js | 159 ++++++++++++++++++++++++++++++++++++--- 9 files changed, 273 insertions(+), 239 deletions(-) diff --git a/js/comfy/layout.js b/js/comfy/layout.js index f796d1b..3dd9974 100644 --- a/js/comfy/layout.js +++ b/js/comfy/layout.js @@ -363,9 +363,8 @@ function setupCanvasMenuPopoverRelocator() { const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]'); if (!sidebar || !popovers.length) return; - const sidebarRect = sidebar.getBoundingClientRect(); const btnRect = btn.getBoundingClientRect(); - const onLeft = sidebarRect.left < window.innerWidth / 2; + const onLeft = isSidebarOnLeft(); popovers.forEach((pop) => { if (pop.style.display !== "none" && !pop.classList.contains("p-overlay-hidden")) { diff --git a/js/queue/queueService.js b/js/queue/queueService.js index ad80de6..3572716 100644 --- a/js/queue/queueService.js +++ b/js/queue/queueService.js @@ -1,7 +1,8 @@ import { api } from "/scripts/api.js"; import { app } from "/scripts/app.js"; import { store } from "../core/store.js"; -import { PromptStatus, StoreEvents } from "../core/constants.js"; +import { PromptStatus, StoreEvents, SettingIds } from "../core/constants.js"; +import { getSetting } from "../core/settings.js"; import { createPromptState, activatePrompt, finalizePrompt } from "../core/promptState.js"; import { normalizeQueueItem, normalizeHistoryItem } from "./queueNormalizer.js"; import { findImagesInOutputs, findTextsInOutputs, getPrimaryOutputImages, getNodeTitle } from "../utils/utils.js"; @@ -18,6 +19,14 @@ function extractNodeTitles(outputs, workflow, promptGraph = null) { return titles; } +export function extractPromptOutputs(outputs, workflow, promptGraph = null) { + return { + images: getPrimaryOutputImages(outputs, workflow), + texts: findTextsInOutputs(outputs, workflow), + nodeTitles: extractNodeTitles(outputs, workflow, promptGraph) + }; +} + export async function syncQueue() { try { const q = await api.getQueue(); @@ -36,19 +45,17 @@ export async function syncQueue() { const existing = store.getPrompt(item.pid); const activeWorkflow = item.workflow || (app.graph?.serialize?.() || null); - const titles = extractNodeTitles(item.outputs, activeWorkflow); if (!existing) { const seq = store.nextSequence(); - const images = getPrimaryOutputImages(item.outputs, activeWorkflow); - const texts = findTextsInOutputs(item.outputs, activeWorkflow); + const { images, texts, nodeTitles } = extractPromptOutputs(item.outputs, activeWorkflow); const newState = createPromptState(item.pid, { status: PromptStatus.ACTIVE, images, texts, nodeOutputs: item.outputs, - nodeTitles: titles, + nodeTitles, progress: 0, progressText: "Processing...", timestamp: seq, @@ -57,6 +64,7 @@ export async function syncQueue() { }); store.setPrompt(item.pid, newState); } else { + const titles = extractNodeTitles(item.outputs, activeWorkflow); if (existing.status !== PromptStatus.ACTIVE) { activatePrompt(existing, { workflow: activeWorkflow, nodeTitles: titles }); store.updatePrompt(item.pid, existing); @@ -145,10 +153,9 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED) state.status = norm.status; const promptGraph = hItem[key].prompt?.[2] || null; - const titles = extractNodeTitles(state.nodeOutputs, state.workflow, promptGraph); + const { images: primaryImgs, texts: fetchedTexts, nodeTitles: titles } = extractPromptOutputs(norm.outputs, state.workflow, promptGraph); Object.assign(state.nodeTitles, titles); - const primaryImgs = getPrimaryOutputImages(norm.outputs, state.workflow); const hasActualSavedOutputs = primaryImgs.some(img => !img.isFallback); // If cancelled while sampling, do NOT overwrite with older nodes; keep the exact step preview! @@ -158,7 +165,6 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED) } } - const fetchedTexts = findTextsInOutputs(norm.outputs, state.workflow); if (fetchedTexts.length > 0) state.texts = fetchedTexts; break; } @@ -173,7 +179,7 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED) } } - const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64; + const maxItems = getSetting(SettingIds.MAX_HISTORY_ITEMS, 64); store.pruneHistory(maxItems); store.updatePrompt(key, state); syncQueue(); @@ -204,9 +210,7 @@ export async function initSessionAndHistory() { normalized.sort((a, b) => a.norm.queueNumber - b.norm.queueNumber); normalized.forEach(({ norm: item, promptGraph }) => { - const images = getPrimaryOutputImages(item.outputs, item.workflow); - const texts = findTextsInOutputs(item.outputs, item.workflow); - const titles = extractNodeTitles(item.outputs, item.workflow, promptGraph); + const { images, texts, nodeTitles: titles } = extractPromptOutputs(item.outputs, item.workflow, promptGraph); if (images.length === 0 && texts.length === 0 && item.status === PromptStatus.COMPLETED) { return; @@ -253,7 +257,7 @@ export async function initSessionAndHistory() { store.setPrompt(item.pid, newState); }); - const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64; + const maxItems = getSetting(SettingIds.MAX_HISTORY_ITEMS, 64); store.pruneHistory(maxItems); await syncQueue(); } catch (err) { diff --git a/js/sidebar.js b/js/sidebar.js index 6e01205..4a660c1 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -12,6 +12,7 @@ import { setupSidebarView, renderSidebar, teardownSidebarView } from "./ui/sideb import { setupGalleryView, teardownGalleryView } from "./ui/galleryView.js"; import { initI18n, t } from "./core/i18n.js"; import { updateCardProgressTargeted, cardPool } from "./ui/cardRenderer.js"; +import { getScrollContainer } from "./utils/utils.js"; import { store } from "./core/store.js"; import { getSetting, setSetting } from "./core/settings.js"; @@ -394,13 +395,6 @@ app.registerExtension({ let galleryScrollPos = 0; let activeSidebarTab = "queue"; - const getRealScrollEl = (childEl) => { - if (!childEl) return null; - return childEl.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]') - || childEl.parentElement - || childEl; - }; - app.extensionManager.registerSidebarTab({ id: "classic-comfy-sidebar", icon: "pi pi-images", @@ -409,14 +403,14 @@ app.registerExtension({ type: "custom", render: (el) => { document.querySelectorAll(".comfy-sidebar-scroll-top-btn").forEach(btn => { btn.style.display = "none"; }); - const scrollParent = getRealScrollEl(el); + const scrollParent = getScrollContainer(el); if (activeSidebarTab === "gallery" && scrollParent) { galleryScrollPos = scrollParent.scrollTop; } activeSidebarTab = "queue"; el.replaceChildren(sidebarContainer); requestAnimationFrame(() => { - const sp = getRealScrollEl(el); + const sp = getScrollContainer(el); if (sp) sp.scrollTop = queueScrollPos; }); } @@ -431,14 +425,14 @@ app.registerExtension({ type: "custom", render: (el) => { document.querySelectorAll(".comfy-sidebar-scroll-top-btn").forEach(btn => { btn.style.display = "none"; }); - const scrollParent = getRealScrollEl(el); + const scrollParent = getScrollContainer(el); if (activeSidebarTab === "queue" && scrollParent) { queueScrollPos = scrollParent.scrollTop; } activeSidebarTab = "gallery"; el.replaceChildren(galleryContainer); requestAnimationFrame(() => { - const sp = getRealScrollEl(el); + const sp = getScrollContainer(el); if (sp) sp.scrollTop = galleryScrollPos; }); } diff --git a/js/ui/cardRenderer.js b/js/ui/cardRenderer.js index f8d3eb6..19a1acf 100644 --- a/js/ui/cardRenderer.js +++ b/js/ui/cardRenderer.js @@ -5,7 +5,7 @@ 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 } from "../utils/utils.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"; @@ -100,13 +100,7 @@ export function syncCardButtonVisibility(cardObj, state) { alert(t("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); + downloadJson(wf, `workflow_${state.pid || "comfy"}.json`); }; } else { cardObj.btnJson.style.setProperty("display", "none", "important"); @@ -304,9 +298,7 @@ export function syncCardButtonVisibility(cardObj, state) { } } - const scrollEl = document.querySelector('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]') - || store.ui.cardStack?.parentElement - || store.ui.cardStack; + const scrollEl = getScrollContainer(store.ui.cardStack); if (scrollEl) { store.ui.mainQueueScrollTop = scrollEl.scrollTop; } @@ -503,31 +495,12 @@ export function getOrCreateCard(state, callbacks = {}) { } }); - 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 = t("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 - ? t("Ctrl+Click again to delete file from DISK / TRASH") - : t("Click again to confirm removing card"); - deleteTimeout = setTimeout(resetDeleteBtn, 2000); - } else { - const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete; - resetDeleteBtn(); - + 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; @@ -569,7 +542,7 @@ export function getOrCreateCard(state, callbacks = {}) { deleteHistoryItem(state.pid); }); } - }; + }); cardPool.set(state.pid, cardObj); return cardObj; @@ -645,9 +618,7 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor } 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; + const scrollEl = getScrollContainer(store.ui.cardStack); if (scrollEl) { store.ui.mainQueueScrollTop = scrollEl.scrollTop; } diff --git a/js/ui/galleryView.js b/js/ui/galleryView.js index 9a0518a..f677ffc 100644 --- a/js/ui/galleryView.js +++ b/js/ui/galleryView.js @@ -2,7 +2,8 @@ import { app } from "/scripts/app.js"; import { SettingIds, PromptStatus } from "../core/constants.js"; import { t } from "../core/i18n.js"; import { deleteFileOnServer, openFileOrFolder } from "./mediaActions.js"; -import { extractWorkflowFromPng, isImageFormat, animateCardRemoval } from "../utils/utils.js"; +import { extractWorkflowFromPng, isImageFormat, animateCardRemoval, getScrollContainer, calculateGridColumns, downloadJson, setupSpotlightTracker, attachDeleteConfirmation } from "../utils/utils.js"; +import { getSetting } from "../core/settings.js"; import { renderCardImages } from "./mediaPreview.js"; import { showNotification } from "../utils/nodeRenumber.js"; @@ -37,19 +38,12 @@ function formatFileSize(bytes) { return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; } -function getScrollContainer() { - if (!cardStackEl) return null; - return cardStackEl.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]') - || cardStackEl.parentElement - || cardStackEl; -} - function updateScrollTopVisibility() { if (!galleryContainerEl || !galleryContainerEl.isConnected) { if (scrollToTopBtnEl) scrollToTopBtnEl.style.display = "none"; return; } - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(cardStackEl); if (scrollEl) { if (scrollToTopBtnEl) { scrollToTopBtnEl.style.display = scrollEl.scrollTop > 200 ? "flex" : "none"; @@ -289,47 +283,22 @@ function createFileCard(file) { alert(t("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_${file.filename.replace(/\.[^/.]+$/, "")}.json`; - a.click(); - setTimeout(() => URL.revokeObjectURL(url), 1500); + downloadJson(wf, `workflow_${file.filename.replace(/\.[^/.]+$/, "")}.json`); }; } const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del comfy-sidebar-gallery-btn-del"; - btnDel.title = t("Delete file (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); - let isDeletePending = false, isDiskDelete = false, deleteTimeout = null; - const resetDelete = () => { - isDeletePending = false; - isDiskDelete = false; - btnDel.classList.remove("confirm-delete", "confirm-delete-disk"); - btnDel.title = t("Delete file (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 - ? t("Ctrl+Click again to delete file from DISK / TRASH") - : t("Click again to confirm deleting file"); - deleteTimeout = setTimeout(resetDelete, 2000); - } else { - resetDelete(); + attachDeleteConfirmation(btnDel, { + idleTitle: t("Delete file (Hold Ctrl to delete file from disk)"), + confirmTitle: t("Click again to confirm deleting file"), + confirmDiskTitle: t("Ctrl+Click again to delete file from DISK / TRASH"), + onConfirm: async () => { const action = await deleteFileOnServer(fileAsset); if (action) { const msg = action === "trashed" ? t("Moved file to Recycle Bin.") : t("Permanently deleted file."); @@ -345,7 +314,7 @@ function createFileCard(file) { }); } } - }; + }); hoverPanel.appendChild(btnDownload); if (btnJson) hoverPanel.appendChild(btnJson); @@ -472,10 +441,10 @@ export function renderGallery() { }); } - const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350; + const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350); const containerWidth = galleryContainerEl.clientWidth || 0; if (containerWidth > 0) { - currentColumnCount = Math.max(1, Math.floor(containerWidth / (threshold / 2))); + currentColumnCount = calculateGridColumns(containerWidth, threshold); } let columnsWrapper = cardStackEl.querySelector(".comfy-sidebar-masonry-wrapper"); @@ -493,7 +462,7 @@ async function navigateToSubfolder(subfolder) { currentSubfolder = (subfolder || "").replace(/\\/g, "/"); currentSearchQuery = ""; await fetchGalleryData(currentSubfolder); - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(cardStackEl); if (scrollEl) scrollEl.scrollTop = 0; renderGallery(); } @@ -611,7 +580,7 @@ export function setupGalleryView() { searchIcon.onclick = (e) => { e.stopPropagation(); - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(cardStackEl); scrollBeforeSearch = scrollEl ? scrollEl.scrollTop : 0; standardHeader.style.display = "none"; searchContainer.style.display = "flex"; @@ -632,7 +601,7 @@ export function setupGalleryView() { if (hadFilter) { fetchGalleryData(currentSubfolder, "").then(() => { renderGallery(); - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(cardStackEl); if (scrollEl && scrollBeforeSearch > 0) { scrollEl.scrollTop = scrollBeforeSearch; requestAnimationFrame(() => { scrollEl.scrollTop = scrollBeforeSearch; }); @@ -682,7 +651,7 @@ export function setupGalleryView() { }); scrollToTopBtnEl.onclick = (e) => { e.stopPropagation(); - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(cardStackEl); if (scrollEl) scrollEl.scrollTo({ top: 0, behavior: "smooth" }); }; container.appendChild(scrollToTopBtnEl); @@ -697,20 +666,13 @@ export function setupGalleryView() { window.addEventListener("keyup", handleCtrlUp); // Reveal Spotlight: track cursor position over Gallery card and folder borders - container.addEventListener("pointermove", (e) => { - const card = e.target.closest(".comfy-sidebar-card, .comfy-sidebar-folder-card"); - if (card) { - const rect = card.getBoundingClientRect(); - card.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`); - card.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`); - } - }, { passive: true }); + setupSpotlightTracker(container, ".comfy-sidebar-card, .comfy-sidebar-folder-card"); document.addEventListener("scroll", updateScrollTopVisibility, { 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))); + const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350); + const cols = calculateGridColumns(entries[0].contentRect.width, threshold); if (cols !== currentColumnCount) { currentColumnCount = cols; renderGallery(); diff --git a/js/ui/mediaActions.js b/js/ui/mediaActions.js index c6b3613..d4fc722 100644 --- a/js/ui/mediaActions.js +++ b/js/ui/mediaActions.js @@ -1,3 +1,5 @@ +import { getSetting } from "../core/settings.js"; + export async function copyImageToClipboard(src) { try { const res = await fetch(src); @@ -24,8 +26,6 @@ export async function copyImageToClipboard(src) { } } -import { app } from "/scripts/app.js"; - export async function openFileOrFolder(img) { if (!img) return; @@ -54,7 +54,7 @@ export async function openFileOrFolder(img) { export async function deleteFileOnServer(fileItem) { if (!fileItem || !fileItem.filename) return null; - const isPermanent = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Permanent Delete on Disk Deletion") ?? false; + const isPermanent = getSetting("Comfy Sidebar.Permanent Delete on Disk Deletion", false); try { const res = await fetch("/comfy-sidebar/delete-file", { method: "POST", @@ -124,4 +124,4 @@ export function findNodeIdForImage(state, img) { } } return null; -} +} \ No newline at end of file diff --git a/js/ui/sidebarView.js b/js/ui/sidebarView.js index f74e1cc..1698e31 100644 --- a/js/ui/sidebarView.js +++ b/js/ui/sidebarView.js @@ -5,7 +5,8 @@ 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 } from "../utils/utils.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"; @@ -90,20 +91,13 @@ function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) { } } -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) return; if (!store.ui.sidebarContainer || !store.ui.sidebarContainer.isConnected) { scrollToTopBtnEl.style.display = "none"; return; } - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(store.ui.cardStack); if (scrollEl) { const viewportEl = scrollEl.parentElement || store.ui.sidebarContainer; if (viewportEl && scrollToTopBtnEl.parentNode !== viewportEl) { @@ -352,27 +346,20 @@ export function setupSidebarView() { scrollToTopBtnEl.addEventListener("click", (e) => { e.stopPropagation(); - const scrollEl = getScrollContainer(); + 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 - container.addEventListener("pointermove", (e) => { - const card = e.target.closest(".comfy-sidebar-card"); - if (card) { - const rect = card.getBoundingClientRect(); - card.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`); - card.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`); - } - }, { passive: true }); + setupSpotlightTracker(container, ".comfy-sidebar-card"); 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))); + const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350); + const cols = calculateGridColumns(entries[0].contentRect.width, threshold); if (cols !== currentColumnCount) { currentColumnCount = cols; renderSidebar(); @@ -411,8 +398,8 @@ export function renderSidebar() { 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 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"); @@ -497,37 +484,16 @@ export function renderSidebar() { const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del"; - btnDel.title = t("Remove from batch (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); - let isDeletePending = false, isDiskDelete = false, deleteTimeout = null; - const resetDelete = () => { - isDeletePending = false; - isDiskDelete = false; - btnDel.classList.remove("confirm-delete", "confirm-delete-disk"); - btnDel.title = t("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 - ? t("Ctrl+Click again to delete file from DISK / TRASH") - : t("Click again to confirm removing from batch"); - deleteTimeout = setTimeout(resetDelete, 2000); - } else { - const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete; - resetDelete(); - + 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); @@ -571,7 +537,7 @@ export function renderSidebar() { renderSidebar(); }); } - }; + }); hoverPanel.append(btnImg, btnDel); @@ -652,7 +618,7 @@ export function renderSidebar() { mountCards(store.ui.cardStack, [], targetElements, currentColumnCount); - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(store.ui.cardStack); if (scrollEl) scrollEl.scrollTop = 0; updateScrollTopBtnVisibility(); return; @@ -770,37 +736,16 @@ export function renderSidebar() { const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del"; - btnDel.title = t("Delete node output (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); - let isDeletePending = false, isDiskDelete = false, deleteTimeout = null; - const resetDelete = () => { - isDeletePending = false; - isDiskDelete = false; - btnDel.classList.remove("confirm-delete", "confirm-delete-disk"); - btnDel.title = t("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 - ? t("Ctrl+Click again to delete this node's files from DISK / TRASH") - : t("Click again to confirm removing output"); - deleteTimeout = setTimeout(resetDelete, 2000); - } else { - const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete; - resetDelete(); - + 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; @@ -833,7 +778,7 @@ export function renderSidebar() { renderSidebar(); }); } - }; + }); hoverPanel.append(btnImg, btnDel); @@ -907,7 +852,7 @@ export function renderSidebar() { nodeOutputs: st.nodeOutputs }, () => { // Clicking (1/N) opens batch drill-down for this specific intermediate node - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(store.ui.cardStack); if (scrollEl) { store.ui.mainQueueScrollTop = scrollEl.scrollTop; } @@ -983,11 +928,8 @@ export function renderSidebar() { 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"); - 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(); }; @@ -1001,11 +943,8 @@ export function renderSidebar() { 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"); - Object.assign(btn.style, { - background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "6px", - cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%" - }); btn.onclick = async () => { await cancelAllPending(); }; @@ -1020,10 +959,10 @@ export function renderSidebar() { activeTasks.forEach(st => cardElements.push(syncTaskCard(st))); completedTasks.forEach(st => cardElements.push(syncTaskCard(st))); - const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350; + const threshold = getSetting(SettingIds.GRID_COLUMNS_THRESHOLD, 350); const containerWidth = store.ui.sidebarContainer?.clientWidth || 0; if (containerWidth > 0) { - currentColumnCount = Math.max(1, Math.floor(containerWidth / (threshold / 2))); + currentColumnCount = calculateGridColumns(containerWidth, threshold); } mountCards(store.ui.cardStack, bannerElements, cardElements, currentColumnCount); @@ -1031,7 +970,7 @@ export function renderSidebar() { if (store.ui.mainQueueScrollTop !== null) { const restorePos = store.ui.mainQueueScrollTop; store.ui.mainQueueScrollTop = null; - const scrollEl = getScrollContainer(); + const scrollEl = getScrollContainer(store.ui.cardStack); if (scrollEl) { scrollEl.scrollTop = restorePos; requestAnimationFrame(() => { scrollEl.scrollTop = restorePos; }); diff --git a/js/utils/styles.js b/js/utils/styles.js index d4c65d2..0479a75 100644 --- a/js/utils/styles.js +++ b/js/utils/styles.js @@ -27,8 +27,8 @@ export function injectStyles() { inset: 0; border-radius: inherit; background: radial-gradient( - 150px circle at var(--mouse-x, -999px) var(--mouse-y, -999px), - color-mix(in srgb, var(--border, var(--secondary, #a855f7)) 75%, white 25%), + 160px circle at var(--mouse-x, -999px) var(--mouse-y, -999px), + color-mix(in srgb, var(--primary, var(--p-primary-color, var(--primary-color, var(--accent, currentColor)))) 60%, transparent), transparent 70% ); opacity: 0; @@ -39,7 +39,7 @@ export function injectStyles() { .comfy-sidebar-card:hover::before, .comfy-sidebar-folder-card:hover::before { - opacity: 0.55; + opacity: 0.65; } /* Active job uses the theme accent (orange); Cancelled & Error remain standout Yellow & Red */ .comfy-sidebar-card.active { border-color: var(--primary, var(--p-primary-color, #ea580c)) !important; --hover-color: var(--primary, var(--p-primary-color, #ea580c)); } @@ -78,25 +78,53 @@ export function injectStyles() { padding: 2px 4px; border-radius: 3px; pointer-events: none; z-index: 10; color: #fff; } + /* Card Cancel / Interrupt Button - matches native ComfyUI Topbar Cancel */ .comfy-sidebar-queue-cancel-btn { display: none; align-items: center !important; justify-content: center !important; - width: 32px !important; - height: 32px !important; - background-color: #7f1d1d !important; - color: #e2e8f0 !important; - font-size: 11px !important; + width: 28px !important; + height: 28px !important; + background: var(--destructive, var(--p-button-danger-background, #ef4444)) !important; + color: var(--foreground, var(--fg-color, inherit)) !important; + border: 1px solid var(--destructive-border, transparent) !important; + font-size: 12px !important; border-radius: 6px !important; cursor: pointer !important; - transition: background-color 0.15s ease, color 0.15s ease !important; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important; + transition: background-color 0.15s ease, filter 0.15s ease !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important; z-index: 25 !important; } .comfy-sidebar-queue-cancel-btn:hover { - background-color: #991b1b !important; - color: #ffffff !important; + background: color-mix(in srgb, var(--destructive, var(--p-button-danger-background, #ef4444)) 88%, var(--foreground, #fff) 12%) !important; + color: var(--foreground, var(--fg-color, inherit)) !important; + filter: brightness(1.06); + } + + /* Cancel All Pending Button */ + .comfy-sidebar-cancel-all-btn { + background: var(--destructive, var(--p-button-danger-background, #ef4444)) !important; + color: var(--foreground, var(--fg-color, inherit)) !important; + border: 1px solid var(--destructive-border, transparent) !important; + border-radius: 6px !important; + padding: 6px 12px !important; + cursor: pointer !important; + font-size: 11px !important; + font-weight: bold !important; + width: 100% !important; + box-sizing: border-box !important; + transition: background-color 0.15s ease, filter 0.15s ease !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2) !important; + } + + .comfy-sidebar-cancel-all-btn:hover { + background: color-mix(in srgb, var(--destructive, var(--p-button-danger-background, #ef4444)) 88%, var(--foreground, #fff) 12%) !important; + filter: brightness(1.06); + } + + .comfy-sidebar-cancel-all-btn:active { + filter: brightness(0.95); } .comfy-sidebar-hover-panel, diff --git a/js/utils/utils.js b/js/utils/utils.js index 35ec964..8c613ee 100644 --- a/js/utils/utils.js +++ b/js/utils/utils.js @@ -1,4 +1,5 @@ import { app } from "/scripts/app.js"; +import { t } from "../core/i18n.js"; const FILE_EXT_REGEX = /\.(safetensors|ckpt|pt|pth|bin|latent|onnx|engine|gguf|lora|zip|tar|gz|7z|json|csv|parquet|txt|yaml|yml|xml|pdf|blend|fbx|obj|glb|gltf|stl|ply|splat|spz|ksplat|png|jpg|jpeg|webp|gif|bmp|tiff|svg|mp4|webm|wav|mp3|ogg|flac|m4a|aac|opus)$/i; @@ -67,17 +68,59 @@ export function parseWorkflow(workflow) { return null; } -export function isNodeIgnored(nodeId, rawWorkflow) { +export function getWorkflowNodeMap(rawWorkflow) { const workflow = parseWorkflow(rawWorkflow); - if (workflow && Array.isArray(workflow.nodes)) { - const node = workflow.nodes.find(n => String(n.id) === String(nodeId)); - if (node?.properties?.ignoreInQueue !== undefined) { - return !!node.properties.ignoreInQueue; + if (!workflow || !Array.isArray(workflow.nodes)) return null; + + if (workflow._nodeMap instanceof Map) { + return workflow._nodeMap; + } + + const map = new Map(); + for (const node of workflow.nodes) { + if (node && node.id !== undefined && node.id !== null) { + map.set(String(node.id), node); } } - if (!workflow && app.graph) { - const canvasNode = app.graph.getNodeById ? (app.graph.getNodeById(Number(nodeId)) || app.graph.getNodeById(String(nodeId))) : null; + // Attach as non-enumerable so JSON.stringify and Object.keys never see it + if (typeof workflow === "object") { + try { + Object.defineProperty(workflow, "_nodeMap", { + value: map, + writable: true, + configurable: true, + enumerable: false + }); + } catch (_) { + workflow._nodeMap = map; + } + } + + return map; +} + +export function isNodeIgnored(nodeId, rawWorkflow) { + const idStr = String(nodeId); + const nodeMap = getWorkflowNodeMap(rawWorkflow); + + if (nodeMap) { + const node = nodeMap.get(idStr); + if (node?.properties?.ignoreInQueue !== undefined) { + return !!node.properties.ignoreInQueue; + } + } else { + const workflow = parseWorkflow(rawWorkflow); + if (workflow && Array.isArray(workflow.nodes)) { + const node = workflow.nodes.find(n => String(n.id) === idStr); + if (node?.properties?.ignoreInQueue !== undefined) { + return !!node.properties.ignoreInQueue; + } + } + } + + if (app.graph) { + const canvasNode = app.graph.getNodeById ? (app.graph.getNodeById(Number(nodeId)) || app.graph.getNodeById(idStr)) : null; if (canvasNode?.properties?.ignoreInQueue !== undefined) { return !!canvasNode.properties.ignoreInQueue; } @@ -87,13 +130,21 @@ export function isNodeIgnored(nodeId, rawWorkflow) { export function getNodeTitle(nodeId, rawWorkflow, promptGraph = null) { const idStr = String(nodeId); + const nodeMap = getWorkflowNodeMap(rawWorkflow); - const workflow = parseWorkflow(rawWorkflow); - if (workflow && Array.isArray(workflow.nodes)) { - const node = workflow.nodes.find(n => String(n.id) === idStr); + if (nodeMap) { + const node = nodeMap.get(idStr); if (node) { return node.title || node.type || null; } + } else { + const workflow = parseWorkflow(rawWorkflow); + if (workflow && Array.isArray(workflow.nodes)) { + const node = workflow.nodes.find(n => String(n.id) === idStr); + if (node) { + return node.title || node.type || null; + } + } } if (promptGraph && promptGraph[idStr]) { @@ -233,9 +284,10 @@ export function findImagesInOutputs(outputs, rawWorkflow) { }; if (outputs) { + const nodeMap = getWorkflowNodeMap(workflow); for (const nodeId in outputs) { if (isNodeIgnored(nodeId, workflow)) continue; - const node = workflow?.nodes?.find(n => String(n.id) === String(nodeId)); + const node = nodeMap ? nodeMap.get(String(nodeId)) : workflow?.nodes?.find(n => String(n.id) === String(nodeId)); const nodeType = node ? (node.type || "") : ""; scan(outputs[nodeId], "", "output", nodeType); } @@ -444,4 +496,89 @@ export function animateCardRemoval(cardEl, callback, duration = 200) { setTimeout(() => { if (callback) callback(); }, duration); +} + +export function getScrollContainer(element) { + if (!element) return null; + return element.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]') + || element.parentElement + || element; +} + +export function calculateGridColumns(width, threshold = 350) { + if (!width || width <= 0) return 1; + return Math.max(1, Math.floor(width / ((threshold || 350) / 2))); +} + +export function downloadJson(data, filename) { + if (!data) return; + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename || "workflow.json"; + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 1500); +} + +export function setupSpotlightTracker(container, cardSelector = ".comfy-sidebar-card") { + if (!container) return; + container.addEventListener("pointermove", (e) => { + const card = e.target.closest(cardSelector); + if (card) { + const rect = card.getBoundingClientRect(); + card.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`); + card.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`); + } + }, { passive: true }); +} + +export function attachDeleteConfirmation(btn, options = {}) { + if (!btn) return () => {}; + const { + canDiskDelete = true, + timeout = 2000, + idleTitle = t("Delete"), + confirmTitle = t("Click again to confirm"), + confirmDiskTitle = t("Ctrl+Click again to delete from disk"), + onConfirm + } = options; + + let isPending = false; + let isDisk = false; + let timer = null; + + const reset = () => { + isPending = false; + isDisk = false; + btn.classList.remove("confirm-delete", "confirm-delete-disk"); + btn.title = typeof idleTitle === "function" ? idleTitle() : idleTitle; + if (timer) { + clearTimeout(timer); + timer = null; + } + }; + + reset(); + + btn.onclick = async (ev) => { + ev.stopPropagation(); + const wantsDisk = (ev.ctrlKey || ev.metaKey) && (typeof canDiskDelete === "function" ? canDiskDelete() : !!canDiskDelete); + + if (!isPending) { + isPending = true; + isDisk = wantsDisk; + btn.classList.add(wantsDisk ? "confirm-delete-disk" : "confirm-delete"); + btn.title = wantsDisk + ? (typeof confirmDiskTitle === "function" ? confirmDiskTitle() : confirmDiskTitle) + : (typeof confirmTitle === "function" ? confirmTitle() : confirmTitle); + timer = setTimeout(reset, timeout); + } else { + const finalDisk = isDisk || wantsDisk; + reset(); + if (onConfirm) await onConfirm(finalDisk); + } + }; + + return reset; } \ No newline at end of file