Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fa512912e | ||
|
|
65900536ce | ||
|
|
7dbde17bcd | ||
|
|
a63c7d2afd | ||
|
|
3ccd3b6a7c | ||
|
|
735cac9c97 | ||
|
|
a9575f1a1c | ||
|
|
984cd108d6 | ||
|
|
ce2379b35c | ||
|
|
2a975bdc9f | ||
|
|
edd7d2f82c |
@@ -11,8 +11,10 @@ A lightweight, high-performance ComfyUI extension that replaces the cluttered st
|
||||
* **Quick Save Buttons**: Dedicated interactive shortcuts appear on hover, allowing you to instantly download generation outputs or export exact workflow JSON files with a single click.
|
||||
* **Compact Adaptive Design**: A clean, smart layout that automatically balances single or multi-column grids based on your sidebar width. It maximizes viewable content and respects your active screen area without wasting precious space.
|
||||
* **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.
|
||||
* **[NEW]** 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.
|
||||
* **[NEW]** Added toggles to hide buttons you don't need from the side panel.
|
||||
|
||||
<img width="287" height="685" alt="image" src="https://github.com/user-attachments/assets/ab414a37-d585-4bcb-aade-cf20ab5b24d8" />
|
||||
<img width="385" height="336" alt="image" src="https://github.com/user-attachments/assets/743fbf85-9281-4561-9dd0-cd9af4d8c0e4" />
|
||||
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
import { app } from "/scripts/app.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);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], imageObj.filename || "dropped_image.png", { 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 });
|
||||
const uploadData = await uploadRes.json();
|
||||
return uploadData.name;
|
||||
} catch(e) {
|
||||
console.error("Comfy Sidebar: Failed to copy image to input.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setupDragAndDrop() {
|
||||
document.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
});
|
||||
|
||||
document.addEventListener("drop", async (e) => {
|
||||
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");
|
||||
if (jsonStr) {
|
||||
try {
|
||||
const workflow = JSON.parse(jsonStr);
|
||||
if (workflow && workflow.nodes) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (app.loadGraphData) {
|
||||
app.loadGraphData(workflow);
|
||||
} else if (app.handleFile) {
|
||||
const file = new File([jsonStr], "workflow.json", { type: "application/json" });
|
||||
await app.handleFile(file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// 2. Check for native image URL drop
|
||||
const url = e.dataTransfer.getData("text/uri-list") || e.dataTransfer.getData("text/plain");
|
||||
if (url) {
|
||||
try {
|
||||
const urlObj = new URL(url, window.location.origin);
|
||||
if (urlObj.pathname === "/view") {
|
||||
const filename = urlObj.searchParams.get("filename");
|
||||
const type = urlObj.searchParams.get("type") || "output";
|
||||
const subfolder = urlObj.searchParams.get("subfolder") || "";
|
||||
|
||||
if (filename) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let targetNode = null;
|
||||
if (canvas.convertEventToCanvasOffset) {
|
||||
const pos = canvas.convertEventToCanvasOffset(e);
|
||||
targetNode = canvas.graph.getNodeOnPos(pos[0], pos[1]);
|
||||
} else {
|
||||
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) {
|
||||
const newFilename = await uploadDroppedImageToInput({ filename, type, subfolder });
|
||||
if (newFilename) {
|
||||
widget.value = newFilename;
|
||||
if (widget.callback) widget.callback(widget.value);
|
||||
targetNode.imgs = null;
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dropped on empty canvas: Load workflow natively
|
||||
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 (app.handleFile) await app.handleFile(file);
|
||||
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Comfy Sidebar: Failed to handle dropped native URL:", err);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
// layout.js - Fully Fixed & Renamed to 'Hide Junk'
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
const STYLE_ID = "comfy-sidebar-classic-layout-override";
|
||||
|
||||
const CLASSIC_LAYOUT_CSS_MEDIA = `
|
||||
/* Style and align the custom plugins bar (Manager, LoRA, etc.) */
|
||||
[class*="actionbar"]:not(.actionbar),
|
||||
[class*="actionbar-buttons"],
|
||||
.actionbar-buttons {
|
||||
position: fixed !important;
|
||||
top: -5px !important;
|
||||
right: 4px !important;
|
||||
left: auto !important;
|
||||
transform: none !important;
|
||||
z-index: 1010 !important;
|
||||
|
||||
/* Nuke the background rectangle, border, and shadows */
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Base rules: Large/Wide desktop screens (>= 1600px width) */
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 150px !important;
|
||||
padding-right: 580px !important;
|
||||
}
|
||||
|
||||
/* Medium desktop screens (between 1200px and 1599px width) */
|
||||
@media (max-width: 1599px) {
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 100px !important;
|
||||
padding-right: 420px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Standard laptops / small monitors (between 900px and 1199px width) */
|
||||
@media (max-width: 1199px) {
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 40px !important;
|
||||
padding-right: 280px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very narrow or heavily zoomed displays (< 900px width) */
|
||||
@media (max-width: 899px) {
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 10px !important;
|
||||
padding-right: 180px !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function applyClassicLayout(enable) {
|
||||
let styleEl = document.getElementById(STYLE_ID);
|
||||
|
||||
if (enable) {
|
||||
if (app.ui && 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) {
|
||||
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 btn = icon.closest('button, [role="button"], .comfyui-menu-item, .p-button');
|
||||
if (btn && !btn.classList.contains("comfy-sidebar-custom-properties-toggle")) {
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTopbarContainer() {
|
||||
const container = document.querySelector('[class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
|
||||
if (container) return container;
|
||||
|
||||
const orig = findOriginalButton();
|
||||
if (orig && 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 (btn.parentNode) return btn.parentNode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPropertiesPanelOpen() {
|
||||
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
|
||||
return !!(icon && icon.className.includes("lucide--panel-left"));
|
||||
}
|
||||
|
||||
function findActiveQueueIndicator() {
|
||||
const container = findTopbarContainer();
|
||||
if (!container) return null;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
const text = (el.textContent || el.getAttribute("title") || "").toLowerCase();
|
||||
if (text.includes("graph") || text.includes("workflow") || el.querySelector('[class*="sitemap"], [class*="workflow"]')) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function syncClassicLayout() {
|
||||
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Exit early and clean up overrides if Classic Topbar layout is off
|
||||
if (!isClassicLayoutEnabled) {
|
||||
const originalBtn = findOriginalButton();
|
||||
if (originalBtn && 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;
|
||||
}
|
||||
|
||||
// 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") {
|
||||
indicator.style.removeProperty("display");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSidebarTabsVisibility();
|
||||
}
|
||||
|
||||
export function setupPropertiesPanelToggleFix() {
|
||||
if (!document.getElementById("comfy-sidebar-layout-fix-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "comfy-sidebar-layout-fix-styles";
|
||||
style.textContent = `
|
||||
.comfy-sidebar-hide-original-properties-btn {
|
||||
display: none !important;
|
||||
}
|
||||
.comfy-sidebar-custom-properties-toggle {
|
||||
order: 99999 !important;
|
||||
margin-left: -8px !important;
|
||||
margin-right: 0px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.comfy-sidebar-custom-properties-toggle.comfy-panel-open {
|
||||
order: 99999 !important;
|
||||
margin-left: 0px !important;
|
||||
margin-right: 0px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
if (domObserver) {
|
||||
domObserver.disconnect();
|
||||
}
|
||||
|
||||
syncClassicLayout();
|
||||
|
||||
domObserver = new MutationObserver((mutations) => {
|
||||
let shouldSync = false;
|
||||
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);
|
||||
|
||||
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 (shouldSync && !syncScheduled) {
|
||||
syncScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
syncClassicLayout();
|
||||
syncScheduled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
domObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
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";
|
||||
|
||||
export let renderDOMFn = () => {};
|
||||
export let updateSidebarBadgeFn = () => {};
|
||||
export function setUIDependencies(renderFn, badgeFn) {
|
||||
renderDOMFn = renderFn;
|
||||
updateSidebarBadgeFn = 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();
|
||||
|
||||
const normalizedPending = pendingList.map((p, idx) => {
|
||||
let pid = null, seq = idx;
|
||||
if (Array.isArray(p)) {
|
||||
seq = typeof p[0] === 'number' ? p[0] : idx;
|
||||
pid = p[1];
|
||||
} else if (p && typeof p === "object") {
|
||||
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 };
|
||||
});
|
||||
|
||||
normalizedPending.sort((a, b) => a.seq - b.seq);
|
||||
normalizedPending.forEach((item, index) => {
|
||||
const pid = item.pid;
|
||||
const number = index + 1;
|
||||
|
||||
if (pid) {
|
||||
pendingIds.add(pid);
|
||||
if (!promptStates.has(pid)) {
|
||||
State.globalOrderCounter++;
|
||||
promptStates.set(pid, {
|
||||
pid: pid, status: "pending", images: [], progress: 0, queueNumber: number,
|
||||
progressText: `Pending... (#${number})`, timestamp: State.globalOrderCounter,
|
||||
workflow: app.graph.serialize()
|
||||
});
|
||||
} else {
|
||||
const st = promptStates.get(pid);
|
||||
if (st.status === "pending") {
|
||||
st.queueNumber = number;
|
||||
st.progressText = `Pending... (#${number})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const [pid, state] of promptStates.entries()) {
|
||||
if (state.status === "pending" && !pendingIds.has(pid)) promptStates.delete(pid);
|
||||
}
|
||||
|
||||
updateSidebarBadgeFn(pendingIds.size + (runningList.length > 0 ? 1 : 0));
|
||||
renderDOMFn();
|
||||
} catch (err) {
|
||||
console.error("Comfy Sidebar: Failed to sync queue state", err);
|
||||
}
|
||||
}
|
||||
|
||||
const concludeRun = async (pid, statusStr) => {
|
||||
if (!pid || !promptStates.has(pid)) return;
|
||||
if (State.currentlyActivePromptId === pid) State.currentlyActivePromptId = null;
|
||||
|
||||
const st = promptStates.get(pid);
|
||||
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) {}
|
||||
pruneHistory(app);
|
||||
syncQueue();
|
||||
};
|
||||
|
||||
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()) {
|
||||
if (s.status === "cancelled" || s.status === "error") {
|
||||
toDelete.push(p);
|
||||
promptStates.delete(p);
|
||||
}
|
||||
}
|
||||
if (toDelete.length > 0) {
|
||||
api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }).catch(()=>{});
|
||||
}
|
||||
}
|
||||
|
||||
const pid = e.detail.prompt_id;
|
||||
State.currentlyActivePromptId = pid;
|
||||
const activeWorkspaceWorkflow = app.graph.serialize();
|
||||
|
||||
if (promptStates.has(pid)) {
|
||||
const st = promptStates.get(pid);
|
||||
st.status = "active"; st.progressText = "Sampling..."; st.workflow = activeWorkspaceWorkflow;
|
||||
st.rendered = false; st.startTime = Date.now(); st.duration = null;
|
||||
} else {
|
||||
State.globalOrderCounter++;
|
||||
promptStates.set(pid, {
|
||||
pid: pid, status: "active", images: [], progress: 0,
|
||||
progressText: "Sampling...", timestamp: State.globalOrderCounter,
|
||||
workflow: activeWorkspaceWorkflow, startTime: Date.now(), duration: null
|
||||
});
|
||||
}
|
||||
renderDOMFn(); // Render immediately to prevent pop-in delay!
|
||||
syncQueue();
|
||||
});
|
||||
|
||||
api.addEventListener("progress", (e) => {
|
||||
const pid = e.detail.prompt_id;
|
||||
if (pid && promptStates.has(pid)) {
|
||||
promptStates.get(pid).progress = Math.round((e.detail.value / e.detail.max) * 100);
|
||||
renderDOMFn();
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("executing", (e) => {
|
||||
const nodeId = e.detail;
|
||||
const showWorkingNode = app.ui.settings.getSettingValue("Comfy Sidebar.Show Working Node Name") ?? true;
|
||||
|
||||
if (showWorkingNode && State.currentlyActivePromptId && promptStates.has(State.currentlyActivePromptId)) {
|
||||
const st = promptStates.get(State.currentlyActivePromptId);
|
||||
if (nodeId) {
|
||||
const node = app.graph.getNodeById(nodeId);
|
||||
st.activeNodeName = node ? (node.title || node.type) : `Node #${nodeId}`;
|
||||
} else {
|
||||
st.activeNodeName = "Finishing...";
|
||||
}
|
||||
renderDOMFn();
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("b_preview", (e) => {
|
||||
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 }];
|
||||
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 finalTexts = findTextsInOutputs({ output: e.detail.output });
|
||||
if (finalTexts.length > 0) {
|
||||
// Overwrite with latest texts to avoid clearing previously set text outputs
|
||||
st.texts = finalTexts;
|
||||
}
|
||||
|
||||
// Dynamically capture each executed node's individual outputs
|
||||
if (!st.nodeOutputs) st.nodeOutputs = {};
|
||||
st.nodeOutputs[e.detail.node] = e.detail.output;
|
||||
|
||||
renderDOMFn();
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("execution_success", (e) => concludeRun(e.detail.prompt_id, "completed"));
|
||||
api.addEventListener("execution_error", (e) => concludeRun(e.detail.prompt_id, "error"));
|
||||
api.addEventListener("execution_interrupted", () => {
|
||||
Array.from(promptStates.values()).filter(t => t.status === "active").forEach(t => concludeRun(t.pid, "cancelled"));
|
||||
syncQueue();
|
||||
});
|
||||
}
|
||||
|
||||
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 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 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
|
||||
});
|
||||
});
|
||||
pruneHistory(app);
|
||||
await syncQueue();
|
||||
}
|
||||
+95
-1515
File diff suppressed because it is too large
Load Diff
+69
@@ -0,0 +1,69 @@
|
||||
export const promptStates = new Map();
|
||||
export const cardElements = new Map();
|
||||
|
||||
// Global shared variables
|
||||
export const State = {
|
||||
currentSearchQuery: "",
|
||||
globalOrderCounter: 0,
|
||||
sidebarContainer: null,
|
||||
cardStack: null,
|
||||
currentlyActivePromptId: null,
|
||||
activeSubmenuPromptId: null, // Active run/outputs explorer pointer
|
||||
activeSubmenuBatchImages: null // Active batch images explorer pointer
|
||||
};
|
||||
|
||||
export function pruneHistory(app) {
|
||||
const maxItems = app.ui.settings.getSettingValue("Comfy.Queue.MaxHistoryItems") ?? 64;
|
||||
const tasks = Array.from(promptStates.entries())
|
||||
.filter(([pid, state]) => state.status !== "pending" && state.status !== "active");
|
||||
|
||||
tasks.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
|
||||
if (tasks.length > maxItems) {
|
||||
const deleteCount = tasks.length - maxItems;
|
||||
for (let i = 0; i < deleteCount; i++) {
|
||||
const [pid] = tasks[i];
|
||||
promptStates.delete(pid);
|
||||
cardElements.delete(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function saveStatesToLocalStorage() {
|
||||
try {
|
||||
const 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
|
||||
return img;
|
||||
}).filter(Boolean);
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
localStorage.setItem("comfy_sidebar_prompt_states", JSON.stringify(serializable));
|
||||
} catch (e) {
|
||||
console.error("Comfy Sidebar: Failed to save state to localStorage", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadStatesFromLocalStorage() {
|
||||
try {
|
||||
const data = localStorage.getItem("comfy_sidebar_prompt_states");
|
||||
if (data) {
|
||||
const list = JSON.parse(data);
|
||||
list.forEach(state => {
|
||||
promptStates.set(state.pid, state);
|
||||
if (state.timestamp > State.globalOrderCounter) State.globalOrderCounter = state.timestamp;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Comfy Sidebar: Failed to load state from localStorage", e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// styles.js
|
||||
export function injectStyles() {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.comfy-sidebar-card {
|
||||
background: var(--comfy-input-bg, #181818);
|
||||
border-radius: 4px; padding: 8px; position: relative;
|
||||
min-height: 80px; margin-bottom: 12px; break-inside: avoid;
|
||||
user-select: none; -webkit-user-select: none;
|
||||
border: 2px solid var(--border-color, #333);
|
||||
color: var(--comfy-input-color, var(--fg-color, #eee));
|
||||
}
|
||||
.comfy-sidebar-card:hover {
|
||||
border-color: var(--p-primary-color, var(--primary-color, #555)) !important;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
.comfy-sidebar-card.active { --border-color: #3b82f6; --hover-color: #60a5fa; }
|
||||
.comfy-sidebar-card.pending { --border-color: #6c757d; --hover-color: #adb5bd; }
|
||||
.comfy-sidebar-card.cancelled { --border-color: #ffc107; --hover-color: #ffe082; }
|
||||
.comfy-sidebar-card.error { --border-color: #dc3545; --hover-color: #f87171; }
|
||||
|
||||
.comfy-sidebar-card.pending .comfy-sidebar-queue-cancel-btn,
|
||||
.comfy-sidebar-card.active .comfy-sidebar-queue-cancel-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.comfy-sidebar-card.pending:hover .comfy-sidebar-queue-cancel-btn,
|
||||
.comfy-sidebar-card.active:hover .comfy-sidebar-queue-cancel-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.comfy-sidebar-card-timer {
|
||||
position: absolute; top: 6px; left: 8px; font-size: 10px;
|
||||
font-family: monospace; opacity: 0.7; background: rgba(0, 0, 0, 0.6);
|
||||
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 */
|
||||
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 */
|
||||
color: #ffffff !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
export const isVideoFormat = (url) => {
|
||||
const s = url.toLowerCase();
|
||||
return s.includes(".mp4") || s.includes(".webm");
|
||||
};
|
||||
|
||||
export function findImagesInOutputs(outputs) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function findTextsInOutputs(outputs) {
|
||||
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 (typeof item === 'string') {
|
||||
list.push(item);
|
||||
} else if (item && typeof item === 'object' && item.text) {
|
||||
if (Array.isArray(item.text)) list.push(...item.text);
|
||||
else if (typeof item.text === 'string') list.push(item.text);
|
||||
}
|
||||
});
|
||||
} else if (typeof val === 'string') {
|
||||
list.push(val);
|
||||
} else if (val && typeof val === 'object' && val.text) {
|
||||
if (Array.isArray(val.text)) list.push(...val.text);
|
||||
else if (typeof val.text === 'string') list.push(val.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// Organizes raw outputs dictionaries into lists of individual node-level output blocks (ignoring text-only outputs)
|
||||
export function getRunOutputs(nodeOutputs) {
|
||||
const list = [];
|
||||
if (!nodeOutputs) return list;
|
||||
for (const nodeId in nodeOutputs) {
|
||||
const imgs = findImagesInOutputs({ [nodeId]: nodeOutputs[nodeId] });
|
||||
if (imgs.length > 0) {
|
||||
list.push({ nodeId, images: imgs });
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function matchesFilter(state, query) {
|
||||
if (!query) return true;
|
||||
const q = query.toLowerCase();
|
||||
|
||||
if (state.pid && String(state.pid).toLowerCase().includes(q)) return true;
|
||||
if (state.texts && state.texts.some(t => String(t).toLowerCase().includes(q))) return true;
|
||||
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) {
|
||||
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)) {
|
||||
for (const w of node.widgets) {
|
||||
if (w && w.value !== undefined && w.value !== null) {
|
||||
if (String(w.value).toLowerCase().includes(q)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user