Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf2cdc4bcd | ||
|
|
d85c54232a | ||
|
|
c49859a75b | ||
|
|
08454ff7db | ||
|
|
2fa512912e | ||
|
|
65900536ce | ||
|
|
7dbde17bcd | ||
|
|
a63c7d2afd | ||
|
|
3ccd3b6a7c | ||
|
|
735cac9c97 | ||
|
|
a9575f1a1c | ||
|
|
984cd108d6 | ||
|
|
ce2379b35c | ||
|
|
2a975bdc9f |
@@ -11,8 +11,11 @@ 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.
|
||||
* 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 click another to load it alongside the first one for pixel by pixel comparison.
|
||||
|
||||
<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
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import { State } from "./state.js";
|
||||
import { isVideoFormat } from "./utils.js";
|
||||
|
||||
let activeComparisonViewer = null;
|
||||
let videoSyncActive = false;
|
||||
let syncAnimationFrameId = null;
|
||||
|
||||
const createMediaElement = (src, muted = false) => {
|
||||
const isVideo = isVideoFormat(src);
|
||||
const el = isVideo ? document.createElement("video") : document.createElement("img");
|
||||
if (isVideo) {
|
||||
el.muted = muted;
|
||||
el.playsInline = true;
|
||||
el.autoplay = true;
|
||||
el.loop = true;
|
||||
el.controls = false; // Disable native controls to prevent layout shifts
|
||||
}
|
||||
Object.assign(el.style, {
|
||||
gridArea: "1 / 1", maxWidth: "100%", maxHeight: "80vh",
|
||||
objectFit: "contain", pointerEvents: "none"
|
||||
});
|
||||
el.src = isVideo ? src + "#t=0.001" : src;
|
||||
return el;
|
||||
};
|
||||
|
||||
// Video Playback Manager: Binds timeline progress bar, timeline scrubbing, and play/pause triggers
|
||||
const setupVideoPlayback = (vid, container) => {
|
||||
if (!vid) return;
|
||||
|
||||
const onMetaLoaded = () => {
|
||||
vid.play().catch(()=>{});
|
||||
};
|
||||
vid.addEventListener("loadedmetadata", onMetaLoaded);
|
||||
if (vid.readyState >= 1) onMetaLoaded();
|
||||
|
||||
// Synced Scrubber Progress Bar Overlay
|
||||
const controlBar = document.createElement("div");
|
||||
Object.assign(controlBar.style, {
|
||||
position: "absolute", bottom: "16px", left: "50%", transform: "translateX(-50%)",
|
||||
display: "flex", alignItems: "center", gap: "12px", background: "rgba(10,10,10,0.85)",
|
||||
padding: "8px 16px", borderRadius: "8px", zIndex: "40", fontSize: "11px",
|
||||
fontFamily: "monospace", color: "#eee", width: "80%", maxWidth: "500px",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.5)", transform: "translate3d(-50%, 0, 0)",
|
||||
pointerEvents: "auto"
|
||||
});
|
||||
|
||||
const playBtn = document.createElement("span");
|
||||
playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause";
|
||||
playBtn.style.cursor = "pointer";
|
||||
playBtn.style.fontSize = "14px";
|
||||
playBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (vid.paused) {
|
||||
vid.play().catch(()=>{});
|
||||
playBtn.className = "pi pi-pause";
|
||||
} else {
|
||||
vid.pause();
|
||||
playBtn.className = "pi pi-play";
|
||||
}
|
||||
};
|
||||
|
||||
const scrubberContainer = document.createElement("div");
|
||||
Object.assign(scrubberContainer.style, {
|
||||
flex: "1", height: "4px", background: "#444", borderRadius: "2px",
|
||||
position: "relative", cursor: "pointer"
|
||||
});
|
||||
const scrubberFill = document.createElement("div");
|
||||
Object.assign(scrubberFill.style, {
|
||||
width: "0%", height: "100%", background: "#3b82f6", borderRadius: "2px"
|
||||
});
|
||||
scrubberContainer.appendChild(scrubberFill);
|
||||
|
||||
const timeLabel = document.createElement("span");
|
||||
timeLabel.textContent = "0:00 / 0:00";
|
||||
|
||||
const scrub = (e) => {
|
||||
const rect = scrubberContainer.getBoundingClientRect();
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const percent = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width));
|
||||
const duration = vid.duration || 0;
|
||||
if (duration > 0) {
|
||||
vid.currentTime = (percent / 100) * duration;
|
||||
scrubberFill.style.width = `${percent}%`;
|
||||
}
|
||||
};
|
||||
|
||||
let isScrubbing = false;
|
||||
scrubberContainer.onmousedown = (e) => { e.stopPropagation(); isScrubbing = true; scrub(e); };
|
||||
scrubberContainer.ontouchstart = (e) => { e.stopPropagation(); isScrubbing = true; scrub(e); };
|
||||
|
||||
const handleWindowMove = (e) => { if (isScrubbing) scrub(e); };
|
||||
const handleWindowUp = () => { isScrubbing = false; };
|
||||
window.addEventListener("mousemove", handleWindowMove);
|
||||
window.addEventListener("touchmove", handleWindowMove);
|
||||
window.addEventListener("mouseup", handleWindowUp);
|
||||
window.addEventListener("touchend", handleWindowUp);
|
||||
|
||||
controlBar.append(playBtn, scrubberContainer, timeLabel);
|
||||
container.appendChild(controlBar);
|
||||
|
||||
videoSyncActive = true;
|
||||
const syncLoop = () => {
|
||||
if (!videoSyncActive) return;
|
||||
|
||||
// Update progress & labels
|
||||
const cur = vid.currentTime || 0;
|
||||
const dur = vid.duration || 0;
|
||||
if (dur > 0) {
|
||||
scrubberFill.style.width = `${(cur / dur) * 100}%`;
|
||||
const formatTime = (t) => {
|
||||
const m = Math.floor(t / 60);
|
||||
const s = Math.floor(t % 60);
|
||||
return `${m}:${s < 10 ? '0' : ''}${s}`;
|
||||
};
|
||||
timeLabel.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
|
||||
}
|
||||
|
||||
// Dynamically match play button in case video loops
|
||||
playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause";
|
||||
|
||||
syncAnimationFrameId = requestAnimationFrame(syncLoop);
|
||||
};
|
||||
requestAnimationFrame(syncLoop);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleWindowMove);
|
||||
window.removeEventListener("touchmove", handleWindowMove);
|
||||
window.removeEventListener("mouseup", handleWindowUp);
|
||||
window.removeEventListener("touchend", handleWindowUp);
|
||||
vid.removeEventListener("loadedmetadata", onMetaLoaded);
|
||||
controlBar.remove();
|
||||
};
|
||||
};
|
||||
|
||||
function createComparisonViewer(baseSrc) {
|
||||
const isBaseVideo = isVideoFormat(baseSrc);
|
||||
|
||||
// Mount the viewer overlay container directly inside ComfyUI's canvas parent.
|
||||
// This allows menus/sidebars outside the canvas area to naturally render on top.
|
||||
const canvasEl = document.querySelector("#graph-canvas, canvas");
|
||||
const targetContainer = canvasEl ? canvasEl.parentNode : document.body;
|
||||
|
||||
// Ensure the canvas container is positioned relatively so our overlay stretches accurately
|
||||
if (targetContainer && targetContainer !== document.body) {
|
||||
targetContainer.style.position = "relative";
|
||||
}
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.className = "comfy-sidebar-comparison-overlay";
|
||||
Object.assign(container.style, {
|
||||
position: "absolute", top: "0", left: "0", width: "100%", height: "100%",
|
||||
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
zIndex: "10", // Above base LGraph grid but strictly below other UI components
|
||||
pointerEvents: "auto", userSelect: "none", "-webkit-user-select": "none"
|
||||
});
|
||||
|
||||
const header = document.createElement("div");
|
||||
Object.assign(header.style, {
|
||||
position: "absolute", top: "16px", display: "flex", gap: "16px",
|
||||
zIndex: "30", color: "#aaa", fontSize: "12px", fontFamily: "sans-serif",
|
||||
pointerEvents: "none"
|
||||
});
|
||||
const infoText = document.createElement("span");
|
||||
infoText.textContent = isBaseVideo
|
||||
? "Video playback. Press Esc to close."
|
||||
: "Reference loaded. Click another card image in the sidebar to compare.";
|
||||
header.appendChild(infoText);
|
||||
container.appendChild(header);
|
||||
|
||||
// Close button
|
||||
const closeBtn = document.createElement("span");
|
||||
closeBtn.className = "pi pi-times";
|
||||
closeBtn.title = "Close Comparison (Esc)";
|
||||
Object.assign(closeBtn.style, {
|
||||
position: "absolute", top: "16px", right: "24px", zIndex: "30",
|
||||
cursor: "pointer", fontSize: "20px", color: "#aaa", transition: "color 0.15s ease"
|
||||
});
|
||||
closeBtn.onmouseenter = () => closeBtn.style.color = "#fff";
|
||||
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
|
||||
container.appendChild(closeBtn);
|
||||
|
||||
// Workspace wrapper holding both elements aligned pixel-perfectly
|
||||
const wrapper = document.createElement("div");
|
||||
Object.assign(wrapper.style, {
|
||||
position: "relative", display: "grid", placeItems: "center",
|
||||
maxWidth: "85%", maxHeight: "85%"
|
||||
});
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Close strictly on clicking empty background space of the overlay backdrop
|
||||
container.onclick = (e) => {
|
||||
if (e.target === container) {
|
||||
destroy();
|
||||
}
|
||||
};
|
||||
|
||||
// Media A (Base reference - Left. Created unmuted to enable audio playback)
|
||||
let mediaA = createMediaElement(baseSrc, false);
|
||||
wrapper.appendChild(mediaA);
|
||||
|
||||
// Media B (Comparison target - Right)
|
||||
let mediaB = null;
|
||||
|
||||
// Draggable Split Slider Handle
|
||||
const slider = document.createElement("div");
|
||||
Object.assign(slider.style, {
|
||||
position: "absolute", top: "0", bottom: "0", left: "50%",
|
||||
width: "2px", background: "#fff", cursor: "ew-resize",
|
||||
zIndex: "25", display: "none", pointerEvents: "auto",
|
||||
boxShadow: "0 0 8px rgba(0,0,0,0.5)"
|
||||
});
|
||||
const sliderButton = document.createElement("div");
|
||||
Object.assign(sliderButton.style, {
|
||||
position: "absolute", top: "50%", left: "50%",
|
||||
transform: "translate(-50%, -50%)", width: "32px", height: "32px",
|
||||
borderRadius: "50%", background: "#fff", color: "#333",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
boxShadow: "0 2px 6px rgba(0,0,0,0.4)", pointerEvents: "auto",
|
||||
userSelect: "none", webkitUserSelect: "none"
|
||||
});
|
||||
sliderButton.innerHTML = `<span class="pi pi-arrows-h" style="font-size: 12px;"></span>`;
|
||||
slider.appendChild(sliderButton);
|
||||
wrapper.appendChild(slider);
|
||||
|
||||
let splitRatio = 50; // Percentage
|
||||
let isDragging = false;
|
||||
let destroyVideoPlaybackFn = null;
|
||||
|
||||
const updateSliderPosition = (percent) => {
|
||||
splitRatio = Math.max(0, Math.min(100, percent));
|
||||
slider.style.left = `${splitRatio}%`;
|
||||
if (mediaB) {
|
||||
// Symmetrical split comparison layout: Image A on Left, Image B on Right
|
||||
mediaB.style.clipPath = `polygon(${splitRatio}% 0, 100% 0, 100% 100%, ${splitRatio}% 100%)`;
|
||||
}
|
||||
};
|
||||
|
||||
const startDrag = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isDragging = true;
|
||||
|
||||
// Symmetrical Click Teleportation: Snaps split-line directly to mouse coordinates
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const percent = ((clientX - rect.left) / rect.width) * 100;
|
||||
updateSliderPosition(percent);
|
||||
};
|
||||
const doDrag = (e) => {
|
||||
if (!isDragging) return;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const percent = ((clientX - rect.left) / rect.width) * 100;
|
||||
updateSliderPosition(percent);
|
||||
};
|
||||
const endDrag = () => { isDragging = false; };
|
||||
|
||||
// Allows dragging/clicking from any region of the wrapper
|
||||
wrapper.addEventListener("mousedown", startDrag);
|
||||
wrapper.addEventListener("touchstart", startDrag);
|
||||
|
||||
window.addEventListener("mousemove", doDrag);
|
||||
window.addEventListener("touchmove", doDrag);
|
||||
window.addEventListener("mouseup", endDrag);
|
||||
window.addEventListener("touchend", endDrag);
|
||||
|
||||
const destroy = () => {
|
||||
window.removeEventListener("mousemove", doDrag);
|
||||
window.removeEventListener("touchmove", doDrag);
|
||||
window.removeEventListener("mouseup", endDrag);
|
||||
window.removeEventListener("touchend", endDrag);
|
||||
document.removeEventListener("keydown", handleKeys);
|
||||
|
||||
videoSyncActive = false;
|
||||
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
|
||||
if (destroyVideoPlaybackFn) destroyVideoPlaybackFn();
|
||||
|
||||
container.remove();
|
||||
activeComparisonViewer = null;
|
||||
};
|
||||
|
||||
closeBtn.onclick = destroy;
|
||||
|
||||
const handleKeys = (e) => {
|
||||
if (e.key === "Escape") destroy();
|
||||
if (e.key === " " && mediaB) {
|
||||
e.preventDefault();
|
||||
updateSliderPosition(splitRatio > 50 ? 0 : 100);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleKeys);
|
||||
|
||||
targetContainer.appendChild(container);
|
||||
|
||||
// Initialize single-video playbacks if the base output is an MP4
|
||||
if (isBaseVideo) {
|
||||
destroyVideoPlaybackFn = setupVideoPlayback(mediaA, container);
|
||||
}
|
||||
|
||||
return {
|
||||
loadTarget(targetSrc) {
|
||||
const isTargetVideo = isVideoFormat(targetSrc);
|
||||
|
||||
if (isBaseVideo) {
|
||||
// If in Video Playback Mode: clicking another item replaces the player or opens image comparison
|
||||
destroy();
|
||||
showFullscreenPreview([targetSrc]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If in Image Comparison Mode: ignore video targets ("images compare only to other images")
|
||||
if (isTargetVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaB) mediaB.remove();
|
||||
|
||||
mediaB = createMediaElement(targetSrc, false);
|
||||
mediaB.style.pointerEvents = "none";
|
||||
wrapper.appendChild(mediaB);
|
||||
|
||||
slider.style.display = "block";
|
||||
infoText.textContent = "Drag the slider to compare. Click other card images to update target | Esc to close.";
|
||||
updateSliderPosition(50);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function showFullscreenPreview(imgSrcs) {
|
||||
if (!imgSrcs || imgSrcs.length === 0) return;
|
||||
|
||||
// If the comparison overlay is already open, dynamically load the target
|
||||
if (activeComparisonViewer) {
|
||||
activeComparisonViewer.loadTarget(imgSrcs[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, open a fresh side-by-side comparison workspace
|
||||
activeComparisonViewer = createComparisonViewer(imgSrcs[0]);
|
||||
}
|
||||
Vendored
+66
-56
@@ -1,5 +1,4 @@
|
||||
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 || '')}`;
|
||||
@@ -22,72 +21,83 @@ async function uploadDroppedImageToInput(imageObj) {
|
||||
|
||||
export function setupDragAndDrop() {
|
||||
document.addEventListener("dragover", (e) => {
|
||||
if (State.currentDraggedImgData) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
});
|
||||
|
||||
document.addEventListener("drop", async (e) => {
|
||||
if (!State.currentDraggedImgData) return;
|
||||
|
||||
const canvas = app.canvas;
|
||||
if (canvas && canvas.graph) {
|
||||
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 (!canvas || !canvas.graph) return;
|
||||
|
||||
let droppedOnImageNode = false;
|
||||
|
||||
if (targetNode && (targetNode.type.includes("LoadImage") || targetNode.widgets?.some(w => w.name === "image"))) {
|
||||
if (State.currentDraggedImgData.filename) {
|
||||
// 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();
|
||||
droppedOnImageNode = true;
|
||||
const widget = targetNode.widgets.find(w => w.name === "image");
|
||||
if (widget) {
|
||||
const newFilename = await uploadDroppedImageToInput(State.currentDraggedImgData);
|
||||
if (newFilename) {
|
||||
widget.value = newFilename;
|
||||
if (widget.callback) widget.callback(widget.value);
|
||||
targetNode.imgs = null;
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!droppedOnImageNode) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
if (State.currentDraggedImgData.workflow) {
|
||||
if (app.loadGraphData) {
|
||||
app.loadGraphData(State.currentDraggedImgData.workflow);
|
||||
} else if (app.handleFile) {
|
||||
const jsonStr = JSON.stringify(State.currentDraggedImgData.workflow);
|
||||
const file = new File([jsonStr], "workflow.json", { type: "application/json" });
|
||||
await app.handleFile(file);
|
||||
}
|
||||
} else if (State.currentDraggedImgData.filename) {
|
||||
const src = State.currentDraggedImgData.url || `/view?filename=${encodeURIComponent(State.currentDraggedImgData.filename)}&type=${State.currentDraggedImgData.type || 'output'}&subfolder=${encodeURIComponent(State.currentDraggedImgData.subfolder || '')}`;
|
||||
const res = await fetch(src);
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], State.currentDraggedImgData.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 synthesize workflow file drop:", err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Comfy Sidebar: Failed to handle dropped native URL:", err);
|
||||
}
|
||||
}
|
||||
State.currentDraggedImgData = null;
|
||||
}, 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
|
||||
});
|
||||
}
|
||||
+32
-2
@@ -80,6 +80,7 @@ const concludeRun = async (pid, statusStr) => {
|
||||
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);
|
||||
}
|
||||
@@ -92,6 +93,20 @@ 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();
|
||||
@@ -108,6 +123,7 @@ export function setupApiListeners() {
|
||||
workflow: activeWorkspaceWorkflow, startTime: Date.now(), duration: null
|
||||
});
|
||||
}
|
||||
renderDOMFn(); // Render immediately to prevent pop-in delay!
|
||||
syncQueue();
|
||||
});
|
||||
|
||||
@@ -148,9 +164,22 @@ export function setupApiListeners() {
|
||||
|
||||
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) promptStates.get(e.detail.prompt_id).images = finalImgs;
|
||||
promptStates.get(e.detail.prompt_id).texts = findTextsInOutputs({ 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();
|
||||
}
|
||||
});
|
||||
@@ -192,6 +221,7 @@ export async function initSessionAndHistory() {
|
||||
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
|
||||
});
|
||||
|
||||
+65
-1
@@ -1,23 +1,87 @@
|
||||
// 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";
|
||||
|
||||
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.Override Stock Job History Tab", name: "Replaces the stock Job History sidebar with Comfy Queue", type: "boolean", defaultValue: false });
|
||||
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"];
|
||||
sidebarTabs.forEach(tab => {
|
||||
app.ui.settings.addSetting({
|
||||
id: `Comfy Sidebar.Hide Junk.${tab}`,
|
||||
name: `Hide Tab: ${tab}`,
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => {
|
||||
if (typeof syncClassicLayout === "function") 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",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => {
|
||||
if (typeof syncClassicLayout === "function") 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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
// Run properties panel toggle correction
|
||||
setupPropertiesPanelToggleFix();
|
||||
|
||||
// Safely resolve circular dependencies using Dependency Injection Hooks
|
||||
setSyncQueue(syncQueue);
|
||||
setUIDependencies(renderDOM, updateSidebarBadge);
|
||||
|
||||
+4
-2
@@ -3,12 +3,13 @@ export const cardElements = new Map();
|
||||
|
||||
// Global shared variables
|
||||
export const State = {
|
||||
currentDraggedImgData: null,
|
||||
currentSearchQuery: "",
|
||||
globalOrderCounter: 0,
|
||||
sidebarContainer: null,
|
||||
cardStack: null,
|
||||
currentlyActivePromptId: null
|
||||
currentlyActivePromptId: null,
|
||||
activeSubmenuPromptId: null, // Active run/outputs explorer pointer
|
||||
activeSubmenuBatchImages: null // Active batch images explorer pointer
|
||||
};
|
||||
|
||||
export function pruneHistory(app) {
|
||||
@@ -39,6 +40,7 @@ export function saveStatesToLocalStorage() {
|
||||
|
||||
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,
|
||||
|
||||
+37
-4
@@ -1,3 +1,4 @@
|
||||
// styles.js
|
||||
export function injectStyles() {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@@ -6,22 +7,54 @@ export function injectStyles() {
|
||||
border-radius: 4px; padding: 8px; position: relative;
|
||||
min-height: 80px; margin-bottom: 12px; break-inside: avoid;
|
||||
user-select: none; -webkit-user-select: none;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
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; }
|
||||
.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 .pi-times { display: none !important; }
|
||||
.comfy-sidebar-card.pending:hover .pi-times { display: flex !important; }
|
||||
|
||||
.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);
|
||||
}
|
||||
+16
@@ -3,6 +3,7 @@ export const isVideoFormat = (url) => {
|
||||
return s.includes(".mp4") || s.includes(".webm");
|
||||
};
|
||||
|
||||
// Robust media output scanner supporting both arrays and single objects (gifs, mp4s, etc.)
|
||||
export function findImagesInOutputs(outputs) {
|
||||
const list = [];
|
||||
if (!outputs) return list;
|
||||
@@ -13,6 +14,8 @@ export function findImagesInOutputs(outputs) {
|
||||
val.forEach(item => {
|
||||
if (item && typeof item === 'object' && item.filename) list.push(item);
|
||||
});
|
||||
} else if (val && typeof val === 'object' && val.filename) {
|
||||
list.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +48,19 @@ 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) {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user