Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72407690c | ||
|
|
31c994a1b6 | ||
|
|
9219f2d042 | ||
|
|
b3e6b1bd62 | ||
|
|
2dc92d3e6f | ||
|
|
e83c999da7 | ||
|
|
f61d6c8902 | ||
|
|
689e020230 | ||
|
|
ad4e138eac | ||
|
|
8487150e6f | ||
|
|
0e120f1af1 | ||
|
|
b6bdaefacc | ||
|
|
5adf5ea513 | ||
|
|
f6e76b1789 | ||
|
|
0e648a9fa9 | ||
|
|
684726789f | ||
|
|
c2c82065a7 | ||
|
|
5fde92b51a |
@@ -0,0 +1,42 @@
|
||||
name: Publish to Comfy registry & GitHub Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Gives GitHub permission to create tags & releases
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
with:
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
# 1. Read version string from pyproject.toml
|
||||
- name: Extract Version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(grep -m 1 'version' pyproject.toml | cut -d '"' -f 2)
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
# 2. Automatically create Git Tag & GitHub Release
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ env.VERSION }}
|
||||
name: v${{ env.VERSION }}
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -13,7 +13,8 @@ A lightweight, high-performance ComfyUI extension that replaces the cluttered st
|
||||
* **Live Queue Tracking**: Stay informed on your server state. Includes a visual progress bar that tracks generations dynamically and displays the exact node title currently being executed.
|
||||
* A compact topbar layout displaying both workflow tabs and the control panel on a single unified top bar. See the 'Comfy Layout' toggle in the settings.
|
||||
* Toggles to hide buttons you don't need from the side panel.
|
||||
* **[NEW]** Preview compare slider - just click the result image preview in the sidebar, and then shift+click another to load it alongside the first one for pixel by pixel comparison.
|
||||
* **[NEW]** Preview compare slider - just click the result image preview in the sidebar, and then `Shift+Click` another to load it alongside the first one for pixel by pixel comparison.
|
||||
* **[NEW]** Prevent selected nodes from showing the output in the queue bar and intermediate results. `Ctrl+Q` to toggle.
|
||||
|
||||
<img width="385" height="336" alt="image" src="https://github.com/user-attachments/assets/743fbf85-9281-4561-9dd0-cd9af4d8c0e4" />
|
||||
|
||||
@@ -32,8 +33,10 @@ Restart your ComfyUI server and refresh the browser tab.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Additional tweaks are seamlessly integrated into the standard ComfyUI Settings panel. From there, you can adjust thresholds or fully replace the stock V2 panel by enabling options like **"Override Stock Job History Tab"**.
|
||||
See additional tweaks in ComfyUI settings menu > Comfy Sidebar. From there, you can adjust the layout or fully replace the stock V2 panel by enabling options like **"Comfy Layout"** and **"Replace the stock Job History sidebar with Comfy Queue"**.
|
||||
|
||||
## ⌨️ Shortcuts
|
||||
|
||||
* **`Q`**: Instantly toggles the sidebar panel open or closed. Safely ignores inputs if you are currently typing inside a text field, input area, or combo box.
|
||||
* **`Q`**: Toggle sidebar queue panel.
|
||||
* **`Ctrl+Q on a selected node`**: Sets the node to be ignored in the sidebar panel results.
|
||||
* **`Click`** on a card to enter preview. **`Shift+Click`** on another card in preview to enter compare mode.
|
||||
+45
-20
@@ -1,13 +1,15 @@
|
||||
import { State } from "./state.js";
|
||||
import { isVideoFormat } from "./utils.js";
|
||||
|
||||
let activeComparisonViewer = null;
|
||||
let videoSyncActive = false;
|
||||
let syncAnimationFrameId = null;
|
||||
|
||||
// Track the global keydown reference across instances defensively
|
||||
let globalKeydownHandler = null;
|
||||
|
||||
// Safe clientX resolver for mouse and touch events
|
||||
const getClientX = (e) => {
|
||||
if (e.touches && e.touches.length > 0) return e.touches[0].clientX;
|
||||
if (e.changedTouches && e.changedTouches.length > 0) return e.changedTouches[0].clientX;
|
||||
return e.clientX || 0;
|
||||
};
|
||||
|
||||
const createMediaElement = (src, muted = false) => {
|
||||
const isVideo = isVideoFormat(src);
|
||||
const el = isVideo ? document.createElement("video") : document.createElement("img");
|
||||
@@ -27,7 +29,10 @@ const createMediaElement = (src, muted = false) => {
|
||||
};
|
||||
|
||||
const setupVideoPlayback = (vid, container) => {
|
||||
if (!vid) return;
|
||||
if (!vid) return () => {};
|
||||
|
||||
let videoSyncActive = true;
|
||||
let syncAnimationFrameId = null;
|
||||
|
||||
const onMetaLoaded = () => {
|
||||
vid.play().catch(()=>{});
|
||||
@@ -76,7 +81,7 @@ const setupVideoPlayback = (vid, container) => {
|
||||
|
||||
const scrub = (e) => {
|
||||
const rect = scrubberContainer.getBoundingClientRect();
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientX = getClientX(e);
|
||||
const percent = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width));
|
||||
const duration = vid.duration || 0;
|
||||
if (duration > 0) {
|
||||
@@ -99,7 +104,6 @@ const setupVideoPlayback = (vid, container) => {
|
||||
controlBar.append(playBtn, scrubberContainer, timeLabel);
|
||||
container.appendChild(controlBar);
|
||||
|
||||
videoSyncActive = true;
|
||||
const syncLoop = () => {
|
||||
if (!videoSyncActive) return;
|
||||
|
||||
@@ -121,6 +125,8 @@ const setupVideoPlayback = (vid, container) => {
|
||||
requestAnimationFrame(syncLoop);
|
||||
|
||||
return () => {
|
||||
videoSyncActive = false;
|
||||
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
|
||||
window.removeEventListener("mousemove", handleWindowMove);
|
||||
window.removeEventListener("touchmove", handleWindowMove);
|
||||
window.removeEventListener("mouseup", handleWindowUp);
|
||||
@@ -131,7 +137,6 @@ const setupVideoPlayback = (vid, container) => {
|
||||
};
|
||||
|
||||
function createComparisonViewer(baseSrc) {
|
||||
// DEFENSIVE CHECK: Force-remove any globally orphaned keydown handlers before binding a new one
|
||||
if (globalKeydownHandler) {
|
||||
document.removeEventListener("keydown", globalKeydownHandler);
|
||||
globalKeydownHandler = null;
|
||||
@@ -186,21 +191,26 @@ function createComparisonViewer(baseSrc) {
|
||||
});
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Muted bottom tooltip helper
|
||||
const hintPrompt = document.createElement("div");
|
||||
Object.assign(hintPrompt.style, {
|
||||
position: "absolute", bottom: "16px", zIndex: "30",
|
||||
color: "#555", fontSize: "10px", fontFamily: "sans-serif",
|
||||
color: "#888", fontSize: "12px", fontFamily: "sans-serif",
|
||||
pointerEvents: "none"
|
||||
});
|
||||
hintPrompt.innerHTML = 'Tip: Hold <span style="color:#777;font-weight:bold;">Shift</span> while clicking sidebar cards to compare outputs side-by-side.';
|
||||
hintPrompt.innerHTML = 'Tip: Hold <span style="color:#aaa;font-weight:bold;">Shift</span> while clicking sidebar cards to compare outputs side-by-side.';
|
||||
if (isBaseVideo) {
|
||||
hintPrompt.style.display = "none";
|
||||
}
|
||||
container.appendChild(hintPrompt);
|
||||
|
||||
let wasDragging = false;
|
||||
|
||||
container.onclick = (e) => {
|
||||
if (e.target === container) {
|
||||
if (wasDragging) {
|
||||
wasDragging = false;
|
||||
return;
|
||||
}
|
||||
destroy();
|
||||
}
|
||||
};
|
||||
@@ -246,20 +256,24 @@ function createComparisonViewer(baseSrc) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isDragging = true;
|
||||
wasDragging = false;
|
||||
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientX = getClientX(e);
|
||||
const percent = ((clientX - rect.left) / rect.width) * 100;
|
||||
updateSliderPosition(percent);
|
||||
};
|
||||
const doDrag = (e) => {
|
||||
if (!isDragging) return;
|
||||
wasDragging = true;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientX = getClientX(e);
|
||||
const percent = ((clientX - rect.left) / rect.width) * 100;
|
||||
updateSliderPosition(percent);
|
||||
};
|
||||
const endDrag = () => { isDragging = false; };
|
||||
const endDrag = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
wrapper.addEventListener("mousedown", startDrag);
|
||||
wrapper.addEventListener("touchstart", startDrag);
|
||||
@@ -269,6 +283,14 @@ function createComparisonViewer(baseSrc) {
|
||||
window.addEventListener("mouseup", endDrag);
|
||||
window.addEventListener("touchend", endDrag);
|
||||
|
||||
const cleanupMedia = (el) => {
|
||||
if (el && el.tagName === "VIDEO") {
|
||||
el.pause();
|
||||
el.src = "";
|
||||
el.load();
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
window.removeEventListener("mousemove", doDrag);
|
||||
window.removeEventListener("touchmove", doDrag);
|
||||
@@ -280,10 +302,11 @@ function createComparisonViewer(baseSrc) {
|
||||
globalKeydownHandler = null;
|
||||
}
|
||||
|
||||
videoSyncActive = false;
|
||||
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
|
||||
if (destroyVideoPlaybackFn) destroyVideoPlaybackFn();
|
||||
|
||||
cleanupMedia(mediaA);
|
||||
cleanupMedia(mediaB);
|
||||
|
||||
container.remove();
|
||||
activeComparisonViewer = null;
|
||||
};
|
||||
@@ -322,8 +345,10 @@ function createComparisonViewer(baseSrc) {
|
||||
}
|
||||
|
||||
if (isShiftClick) {
|
||||
// Shift+Click: Sideload target image B for comparison split
|
||||
if (mediaB) mediaB.remove();
|
||||
if (mediaB) {
|
||||
cleanupMedia(mediaB);
|
||||
mediaB.remove();
|
||||
}
|
||||
|
||||
mediaB = createMediaElement(targetSrc, false);
|
||||
mediaB.style.pointerEvents = "none";
|
||||
@@ -333,8 +358,8 @@ function createComparisonViewer(baseSrc) {
|
||||
infoText.textContent = "Drag the slider to compare. Shift+Click other card images to update target | Esc to close.";
|
||||
updateSliderPosition(50);
|
||||
} else {
|
||||
// Standard Click: Replace the main base reference image entirely
|
||||
if (mediaB) {
|
||||
cleanupMedia(mediaB);
|
||||
mediaB.remove();
|
||||
mediaB = null;
|
||||
}
|
||||
|
||||
Vendored
+43
-17
@@ -1,20 +1,23 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { State } from "./state.js";
|
||||
|
||||
async function uploadDroppedImageToInput(imageObj) {
|
||||
const src = `/view?filename=${encodeURIComponent(imageObj.filename)}&type=${imageObj.type || 'output'}&subfolder=${encodeURIComponent(imageObj.subfolder || '')}`;
|
||||
try {
|
||||
const response = await fetch(src);
|
||||
if (!response.ok) return null;
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], imageObj.filename || "dropped_image.png", { type: blob.type });
|
||||
const file = new File([blob], imageObj.filename || "dropped_file", { type: blob.type });
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
formData.append("overwrite", "true");
|
||||
formData.append("type", "input");
|
||||
const uploadRes = await fetch("/upload/image", { method: "POST", body: formData });
|
||||
if (!uploadRes.ok) return null;
|
||||
const uploadData = await uploadRes.json();
|
||||
return uploadData.name;
|
||||
} catch(e) {
|
||||
console.error("Comfy Sidebar: Failed to copy image to input.", e);
|
||||
console.error("Comfy Sidebar: Failed to copy file to input.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -26,11 +29,19 @@ export function setupDragAndDrop() {
|
||||
});
|
||||
|
||||
document.addEventListener("drop", async (e) => {
|
||||
const isSidebarDrop = e.target.closest('.comfyui-sidebar, .comfy-sidebar, [class*="sidebar"]') ||
|
||||
(State.sidebarContainer && State.sidebarContainer.contains(e.target));
|
||||
if (isSidebarDrop) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = app.canvas;
|
||||
if (!canvas || !canvas.graph) return;
|
||||
|
||||
// 1. Check for standard workflow JSON data drop
|
||||
const jsonStr = e.dataTransfer.getData("application/json") || e.dataTransfer.getData("text/plain");
|
||||
// 1. Check for workflow JSON drop (text-only and unfinished cards)
|
||||
const jsonStr = e.dataTransfer.getData("application/json");
|
||||
if (jsonStr) {
|
||||
try {
|
||||
const workflow = JSON.parse(jsonStr);
|
||||
@@ -48,7 +59,7 @@ export function setupDragAndDrop() {
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// 2. Check for native image URL drop
|
||||
// 2. Check for image / video URL drop
|
||||
const url = e.dataTransfer.getData("text/uri-list") || e.dataTransfer.getData("text/plain");
|
||||
if (url) {
|
||||
try {
|
||||
@@ -66,32 +77,47 @@ export function setupDragAndDrop() {
|
||||
if (canvas.convertEventToCanvasOffset) {
|
||||
const pos = canvas.convertEventToCanvasOffset(e);
|
||||
targetNode = canvas.graph.getNodeOnPos(pos[0], pos[1]);
|
||||
} else {
|
||||
} else if (canvas.canvas) {
|
||||
const rect = canvas.canvas.getBoundingClientRect();
|
||||
targetNode = canvas.graph.getNodeOnPos((e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale, (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale);
|
||||
}
|
||||
|
||||
if (targetNode && (targetNode.type.includes("LoadImage") || targetNode.widgets?.some(w => w.name === "image"))) {
|
||||
// Dropped on a node with image input: Copy/Upload image to inputs
|
||||
const widget = targetNode.widgets.find(w => w.name === "image");
|
||||
if (widget) {
|
||||
// Check if dropped on a LoadImage, LoadVideo, VHS_LoadVideo, or image/video widget node
|
||||
const widget = targetNode?.widgets?.find(w =>
|
||||
w.name === "image" || w.name === "video" || w.name === "file" || w.name === "video_path" || w.type === "image" || w.type === "customtext"
|
||||
);
|
||||
|
||||
const isLoadNode = targetNode && (widget || targetNode.type?.includes("LoadImage") || targetNode.type?.includes("LoadVideo") || targetNode.type?.includes("VHS_LoadVideo"));
|
||||
|
||||
if (isLoadNode) {
|
||||
const targetWidget = widget || targetNode.widgets?.[0];
|
||||
if (targetWidget) {
|
||||
const newFilename = await uploadDroppedImageToInput({ filename, type, subfolder });
|
||||
if (newFilename) {
|
||||
widget.value = newFilename;
|
||||
if (widget.callback) widget.callback(widget.value);
|
||||
// Populate dropdown values list so combo widgets (like VHS_LoadVideo) accept the new video immediately!
|
||||
if (targetWidget.options && Array.isArray(targetWidget.options.values)) {
|
||||
if (!targetWidget.options.values.includes(newFilename)) {
|
||||
targetWidget.options.values.push(newFilename);
|
||||
}
|
||||
}
|
||||
targetWidget.value = newFilename;
|
||||
if (targetWidget.callback) targetWidget.callback(targetWidget.value);
|
||||
targetNode.imgs = null;
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dropped on empty canvas: Load workflow natively
|
||||
// Dropped on empty canvas area: fetch file and load embedded workflow metadata
|
||||
const src = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`;
|
||||
const res = await fetch(src);
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], filename || "workflow.png", { type: blob.type });
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], filename, { type: blob.type });
|
||||
|
||||
if (app.handleFile) await app.handleFile(file);
|
||||
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
|
||||
if (app.handleFile) await app.handleFile(file);
|
||||
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+315
-169
@@ -1,4 +1,3 @@
|
||||
// layout.js - Fully Fixed & Renamed to 'Hide Junk'
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
const STYLE_ID = "comfy-sidebar-classic-layout-override";
|
||||
@@ -15,7 +14,6 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
|
||||
transform: none !important;
|
||||
z-index: 1010 !important;
|
||||
|
||||
/* Nuke the background rectangle, border, and shadows */
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -25,6 +23,45 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Style overrides for the new native Extensions button container when embedded in top actionbar */
|
||||
.comfy-sidebar-extensions-override {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 8px 0 0 !important;
|
||||
height: auto !important;
|
||||
position: static !important;
|
||||
order: -10 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Scale down performance indicators (Crystools) uniformly */
|
||||
#crysmonitor-monitors-root,
|
||||
#crystools-monitors-root,
|
||||
.crysmonitor-monitors-container {
|
||||
zoom: 0.8 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Hide user account profile button and PrimeVue components */
|
||||
img[alt*="User Avatar"],
|
||||
img[alt*="user avatar"],
|
||||
button:has(img[alt*="User Avatar"]),
|
||||
[role="button"]:has(img[alt*="User Avatar"]),
|
||||
button:has(img[alt*="user avatar"]),
|
||||
[role="button"]:has(img[alt*="user avatar"]),
|
||||
.p-avatar,
|
||||
[data-pc-name="avatar"],
|
||||
button:has(.p-avatar),
|
||||
[role="button"]:has(.p-avatar),
|
||||
button:has([data-pc-name="avatar"]),
|
||||
[role="button"]:has([data-pc-name="avatar"]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Base rules: Large/Wide desktop screens (>= 1600px width) */
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
@@ -60,59 +97,106 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
|
||||
}
|
||||
`;
|
||||
|
||||
export function applyClassicLayout(enable) {
|
||||
export function syncStockHistoryAndProgressSettings(enable) {
|
||||
if (!app.ui?.settings) return;
|
||||
|
||||
const dockedVal = !!enable;
|
||||
const progressVal = !enable;
|
||||
const QPOV2_ID = "Comfy.Queue.QPOV2";
|
||||
|
||||
try {
|
||||
app.ui.settings.setSettingValue(QPOV2_ID, dockedVal);
|
||||
localStorage.setItem(QPOV2_ID, JSON.stringify(dockedVal));
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
const qpovContainer = document.querySelector('[data-setting-id="Comfy.Queue.QPOV2"]') ||
|
||||
document.getElementById(QPOV2_ID)?.closest('.setting-item, tr, div');
|
||||
if (qpovContainer) {
|
||||
const toggle = qpovContainer.querySelector('input[type="checkbox"], [role="switch"], .p-toggleswitch');
|
||||
if (toggle) {
|
||||
const isChecked = toggle.checked ||
|
||||
toggle.getAttribute('data-p-checked') === 'true' ||
|
||||
toggle.classList.contains('p-toggleswitch-checked');
|
||||
if (isChecked !== dockedVal) toggle.click();
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const progressKey = "Comfy.Queue.ShowRunProgressBar";
|
||||
try {
|
||||
app.ui.settings.setSettingValue(progressKey, progressVal);
|
||||
localStorage.setItem(progressKey, JSON.stringify(progressVal));
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
const progressBarBtn = document.querySelector('[data-testid="show-run-progress-bar-action"]');
|
||||
if (progressBarBtn) {
|
||||
const isChecked = !!progressBarBtn.querySelector('[class*="lucide--check"], .icon-\\[lucide--check\\]');
|
||||
if (isChecked !== progressVal) progressBarBtn.click();
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try { window.dispatchEvent(new Event("storage")); } catch (e) {}
|
||||
}
|
||||
|
||||
export function applyClassicLayout(enable, updateSetting = false) {
|
||||
let styleEl = document.getElementById(STYLE_ID);
|
||||
|
||||
if (enable) {
|
||||
if (app.ui && app.ui.settings) {
|
||||
if (updateSetting && app.ui?.settings) {
|
||||
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
|
||||
}
|
||||
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement("style");
|
||||
styleEl.id = STYLE_ID;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
styleEl.textContent = CLASSIC_LAYOUT_CSS_MEDIA;
|
||||
|
||||
} else {
|
||||
if (styleEl) styleEl.remove();
|
||||
|
||||
if (app.ui && app.ui.settings) {
|
||||
if (updateSetting && app.ui?.settings) {
|
||||
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Properties Panel Toggle Corrections --- */
|
||||
|
||||
let savedButtonData = null;
|
||||
let domObserver = null;
|
||||
let syncScheduled = false;
|
||||
|
||||
function findOriginalButton() {
|
||||
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
|
||||
if (icon) {
|
||||
const icons = document.querySelectorAll('[class*="lucide--panel-right"], [class*="lucide--panel-left"], [class*="lucide--panel-bottom"]');
|
||||
for (const icon of icons) {
|
||||
const btn = icon.closest('button, [role="button"], .comfyui-menu-item, .p-button');
|
||||
if (btn && !btn.classList.contains("comfy-sidebar-custom-properties-toggle")) {
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = document.querySelectorAll('button, [role="button"], .p-button');
|
||||
for (const btn of buttons) {
|
||||
if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue;
|
||||
const title = (btn.getAttribute("title") || btn.getAttribute("aria-label") || "").toLowerCase();
|
||||
if (title.includes("workflow overview") || title.includes("properties") || title.includes("toggle panel")) {
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTopbarContainer() {
|
||||
const container = document.querySelector('[class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
|
||||
const container = document.querySelector('[data-testid="action-bar-buttons"], [class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
|
||||
if (container) return container;
|
||||
|
||||
const orig = findOriginalButton();
|
||||
if (orig && orig.parentNode) return orig.parentNode;
|
||||
if (orig?.parentNode) return orig.parentNode;
|
||||
|
||||
const buttons = document.querySelectorAll("button, .p-button");
|
||||
for (const btn of buttons) {
|
||||
if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue;
|
||||
const title = (btn.getAttribute("title") || btn.getAttribute("aria-label") || "").toLowerCase();
|
||||
if (title.includes("run") || title.includes("queue") || title.includes("manager")) {
|
||||
if (title.includes("run") || title.includes("manager")) {
|
||||
if (btn.parentNode) return btn.parentNode;
|
||||
}
|
||||
}
|
||||
@@ -120,8 +204,29 @@ function findTopbarContainer() {
|
||||
}
|
||||
|
||||
function isPropertiesPanelOpen() {
|
||||
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
|
||||
return !!(icon && icon.className.includes("lucide--panel-left"));
|
||||
const panel = document.querySelector('[data-testid="properties-panel"]');
|
||||
if (panel && panel.offsetWidth > 0 && panel.offsetHeight > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const headings = document.querySelectorAll('h1, h2, h3, h4');
|
||||
for (const h of headings) {
|
||||
if (h.textContent.trim() === "Workflow Overview" && h.offsetWidth > 0 && h.offsetHeight > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const origBtn = findOriginalButton();
|
||||
if (origBtn) {
|
||||
if (origBtn.classList.contains("p-button-active") ||
|
||||
origBtn.classList.contains("active") ||
|
||||
origBtn.getAttribute("aria-expanded") === "true" ||
|
||||
origBtn.getAttribute("aria-pressed") === "true") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function findActiveQueueIndicator() {
|
||||
@@ -131,7 +236,6 @@ function findActiveQueueIndicator() {
|
||||
const buttons = container.querySelectorAll("button, .p-button, .comfyui-menu-item");
|
||||
for (const btn of buttons) {
|
||||
if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue;
|
||||
|
||||
const text = (btn.textContent || "").toLowerCase();
|
||||
if (text.includes("active") || text.includes("queued") || text.includes("running") || text.includes("pending")) {
|
||||
return btn;
|
||||
@@ -140,75 +244,59 @@ function findActiveQueueIndicator() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findNativeExtensionsPanel() {
|
||||
return Array.from(document.querySelectorAll('.shadow-interface:not(.actionbar-container)'))
|
||||
.find(el => el.textContent.toLowerCase().includes('extensions') || el.querySelector('button')?.textContent.toLowerCase().includes('extensions'));
|
||||
}
|
||||
|
||||
function updateSidebarTabsVisibility() {
|
||||
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
|
||||
if (!sidebar) return;
|
||||
|
||||
// Direct mapping from tab settings to their unique class-based HTML icon tags
|
||||
const tabIconSelectors = {
|
||||
"Assets": '[class*="comfy--image-ai-edit"]',
|
||||
"Nodes": '[class*="comfy--node"]',
|
||||
"Models": '[class*="comfy--ai-model"]',
|
||||
"Workflows": '[class*="comfy--workflow"]',
|
||||
"Apps": '[class*="lucide--panels-top-left"]',
|
||||
"NodesMap": '[class*="pi-sitemap"]',
|
||||
"Templates": '[class*="comfy--template"]'
|
||||
};
|
||||
|
||||
Object.entries(tabIconSelectors).forEach(([tab, selector]) => {
|
||||
// Read from renamed "Hide Junk" path
|
||||
const shouldHide = app.ui?.settings?.getSettingValue(`Comfy Sidebar.Hide Junk.${tab}`) ?? false;
|
||||
|
||||
const icon = sidebar.querySelector(selector);
|
||||
if (icon) {
|
||||
const tabBtn = icon.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || icon;
|
||||
|
||||
if (shouldHide) {
|
||||
if (tabBtn.style.display !== "none") {
|
||||
tabBtn.style.setProperty("display", "none", "important");
|
||||
}
|
||||
if (tabBtn.style.display !== "none") tabBtn.style.setProperty("display", "none", "important");
|
||||
} else {
|
||||
if (tabBtn.style.display === "none") {
|
||||
tabBtn.style.removeProperty("display");
|
||||
}
|
||||
if (tabBtn.style.display === "none") tabBtn.style.removeProperty("display");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle "Override Stock Job History Tab" using its renamed setting path
|
||||
const hideStockHistory = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
|
||||
const historyIcon = sidebar.querySelector('[class*="lucide--history"]');
|
||||
if (historyIcon) {
|
||||
const historyBtn = historyIcon.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || historyIcon;
|
||||
if (hideStockHistory) {
|
||||
if (historyBtn.style.display !== "none") {
|
||||
historyBtn.style.setProperty("display", "none", "important");
|
||||
}
|
||||
if (historyBtn.style.display !== "none") historyBtn.style.setProperty("display", "none", "important");
|
||||
} else {
|
||||
if (historyBtn.style.display === "none") {
|
||||
historyBtn.style.removeProperty("display");
|
||||
}
|
||||
if (historyBtn.style.display === "none") historyBtn.style.removeProperty("display");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findGraphButton() {
|
||||
// 1. Target elements with Tailwind classes, but IGNORE those inside modal settings/sidebars
|
||||
const elements = document.querySelectorAll('.bg-secondary-background.rounded-lg.items-center.inline-flex.pointer-events-auto');
|
||||
for (const el of elements) {
|
||||
// Filter out dialogs, sidebar overlays, settings drawers, and modals
|
||||
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) {
|
||||
continue;
|
||||
}
|
||||
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) continue;
|
||||
return el;
|
||||
}
|
||||
|
||||
// 2. Same fallback logic, excluding modals
|
||||
const fallbacks = document.querySelectorAll('button, .bg-secondary-background');
|
||||
for (const el of fallbacks) {
|
||||
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) {
|
||||
continue;
|
||||
}
|
||||
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) continue;
|
||||
const text = (el.textContent || el.getAttribute("title") || "").toLowerCase();
|
||||
if (text.includes("graph") || text.includes("workflow") || el.querySelector('[class*="sitemap"], [class*="workflow"]')) {
|
||||
return el;
|
||||
@@ -217,122 +305,179 @@ function findGraphButton() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function syncErrorBadge(originalBtn, customBtn) {
|
||||
if (!customBtn) return;
|
||||
|
||||
const stockDot = document.querySelector('.comfy-sidebar-hide-original-properties-btn ~ span, .comfy-sidebar-hide-original-properties-btn + span');
|
||||
let customDot = customBtn.querySelector('.comfy-sidebar-error-dot');
|
||||
|
||||
if (stockDot) {
|
||||
if (!customDot) {
|
||||
customDot = document.createElement("span");
|
||||
customDot.className = "comfy-sidebar-error-dot inline-flex items-center justify-center rounded-full bg-destructive-background text-white size-2 absolute -top-1 -right-1";
|
||||
customBtn.appendChild(customDot);
|
||||
}
|
||||
} else if (customDot) {
|
||||
customDot.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanHTML(html) {
|
||||
if (!html) return "";
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = html;
|
||||
const tempDot = tempDiv.querySelector('[class*="destructive"]');
|
||||
if (tempDot) tempDot.remove();
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
export function syncClassicLayout() {
|
||||
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
|
||||
if (domObserver) domObserver.disconnect();
|
||||
|
||||
// 1. Handle "Hide Graph Button" Toggle under the renamed "Hide Junk" path
|
||||
const hideGraphBtn = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Graph Button") ?? false;
|
||||
const graphBtn = findGraphButton();
|
||||
if (graphBtn) {
|
||||
if (hideGraphBtn) {
|
||||
if (graphBtn.style.display !== "none") {
|
||||
graphBtn.style.setProperty("display", "none", "important");
|
||||
}
|
||||
} else {
|
||||
if (graphBtn.style.display === "none") {
|
||||
graphBtn.style.removeProperty("display");
|
||||
try {
|
||||
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
|
||||
|
||||
const hideGraphBtn = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Graph Button") ?? false;
|
||||
const graphBtn = findGraphButton();
|
||||
if (graphBtn) {
|
||||
if (hideGraphBtn) {
|
||||
if (graphBtn.style.display !== "none") graphBtn.style.setProperty("display", "none", "important");
|
||||
} else {
|
||||
if (graphBtn.style.display === "none") graphBtn.style.removeProperty("display");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Exit early and clean up overrides if Classic Topbar layout is off
|
||||
if (!isClassicLayoutEnabled) {
|
||||
const extensionsPanel = findNativeExtensionsPanel();
|
||||
if (extensionsPanel) {
|
||||
if (isClassicLayoutEnabled) {
|
||||
const container = findTopbarContainer();
|
||||
if (container && extensionsPanel.parentNode !== container) {
|
||||
if (!extensionsPanel._originalParent) {
|
||||
extensionsPanel._originalParent = extensionsPanel.parentNode;
|
||||
extensionsPanel._originalNextSibling = extensionsPanel.nextSibling;
|
||||
}
|
||||
container.appendChild(extensionsPanel);
|
||||
extensionsPanel.classList.add("comfy-sidebar-extensions-override");
|
||||
}
|
||||
} else if (extensionsPanel._originalParent && extensionsPanel.parentNode !== extensionsPanel._originalParent) {
|
||||
extensionsPanel._originalParent.insertBefore(extensionsPanel, extensionsPanel._originalNextSibling || null);
|
||||
extensionsPanel.classList.remove("comfy-sidebar-extensions-override");
|
||||
}
|
||||
}
|
||||
|
||||
if (!isClassicLayoutEnabled) {
|
||||
const originalBtn = findOriginalButton();
|
||||
if (originalBtn?.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
|
||||
originalBtn.classList.remove("comfy-sidebar-hide-original-properties-btn");
|
||||
}
|
||||
|
||||
const customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
|
||||
if (customBtn) customBtn.remove();
|
||||
|
||||
const indicator = findActiveQueueIndicator();
|
||||
if (indicator && indicator.style.display === "none") indicator.style.removeProperty("display");
|
||||
|
||||
updateSidebarTabsVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
const originalBtn = findOriginalButton();
|
||||
if (originalBtn && originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
|
||||
originalBtn.classList.remove("comfy-sidebar-hide-original-properties-btn");
|
||||
const container = findTopbarContainer();
|
||||
const openState = isPropertiesPanelOpen();
|
||||
|
||||
if (originalBtn) {
|
||||
if (!originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
|
||||
originalBtn.classList.add("comfy-sidebar-hide-original-properties-btn");
|
||||
}
|
||||
|
||||
savedButtonData = {
|
||||
className: originalBtn.className.replace("comfy-sidebar-hide-original-properties-btn", "").trim(),
|
||||
innerHTML: originalBtn.innerHTML,
|
||||
tagName: originalBtn.tagName,
|
||||
attributes: Array.from(originalBtn.attributes).map(attr => ({
|
||||
name: attr.name,
|
||||
value: attr.value
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
let customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
|
||||
|
||||
if (!customBtn && savedButtonData && container) {
|
||||
customBtn = document.createElement(savedButtonData.tagName);
|
||||
customBtn.className = savedButtonData.className + " comfy-sidebar-custom-properties-toggle";
|
||||
customBtn.innerHTML = cleanHTML(savedButtonData.innerHTML);
|
||||
|
||||
for (const attr of savedButtonData.attributes) {
|
||||
if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") {
|
||||
customBtn.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
|
||||
customBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const nativeBtn = findOriginalButton();
|
||||
if (nativeBtn) {
|
||||
nativeBtn.click();
|
||||
requestAnimationFrame(() => syncClassicLayout());
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(customBtn);
|
||||
}
|
||||
|
||||
const customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
|
||||
if (customBtn) {
|
||||
customBtn.remove();
|
||||
if (originalBtn) {
|
||||
const cleanedHTML = cleanHTML(originalBtn.innerHTML);
|
||||
if (cleanedHTML && customBtn.innerHTML !== cleanedHTML) {
|
||||
const existingDot = customBtn.querySelector('.comfy-sidebar-error-dot');
|
||||
customBtn.innerHTML = cleanedHTML;
|
||||
if (existingDot) customBtn.appendChild(existingDot);
|
||||
}
|
||||
|
||||
const titleVal = originalBtn.getAttribute("title") || originalBtn.getAttribute("aria-label") || "Toggle properties panel";
|
||||
customBtn.setAttribute("title", titleVal);
|
||||
customBtn.setAttribute("aria-label", titleVal);
|
||||
|
||||
for (const cls of originalBtn.classList) {
|
||||
if (cls !== "comfy-sidebar-hide-original-properties-btn" && !customBtn.classList.contains(cls)) {
|
||||
customBtn.classList.add(cls);
|
||||
}
|
||||
}
|
||||
|
||||
syncErrorBadge(originalBtn, customBtn);
|
||||
} else if (!customBtn.getAttribute("title")) {
|
||||
customBtn.setAttribute("title", "Toggle properties panel");
|
||||
customBtn.setAttribute("aria-label", "Toggle properties panel");
|
||||
}
|
||||
customBtn.classList.toggle("comfy-panel-open", openState);
|
||||
customBtn.style.display = "inline-flex";
|
||||
}
|
||||
|
||||
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
|
||||
const indicator = findActiveQueueIndicator();
|
||||
if (indicator && indicator.style.display === "none") {
|
||||
indicator.style.removeProperty("display");
|
||||
}
|
||||
|
||||
updateSidebarTabsVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Handle Active Layout elements synchronization
|
||||
const originalBtn = findOriginalButton();
|
||||
const container = findTopbarContainer();
|
||||
const openState = isPropertiesPanelOpen();
|
||||
|
||||
if (originalBtn) {
|
||||
if (!originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
|
||||
originalBtn.classList.add("comfy-sidebar-hide-original-properties-btn");
|
||||
}
|
||||
|
||||
savedButtonData = {
|
||||
className: originalBtn.className.replace("comfy-sidebar-hide-original-properties-btn", "").trim(),
|
||||
innerHTML: originalBtn.innerHTML,
|
||||
tagName: originalBtn.tagName,
|
||||
attributes: Array.from(originalBtn.attributes).map(attr => ({
|
||||
name: attr.name,
|
||||
value: attr.value
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
let customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
|
||||
|
||||
if (!customBtn && savedButtonData && container) {
|
||||
customBtn = document.createElement(savedButtonData.tagName);
|
||||
|
||||
customBtn.className = savedButtonData.className + " comfy-sidebar-custom-properties-toggle";
|
||||
customBtn.innerHTML = savedButtonData.innerHTML;
|
||||
|
||||
for (const attr of savedButtonData.attributes) {
|
||||
if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") {
|
||||
customBtn.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(customBtn.style, {
|
||||
position: "relative",
|
||||
zIndex: "1000",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
opacity: "1",
|
||||
pointerEvents: "auto",
|
||||
display: "inline-flex"
|
||||
});
|
||||
|
||||
customBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const nativeBtn = findOriginalButton();
|
||||
if (nativeBtn) nativeBtn.click();
|
||||
});
|
||||
|
||||
container.appendChild(customBtn);
|
||||
}
|
||||
|
||||
if (customBtn) {
|
||||
customBtn.classList.toggle("comfy-panel-open", openState);
|
||||
customBtn.style.display = "inline-flex";
|
||||
}
|
||||
|
||||
// 4. Handle "0 active" indicator visibility based on history tab settings
|
||||
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
|
||||
const indicator = findActiveQueueIndicator();
|
||||
if (indicator) {
|
||||
if (hideQueueIndicator) {
|
||||
if (indicator.style.display !== "none") {
|
||||
indicator.style.setProperty("display", "none", "important");
|
||||
}
|
||||
} else {
|
||||
if (indicator.style.display === "none") {
|
||||
if (indicator) {
|
||||
if (hideQueueIndicator) {
|
||||
if (indicator.style.display !== "none") indicator.style.setProperty("display", "none", "important");
|
||||
} else if (indicator.style.display === "none") {
|
||||
indicator.style.removeProperty("display");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSidebarTabsVisibility();
|
||||
updateSidebarTabsVisibility();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Comfy Sidebar: Error inside layout sync routine:", err);
|
||||
} finally {
|
||||
if (domObserver) {
|
||||
domObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setupPropertiesPanelToggleFix() {
|
||||
@@ -343,8 +488,14 @@ export function setupPropertiesPanelToggleFix() {
|
||||
.comfy-sidebar-hide-original-properties-btn {
|
||||
display: none !important;
|
||||
}
|
||||
.comfy-sidebar-hide-original-properties-btn ~ span,
|
||||
.comfy-sidebar-hide-original-properties-btn + span {
|
||||
display: none !important;
|
||||
}
|
||||
.comfy-sidebar-custom-properties-toggle {
|
||||
order: 99999 !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
margin-left: -8px !important;
|
||||
margin-right: 0px !important;
|
||||
display: inline-flex !important;
|
||||
@@ -353,43 +504,36 @@ export function setupPropertiesPanelToggleFix() {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.comfy-sidebar-custom-properties-toggle.comfy-panel-open {
|
||||
order: 99999 !important;
|
||||
margin-left: 0px !important;
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
.comfy-sidebar-custom-properties-toggle > span.comfy-sidebar-error-dot {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
if (domObserver) {
|
||||
domObserver.disconnect();
|
||||
}
|
||||
|
||||
syncClassicLayout();
|
||||
if (domObserver) domObserver.disconnect();
|
||||
|
||||
domObserver = new MutationObserver((mutations) => {
|
||||
let shouldSync = false;
|
||||
|
||||
const isInternalSidebarMutation = (node) => {
|
||||
if (!node || node.nodeType !== 1) return false;
|
||||
return node.closest?.('.comfyui-sidebar, .comfy-sidebar, .comfy-sidebar-comparison-overlay') ||
|
||||
node.classList?.contains('comfy-sidebar-card');
|
||||
};
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
|
||||
// Ensure mutation is on an actual Element node (nodeType 1) and not just text node updates
|
||||
const hasElementMutation = Array.from(mutation.addedNodes).some(node => node.nodeType === 1) ||
|
||||
Array.from(mutation.removedNodes).some(node => node.nodeType === 1);
|
||||
const hasExternalElementMutation =
|
||||
Array.from(mutation.addedNodes).some(node => node.nodeType === 1 && !isInternalSidebarMutation(node)) ||
|
||||
Array.from(mutation.removedNodes).some(node => node.nodeType === 1 && !isInternalSidebarMutation(node));
|
||||
|
||||
if (!hasElementMutation) continue;
|
||||
|
||||
// Ignore mutations on our own components to avoid feedback loops
|
||||
const isOurNode = Array.from(mutation.addedNodes).some(node =>
|
||||
node.classList?.contains?.("comfy-sidebar-custom-properties-toggle") ||
|
||||
node.id === "comfy-sidebar-layout-fix-styles"
|
||||
);
|
||||
if (isOurNode) continue;
|
||||
|
||||
shouldSync = true;
|
||||
break;
|
||||
if (hasExternalElementMutation) {
|
||||
shouldSync = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldSync && !syncScheduled) {
|
||||
@@ -405,4 +549,6 @@ export function setupPropertiesPanelToggleFix() {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
syncClassicLayout();
|
||||
}
|
||||
+125
-67
@@ -1,7 +1,7 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { State, promptStates, pruneHistory, loadStatesFromLocalStorage } from "./state.js";
|
||||
import { findImagesInOutputs, findTextsInOutputs } from "./utils.js";
|
||||
import { State, promptStates, pruneHistory, cardElements, saveStatesToLocalStorage } from "./state.js";
|
||||
import { findImagesInOutputs, findTextsInOutputs, parseWorkflow, getPrimaryOutputImages } from "./utils.js";
|
||||
|
||||
export let renderDOMFn = () => {};
|
||||
export let updateSidebarBadgeFn = () => {};
|
||||
@@ -13,6 +13,7 @@ export function setUIDependencies(renderFn, badgeFn) {
|
||||
export async function syncQueue() {
|
||||
try {
|
||||
const q = await api.getQueue();
|
||||
|
||||
const runningList = q.Running || q.queue_running || [];
|
||||
const pendingList = q.Pending || q.queue_pending || [];
|
||||
const pendingIds = new Set();
|
||||
@@ -26,7 +27,7 @@ export async function syncQueue() {
|
||||
pid = p.prompt_id || p.id || p.uuid;
|
||||
seq = typeof p.number === 'number' ? p.number : (typeof p.prompt_number === 'number' ? p.prompt_number : idx);
|
||||
}
|
||||
return { pid, seq, original: p };
|
||||
return { pid: pid ? String(pid) : null, seq, original: p };
|
||||
});
|
||||
|
||||
normalizedPending.sort((a, b) => a.seq - b.seq);
|
||||
@@ -65,27 +66,49 @@ export async function syncQueue() {
|
||||
}
|
||||
|
||||
const concludeRun = async (pid, statusStr) => {
|
||||
if (!pid || !promptStates.has(pid)) return;
|
||||
if (State.currentlyActivePromptId === pid) State.currentlyActivePromptId = null;
|
||||
const key = String(pid);
|
||||
if (!key || !promptStates.has(key)) return;
|
||||
if (State.currentlyActivePromptId === key) State.currentlyActivePromptId = null;
|
||||
|
||||
const st = promptStates.get(pid);
|
||||
const st = promptStates.get(key);
|
||||
|
||||
if (st._previewBlobUrl) {
|
||||
try { URL.revokeObjectURL(st._previewBlobUrl); } catch(e){}
|
||||
delete st._previewBlobUrl;
|
||||
}
|
||||
if (st._oldPreviewBlobUrl) {
|
||||
try { URL.revokeObjectURL(st._oldPreviewBlobUrl); } catch(e){}
|
||||
delete st._oldPreviewBlobUrl;
|
||||
}
|
||||
|
||||
st.status = statusStr;
|
||||
st.progressText = "";
|
||||
st.rendered = false;
|
||||
st.endTime = Date.now();
|
||||
if (st.startTime) st.duration = (st.endTime - st.startTime) / 1000;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/history/${pid}`);
|
||||
const hItem = await res.json();
|
||||
if (hItem && hItem[pid]) {
|
||||
if (!st.workflow) st.workflow = hItem[pid].extra_data?.extra_pnginfo?.workflow || null;
|
||||
st.nodeOutputs = hItem[pid].outputs; // Store full outputs dictionary!
|
||||
if (st.images.length === 0) st.images = findImagesInOutputs(hItem[pid].outputs);
|
||||
st.texts = findTextsInOutputs(hItem[pid].outputs);
|
||||
}
|
||||
} catch (err) {}
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const res = await fetch(`/history/${key}`);
|
||||
const hItem = await res.json();
|
||||
if (hItem && hItem[key]) {
|
||||
const rawWf = hItem[key].extra_data?.extra_pnginfo?.workflow || hItem[key].prompt?.[3]?.extra_pnginfo?.workflow || null;
|
||||
st.workflow = parseWorkflow(rawWf) || st.workflow;
|
||||
st.nodeOutputs = hItem[key].outputs;
|
||||
|
||||
const primaryImgs = getPrimaryOutputImages(hItem[key].outputs, st.workflow);
|
||||
if (primaryImgs.length > 0) st.images = primaryImgs;
|
||||
|
||||
const fetchedTexts = findTextsInOutputs(hItem[key].outputs, st.workflow);
|
||||
if (fetchedTexts.length > 0) st.texts = fetchedTexts;
|
||||
break;
|
||||
}
|
||||
} catch (err) {}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
pruneHistory(app);
|
||||
saveStatesToLocalStorage();
|
||||
syncQueue();
|
||||
};
|
||||
|
||||
@@ -93,7 +116,6 @@ export function setupApiListeners() {
|
||||
api.addEventListener("status", syncQueue);
|
||||
|
||||
api.addEventListener("execution_start", (e) => {
|
||||
// Auto-sweep cancelled and errored jobs if setting is enabled
|
||||
if (app.ui.settings.getSettingValue("Comfy Sidebar.Auto Clear Interrupted") ?? false) {
|
||||
const toDelete = [];
|
||||
for (const [p, s] of promptStates.entries()) {
|
||||
@@ -104,10 +126,11 @@ export function setupApiListeners() {
|
||||
}
|
||||
if (toDelete.length > 0) {
|
||||
api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }).catch(()=>{});
|
||||
saveStatesToLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
const pid = e.detail.prompt_id;
|
||||
const pid = String(e.detail.prompt_id);
|
||||
State.currentlyActivePromptId = pid;
|
||||
const activeWorkspaceWorkflow = app.graph.serialize();
|
||||
|
||||
@@ -123,15 +146,26 @@ export function setupApiListeners() {
|
||||
workflow: activeWorkspaceWorkflow, startTime: Date.now(), duration: null
|
||||
});
|
||||
}
|
||||
renderDOMFn(); // Render immediately to prevent pop-in delay!
|
||||
renderDOMFn();
|
||||
syncQueue();
|
||||
});
|
||||
|
||||
api.addEventListener("progress", (e) => {
|
||||
const pid = e.detail.prompt_id;
|
||||
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
|
||||
if (pid && promptStates.has(pid)) {
|
||||
promptStates.get(pid).progress = Math.round((e.detail.value / e.detail.max) * 100);
|
||||
renderDOMFn();
|
||||
const st = promptStates.get(pid);
|
||||
st.progress = Math.round((e.detail.value / e.detail.max) * 100);
|
||||
|
||||
const cardObj = cardElements.get(pid);
|
||||
if (cardObj && cardObj.progressBar) {
|
||||
cardObj.progressBar.style.width = `${st.progress}%`;
|
||||
if (cardObj.statusText && cardObj.statusText.style.display !== "none") {
|
||||
const nodeName = st.activeNodeName ? (st.activeNodeName === "Finishing..." ? "Finishing..." : `[${st.activeNodeName}]`) : "Sampling...";
|
||||
cardObj.statusText.textContent = `${nodeName} ${st.progress}%`;
|
||||
}
|
||||
} else {
|
||||
renderDOMFn();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -155,28 +189,28 @@ export function setupApiListeners() {
|
||||
const activeTasks = Array.from(promptStates.values()).filter(t => t.status === "active");
|
||||
if (activeTasks.length > 0) {
|
||||
const st = activeTasks[0];
|
||||
if (st._previewBlobUrl) try { URL.revokeObjectURL(st._previewBlobUrl); } catch(e){}
|
||||
st._previewBlobUrl = URL.createObjectURL(e.detail);
|
||||
st.images = [{ url: st._previewBlobUrl }];
|
||||
const newBlobUrl = URL.createObjectURL(e.detail);
|
||||
|
||||
st._oldPreviewBlobUrl = st._previewBlobUrl;
|
||||
st._previewBlobUrl = newBlobUrl;
|
||||
st.images = [{ url: newBlobUrl }];
|
||||
renderDOMFn();
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("executed", (e) => {
|
||||
if (promptStates.has(e.detail.prompt_id)) {
|
||||
const st = promptStates.get(e.detail.prompt_id);
|
||||
const finalImgs = findImagesInOutputs({ output: e.detail.output });
|
||||
if (finalImgs.length > 0) {
|
||||
// Overwrite with latest images to avoid accumulation across sequential nodes
|
||||
st.images = finalImgs;
|
||||
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
|
||||
if (pid && promptStates.has(pid)) {
|
||||
const st = promptStates.get(pid);
|
||||
const nodeImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
|
||||
if (nodeImgs.length > 0) {
|
||||
st.images = nodeImgs;
|
||||
}
|
||||
const finalTexts = findTextsInOutputs({ output: e.detail.output });
|
||||
if (finalTexts.length > 0) {
|
||||
// Overwrite with latest texts to avoid clearing previously set text outputs
|
||||
st.texts = finalTexts;
|
||||
const nodeTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
|
||||
if (nodeTexts.length > 0) {
|
||||
st.texts = nodeTexts;
|
||||
}
|
||||
|
||||
// Dynamically capture each executed node's individual outputs
|
||||
if (!st.nodeOutputs) st.nodeOutputs = {};
|
||||
st.nodeOutputs[e.detail.node] = e.detail.output;
|
||||
|
||||
@@ -193,39 +227,63 @@ export function setupApiListeners() {
|
||||
}
|
||||
|
||||
export async function initSessionAndHistory() {
|
||||
let backendSessionId = null;
|
||||
try {
|
||||
const res = await fetch("/classic-sidebar/session");
|
||||
const data = await res.json();
|
||||
backendSessionId = data.session_id;
|
||||
} catch (err) {
|
||||
console.warn("Comfy Sidebar: Custom session endpoint not found. Queue persistence will fall back to cache.");
|
||||
}
|
||||
const historyData = await api.getHistory();
|
||||
if (!historyData) return;
|
||||
|
||||
const storedSessionId = localStorage.getItem("comfy_sidebar_backend_session_id");
|
||||
if (backendSessionId && backendSessionId === storedSessionId) {
|
||||
loadStatesFromLocalStorage();
|
||||
} else {
|
||||
localStorage.removeItem("comfy_sidebar_prompt_states");
|
||||
if (backendSessionId) localStorage.setItem("comfy_sidebar_backend_session_id", backendSessionId);
|
||||
}
|
||||
const rawItems = [];
|
||||
if (Array.isArray(historyData.History)) {
|
||||
historyData.History.forEach(item => {
|
||||
if (item && item.prompt_id) rawItems.push({ pid: String(item.prompt_id), data: item });
|
||||
});
|
||||
} else if (typeof historyData === 'object') {
|
||||
Object.keys(historyData).forEach(pidKey => {
|
||||
const item = historyData[pidKey];
|
||||
rawItems.push({ pid: String(pidKey), data: Array.isArray(item) ? { prompt: item[0], outputs: item[1], status: item[2] } : item });
|
||||
});
|
||||
}
|
||||
|
||||
const historyData = await api.getHistory();
|
||||
const ids = Object.keys(historyData).sort((a,b) => Number(a)-Number(b));
|
||||
ids.forEach(id => {
|
||||
if (promptStates.has(id)) return;
|
||||
const images = findImagesInOutputs(historyData[id].outputs);
|
||||
const texts = findTextsInOutputs(historyData[id].outputs);
|
||||
if (images.length === 0 && texts.length === 0) return;
|
||||
|
||||
State.globalOrderCounter++;
|
||||
promptStates.set(id, {
|
||||
pid: id, status: "completed", images, texts,
|
||||
nodeOutputs: historyData[id].outputs, // Store full outputs dictionary!
|
||||
workflow: historyData[id].extra_data?.extra_pnginfo?.workflow || null,
|
||||
progressText: "", timestamp: State.globalOrderCounter, rendered: true
|
||||
rawItems.sort((a, b) => {
|
||||
const numA = a.data.prompt?.[0] ?? 0;
|
||||
const numB = b.data.prompt?.[0] ?? 0;
|
||||
return numA - numB;
|
||||
});
|
||||
});
|
||||
pruneHistory(app);
|
||||
await syncQueue();
|
||||
|
||||
rawItems.forEach(({ pid, data }) => {
|
||||
if (promptStates.has(pid)) return;
|
||||
|
||||
const extraData = data.extra_data || data.prompt?.[3] || {};
|
||||
const workflow = parseWorkflow(extraData.extra_pnginfo?.workflow || extraData.workflow);
|
||||
const outputs = data.outputs || {};
|
||||
|
||||
const images = getPrimaryOutputImages(outputs, workflow);
|
||||
const texts = findTextsInOutputs(outputs, workflow);
|
||||
|
||||
const statusStr = data.status?.status_str || "completed";
|
||||
const status = (statusStr === "success" || statusStr === "completed") ? "completed" :
|
||||
(statusStr === "error" ? "error" : "cancelled");
|
||||
|
||||
// Filter out empty records from history so dummy "No Outputs" cards don't pollute queue
|
||||
if (images.length === 0 && texts.length === 0 && status === "completed") return;
|
||||
|
||||
State.globalOrderCounter++;
|
||||
promptStates.set(pid, {
|
||||
pid: pid,
|
||||
status: status,
|
||||
images: images,
|
||||
texts: texts,
|
||||
nodeOutputs: outputs,
|
||||
workflow: workflow,
|
||||
progressText: "",
|
||||
timestamp: State.globalOrderCounter,
|
||||
rendered: true
|
||||
});
|
||||
});
|
||||
|
||||
pruneHistory(app);
|
||||
saveStatesToLocalStorage();
|
||||
await syncQueue();
|
||||
} catch (err) {
|
||||
console.error("Comfy Sidebar: Failed to initialize history from server API", err);
|
||||
}
|
||||
}
|
||||
+174
-33
@@ -1,25 +1,145 @@
|
||||
// sidebar.js - Fully Fixed Real-Time UI Sync (With setTimeout deferred updates)
|
||||
import { app } from "/scripts/app.js";
|
||||
import { injectStyles } from "./styles.js";
|
||||
import { setupDragAndDrop } from "./dragdrop.js";
|
||||
import { setupSidebarUI, applySidebarOverride, findOurSidebarButton, setSyncQueue, updateSidebarBadge, renderDOM } from "./ui.js";
|
||||
import { setupApiListeners, initSessionAndHistory, syncQueue, setUIDependencies } from "./queue.js";
|
||||
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout } from "./layout.js";
|
||||
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings } from "./layout.js";
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
// Helper to scan node container for the exact text element rendering the title (Vue Mode)
|
||||
function findHeaderByText(parent, text) {
|
||||
if (!text) return null;
|
||||
const cleanText = text.trim();
|
||||
|
||||
const elements = parent.querySelectorAll("span, div, h1, h2, h3, h4, p, [class*='title'], [class*='text']");
|
||||
for (const el of elements) {
|
||||
if (el.textContent.trim() === cleanText) {
|
||||
if (el.offsetWidth > 0 && el.offsetHeight > 0 && el.offsetWidth < parent.offsetWidth * 0.9) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function syncNodeVueBadge(node, isIgnored) {
|
||||
const nodeEl = document.querySelector(`.comfy-node[data-node-id="${node.id}"], [data-node-id="${node.id}"]`);
|
||||
if (!nodeEl) return;
|
||||
|
||||
const titleTextNode = findHeaderByText(nodeEl, node.title || node.type);
|
||||
if (!titleTextNode) {
|
||||
const badge = nodeEl.querySelector(".comfy-sidebar-ignore-badge");
|
||||
if (badge && !isIgnored) badge.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const headerEl = titleTextNode.parentNode;
|
||||
let badge = headerEl.querySelector(".comfy-sidebar-ignore-badge");
|
||||
|
||||
if (isIgnored) {
|
||||
if (!badge) {
|
||||
badge = document.createElement("div");
|
||||
badge.className = "comfy-sidebar-ignore-badge";
|
||||
badge.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/>
|
||||
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/>
|
||||
<path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/>
|
||||
<line x1="2" y1="2" x2="22" y2="22"/>
|
||||
</svg>
|
||||
`;
|
||||
Object.assign(badge.style, {
|
||||
marginLeft: "auto",
|
||||
marginRight: "6px",
|
||||
float: "right",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "inherit",
|
||||
pointerEvents: "none"
|
||||
});
|
||||
headerEl.appendChild(badge);
|
||||
}
|
||||
} else {
|
||||
if (badge) badge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function syncAllNodeBadges() {
|
||||
applySidebarOverride();
|
||||
if (!app.graph || !app.graph._nodes) return;
|
||||
|
||||
app.graph._nodes.forEach(node => {
|
||||
if (!node.properties) node.properties = {};
|
||||
const isIgnored = !!node.properties.ignoreInQueue;
|
||||
|
||||
if (isIgnored) {
|
||||
if (node.boxcolor !== "#ff3333") {
|
||||
node._oldBoxcolor = node.boxcolor || "";
|
||||
node.boxcolor = "#ff3333";
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
} else {
|
||||
if (node.boxcolor === "#ff3333") {
|
||||
node.boxcolor = node._oldBoxcolor || "";
|
||||
delete node._oldBoxcolor;
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
syncNodeVueBadge(node, isIgnored);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleIgnoreActiveNode() {
|
||||
const canvas = app.canvas;
|
||||
if (!canvas) return;
|
||||
|
||||
let nodes = [];
|
||||
if (canvas.selected_nodes && Object.keys(canvas.selected_nodes).length > 0) {
|
||||
nodes = Object.values(canvas.selected_nodes);
|
||||
} else if (canvas.current_active_node) {
|
||||
nodes = [canvas.current_active_node];
|
||||
}
|
||||
|
||||
if (nodes.length === 0) return;
|
||||
|
||||
nodes.forEach(node => {
|
||||
if (!node.properties) node.properties = {};
|
||||
node.properties.ignoreInQueue = !node.properties.ignoreInQueue;
|
||||
const isIgnored = !!node.properties.ignoreInQueue;
|
||||
|
||||
if (isIgnored) {
|
||||
if (node.boxcolor !== "#ff3333") {
|
||||
node._oldBoxcolor = node.boxcolor || "";
|
||||
node.boxcolor = "#ff3333";
|
||||
}
|
||||
} else {
|
||||
if (node.boxcolor === "#ff3333") {
|
||||
node.boxcolor = node._oldBoxcolor || "";
|
||||
delete node._oldBoxcolor;
|
||||
}
|
||||
}
|
||||
|
||||
syncNodeVueBadge(node, isIgnored);
|
||||
});
|
||||
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
if (app.canvas) app.canvas.setDirty(true, true);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfySidebar.ClassicRestore",
|
||||
|
||||
init() {
|
||||
// Standard Comfy Queue Settings
|
||||
app.ui.settings.addSetting({ id: "Comfy Sidebar.Grid Columns Threshold", name: "Width Threshold for Queue Columns (px)", type: "number", defaultValue: 350 });
|
||||
app.ui.settings.addSetting({ id: "Comfy Sidebar.Keep Object Aspect Ratio", name: "If disabled, cards in the queue will be cropped to the same size.", type: "boolean", defaultValue: true });
|
||||
app.ui.settings.addSetting({ id: "Comfy Sidebar.Show Pending Count Only", name: "If disabled, each queued job will have a separate individual card", type: "boolean", defaultValue: true });
|
||||
app.ui.settings.addSetting({ id: "Comfy Sidebar.Show Working Node Name", name: "Shows the name of the node which is currently in the process", type: "boolean", defaultValue: true });
|
||||
app.ui.settings.addSetting({ id: "Comfy Sidebar.Auto Clear Interrupted", name: "Auto-clear cancelled & failed jobs on new generation", type: "boolean", defaultValue: false });
|
||||
|
||||
// Add settings to hide individual sidebar tabs under the "Hide Junk" sub-category
|
||||
// We use setTimeout to let Comfy finish writing the setting value before reading it
|
||||
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "NodesMap", "Templates"];
|
||||
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"];
|
||||
sidebarTabs.forEach(tab => {
|
||||
app.ui.settings.addSetting({
|
||||
id: `Comfy Sidebar.Hide Junk.${tab}`,
|
||||
@@ -27,47 +147,43 @@ app.registerExtension({
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => {
|
||||
if (typeof syncClassicLayout === "function") syncClassicLayout();
|
||||
}, 0);
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Group the Stock History Tab override into "Hide Junk"
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Override Stock Job History Tab",
|
||||
name: "Replaces the stock Job History sidebar with Comfy Queue",
|
||||
name: "Replace the stock Job History sidebar with Comfy Queue",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => {
|
||||
if (typeof syncClassicLayout === "function") syncClassicLayout();
|
||||
}, 0);
|
||||
onChange: (value) => {
|
||||
if (isInitialized && app.ui && app.ui.settings) {
|
||||
syncStockHistoryAndProgressSettings(value);
|
||||
}
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the toggle setting for Graph Button under "Hide Junk"
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Graph Button",
|
||||
name: "Hide floating 'Graph' (Workflow/Node Map) button",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => {
|
||||
if (typeof syncClassicLayout === "function") syncClassicLayout();
|
||||
}, 0);
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the toggle setting for Classic / Comfy Layout
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Comfy Layout",
|
||||
name: "Places the controls and the open workflow tabs on a single unified top bar",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: (value) => {
|
||||
applyClassicLayout(value);
|
||||
if (isInitialized) {
|
||||
applyClassicLayout(value, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -75,44 +191,69 @@ app.registerExtension({
|
||||
async setup() {
|
||||
if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return;
|
||||
|
||||
// Apply saved layout configuration on startup
|
||||
const isClassicLayoutEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
|
||||
applyClassicLayout(isClassicLayoutEnabled);
|
||||
applyClassicLayout(isClassicLayoutEnabled, false);
|
||||
|
||||
const isHistoryOverrideEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
|
||||
if (isHistoryOverrideEnabled) {
|
||||
syncStockHistoryAndProgressSettings(true);
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
// Run properties panel toggle correction
|
||||
setupPropertiesPanelToggleFix();
|
||||
|
||||
// Safely resolve circular dependencies using Dependency Injection Hooks
|
||||
setSyncQueue(syncQueue);
|
||||
setUIDependencies(renderDOM, updateSidebarBadge);
|
||||
|
||||
// 1. Initialize DOM Independent Services
|
||||
injectStyles();
|
||||
setupDragAndDrop();
|
||||
|
||||
// 2. Initialize UI Hierarchy
|
||||
const sidebarContainer = setupSidebarUI();
|
||||
|
||||
// 3. Connect API Hooks
|
||||
setupApiListeners();
|
||||
|
||||
// 4. Connect Storage and History Sync
|
||||
await initSessionAndHistory();
|
||||
|
||||
// 5. Connect UI Keyboard & Override Hacks
|
||||
// Keyboard shortcuts listener
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const activeEl = document.activeElement;
|
||||
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable || activeEl.tagName === "SELECT")) return;
|
||||
if (e.key.toLowerCase() === "q") {
|
||||
|
||||
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const ourBtn = findOurSidebarButton();
|
||||
if (ourBtn) ourBtn.click();
|
||||
}
|
||||
|
||||
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
toggleIgnoreActiveNode();
|
||||
}
|
||||
}, true);
|
||||
|
||||
setInterval(() => applySidebarOverride(), 500);
|
||||
// Event hooks for node additions and workflow loading (eliminates continuous polling loops)
|
||||
if (app.graph) {
|
||||
const originalConfigure = app.graph.configure;
|
||||
app.graph.configure = function() {
|
||||
const res = originalConfigure ? originalConfigure.apply(this, arguments) : undefined;
|
||||
requestAnimationFrame(() => syncAllNodeBadges());
|
||||
return res;
|
||||
};
|
||||
|
||||
const originalOnNodeAdded = app.graph.onNodeAdded;
|
||||
app.graph.onNodeAdded = function(node) {
|
||||
if (originalOnNodeAdded) originalOnNodeAdded.apply(this, arguments);
|
||||
if (node && node.properties?.ignoreInQueue) {
|
||||
node.boxcolor = "#ff3333";
|
||||
syncNodeVueBadge(node, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Apply sidebar override when initializing
|
||||
applySidebarOverride();
|
||||
|
||||
// 6. Final Registration
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "classic-comfy-sidebar",
|
||||
icon: "pi pi-images",
|
||||
|
||||
+66
-21
@@ -1,17 +1,32 @@
|
||||
export const promptStates = new Map();
|
||||
export const cardElements = new Map();
|
||||
|
||||
// Global shared variables
|
||||
export const State = {
|
||||
currentSearchQuery: "",
|
||||
globalOrderCounter: 0,
|
||||
sidebarContainer: null,
|
||||
cardStack: null,
|
||||
cardStack: null, // Main queue scroll container
|
||||
submenuStack: null, // Submenu scroll container
|
||||
currentlyActivePromptId: null,
|
||||
activeSubmenuPromptId: null, // Active run/outputs explorer pointer
|
||||
activeSubmenuBatchImages: null // Active batch images explorer pointer
|
||||
};
|
||||
|
||||
export function deletePromptState(pid) {
|
||||
const key = String(pid);
|
||||
const state = promptStates.get(key);
|
||||
if (state) {
|
||||
if (state._previewBlobUrl) {
|
||||
try { URL.revokeObjectURL(state._previewBlobUrl); } catch (e) {}
|
||||
}
|
||||
if (state._oldPreviewBlobUrl) {
|
||||
try { URL.revokeObjectURL(state._oldPreviewBlobUrl); } catch (e) {}
|
||||
}
|
||||
}
|
||||
promptStates.delete(key);
|
||||
cardElements.delete(key);
|
||||
}
|
||||
|
||||
export function pruneHistory(app) {
|
||||
const maxItems = app.ui.settings.getSettingValue("Comfy.Queue.MaxHistoryItems") ?? 64;
|
||||
const tasks = Array.from(promptStates.entries())
|
||||
@@ -23,34 +38,63 @@ export function pruneHistory(app) {
|
||||
const deleteCount = tasks.length - maxItems;
|
||||
for (let i = 0; i < deleteCount; i++) {
|
||||
const [pid] = tasks[i];
|
||||
promptStates.delete(pid);
|
||||
cardElements.delete(pid);
|
||||
deletePromptState(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function saveStatesToLocalStorage() {
|
||||
try {
|
||||
const serializable = [];
|
||||
let serializable = [];
|
||||
for (const [pid, state] of promptStates.entries()) {
|
||||
const cleanedImages = (state.images || []).map(img => {
|
||||
if (img.url && img.url.startsWith("blob:")) return null; // Skip blob URLs
|
||||
if (img.url && img.url.startsWith("blob:")) return null;
|
||||
return img;
|
||||
}).filter(Boolean);
|
||||
|
||||
// Include workflow JSON ONLY for image-less / unfinished / failed tasks
|
||||
// so users can load or save their workflows even after browser reloads!
|
||||
const includeWorkflow = cleanedImages.length === 0 || state.status !== "completed";
|
||||
|
||||
serializable.push({
|
||||
pid: state.pid, status: state.status, images: cleanedImages, texts: state.texts || [],
|
||||
nodeOutputs: state.nodeOutputs, // Persist node level outputs
|
||||
workflow: state.workflow, progress: state.progress || 0, queueNumber: state.queueNumber,
|
||||
progressText: state.progressText || "", timestamp: state.timestamp,
|
||||
activeNodeName: state.activeNodeName || "", rendered: state.rendered || false,
|
||||
startTime: state.startTime, endTime: state.endTime, duration: state.duration
|
||||
pid: String(state.pid),
|
||||
status: state.status,
|
||||
images: cleanedImages,
|
||||
texts: state.texts || [],
|
||||
nodeOutputs: state.nodeOutputs,
|
||||
workflow: includeWorkflow ? state.workflow : null,
|
||||
progress: state.progress || 0,
|
||||
queueNumber: state.queueNumber,
|
||||
progressText: state.progressText || "",
|
||||
timestamp: state.timestamp,
|
||||
activeNodeName: state.activeNodeName || "",
|
||||
rendered: state.rendered || false,
|
||||
startTime: state.startTime,
|
||||
endTime: state.endTime,
|
||||
duration: state.duration
|
||||
});
|
||||
}
|
||||
localStorage.setItem("comfy_sidebar_prompt_states", JSON.stringify(serializable));
|
||||
} catch (e) {
|
||||
console.error("Comfy Sidebar: Failed to save state to localStorage", e);
|
||||
}
|
||||
|
||||
if (serializable.length === 0) {
|
||||
localStorage.removeItem("comfy_sidebar_prompt_states");
|
||||
return;
|
||||
}
|
||||
|
||||
serializable.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
while (serializable.length > 0) {
|
||||
try {
|
||||
localStorage.setItem("comfy_sidebar_prompt_states", JSON.stringify(serializable));
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.name === "QuotaExceededError" || e.code === 22 || e.code === 1014) {
|
||||
serializable.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function loadStatesFromLocalStorage() {
|
||||
@@ -59,11 +103,12 @@ export function loadStatesFromLocalStorage() {
|
||||
if (data) {
|
||||
const list = JSON.parse(data);
|
||||
list.forEach(state => {
|
||||
promptStates.set(state.pid, state);
|
||||
const key = String(state.pid);
|
||||
promptStates.set(key, state);
|
||||
if (state.timestamp > State.globalOrderCounter) State.globalOrderCounter = state.timestamp;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Comfy Sidebar: Failed to load state from localStorage", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
loadStatesFromLocalStorage();
|
||||
+49
-7
@@ -1,6 +1,7 @@
|
||||
// styles.js
|
||||
export function injectStyles() {
|
||||
if (document.getElementById("comfy-sidebar-styles")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "comfy-sidebar-styles";
|
||||
style.textContent = `
|
||||
.comfy-sidebar-card {
|
||||
background: var(--comfy-input-bg, #181818);
|
||||
@@ -35,26 +36,67 @@ export function injectStyles() {
|
||||
padding: 2px 4px; border-radius: 3px; pointer-events: none; z-index: 5; color: #fff;
|
||||
}
|
||||
|
||||
/* --- Custom Queue Cancel Button to Mimic ComfyUI Native Destructive Button --- */
|
||||
.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; /* Native deep red */
|
||||
color: #e2e8f0 !important; /* Native slate white icon */
|
||||
font-size: 11px !important; /* Muted native cross size */
|
||||
border-radius: 6px !important; /* Native corner rounding */
|
||||
background-color: #7f1d1d !important;
|
||||
color: #e2e8f0 !important;
|
||||
font-size: 11px !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;
|
||||
}
|
||||
|
||||
.comfy-sidebar-queue-cancel-btn:hover {
|
||||
background-color: #991b1b !important; /* Slightly brighter red on hover */
|
||||
background-color: #991b1b !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Hover Panels CSS Hover Control */
|
||||
.comfy-sidebar-hover-panel,
|
||||
.comfy-sidebar-left-hover-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.comfy-sidebar-card:hover .comfy-sidebar-hover-panel,
|
||||
.comfy-sidebar-card:hover .comfy-sidebar-left-hover-panel {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Card Action Buttons */
|
||||
.comfy-sidebar-card-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
background-color: rgba(0, 0, 0, 0.75) !important;
|
||||
color: #e2e8f0 !important;
|
||||
font-size: 14px !important;
|
||||
border-radius: 6px !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.15s ease !important;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
|
||||
z-index: 20 !important;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.comfy-sidebar-card-action-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.95) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Delete Confirmation Active State */
|
||||
.comfy-sidebar-card-action-btn.confirm-delete,
|
||||
.comfy-sidebar-card-action-btn.confirm-delete:hover {
|
||||
background-color: #dc3545 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 0 8px rgba(220, 53, 69, 0.6) !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { State, promptStates, cardElements, saveStatesToLocalStorage } from "./state.js";
|
||||
import { State, promptStates, cardElements, saveStatesToLocalStorage, deletePromptState } from "./state.js";
|
||||
import { isVideoFormat, matchesFilter, getRunOutputs } from "./utils.js";
|
||||
import { showFullscreenPreview } from "./comparison.js";
|
||||
|
||||
// DI Hook for cyclic imports
|
||||
export let syncQueueFn = async () => {};
|
||||
export function setSyncQueue(fn) { syncQueueFn = fn; }
|
||||
|
||||
// Resets any stale hover states when transitioning between submenus and the main queue
|
||||
function resetAllCardHoverStates() {
|
||||
for (const cardObj of cardElements.values()) {
|
||||
if (cardObj.hoverPanel) cardObj.hoverPanel.style.display = "none";
|
||||
@@ -16,12 +14,45 @@ function resetAllCardHoverStates() {
|
||||
}
|
||||
}
|
||||
|
||||
// Traverses node execution metadata to resolve the exact node ID that generated a specific image
|
||||
let scrollToTopBtnEl = null;
|
||||
|
||||
function getScrollContainer() {
|
||||
if (!State.cardStack) return null;
|
||||
return State.cardStack.closest('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|
||||
|| State.cardStack.parentElement
|
||||
|| State.cardStack;
|
||||
}
|
||||
|
||||
function updateScrollTopBtnVisibility() {
|
||||
if (!scrollToTopBtnEl || !State.sidebarContainer || !State.sidebarContainer.isConnected) return;
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) {
|
||||
const viewportEl = scrollEl.parentElement || State.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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("scroll", () => {
|
||||
if (State.sidebarContainer && State.sidebarContainer.isConnected) {
|
||||
updateScrollTopBtnVisibility();
|
||||
}
|
||||
}, { capture: true, passive: true });
|
||||
|
||||
function findNodeIdForImage(state, img) {
|
||||
if (!state || !state.nodeOutputs || !img) return null;
|
||||
for (const nodeId in state.nodeOutputs) {
|
||||
const out = state.nodeOutputs[nodeId];
|
||||
// Safely check both gifs, images, or videos under this specific node
|
||||
for (const key in out) {
|
||||
const val = out[key];
|
||||
if (Array.isArray(val)) {
|
||||
@@ -118,7 +149,7 @@ export function setupSidebarUI() {
|
||||
State.sidebarContainer = document.createElement("div");
|
||||
Object.assign(State.sidebarContainer.style, {
|
||||
display: "flex", flexDirection: "column", height: "100%", padding: "14px", boxSizing: "border-box",
|
||||
background: "var(--comfy-menu-bg, #121212)", color: "var(--fg-color, #eee)"
|
||||
background: "var(--comfy-menu-bg, #121212)", color: "var(--fg-color, #eee)", position: "relative"
|
||||
});
|
||||
|
||||
const header = document.createElement("div");
|
||||
@@ -145,7 +176,6 @@ export function setupSidebarUI() {
|
||||
titleGroup.appendChild(title);
|
||||
standardHeader.appendChild(titleGroup);
|
||||
|
||||
// Dynamic Compact Actions Group
|
||||
const actionsGroup = document.createElement("div");
|
||||
Object.assign(actionsGroup.style, { display: "flex", gap: "6px", alignItems: "center" });
|
||||
|
||||
@@ -173,7 +203,7 @@ export function setupSidebarUI() {
|
||||
if (!isPending) {
|
||||
isPending = true;
|
||||
Object.assign(btn.style, { color: "#fff", background: hoverColor, borderColor: hoverColor, boxShadow: `0 0 8px ${hoverColor}80` });
|
||||
timeout = setTimeout(reset, 1500); // 1.5s to confirm
|
||||
timeout = setTimeout(reset, 1500);
|
||||
} else {
|
||||
reset();
|
||||
await onClickFn();
|
||||
@@ -192,6 +222,7 @@ export function setupSidebarUI() {
|
||||
}
|
||||
if (toDelete.length > 0) {
|
||||
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }); } catch (err) {}
|
||||
saveStatesToLocalStorage();
|
||||
renderDOM();
|
||||
}
|
||||
});
|
||||
@@ -202,7 +233,8 @@ export function setupSidebarUI() {
|
||||
promptStates.delete(pid);
|
||||
}
|
||||
}
|
||||
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clear: true }) }); } catch (err) {}
|
||||
try { await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ clear: true }) }); } catch (err) {}
|
||||
saveStatesToLocalStorage();
|
||||
renderDOM();
|
||||
});
|
||||
|
||||
@@ -234,17 +266,49 @@ export function setupSidebarUI() {
|
||||
searchInput.oninput = () => { State.currentSearchQuery = searchInput.value.trim(); renderDOM(); };
|
||||
|
||||
State.cardStack = document.createElement("div");
|
||||
Object.assign(State.cardStack.style, { flex: "1", overflowY: "auto", scrollbarWidth: "thin", display: "block" });
|
||||
Object.assign(State.cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" });
|
||||
State.sidebarContainer.appendChild(State.cardStack);
|
||||
|
||||
// Global background click handler: return to main queue if clicking empty space of the entire container
|
||||
scrollToTopBtnEl = document.createElement("button");
|
||||
scrollToTopBtnEl.className = "pi pi-chevron-up";
|
||||
scrollToTopBtnEl.title = "Scroll to Top";
|
||||
Object.assign(scrollToTopBtnEl.style, {
|
||||
position: "absolute", bottom: "12px", right: "12px", width: "32px", height: "32px",
|
||||
borderRadius: "50%", background: "rgba(30, 30, 30, 0.9)", color: "#eee",
|
||||
border: "1px solid var(--border-color, #555)", cursor: "pointer", fontSize: "13px",
|
||||
display: "none", alignItems: "center", justifyContent: "center", zIndex: "999",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.6)", transition: "all 0.15s ease-in-out", opacity: "0.85"
|
||||
});
|
||||
|
||||
scrollToTopBtnEl.onmouseenter = () => {
|
||||
scrollToTopBtnEl.style.opacity = "1";
|
||||
scrollToTopBtnEl.style.background = "var(--p-primary-color, #3b82f6)";
|
||||
scrollToTopBtnEl.style.color = "#fff";
|
||||
scrollToTopBtnEl.style.borderColor = "var(--p-primary-color, #3b82f6)";
|
||||
};
|
||||
scrollToTopBtnEl.onmouseleave = () => {
|
||||
scrollToTopBtnEl.style.opacity = "0.85";
|
||||
scrollToTopBtnEl.style.background = "rgba(30, 30, 30, 0.9)";
|
||||
scrollToTopBtnEl.style.color = "#eee";
|
||||
scrollToTopBtnEl.style.borderColor = "var(--border-color, #555)";
|
||||
};
|
||||
|
||||
scrollToTopBtnEl.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) {
|
||||
scrollEl.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
State.sidebarContainer.appendChild(scrollToTopBtnEl);
|
||||
|
||||
State.sidebarContainer.onclick = (e) => {
|
||||
if (State.activeSubmenuPromptId || State.activeSubmenuBatchImages) {
|
||||
// Stop closure if we clicked on standard interactive elements (images, buttons, spans, cancels)
|
||||
if (e.target.closest('img, video, .comfy-sidebar-card-timer, .pi-times, .comfy-sidebar-left-hover-btn, .comfy-sidebar-queue-cancel-btn, button, span')) return;
|
||||
State.activeSubmenuPromptId = null;
|
||||
State.activeSubmenuBatchImages = null;
|
||||
resetAllCardHoverStates(); // Instantly wipe stale hover indicators on exit click
|
||||
resetAllCardHoverStates();
|
||||
renderDOM();
|
||||
document.removeEventListener("click", handleGlobalClick, true);
|
||||
globalClickRegistered = false;
|
||||
@@ -254,23 +318,108 @@ export function setupSidebarUI() {
|
||||
new ResizeObserver((entries) => {
|
||||
const threshold = app.ui.settings.getSettingValue("Comfy Sidebar.Grid Columns Threshold") ?? 350;
|
||||
const cols = Math.max(1, Math.floor(entries[0].contentRect.width / (threshold / 2)));
|
||||
State.cardStack.style.columnCount = cols.toString();
|
||||
State.cardStack.style.columnGap = cols > 1 ? "12px" : "0";
|
||||
if (State.cardStack) {
|
||||
State.cardStack.style.columnCount = cols.toString();
|
||||
State.cardStack.style.columnGap = cols > 1 ? "12px" : "0";
|
||||
}
|
||||
}).observe(State.sidebarContainer);
|
||||
|
||||
setInterval(() => {
|
||||
if (State.currentlyActivePromptId === null) return;
|
||||
for (const [pid, state] of promptStates.entries()) {
|
||||
if (state.status === "active" && state.startTime) {
|
||||
const cardObj = cardElements.get(pid);
|
||||
if (cardObj && cardObj.timerEl) cardObj.timerEl.textContent = ((Date.now() - state.startTime) / 1000).toFixed(2) + "s";
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}, 250);
|
||||
|
||||
return State.sidebarContainer;
|
||||
}
|
||||
|
||||
// Unified image batch pagination renderer (supports standard queue cards & both explorer submenus)
|
||||
function syncCardButtonVisibility(cardObj, state) {
|
||||
if (!cardObj) return;
|
||||
|
||||
const isCompleted = state.status === "completed";
|
||||
const hasRealImages = isCompleted && state.images && state.images.length > 0 && !state.images.some(img => img.url && img.url.startsWith("blob:"));
|
||||
|
||||
if (cardObj.btnImg) {
|
||||
if (hasRealImages) {
|
||||
cardObj.btnImg.style.removeProperty("display");
|
||||
cardObj.btnImg.style.display = "inline-flex";
|
||||
cardObj.btnImg.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
state.images.forEach(img => {
|
||||
const a = document.createElement("a");
|
||||
a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
a.download = img.filename || "output";
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
} else {
|
||||
cardObj.btnImg.style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
|
||||
if (cardObj.btnJson) {
|
||||
if (state.workflow) {
|
||||
cardObj.btnJson.style.removeProperty("display");
|
||||
cardObj.btnJson.style.display = "inline-flex";
|
||||
cardObj.btnJson.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const blob = new Blob([JSON.stringify(state.workflow, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `workflow_${state.pid}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
} else {
|
||||
cardObj.btnJson.style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
|
||||
if (cardObj.btnFocus) {
|
||||
const currentImg = hasRealImages ? state.images[cardObj.currentImageIndex || 0] : null;
|
||||
const nodeId = currentImg ? findNodeIdForImage(state, currentImg) : null;
|
||||
if (nodeId) {
|
||||
cardObj.btnFocus.style.removeProperty("display");
|
||||
cardObj.btnFocus.style.display = "inline-flex";
|
||||
cardObj.btnFocus.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const node = app.graph.getNodeById(Number(nodeId));
|
||||
if (node) {
|
||||
app.canvas.centerOnNode(node);
|
||||
app.canvas.selectNode(node);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
cardObj.btnFocus.style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
|
||||
if (cardObj.leftHoverBtn) {
|
||||
const outputs = getRunOutputs(state.nodeOutputs, state.workflow);
|
||||
if (outputs.length > 1) {
|
||||
cardObj.leftHoverBtn.style.removeProperty("display");
|
||||
cardObj.leftHoverBtn.style.display = "inline-flex";
|
||||
cardObj.leftHoverBtn.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!State.activeSubmenuBatchImages && !State.activeSubmenuPromptId) {
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) State.mainQueueScrollTop = scrollEl.scrollTop;
|
||||
}
|
||||
State.activeSubmenuPromptId = state.pid;
|
||||
resetAllCardHoverStates();
|
||||
renderDOM();
|
||||
};
|
||||
} else {
|
||||
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderCardImages(cardObj, state, keepAspect) {
|
||||
cardObj.currentImageIndex = cardObj.currentImageIndex || 0;
|
||||
if (cardObj.currentImageIndex >= state.images.length) {
|
||||
@@ -289,7 +438,6 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const isVideo = isVideoFormat(src);
|
||||
|
||||
// Find or create the native wrapper
|
||||
let wrapper = cardObj.grid.querySelector(".comfy-sidebar-media-wrapper");
|
||||
if (!wrapper) {
|
||||
cardObj.grid.innerHTML = "";
|
||||
@@ -299,7 +447,6 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
cardObj.grid.appendChild(wrapper);
|
||||
}
|
||||
|
||||
// Find or create the media tag in-place
|
||||
let mediaEl = wrapper.querySelector("img, video");
|
||||
const needsRebuild = !mediaEl || (isVideo !== (mediaEl.tagName.toLowerCase() === "video"));
|
||||
|
||||
@@ -319,13 +466,20 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
|
||||
cardObj.firstImgElement = mediaEl;
|
||||
|
||||
// Binds interactive event listeners. Passes shiftKey context natively to support split side-loading
|
||||
mediaEl.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
showFullscreenPreview([src], ev.shiftKey);
|
||||
};
|
||||
|
||||
// Shared dimensions layout compiler
|
||||
// Auto-prune card if file was deleted by startup temp cleanup script
|
||||
mediaEl.onerror = () => {
|
||||
if (src && !src.startsWith("blob:")) {
|
||||
deletePromptState(state.pid);
|
||||
saveStatesToLocalStorage();
|
||||
if (cardObj.element) cardObj.element.remove();
|
||||
}
|
||||
};
|
||||
|
||||
const applyDimensions = (width, height) => {
|
||||
if (keepAspect && width && height) {
|
||||
mediaEl.style.aspectRatio = `${width} / ${height}`;
|
||||
@@ -336,16 +490,58 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
}
|
||||
};
|
||||
|
||||
const isUnfinished = state.status && state.status !== "completed";
|
||||
if (isUnfinished) {
|
||||
mediaEl.setAttribute("draggable", "false");
|
||||
mediaEl.style.cursor = "grab";
|
||||
if (mediaEl._currentDragStart) {
|
||||
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
|
||||
delete mediaEl._currentDragStart;
|
||||
}
|
||||
} else {
|
||||
mediaEl.setAttribute("draggable", "true");
|
||||
mediaEl.style.cursor = "zoom-in";
|
||||
|
||||
if (isVideo) {
|
||||
const fullSrc = src.startsWith("http") ? src : window.location.origin + src;
|
||||
const filename = img.filename || "output.mp4";
|
||||
const mimeType = src.includes(".webm") ? "video/webm" : "video/mp4";
|
||||
|
||||
const videoDragHandler = (e) => {
|
||||
try {
|
||||
e.dataTransfer.setData("text/uri-list", fullSrc);
|
||||
e.dataTransfer.setData("text/plain", fullSrc);
|
||||
e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullSrc}`);
|
||||
} catch (err) {}
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
if (mediaEl._currentDragStart) {
|
||||
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
|
||||
}
|
||||
mediaEl.addEventListener("dragstart", videoDragHandler);
|
||||
mediaEl._currentDragStart = videoDragHandler;
|
||||
} else {
|
||||
if (mediaEl._currentDragStart) {
|
||||
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
|
||||
delete mediaEl._currentDragStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
mediaEl.muted = true;
|
||||
mediaEl.playsInline = true;
|
||||
mediaEl.preload = "metadata";
|
||||
mediaEl.loop = true;
|
||||
mediaEl.setAttribute("draggable", "false");
|
||||
|
||||
mediaEl.onloadedmetadata = () => {
|
||||
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
|
||||
};
|
||||
if (mediaEl.readyState >= 1) {
|
||||
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
|
||||
}
|
||||
|
||||
let playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
|
||||
if (!playIcon) {
|
||||
@@ -367,29 +563,21 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
mediaEl.onmouseenter = () => { playIcon.style.opacity = "0"; mediaEl.play().catch(()=>{}); };
|
||||
mediaEl.onmouseleave = () => { playIcon.style.opacity = "1"; mediaEl.pause(); };
|
||||
} else {
|
||||
mediaEl.setAttribute("draggable", "true");
|
||||
mediaEl.onload = () => {
|
||||
applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
|
||||
if (state._oldPreviewBlobUrl) {
|
||||
try { URL.revokeObjectURL(state._oldPreviewBlobUrl); } catch(e){}
|
||||
delete state._oldPreviewBlobUrl;
|
||||
}
|
||||
};
|
||||
if (mediaEl.complete && mediaEl.naturalWidth) {
|
||||
applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
|
||||
}
|
||||
|
||||
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
|
||||
if (playIcon) playIcon.remove();
|
||||
|
||||
const dragStartHandler = (e) => {
|
||||
State.currentDraggedImgData = { ...img, workflow: state.workflow };
|
||||
try {
|
||||
e.dataTransfer.setData("text/uri-list", src);
|
||||
e.dataTransfer.setData("text/plain", src);
|
||||
} catch (err) {}
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
e.stopPropagation();
|
||||
};
|
||||
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
|
||||
mediaEl.addEventListener("dragstart", dragStartHandler);
|
||||
mediaEl._currentDragStart = dragStartHandler;
|
||||
}
|
||||
|
||||
// Smooth in-memory source swapping for blob live previews
|
||||
const currentSrc = mediaEl.getAttribute("src") || mediaEl.src;
|
||||
if (currentSrc !== src && currentSrc !== (currentSrc + "#t=0.001")) {
|
||||
if (src.startsWith("blob:") && !isVideo) {
|
||||
@@ -399,7 +587,7 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
mediaEl.src = src;
|
||||
mediaEl._lastBlob = src;
|
||||
applyDimensions(tempImg.naturalWidth, tempImg.naturalHeight);
|
||||
if (oldBlob && oldBlob !== src) {
|
||||
if (oldBlob && oldBlob !== src && oldBlob.startsWith("blob:")) {
|
||||
try { URL.revokeObjectURL(oldBlob); } catch(e){}
|
||||
}
|
||||
};
|
||||
@@ -408,7 +596,6 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
mediaEl.src = isVideo ? src + "#t=0.001" : src;
|
||||
}
|
||||
} else {
|
||||
// If the source is already correct, make sure we still force dimensions calculations
|
||||
if (isVideo) {
|
||||
if (mediaEl.videoWidth) applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
|
||||
} else {
|
||||
@@ -416,7 +603,6 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync batch pagination controller overlay
|
||||
let navBar = wrapper.querySelector(".comfy-sidebar-batch-navbar");
|
||||
if (state.images.length > 1) {
|
||||
if (!navBar) {
|
||||
@@ -445,15 +631,19 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
label.style.cursor = "pointer";
|
||||
label.title = "View all images in this batch";
|
||||
|
||||
// Open the dynamic batch explorer explorer view when the middle counter label is clicked
|
||||
label.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!State.activeSubmenuBatchImages && !State.activeSubmenuPromptId) {
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) State.mainQueueScrollTop = scrollEl.scrollTop;
|
||||
}
|
||||
State.activeSubmenuBatchImages = {
|
||||
pid: state.pid || cardObj.element.id.replace("card-", ""),
|
||||
images: state.images,
|
||||
workflow: state.workflow
|
||||
workflow: state.workflow,
|
||||
nodeOutputs: state.nodeOutputs
|
||||
};
|
||||
resetAllCardHoverStates(); // Prevent ghost hover handles
|
||||
resetAllCardHoverStates();
|
||||
renderDOM();
|
||||
};
|
||||
|
||||
@@ -479,7 +669,6 @@ function renderCardImages(cardObj, state, keepAspect) {
|
||||
|
||||
let globalClickRegistered = false;
|
||||
|
||||
// Global document mousedown-capturer to reliably trigger click-aways outside the sidebar
|
||||
const handleGlobalClick = (e) => {
|
||||
if (!State.activeSubmenuPromptId && !State.activeSubmenuBatchImages) {
|
||||
document.removeEventListener("click", handleGlobalClick, true);
|
||||
@@ -493,7 +682,7 @@ const handleGlobalClick = (e) => {
|
||||
if (!clickedInsideSidebar && !clickedFullscreenOverlay) {
|
||||
State.activeSubmenuPromptId = null;
|
||||
State.activeSubmenuBatchImages = null;
|
||||
resetAllCardHoverStates(); // Reset stale overlays when clicking away
|
||||
resetAllCardHoverStates();
|
||||
renderDOM();
|
||||
document.removeEventListener("click", handleGlobalClick, true);
|
||||
globalClickRegistered = false;
|
||||
@@ -512,26 +701,7 @@ export function renderDOM() {
|
||||
const headerSearchIcon = State.sidebarContainer.querySelector(".pi-search");
|
||||
const headerActions = State.sidebarContainer.querySelector(".pi-eraser")?.parentNode;
|
||||
|
||||
const styleActionBtn = (btn) => {
|
||||
Object.assign(btn.style, {
|
||||
display: "inline-flex",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
width: "32px", height: "32px",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.75)",
|
||||
color: "#e2e8f0",
|
||||
fontSize: "14px",
|
||||
borderRadius: "6px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.15s ease",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.3)",
|
||||
zIndex: "20",
|
||||
transform: "translateZ(0)" // Forces a GPU compositor stacking context to prevent video clipping
|
||||
});
|
||||
btn.onmouseenter = () => { btn.style.backgroundColor = "rgba(0, 0, 0, 0.95)"; btn.style.color = "#fff"; };
|
||||
btn.onmouseleave = () => { btn.style.backgroundColor = "rgba(0, 0, 0, 0.75)"; btn.style.color = "#e2e8f0"; };
|
||||
};
|
||||
|
||||
// --- SUBMENU 2: BATCH IMAGES EXPLORER ---
|
||||
// Submenu 1: Batch Submenu View
|
||||
if (State.activeSubmenuBatchImages) {
|
||||
const batchInfo = State.activeSubmenuBatchImages;
|
||||
|
||||
@@ -539,7 +709,11 @@ export function renderDOM() {
|
||||
headerTitle.textContent = `Batch of #${batchInfo.pid}`;
|
||||
headerTitle.style.cursor = "pointer";
|
||||
headerTitle.title = "Go Back to Queue";
|
||||
headerTitle.onclick = () => { State.activeSubmenuBatchImages = null; resetAllCardHoverStates(); renderDOM(); };
|
||||
headerTitle.onclick = () => {
|
||||
State.activeSubmenuBatchImages = null;
|
||||
resetAllCardHoverStates();
|
||||
renderDOM();
|
||||
};
|
||||
}
|
||||
if (headerSearchIcon) headerSearchIcon.style.display = "none";
|
||||
if (headerActions) headerActions.style.display = "none";
|
||||
@@ -582,30 +756,28 @@ export function renderDOM() {
|
||||
fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px", userSelect: "none"
|
||||
});
|
||||
|
||||
// Symmetrical hover panel on the right with ONLY the Download Object button
|
||||
const hoverPanel = document.createElement("div");
|
||||
hoverPanel.className = "comfy-sidebar-hover-panel";
|
||||
Object.assign(hoverPanel.style, {
|
||||
position: "absolute", bottom: "4px", right: "4px", display: "none",
|
||||
position: "absolute", bottom: "4px", right: "4px",
|
||||
flexDirection: "column", gap: "4px", zIndex: "20"
|
||||
});
|
||||
|
||||
const btnImg = document.createElement("span");
|
||||
btnImg.className = "pi pi-image";
|
||||
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
|
||||
btnImg.title = "Download Object";
|
||||
styleActionBtn(btnImg);
|
||||
hoverPanel.appendChild(btnImg);
|
||||
|
||||
// Symmetrical left hover panel containing ONLY btnFocus
|
||||
const leftHoverPanel = document.createElement("div");
|
||||
leftHoverPanel.className = "comfy-sidebar-left-hover-panel";
|
||||
Object.assign(leftHoverPanel.style, {
|
||||
position: "absolute", bottom: "4px", left: "4px", display: "none",
|
||||
position: "absolute", bottom: "4px", left: "4px",
|
||||
flexDirection: "column", gap: "4px", zIndex: "20"
|
||||
});
|
||||
|
||||
const btnFocus = document.createElement("span");
|
||||
btnFocus.className = "pi pi-eye";
|
||||
btnFocus.title = "Show Node"; // Renamed
|
||||
styleActionBtn(btnFocus);
|
||||
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
|
||||
btnFocus.title = "Show Node";
|
||||
leftHoverPanel.appendChild(btnFocus);
|
||||
|
||||
card.append(timerEl, dimEl, grid, p, hoverPanel, leftHoverPanel);
|
||||
@@ -613,53 +785,27 @@ export function renderDOM() {
|
||||
cardElements.set(cardId, cardObj);
|
||||
|
||||
card.addEventListener("mouseenter", () => {
|
||||
cardObj.hoverPanel.style.display = "flex";
|
||||
cardObj.leftHoverPanel.style.display = "flex";
|
||||
});
|
||||
card.addEventListener("mouseleave", () => {
|
||||
cardObj.hoverPanel.style.display = "none";
|
||||
cardObj.leftHoverPanel.style.display = "none";
|
||||
syncCardButtonVisibility(cardObj, { status: "completed", images: [img], workflow: batchInfo.workflow, nodeOutputs: batchInfo.nodeOutputs });
|
||||
});
|
||||
}
|
||||
|
||||
cardObj.placeholder.style.display = "none";
|
||||
|
||||
cardObj.btnImg.style.display = "inline-flex";
|
||||
cardObj.btnImg.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const a = document.createElement("a");
|
||||
a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`;
|
||||
a.download = img.filename || "output";
|
||||
a.click();
|
||||
};
|
||||
|
||||
// Focus/center target node inside Batch Explorer Submenu
|
||||
const nodeId = findNodeIdForImage(batchInfo, img);
|
||||
if (nodeId) {
|
||||
cardObj.btnFocus.style.display = "inline-flex";
|
||||
cardObj.btnFocus.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const node = app.graph.getNodeById(Number(nodeId));
|
||||
if (node) {
|
||||
app.canvas.centerOnNode(node);
|
||||
app.canvas.selectNode(node);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
cardObj.btnFocus.style.display = "none";
|
||||
}
|
||||
|
||||
renderCardImages(cardObj, { pid: batchInfo.pid, images: [img], workflow: batchInfo.workflow }, keepAspect);
|
||||
renderCardImages(cardObj, { pid: batchInfo.pid, images: [img], workflow: batchInfo.workflow, nodeOutputs: batchInfo.nodeOutputs }, keepAspect);
|
||||
|
||||
targetElements.push(cardObj.element);
|
||||
});
|
||||
|
||||
targetElements.forEach((el, index) => { if (State.cardStack.children[index] !== el) State.cardStack.insertBefore(el, State.cardStack.children[index] || null); });
|
||||
while (State.cardStack.children.length > targetElements.length) State.cardStack.removeChild(State.cardStack.lastChild);
|
||||
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) scrollEl.scrollTop = 0;
|
||||
updateScrollTopBtnVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SUBMENU 1: WORKFLOW OUTPUTS EXPLORER ---
|
||||
// Submenu 2: Intermediate Outputs View
|
||||
if (State.activeSubmenuPromptId) {
|
||||
const st = promptStates.get(State.activeSubmenuPromptId);
|
||||
if (!st) {
|
||||
@@ -668,12 +814,15 @@ export function renderDOM() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync Header UI State for the explorer view
|
||||
if (headerTitle) {
|
||||
headerTitle.textContent = `Outputs of #${State.activeSubmenuPromptId}`;
|
||||
headerTitle.style.cursor = "pointer";
|
||||
headerTitle.title = "Go Back to Queue";
|
||||
headerTitle.onclick = () => { State.activeSubmenuPromptId = null; resetAllCardHoverStates(); renderDOM(); };
|
||||
headerTitle.onclick = () => {
|
||||
State.activeSubmenuPromptId = null;
|
||||
resetAllCardHoverStates();
|
||||
renderDOM();
|
||||
};
|
||||
}
|
||||
if (headerSearchIcon) headerSearchIcon.style.display = "none";
|
||||
if (headerActions) headerActions.style.display = "none";
|
||||
@@ -683,7 +832,7 @@ export function renderDOM() {
|
||||
globalClickRegistered = true;
|
||||
}
|
||||
|
||||
const outputs = getRunOutputs(st.nodeOutputs);
|
||||
const outputs = getRunOutputs(st.nodeOutputs, st.workflow);
|
||||
const targetElements = [];
|
||||
|
||||
outputs.forEach((out) => {
|
||||
@@ -718,30 +867,28 @@ export function renderDOM() {
|
||||
fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px", userSelect: "none"
|
||||
});
|
||||
|
||||
// Action overlay on hover containing only Download Object
|
||||
const hoverPanel = document.createElement("div");
|
||||
hoverPanel.className = "comfy-sidebar-hover-panel";
|
||||
Object.assign(hoverPanel.style, {
|
||||
position: "absolute", bottom: "4px", right: "4px", display: "none",
|
||||
position: "absolute", bottom: "4px", right: "4px",
|
||||
flexDirection: "column", gap: "4px", zIndex: "20"
|
||||
});
|
||||
|
||||
const btnImg = document.createElement("span");
|
||||
btnImg.className = "pi pi-image";
|
||||
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
|
||||
btnImg.title = "Download Object";
|
||||
styleActionBtn(btnImg);
|
||||
hoverPanel.appendChild(btnImg);
|
||||
|
||||
// Symmetrical left hover panel containing ONLY btnFocus
|
||||
const leftHoverPanel = document.createElement("div");
|
||||
leftHoverPanel.className = "comfy-sidebar-left-hover-panel";
|
||||
Object.assign(leftHoverPanel.style, {
|
||||
position: "absolute", bottom: "4px", left: "4px", display: "none",
|
||||
position: "absolute", bottom: "4px", left: "4px",
|
||||
flexDirection: "column", gap: "4px", zIndex: "20"
|
||||
});
|
||||
|
||||
const btnFocus = document.createElement("span");
|
||||
btnFocus.className = "pi pi-eye";
|
||||
btnFocus.title = "Show Node"; // Renamed
|
||||
styleActionBtn(btnFocus);
|
||||
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
|
||||
btnFocus.title = "Show Node";
|
||||
leftHoverPanel.appendChild(btnFocus);
|
||||
|
||||
card.append(timerEl, dimEl, grid, p, hoverPanel, leftHoverPanel);
|
||||
@@ -749,42 +896,14 @@ export function renderDOM() {
|
||||
cardElements.set(cardId, cardObj);
|
||||
|
||||
card.addEventListener("mouseenter", () => {
|
||||
cardObj.hoverPanel.style.display = "flex";
|
||||
cardObj.leftHoverPanel.style.display = "flex";
|
||||
});
|
||||
card.addEventListener("mouseleave", () => {
|
||||
cardObj.hoverPanel.style.display = "none";
|
||||
cardObj.leftHoverPanel.style.display = "none";
|
||||
syncCardButtonVisibility(cardObj, { status: "completed", images: out.images, workflow: st.workflow, nodeOutputs: st.nodeOutputs });
|
||||
});
|
||||
}
|
||||
|
||||
if (out.images && out.images.length > 0) {
|
||||
cardObj.placeholder.style.display = "none";
|
||||
|
||||
cardObj.btnImg.style.display = "inline-flex";
|
||||
cardObj.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}&subfolder=${encodeURIComponent(img.subfolder)}`;
|
||||
a.download = img.filename || "output";
|
||||
a.click();
|
||||
});
|
||||
};
|
||||
|
||||
// Focus/center target node inside Workflow outputs Submenu
|
||||
cardObj.btnFocus.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const node = app.graph.getNodeById(Number(out.nodeId));
|
||||
if (node) {
|
||||
app.canvas.centerOnNode(node);
|
||||
app.canvas.selectNode(node);
|
||||
}
|
||||
};
|
||||
|
||||
renderCardImages(cardObj, { pid: st.pid, images: out.images, workflow: st.workflow }, keepAspect);
|
||||
renderCardImages(cardObj, { pid: st.pid, images: out.images, workflow: st.workflow, nodeOutputs: st.nodeOutputs }, keepAspect);
|
||||
} else {
|
||||
cardObj.btnImg.style.display = "none";
|
||||
cardObj.placeholder.style.display = "block";
|
||||
cardObj.placeholder.textContent = "No Outputs";
|
||||
}
|
||||
@@ -794,10 +913,14 @@ export function renderDOM() {
|
||||
|
||||
targetElements.forEach((el, index) => { if (State.cardStack.children[index] !== el) State.cardStack.insertBefore(el, State.cardStack.children[index] || null); });
|
||||
while (State.cardStack.children.length > targetElements.length) State.cardStack.removeChild(State.cardStack.lastChild);
|
||||
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) scrollEl.scrollTop = 0;
|
||||
updateScrollTopBtnVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- STANDARD QUEUE RENDER LOGIC ---
|
||||
// Standard Queue Main View
|
||||
if (headerTitle) {
|
||||
headerTitle.textContent = "Queue";
|
||||
headerTitle.style.cursor = "default";
|
||||
@@ -818,7 +941,10 @@ export function renderDOM() {
|
||||
const syncCardElement = (state) => {
|
||||
let cardObj = cardElements.get(state.pid);
|
||||
const isFinalStatus = state.status === "completed" || state.status === "cancelled" || state.status === "error";
|
||||
if (cardObj && isFinalStatus && state.rendered) return cardObj.element;
|
||||
if (cardObj && isFinalStatus && state.rendered) {
|
||||
syncCardButtonVisibility(cardObj, state);
|
||||
return cardObj.element;
|
||||
}
|
||||
|
||||
if (!cardObj) {
|
||||
const card = document.createElement("div");
|
||||
@@ -845,40 +971,31 @@ export function renderDOM() {
|
||||
pt.appendChild(pb);
|
||||
const statusText = document.createElement("div"); Object.assign(statusText.style, { fontSize: "11px", opacity: "0.9", color: "#3b82f6", textAlign: "center", marginTop: "6px", display: "none", fontWeight: "bold" });
|
||||
|
||||
// Absolute Vertical Actions Panel on the right of the card
|
||||
const hoverPanel = document.createElement("div");
|
||||
hoverPanel.className = "comfy-sidebar-hover-panel";
|
||||
Object.assign(hoverPanel.style, {
|
||||
position: "absolute", bottom: "4px", right: "4px", display: "none",
|
||||
position: "absolute", bottom: "4px", right: "4px",
|
||||
flexDirection: "column", gap: "4px", zIndex: "20"
|
||||
});
|
||||
|
||||
const btnImg = document.createElement("span"); btnImg.className = "pi pi-image"; btnImg.title = "Download Object"; styleActionBtn(btnImg);
|
||||
const btnJson = document.createElement("span"); btnJson.className = "pi pi-file"; btnJson.title = "Download JSON"; styleActionBtn(btnJson);
|
||||
const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash"; btnDel.title = "Delete Card"; styleActionBtn(btnDel);
|
||||
const btnImg = document.createElement("span"); btnImg.className = "pi pi-image comfy-sidebar-card-action-btn"; btnImg.title = "Download Object";
|
||||
const btnJson = document.createElement("span"); btnJson.className = "pi pi-file comfy-sidebar-card-action-btn"; btnJson.title = "Download JSON";
|
||||
const btnDel = document.createElement("span"); btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn"; btnDel.title = "Delete Card";
|
||||
|
||||
// Absolute Vertical Left Actions Panel containing btnFocus and leftHoverBtn
|
||||
const leftHoverPanel = document.createElement("div");
|
||||
leftHoverPanel.className = "comfy-sidebar-left-hover-panel";
|
||||
Object.assign(leftHoverPanel.style, {
|
||||
position: "absolute", bottom: "4px", left: "4px", display: "none",
|
||||
position: "absolute", bottom: "4px", left: "4px",
|
||||
flexDirection: "column", gap: "4px", zIndex: "20"
|
||||
});
|
||||
|
||||
const btnFocus = document.createElement("span");
|
||||
btnFocus.className = "pi pi-eye";
|
||||
btnFocus.title = "Show Node"; // Renamed
|
||||
styleActionBtn(btnFocus);
|
||||
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
|
||||
btnFocus.title = "Show Node";
|
||||
|
||||
const leftHoverBtn = document.createElement("span");
|
||||
leftHoverBtn.className = "pi pi-images";
|
||||
leftHoverBtn.className = "pi pi-images comfy-sidebar-card-action-btn";
|
||||
leftHoverBtn.title = "View all intermediate outputs";
|
||||
styleActionBtn(leftHoverBtn);
|
||||
|
||||
leftHoverBtn.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
State.activeSubmenuPromptId = state.pid;
|
||||
resetAllCardHoverStates(); // Reset standard sidebar hover states before opening submenu
|
||||
renderDOM();
|
||||
};
|
||||
|
||||
hoverPanel.append(btnImg, btnJson, btnDel);
|
||||
leftHoverPanel.append(btnFocus, leftHoverBtn);
|
||||
@@ -888,64 +1005,38 @@ export function renderDOM() {
|
||||
cardObj = { element: card, timerEl, statusBadge: sBadge, grid, placeholder: p, progressContainer: pt, progressBar: pb, cancelBtn: cancelX, hoverPanel, leftHoverPanel, btnFocus, leftHoverBtn, btnImg, btnJson, btnDel, statusText, firstImgElement: null, lastImagesSignature: "" };
|
||||
|
||||
card.addEventListener("mouseenter", () => {
|
||||
if (state.status !== "active" && state.status !== "pending") {
|
||||
cardObj.hoverPanel.style.display = "flex";
|
||||
cardObj.leftHoverPanel.style.display = "flex";
|
||||
|
||||
// Focus/Center the exact node that generated the currently active displayed image
|
||||
const currentImg = state.images[cardObj.currentImageIndex || 0];
|
||||
const nodeId = findNodeIdForImage(state, currentImg);
|
||||
if (nodeId) {
|
||||
cardObj.btnFocus.style.display = "inline-flex";
|
||||
cardObj.btnFocus.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const node = app.graph.getNodeById(Number(nodeId));
|
||||
if (node) {
|
||||
app.canvas.centerOnNode(node);
|
||||
app.canvas.selectNode(node);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
cardObj.btnFocus.style.display = "none";
|
||||
}
|
||||
|
||||
// Show explorer view button if this run yielded multiple node outputs
|
||||
const outputs = getRunOutputs(state.nodeOutputs);
|
||||
if (outputs.length > 1) {
|
||||
cardObj.leftHoverBtn.style.display = "inline-flex";
|
||||
} else {
|
||||
cardObj.leftHoverBtn.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
card.addEventListener("mouseleave", () => {
|
||||
cardObj.hoverPanel.style.display = "none";
|
||||
cardObj.leftHoverPanel.style.display = "none";
|
||||
syncCardButtonVisibility(cardObj, state);
|
||||
});
|
||||
|
||||
card.addEventListener("dragstart", (e) => {
|
||||
// Custom dragstart is only used for JSON-only workflows to supply a local DownloadURL.
|
||||
// For image cards, dragging is handled natively by the browser on the child <img> tag.
|
||||
if (state.workflow && (!state.images || state.images.length === 0)) {
|
||||
if (cardObj.firstImgElement) e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15);
|
||||
const isUnfinished = state.status && state.status !== "completed";
|
||||
const hasNoImages = !state.images || state.images.length === 0;
|
||||
|
||||
if (state.workflow && (hasNoImages || isUnfinished)) {
|
||||
if (cardObj.firstImgElement) {
|
||||
try { e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15); } catch(err){}
|
||||
}
|
||||
const jsonStr = JSON.stringify(state.workflow, null, 2);
|
||||
const filename = `workflow_${state.pid}.json`;
|
||||
const base64Data = btoa(unescape(encodeURIComponent(jsonStr)));
|
||||
|
||||
try {
|
||||
e.dataTransfer.setData("DownloadURL", `application/json:workflow_${state.pid}.json:data:application/json;base64,` + btoa(unescape(encodeURIComponent(jsonStr))));
|
||||
e.dataTransfer.setData("text/plain", jsonStr);
|
||||
e.dataTransfer.setData("DownloadURL", `application/json:${filename}:data:application/json;base64,${base64Data}`);
|
||||
e.dataTransfer.setData("application/json", jsonStr);
|
||||
} catch (err) {}
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
}
|
||||
});
|
||||
cardObj.element.id = `card-${state.pid}`; // set ID helper
|
||||
cardObj.element.id = `card-${state.pid}`;
|
||||
cardElements.set(state.pid, cardObj);
|
||||
}
|
||||
|
||||
syncCardButtonVisibility(cardObj, state);
|
||||
|
||||
cardObj.element.className = `comfy-sidebar-card ${state.status}`;
|
||||
|
||||
// Native drag isolation: completely remove draggable flags on image containers.
|
||||
// This exposes the child <img> tag directly to Chrome's native filesystem drag APIs.
|
||||
if (state.images && state.images.length > 0) {
|
||||
const isUnfinished = state.status && state.status !== "completed";
|
||||
if (state.images && state.images.length > 0 && !isUnfinished) {
|
||||
cardObj.element.removeAttribute("draggable");
|
||||
} else {
|
||||
cardObj.element.setAttribute("draggable", "true");
|
||||
@@ -975,30 +1066,17 @@ export function renderDOM() {
|
||||
cardObj.cancelBtn.style.display = "flex";
|
||||
cardObj.cancelBtn.onclick = async (ev) => {
|
||||
ev.stopPropagation();
|
||||
await api.interrupt(); // Natively interrupts current execution
|
||||
await api.interrupt();
|
||||
await syncQueueFn();
|
||||
};
|
||||
} else {
|
||||
cardObj.cancelBtn.style.display = "none";
|
||||
}
|
||||
|
||||
if (state.images && state.images.length > 0) {
|
||||
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}&subfolder=${encodeURIComponent(img.subfolder)}`; a.download = img.filename || "output"; a.click(); }); };
|
||||
} else cardObj.btnImg.style.display = "none";
|
||||
|
||||
if (state.workflow) {
|
||||
cardObj.btnJson.style.display = "inline-flex";
|
||||
cardObj.btnJson.onclick = (ev) => { ev.stopPropagation(); const blob = new Blob([JSON.stringify(state.workflow, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `workflow_${state.pid}.json`; a.click(); URL.revokeObjectURL(url); };
|
||||
} else cardObj.btnJson.style.display = "none";
|
||||
|
||||
let deleteTimeout = null, isDeletePending = false;
|
||||
const resetDeleteBtn = () => {
|
||||
isDeletePending = false;
|
||||
Object.assign(cardObj.btnDel.style, {
|
||||
color: "#e2e8f0",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.75)"
|
||||
});
|
||||
cardObj.btnDel.classList.remove("confirm-delete");
|
||||
cardObj.btnDel.title = "Delete Card";
|
||||
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
|
||||
};
|
||||
@@ -1007,16 +1085,14 @@ export function renderDOM() {
|
||||
ev.stopPropagation();
|
||||
if (!isDeletePending) {
|
||||
isDeletePending = true;
|
||||
Object.assign(cardObj.btnDel.style, {
|
||||
color: "#fff",
|
||||
backgroundColor: "#dc3545" // Changes to solid red on warning confirmation
|
||||
});
|
||||
cardObj.btnDel.classList.add("confirm-delete");
|
||||
cardObj.btnDel.title = "Click again to confirm deletion";
|
||||
deleteTimeout = setTimeout(resetDeleteBtn, 1000);
|
||||
deleteTimeout = setTimeout(resetDeleteBtn, 1500);
|
||||
} else {
|
||||
resetDeleteBtn();
|
||||
promptStates.delete(state.pid);
|
||||
await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ delete: [state.pid] }) });
|
||||
saveStatesToLocalStorage();
|
||||
renderDOM();
|
||||
}
|
||||
};
|
||||
@@ -1033,7 +1109,6 @@ export function renderDOM() {
|
||||
}
|
||||
|
||||
if (state.images.length === 0) {
|
||||
// Correctly display accumulated texts (like workflow prompts) while running as requested
|
||||
if (state.texts && state.texts.length > 0) {
|
||||
cardObj.placeholder.textContent = state.texts.join("\n"); Object.assign(cardObj.placeholder.style, { whiteSpace: "pre-wrap", textAlign: "left" });
|
||||
} else {
|
||||
@@ -1087,6 +1162,19 @@ export function renderDOM() {
|
||||
targetElements.forEach((el, index) => { if (State.cardStack.children[index] !== el) State.cardStack.insertBefore(el, State.cardStack.children[index] || null); });
|
||||
while (State.cardStack.children.length > targetElements.length) State.cardStack.removeChild(State.cardStack.lastChild);
|
||||
|
||||
saveStatesToLocalStorage();
|
||||
if (State.mainQueueScrollTop !== null) {
|
||||
const restorePos = State.mainQueueScrollTop;
|
||||
State.mainQueueScrollTop = null;
|
||||
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) {
|
||||
scrollEl.scrollTop = restorePos;
|
||||
requestAnimationFrame(() => {
|
||||
scrollEl.scrollTop = restorePos;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollTopBtnVisibility();
|
||||
});
|
||||
}
|
||||
+64
-18
@@ -1,31 +1,69 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
export const isVideoFormat = (url) => {
|
||||
const s = url.toLowerCase();
|
||||
if (!url) return false;
|
||||
const s = String(url).toLowerCase();
|
||||
return s.includes(".mp4") || s.includes(".webm");
|
||||
};
|
||||
|
||||
// Robust media output scanner supporting both arrays and single objects (gifs, mp4s, etc.)
|
||||
export function findImagesInOutputs(outputs) {
|
||||
export function parseWorkflow(workflow) {
|
||||
if (!workflow) return null;
|
||||
if (typeof workflow === "object") return workflow;
|
||||
if (typeof workflow === "string") {
|
||||
try { return JSON.parse(workflow); } catch (e) { return null; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isNodeIgnored(nodeId, rawWorkflow) {
|
||||
const workflow = parseWorkflow(rawWorkflow);
|
||||
if (!workflow || !Array.isArray(workflow.nodes)) {
|
||||
return false;
|
||||
}
|
||||
const node = workflow.nodes.find(n => String(n.id) === String(nodeId));
|
||||
return !!(node && node.properties && node.properties.ignoreInQueue);
|
||||
}
|
||||
|
||||
export function findImagesInOutputs(outputs, rawWorkflow) {
|
||||
const list = [];
|
||||
if (!outputs) return list;
|
||||
for (const nodeId in outputs) {
|
||||
for (const key in outputs[nodeId]) {
|
||||
const val = outputs[nodeId][key];
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach(item => {
|
||||
if (item && typeof item === 'object' && item.filename) list.push(item);
|
||||
});
|
||||
} else if (val && typeof val === 'object' && val.filename) {
|
||||
list.push(val);
|
||||
|
||||
const scan = (obj) => {
|
||||
if (!obj) return;
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach(item => scan(item));
|
||||
} else if (typeof obj === "object") {
|
||||
if (obj.filename || obj.name) {
|
||||
const fname = obj.filename || obj.name;
|
||||
if (typeof fname === "string" && (fname.includes(".") || obj.type)) {
|
||||
list.push({
|
||||
filename: fname,
|
||||
subfolder: obj.subfolder || "",
|
||||
type: obj.type || "output"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const key in obj) {
|
||||
scan(obj[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const workflow = parseWorkflow(rawWorkflow);
|
||||
for (const nodeId in outputs) {
|
||||
if (isNodeIgnored(nodeId, workflow)) continue;
|
||||
scan(outputs[nodeId]);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function findTextsInOutputs(outputs) {
|
||||
export function findTextsInOutputs(outputs, rawWorkflow) {
|
||||
const workflow = parseWorkflow(rawWorkflow);
|
||||
const list = [];
|
||||
if (!outputs) return list;
|
||||
for (const nodeId in outputs) {
|
||||
if (isNodeIgnored(nodeId, workflow)) continue;
|
||||
for (const key in outputs[nodeId]) {
|
||||
const val = outputs[nodeId][key];
|
||||
if (Array.isArray(val)) {
|
||||
@@ -48,12 +86,12 @@ export function findTextsInOutputs(outputs) {
|
||||
return list;
|
||||
}
|
||||
|
||||
// Organizes raw outputs dictionaries into lists of individual node-level output blocks (ignoring text-only outputs)
|
||||
export function getRunOutputs(nodeOutputs) {
|
||||
export function getRunOutputs(nodeOutputs, rawWorkflow) {
|
||||
const workflow = parseWorkflow(rawWorkflow);
|
||||
const list = [];
|
||||
if (!nodeOutputs) return list;
|
||||
for (const nodeId in nodeOutputs) {
|
||||
const imgs = findImagesInOutputs({ [nodeId]: nodeOutputs[nodeId] });
|
||||
const imgs = findImagesInOutputs({ [nodeId]: nodeOutputs[nodeId] }, workflow);
|
||||
if (imgs.length > 0) {
|
||||
list.push({ nodeId, images: imgs });
|
||||
}
|
||||
@@ -61,6 +99,13 @@ export function getRunOutputs(nodeOutputs) {
|
||||
return list;
|
||||
}
|
||||
|
||||
export function getPrimaryOutputImages(nodeOutputs, rawWorkflow) {
|
||||
if (!nodeOutputs) return [];
|
||||
const runOutputs = getRunOutputs(nodeOutputs, rawWorkflow);
|
||||
if (runOutputs.length === 0) return [];
|
||||
return runOutputs[runOutputs.length - 1].images || [];
|
||||
}
|
||||
|
||||
export function matchesFilter(state, query) {
|
||||
if (!query) return true;
|
||||
const q = query.toLowerCase();
|
||||
@@ -70,8 +115,9 @@ export function matchesFilter(state, query) {
|
||||
if (state.images && state.images.some(img => (img.filename || "").toLowerCase().includes(q))) return true;
|
||||
if (state.activeNodeName && state.activeNodeName.toLowerCase().includes(q)) return true;
|
||||
|
||||
if (state.workflow && Array.isArray(state.workflow.nodes)) {
|
||||
for (const node of state.workflow.nodes) {
|
||||
const workflow = parseWorkflow(state.workflow);
|
||||
if (workflow && Array.isArray(workflow.nodes)) {
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.title && node.title.toLowerCase().includes(q)) return true;
|
||||
if (node.type && node.type.toLowerCase().includes(q)) return true;
|
||||
if (Array.isArray(node.widgets)) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "comfy-sidebar"
|
||||
description = "A custom queue sidebar and image comparison extension that reintroduces and improves a sidebar queue menu and unified compact topbar layout for ComfyUI."
|
||||
version = "1.2.11"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = []
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/m0rtus59/ComfyUI-ComfySidebar"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "soundslikethunder"
|
||||
DisplayName = "Comfy Sidebar"
|
||||
Reference in New Issue
Block a user