Compare commits

...
1 Commits
Author SHA1 Message Date
M0rtus59 fe154b7787 Refactor overlay system, debounce state saving, and fix pending card controls
- Unified Overlay System: Reusable SidebarOverlay for all viewers.

- Performance: Debounced localStorage persistence to prevent UI stutters.

- Memory Cleanup: Proper disposal of global document/API listeners.

- UI Fix: Pending/active cards only show cancel button on hover.
2026-08-14 23:40:07 +05:00
12 changed files with 462 additions and 631 deletions
+64 -253
View File
@@ -1,11 +1,10 @@
import { isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "./utils.js";
import { State } from "./state.js";
import { create3DViewer } from "./viewer3d.js";
import { createTextReader } from "./text_reader.js";
import { stopAllAudioPlayback } from "./ui.js";
import { SidebarOverlay } from "./overlay.js";
let activeComparisonViewer = null;
let globalKeydownHandler = null;
export function isAudioViewerOpen() {
return !!(activeComparisonViewer && activeComparisonViewer.isAudio);
@@ -24,59 +23,17 @@ const getClientY = (e) => {
};
function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () => {}) {
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
let togglePlayFn = () => {};
const container = document.createElement("div");
container.className = "comfy-sidebar-comparison-overlay";
Object.assign(container.style, {
position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh",
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center", zIndex: "1000",
boxSizing: "border-box", overflow: "hidden", pointerEvents: "auto", userSelect: "none"
});
const updateOverlayBounds = () => {
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && sidebarEl.offsetWidth > 0 && sidebarEl.isConnected) {
const rect = sidebarEl.getBoundingClientRect();
if (rect.left < window.innerWidth / 2) {
const leftOffset = Math.max(0, rect.right);
container.style.left = `${leftOffset}px`;
container.style.width = `calc(100vw - ${leftOffset}px)`;
container.style.right = "0px";
} else {
const rightOffset = Math.max(0, window.innerWidth - rect.left);
container.style.left = "0px";
container.style.width = `calc(100vw - ${rightOffset}px)`;
container.style.right = `${rightOffset}px`;
const overlay = new SidebarOverlay({
onDestroy,
onKeyDown: (e) => {
if (e.key === " ") {
e.preventDefault();
togglePlayFn();
}
container.style.top = `${Math.max(0, rect.top)}px`;
container.style.height = `calc(100vh - ${Math.max(0, rect.top)}px)`;
} else {
container.style.left = "0px";
container.style.width = "100vw";
container.style.top = "0px";
container.style.height = "100vh";
}
};
updateOverlayBounds();
window.addEventListener("resize", updateOverlayBounds);
const closeBtn = document.createElement("span");
closeBtn.className = "pi pi-times";
closeBtn.title = "Close (Esc)";
Object.assign(closeBtn.style, {
position: "absolute", top: "16px", right: "24px", zIndex: "30",
cursor: "pointer", fontSize: "20px", color: "#aaa", transition: "color 0.15s ease",
background: "rgba(10,10,10,0.6)", borderRadius: "50%", padding: "4px"
});
closeBtn.onmouseenter = () => closeBtn.style.color = "#fff";
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
container.appendChild(closeBtn);
const playerBox = document.createElement("div");
Object.assign(playerBox.style, {
@@ -101,7 +58,6 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
badge.textContent = parsedExt;
titleRow.append(title, badge);
// Waveform visualization
const soundwave = document.createElement("div");
soundwave.className = "comfy-sidebar-soundwave-container";
soundwave.style.height = "56px";
@@ -115,26 +71,21 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
soundwave.appendChild(bar);
});
// Scrubber
const scrubberContainer = document.createElement("div");
Object.assign(scrubberContainer.style, {
width: "100%", height: "6px", background: "#334155", borderRadius: "3px",
position: "relative", cursor: "pointer"
});
const scrubberFill = document.createElement("div");
Object.assign(scrubberFill.style, {
width: "0%", height: "100%", background: "#c084fc", borderRadius: "3px"
});
Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#c084fc", borderRadius: "3px" });
scrubberContainer.appendChild(scrubberFill);
// Controls Row
const controlsRow = document.createElement("div");
Object.assign(controlsRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" });
const leftControls = document.createElement("div");
Object.assign(leftControls.style, { display: "flex", alignItems: "center", gap: "10px" });
// Play & Stop SVGs
const playIconSvg = `<svg viewBox="0 0 24 24" width="14" height="14" style="margin-left: 2px; pointer-events: none;"><polygon points="6,4 20,12 6,20" fill="#ffffff"/></svg>`;
const stopIconSvg = `<svg viewBox="0 0 24 24" width="12" height="12" style="pointer-events: none;"><rect x="5" y="5" width="14" height="14" rx="2" fill="#ffffff"/></svg>`;
@@ -180,12 +131,12 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
controlsRow.append(leftControls, rightControls);
playerBox.append(titleRow, soundwave, scrubberContainer, controlsRow);
container.appendChild(playerBox);
overlay.container.appendChild(playerBox);
const audioEl = document.createElement("audio");
audioEl.src = baseSrc;
audioEl.preload = "metadata";
container.appendChild(audioEl);
overlay.container.appendChild(audioEl);
const formatTime = (t) => {
const m = Math.floor(t / 60);
@@ -211,7 +162,7 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
scrubberFill.style.width = "0%";
};
const togglePlay = () => {
togglePlayFn = () => {
if (audioEl.paused) {
stopAllAudioPlayback();
audioEl.play().catch(()=>{});
@@ -227,54 +178,25 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
}
};
playBtn.onclick = togglePlay;
playBtn.onclick = togglePlayFn;
loopBtn.onclick = () => {
audioEl.loop = !audioEl.loop;
loopBtn.style.borderColor = audioEl.loop ? "#a855f7" : "#444";
loopBtn.style.color = audioEl.loop ? "#c084fc" : "#aaa";
};
speedSelect.onchange = (e) => {
audioEl.playbackRate = Number(e.target.value);
};
speedSelect.onchange = (e) => { audioEl.playbackRate = Number(e.target.value); };
scrubberContainer.onclick = (e) => {
const rect = scrubberContainer.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
if (audioEl.duration > 0) {
audioEl.currentTime = pos * audioEl.duration;
}
if (audioEl.duration > 0) audioEl.currentTime = pos * audioEl.duration;
};
const destroy = () => {
window.removeEventListener("resize", updateOverlayBounds);
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
overlay.addCleanup(() => {
audioEl.pause();
audioEl.src = "";
container.remove();
onDestroy();
};
});
closeBtn.onclick = destroy;
container.onclick = (e) => {
if (e.target === container) destroy();
};
const handleKeys = (e) => {
if (e.key === "Escape") destroy();
if (e.key === " ") {
e.preventDefault();
togglePlay();
}
};
globalKeydownHandler = handleKeys;
document.addEventListener("keydown", globalKeydownHandler);
document.body.appendChild(container);
audioEl.play().then(() => {
playBtn.innerHTML = stopIconSvg;
playerBox.classList.add("playing");
@@ -284,7 +206,7 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
isAudio: true,
loadTarget(targetSrc) {
if (!isAudioFormat(targetSrc)) {
destroy();
overlay.destroy();
onSwitchMedia(targetSrc);
return;
}
@@ -297,7 +219,7 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
playerBox.classList.add("playing");
}).catch(()=>{});
},
destroy
destroy: () => overlay.destroy()
};
}
@@ -326,9 +248,7 @@ const setupVideoPlayback = (vid, container) => {
let videoSyncActive = true;
let syncAnimationFrameId = null;
const onMetaLoaded = () => {
vid.play().catch(()=>{});
};
const onMetaLoaded = () => { vid.play().catch(()=>{}); };
vid.addEventListener("loadedmetadata", onMetaLoaded);
if (vid.readyState >= 1) onMetaLoaded();
@@ -363,9 +283,7 @@ const setupVideoPlayback = (vid, container) => {
position: "relative", cursor: "pointer"
});
const scrubberFill = document.createElement("div");
Object.assign(scrubberFill.style, {
width: "0%", height: "100%", background: "#3b82f6", borderRadius: "2px"
});
Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#3b82f6", borderRadius: "2px" });
scrubberContainer.appendChild(scrubberFill);
const timeLabel = document.createElement("span");
@@ -398,7 +316,6 @@ const setupVideoPlayback = (vid, container) => {
const syncLoop = () => {
if (!videoSyncActive) return;
const cur = vid.currentTime || 0;
const dur = vid.duration || 0;
if (dur > 0) {
@@ -410,7 +327,6 @@ const setupVideoPlayback = (vid, container) => {
};
timeLabel.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
}
playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause";
syncAnimationFrameId = requestAnimationFrame(syncLoop);
};
@@ -429,57 +345,19 @@ const setupVideoPlayback = (vid, container) => {
};
function createComparisonViewer(baseSrc, onDestroy = () => {}) {
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
const isBaseVideo = isVideoFormat(baseSrc);
let splitRatio = 50;
let mediaB = null;
const container = document.createElement("div");
container.className = "comfy-sidebar-comparison-overlay";
Object.assign(container.style, {
position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh",
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
zIndex: "1000", boxSizing: "border-box", overflow: "hidden",
pointerEvents: "auto", userSelect: "none", "-webkit-user-select": "none"
});
const updateOverlayBounds = () => {
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && sidebarEl.offsetWidth > 0 && sidebarEl.isConnected) {
const rect = sidebarEl.getBoundingClientRect();
if (rect.left < window.innerWidth / 2) {
const leftOffset = Math.max(0, rect.right);
container.style.left = `${leftOffset}px`;
container.style.width = `calc(100vw - ${leftOffset}px)`;
container.style.right = "0px";
} else {
const rightOffset = Math.max(0, window.innerWidth - rect.left);
container.style.left = "0px";
container.style.width = `calc(100vw - ${rightOffset}px)`;
container.style.right = `${rightOffset}px`;
const overlay = new SidebarOverlay({
onDestroy,
onKeyDown: (e) => {
if (e.key === " " && mediaB) {
e.preventDefault();
updateSliderPosition(splitRatio > 50 ? 0 : 100);
}
container.style.top = `${Math.max(0, rect.top)}px`;
container.style.height = `calc(100vh - ${Math.max(0, rect.top)}px)`;
} else {
container.style.left = "0px";
container.style.width = "100vw";
container.style.top = "0px";
container.style.height = "100vh";
}
};
updateOverlayBounds();
window.addEventListener("resize", updateOverlayBounds);
let resizeObserver = null;
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && window.ResizeObserver) {
resizeObserver = new ResizeObserver(() => updateOverlayBounds());
resizeObserver.observe(sidebarEl);
}
});
const scrollContainer = document.createElement("div");
Object.assign(scrollContainer.style, {
@@ -488,7 +366,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
padding: "54px 28px 48px 28px", scrollbarWidth: "thin",
scrollbarColor: "#555 rgba(0, 0, 0, 0.3)", zIndex: "15"
});
container.appendChild(scrollContainer);
overlay.container.appendChild(scrollContainer);
const header = document.createElement("div");
Object.assign(header.style, {
@@ -503,19 +381,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
? "Video playback. Press Esc to close."
: "Click image to zoom (100%/Fit) | Shift+Click another card to compare.";
header.appendChild(infoText);
container.appendChild(header);
const closeBtn = document.createElement("span");
closeBtn.className = "pi pi-times";
closeBtn.title = "Close (Esc)";
Object.assign(closeBtn.style, {
position: "absolute", top: "16px", right: "24px", zIndex: "30",
cursor: "pointer", fontSize: "20px", color: "#aaa", transition: "color 0.15s ease",
background: "rgba(10,10,10,0.6)", borderRadius: "50%", padding: "4px"
});
closeBtn.onmouseenter = () => closeBtn.style.color = "#fff";
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
container.appendChild(closeBtn);
overlay.container.appendChild(header);
const wrapper = document.createElement("div");
Object.assign(wrapper.style, {
@@ -533,15 +399,11 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
borderRadius: "4px", backdropFilter: "blur(4px)", boxShadow: "0 2px 6px rgba(0,0,0,0.4)"
});
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);
if (isBaseVideo) hintPrompt.style.display = "none";
overlay.container.appendChild(hintPrompt);
let mediaA = createMediaElement(baseSrc, false);
wrapper.appendChild(mediaA);
let mediaB = null;
let isZoomed = false;
const slider = document.createElement("div");
@@ -557,16 +419,12 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
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"
boxShadow: "0 2px 6px rgba(0,0,0,0.4)", pointerEvents: "auto"
});
sliderButton.innerHTML = `<span class="pi pi-arrows-h" style="font-size: 12px;"></span>`;
slider.appendChild(sliderButton);
wrapper.appendChild(slider);
let splitRatio = 50;
let destroyVideoPlaybackFn = null;
const updateSliderPosition = (percent) => {
splitRatio = Math.max(0, Math.min(100, percent));
slider.style.left = `${splitRatio}%`;
@@ -575,9 +433,16 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
}
};
const cleanupMedia = (el) => {
if (el && el.tagName === "VIDEO") {
el.pause();
el.src = "";
el.load();
}
};
const syncImageScales = () => {
if (!mediaA) return;
const wA = mediaA.naturalWidth || mediaA.videoWidth || 0;
const hA = mediaA.naturalHeight || mediaA.videoHeight || 0;
@@ -589,7 +454,6 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
wrapper.style.width = `${wA}px`;
wrapper.style.height = `${hA}px`;
wrapper.style.cursor = "zoom-out";
mediaA.style.maxWidth = "none";
mediaA.style.maxHeight = "none";
mediaA.style.width = "100%";
@@ -601,7 +465,6 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
wrapper.style.width = "auto";
wrapper.style.height = "auto";
wrapper.style.cursor = isBaseVideo ? "default" : "zoom-in";
mediaA.style.maxWidth = "100%";
mediaA.style.maxHeight = "80vh";
mediaA.style.width = "auto";
@@ -613,7 +476,6 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
const wB = mediaB.naturalWidth || mediaB.videoWidth || 0;
const hB = mediaB.naturalHeight || mediaB.videoHeight || 0;
if (!wA || !hA || !wB || !hB) return;
const arA = wA / hA;
@@ -696,10 +558,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
}
};
let isDraggingSlider = false;
let dragMoved = false;
let dragStartX = 0;
let dragStartY = 0;
let isDraggingSlider = false, dragMoved = false, dragStartX = 0, dragStartY = 0;
const startDrag = (e) => {
if (e.button && e.button !== 0) return;
@@ -713,11 +572,9 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
if (!isDraggingSlider) return;
const clientX = getClientX(e);
const clientY = getClientY(e);
if (!dragMoved && (Math.abs(clientX - dragStartX) > 4 || Math.abs(clientY - dragStartY) > 4)) {
dragMoved = true;
}
if (dragMoved && mediaB) {
const rect = wrapper.getBoundingClientRect();
const percent = ((clientX - rect.left) / rect.width) * 100;
@@ -728,83 +585,42 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
const endDrag = () => {
if (!isDraggingSlider) return;
isDraggingSlider = false;
if (dragMoved) {
setTimeout(() => { dragMoved = false; }, 50);
}
if (dragMoved) setTimeout(() => { dragMoved = false; }, 50);
};
wrapper.addEventListener("mousedown", startDrag);
wrapper.addEventListener("touchstart", startDrag);
window.addEventListener("mousemove", doDrag);
window.addEventListener("touchmove", doDrag);
window.addEventListener("mouseup", endDrag);
window.addEventListener("touchend", endDrag);
wrapper.addEventListener("click", (e) => {
overlay.addCleanup(() => {
window.removeEventListener("mousemove", doDrag);
window.removeEventListener("touchmove", doDrag);
window.removeEventListener("mouseup", endDrag);
window.removeEventListener("touchend", endDrag);
});
wrapper.addEventListener("click", () => {
if (dragMoved || isBaseVideo) return;
toggleZoom();
});
scrollContainer.addEventListener("click", (e) => {
if (dragMoved) return;
if (e.target === scrollContainer || e.target === container) {
destroy();
}
if (e.target === scrollContainer || e.target === overlay.container) overlay.destroy();
});
const cleanupMedia = (el) => {
if (el && el.tagName === "VIDEO") {
el.pause();
el.src = "";
el.load();
}
};
const destroy = () => {
window.removeEventListener("resize", updateOverlayBounds);
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
window.removeEventListener("mousemove", doDrag);
window.removeEventListener("touchmove", doDrag);
window.removeEventListener("mouseup", endDrag);
window.removeEventListener("touchend", endDrag);
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
if (destroyVideoPlaybackFn) destroyVideoPlaybackFn();
if (isBaseVideo) {
const destroyVideoPlayback = setupVideoPlayback(mediaA, overlay.container);
overlay.addCleanup(destroyVideoPlayback);
}
overlay.addCleanup(() => {
cleanupMedia(mediaA);
cleanupMedia(mediaB);
container.remove();
onDestroy();
};
closeBtn.onclick = destroy;
const handleKeys = (e) => {
if (e.key === "Escape") destroy();
if (e.key === " " && mediaB) {
e.preventDefault();
updateSliderPosition(splitRatio > 50 ? 0 : 100);
}
};
globalKeydownHandler = handleKeys;
document.addEventListener("keydown", globalKeydownHandler);
document.body.appendChild(container);
if (isBaseVideo) {
destroyVideoPlaybackFn = setupVideoPlayback(mediaA, container);
}
});
return {
is3D: false,
@@ -817,7 +633,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
const isTargetText = typeof targetSrc === "object" && targetSrc.text;
if (isBaseVideo || isTargetVideo || isTarget3D || isTargetAudio || isTargetText) {
destroy();
overlay.destroy();
showFullscreenPreview([targetSrc], isShiftClick);
return;
}
@@ -828,16 +644,11 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
mediaB.remove();
mediaB = null;
}
mediaB = createMediaElement(targetSrc, false);
mediaB.style.pointerEvents = "none";
mediaB.onload = () => {
syncImageScales();
};
mediaB.onload = syncImageScales;
wrapper.appendChild(mediaB);
if (mediaB.complete && mediaB.naturalWidth) {
syncImageScales();
}
if (mediaB.complete && mediaB.naturalWidth) syncImageScales();
} else {
if (mediaB) {
cleanupMedia(mediaB);
@@ -853,7 +664,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
};
}
},
destroy
destroy: () => overlay.destroy()
};
}
+133 -103
View File
@@ -1,7 +1,21 @@
import { app } from "/scripts/app.js";
import { State } from "./state.js";
async function uploadDroppedImageToInput(imageObj) {
async function promoteOrUploadToInput(imageObj) {
// 1. Try server-side promotion first (no browser RAM/re-upload overhead)
try {
const promoteRes = await fetch("/comfy-sidebar/promote-output", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(imageObj)
});
if (promoteRes.ok) {
const data = await promoteRes.json();
if (data.name) return data.name;
}
} catch (e) {}
// 2. Fallback to browser blob pipeline if backend promotion endpoint is unavailable
const src = `/view?filename=${encodeURIComponent(imageObj.filename)}&type=${imageObj.type || 'output'}&subfolder=${encodeURIComponent(imageObj.subfolder || '')}`;
try {
const response = await fetch(src);
@@ -22,13 +36,114 @@ async function uploadDroppedImageToInput(imageObj) {
}
}
async function handleWorkflowDrop(e) {
const jsonStr = e.dataTransfer.getData("application/json");
if (!jsonStr) return false;
try {
const workflow = JSON.parse(jsonStr);
if (workflow && workflow.nodes) {
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 true;
}
} catch (err) {}
return false;
}
async function handleMediaDrop(e) {
const url = e.dataTransfer.getData("text/uri-list") || e.dataTransfer.getData("text/plain");
if (!url) return false;
try {
const urlObj = new URL(url, window.location.origin);
if (urlObj.pathname !== "/view") return false;
const filename = urlObj.searchParams.get("filename");
const type = urlObj.searchParams.get("type") || "output";
const subfolder = urlObj.searchParams.get("subfolder") || "";
if (!filename) return false;
const canvas = app.canvas;
if (!canvas || !canvas.graph) return false;
let targetNode = null;
if (canvas.convertEventToCanvasOffset) {
const pos = canvas.convertEventToCanvasOffset(e);
targetNode = canvas.graph.getNodeOnPos(pos[0], pos[1]);
} 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);
}
const widget = targetNode?.widgets?.find(w =>
w.name === "image" || w.name === "video" || w.name === "audio" || w.name === "audio_file" ||
w.name === "file" || w.name === "video_path" || w.name === "model_file" || w.name === "3d_file" ||
w.type === "image" || w.type === "customtext"
);
const isLoadNode = targetNode && (
widget ||
targetNode.type?.includes("LoadImage") ||
targetNode.type?.includes("LoadVideo") ||
targetNode.type?.includes("VHS_LoadVideo") ||
targetNode.type?.includes("Load3D") ||
targetNode.type?.includes("Load 3D") ||
targetNode.type?.includes("LoadAudio") ||
targetNode.type?.includes("Load Audio") ||
targetNode.type?.includes("VHS_LoadAudio") ||
targetNode.type?.includes("Preview3D")
);
if (isLoadNode) {
const targetWidget = widget || targetNode.widgets?.[0];
if (targetWidget) {
const newFilename = await promoteOrUploadToInput({ filename, type, subfolder });
if (newFilename) {
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 true;
}
}
} else {
const src = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`;
const res = await fetch(src);
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 });
return true;
}
}
} catch (err) {
console.error("Comfy Sidebar: Failed to handle dropped native URL:", err);
}
return false;
}
const dropHandlers = [
handleWorkflowDrop,
handleMediaDrop
];
export function setupDragAndDrop() {
document.addEventListener("dragover", (e) => {
const onDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
});
};
document.addEventListener("drop", async (e) => {
const onDrop = async (e) => {
const isSidebarDrop = e.target.closest('.comfyui-sidebar, .comfy-sidebar, [class*="sidebar"]') ||
(State.sidebarContainer && State.sidebarContainer.contains(e.target));
if (isSidebarDrop) {
@@ -37,105 +152,20 @@ export function setupDragAndDrop() {
return;
}
const canvas = app.canvas;
if (!canvas || !canvas.graph) return;
// 1. Check for workflow JSON drop
const jsonStr = e.dataTransfer.getData("application/json");
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 image / video / 3D / audio 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 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);
}
// Check if dropped on a LoadImage, LoadVideo, Load3D, LoadAudio, or widget node
const widget = targetNode?.widgets?.find(w =>
w.name === "image" || w.name === "video" || w.name === "audio" || w.name === "audio_file" ||
w.name === "file" || w.name === "video_path" || w.name === "model_file" || w.name === "3d_file" ||
w.type === "image" || w.type === "customtext"
);
const isLoadNode = targetNode && (
widget ||
targetNode.type?.includes("LoadImage") ||
targetNode.type?.includes("LoadVideo") ||
targetNode.type?.includes("VHS_LoadVideo") ||
targetNode.type?.includes("Load3D") ||
targetNode.type?.includes("Load 3D") ||
targetNode.type?.includes("LoadAudio") ||
targetNode.type?.includes("Load Audio") ||
targetNode.type?.includes("VHS_LoadAudio") ||
targetNode.type?.includes("Preview3D")
);
if (isLoadNode) {
const targetWidget = widget || targetNode.widgets?.[0];
if (targetWidget) {
const newFilename = await uploadDroppedImageToInput({ filename, type, subfolder });
if (newFilename) {
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 area
const src = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`;
const res = await fetch(src);
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 });
}
}
}
}
} catch (err) {
console.error("Comfy Sidebar: Failed to handle dropped native URL:", err);
for (const handler of dropHandlers) {
if (await handler(e)) {
e.preventDefault();
e.stopPropagation();
return;
}
}
}, true);
};
document.addEventListener("dragover", onDragOver);
document.addEventListener("drop", onDrop, true);
return () => {
document.removeEventListener("dragover", onDragOver);
document.removeEventListener("drop", onDrop, true);
};
}
+13 -21
View File
@@ -3,7 +3,6 @@ import { app } from "/scripts/app.js";
const STYLE_ID = "comfy-sidebar-classic-layout-override";
const CLASSIC_LAYOUT_CSS_MEDIA = `
/* Remove the ghost underlay container border & background (the oval) without affecting the topbar header */
.actionbar-container,
.shadow-interface.rounded-lg.bg-comfy-menu-bg,
.shadow-interface.rounded-lg.border-interface-stroke,
@@ -15,7 +14,6 @@ div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]) {
box-shadow: none !important;
}
/* Style and align the custom plugins bar (Manager, LoRA, etc.) */
[class*="actionbar"]:not(.actionbar),
[class*="actionbar-buttons"],
.actionbar-buttons {
@@ -25,7 +23,6 @@ div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]) {
left: auto !important;
transform: none !important;
z-index: 1010 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
@@ -35,7 +32,6 @@ div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]) {
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;
@@ -49,7 +45,6 @@ div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]) {
align-items: center !important;
}
/* Scale down performance indicators (Crystools) uniformly */
#crysmonitor-monitors-root,
#crystools-monitors-root,
.crysmonitor-monitors-container {
@@ -58,7 +53,6 @@ div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]) {
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"]),
@@ -74,14 +68,12 @@ button:has([data-pc-name="avatar"]),
display: none !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"] {
@@ -90,7 +82,6 @@ button:has([data-pc-name="avatar"]),
}
}
/* Standard laptops / small monitors (between 900px and 1199px width) */
@media (max-width: 1199px) {
.p-tabview-nav-content,
[class*="tabview"] {
@@ -99,7 +90,6 @@ button:has([data-pc-name="avatar"]),
}
}
/* Very narrow or heavily zoomed displays (< 900px width) */
@media (max-width: 899px) {
.p-tabview-nav-content,
[class*="tabview"] {
@@ -109,16 +99,12 @@ button:has([data-pc-name="avatar"]),
}
`;
/**
* Native settings synchronization without direct DOM element querying or simulated clicks.
*/
export function syncStockHistoryAndProgressSettings(enable) {
const dockedVal = !!enable;
const progressVal = !enable;
const QPOV2_ID = "Comfy.Queue.QPOV2";
const progressKey = "Comfy.Queue.ShowRunProgressBar";
// Use official extensionManager setting API if present, with fallback to app.ui.settings
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set(QPOV2_ID, dockedVal); } catch (e) {}
try { app.extensionManager.setting.set(progressKey, progressVal); } catch (e) {}
@@ -210,15 +196,11 @@ function findTopbarContainer() {
function isPropertiesPanelOpen() {
const panel = document.querySelector('[data-testid="properties-panel"]');
if (panel && panel.offsetWidth > 0 && panel.offsetHeight > 0) {
return true;
}
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;
}
if (h.textContent.trim() === "Workflow Overview" && h.offsetWidth > 0 && h.offsetHeight > 0) return true;
}
const origBtn = findOriginalButton();
@@ -230,7 +212,6 @@ function isPropertiesPanelOpen() {
return true;
}
}
return false;
}
@@ -556,4 +537,15 @@ export function setupPropertiesPanelToggleFix() {
});
syncClassicLayout();
}
export function destroyLayoutFix() {
if (domObserver) {
domObserver.disconnect();
domObserver = null;
}
const styleEl = document.getElementById(STYLE_ID);
if (styleEl) styleEl.remove();
const fixStyles = document.getElementById("comfy-sidebar-layout-fix-styles");
if (fixStyles) fixStyles.remove();
}
+101
View File
@@ -0,0 +1,101 @@
import { State } from "./state.js";
export class SidebarOverlay {
constructor(options = {}) {
this.onDestroy = options.onDestroy || (() => {});
this.cleanupFns = [];
// Root container
this.container = document.createElement("div");
this.container.className = `comfy-sidebar-comparison-overlay ${options.className || ""}`;
Object.assign(this.container.style, {
position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh",
background: options.background || "rgba(10, 10, 10, 0.95)",
display: "flex", flexDirection: "column", alignItems: "center",
justifyContent: "center", zIndex: "1000", boxSizing: "border-box",
overflow: "hidden", pointerEvents: "auto", userSelect: "none"
});
// Close button
this.closeBtn = document.createElement("span");
this.closeBtn.className = "pi pi-times";
this.closeBtn.title = "Close (Esc)";
Object.assign(this.closeBtn.style, {
position: "absolute", top: "16px", right: "24px", zIndex: "30",
cursor: "pointer", fontSize: "20px", color: "#aaa", transition: "color 0.15s ease",
background: "rgba(10,10,10,0.6)", borderRadius: "50%", padding: "4px"
});
this.closeBtn.onmouseenter = () => this.closeBtn.style.color = "#fff";
this.closeBtn.onmouseleave = () => this.closeBtn.style.color = "#aaa";
this.closeBtn.onclick = () => this.destroy();
this.container.appendChild(this.closeBtn);
// Backdrop click to close
this.container.addEventListener("click", (e) => {
if (e.target === this.container) this.destroy();
});
// Sidebar boundary alignment
this.updateOverlayBounds = this.updateOverlayBounds.bind(this);
this.updateOverlayBounds();
window.addEventListener("resize", this.updateOverlayBounds);
this.cleanupFns.push(() => window.removeEventListener("resize", this.updateOverlayBounds));
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && window.ResizeObserver) {
const ro = new ResizeObserver(this.updateOverlayBounds);
ro.observe(sidebarEl);
this.cleanupFns.push(() => ro.disconnect());
}
// Global Key Handler (Esc to close)
const onKeyDown = (e) => {
if (e.key === "Escape") {
e.preventDefault();
this.destroy();
}
if (options.onKeyDown) options.onKeyDown(e);
};
document.addEventListener("keydown", onKeyDown);
this.cleanupFns.push(() => document.removeEventListener("keydown", onKeyDown));
document.body.appendChild(this.container);
}
updateOverlayBounds() {
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && sidebarEl.offsetWidth > 0 && sidebarEl.isConnected) {
const rect = sidebarEl.getBoundingClientRect();
if (rect.left < window.innerWidth / 2) {
const leftOffset = Math.max(0, rect.right);
this.container.style.left = `${leftOffset}px`;
this.container.style.width = `calc(100vw - ${leftOffset}px)`;
this.container.style.right = "0px";
} else {
const rightOffset = Math.max(0, window.innerWidth - rect.left);
this.container.style.left = "0px";
this.container.style.width = `calc(100vw - ${rightOffset}px)`;
this.container.style.right = `${rightOffset}px`;
}
this.container.style.top = `${Math.max(0, rect.top)}px`;
this.container.style.height = `calc(100vh - ${Math.max(0, rect.top)}px)`;
} else {
this.container.style.left = "0px";
this.container.style.width = "100vw";
this.container.style.top = "0px";
this.container.style.height = "100vh";
}
}
addCleanup(fn) {
this.cleanupFns.push(fn);
}
destroy() {
for (const fn of this.cleanupFns.splice(0)) {
try { fn(); } catch (e) {}
}
this.container.remove();
this.onDestroy();
}
}
+53 -30
View File
@@ -1,6 +1,6 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { State, promptStates, pruneHistory, cardElements, saveStatesToLocalStorage } from "./state.js";
import { State, promptStates, pruneHistory, cardElements, scheduleStateSave } from "./state.js";
import { findImagesInOutputs, findTextsInOutputs, parseWorkflow, getPrimaryOutputImages } from "./utils.js";
export let renderDOMFn = () => {};
@@ -38,10 +38,10 @@ export async function syncQueue() {
if (pid) {
pendingIds.add(pid);
if (!promptStates.has(pid)) {
State.globalOrderCounter++;
State.sequenceNumber++;
promptStates.set(pid, {
pid: pid, status: "pending", images: [], progress: 0, queueNumber: number,
progressText: `Pending... (#${number})`, timestamp: State.globalOrderCounter,
progressText: `Pending... (#${number})`, timestamp: State.sequenceNumber,
workflow: app.graph.serialize()
});
} else {
@@ -108,20 +108,19 @@ const concludeRun = async (pid, statusStr) => {
}
pruneHistory(app);
saveStatesToLocalStorage();
scheduleStateSave();
syncQueue();
};
export function setupApiListeners() {
api.addEventListener("status", syncQueue);
api.addEventListener("reconnected", async () => {
const onStatus = () => syncQueue();
const onReconnected = async () => {
console.log("Comfy Sidebar: Server reconnected, syncing state and history");
await initSessionAndHistory();
await syncQueue();
});
api.addEventListener("execution_start", (e) => {
};
const onExecutionStart = (e) => {
if (app.ui.settings.getSettingValue("Comfy Sidebar.Auto Clear Interrupted") ?? false) {
const toDelete = [];
for (const [p, s] of promptStates.entries()) {
@@ -132,7 +131,7 @@ export function setupApiListeners() {
}
if (toDelete.length > 0) {
api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }).catch(()=>{});
saveStatesToLocalStorage();
scheduleStateSave();
}
}
@@ -145,18 +144,18 @@ export function setupApiListeners() {
st.status = "active"; st.progressText = "Sampling..."; st.workflow = activeWorkspaceWorkflow;
st.rendered = false; st.startTime = Date.now(); st.duration = null;
} else {
State.globalOrderCounter++;
State.sequenceNumber++;
promptStates.set(pid, {
pid: pid, status: "active", images: [], progress: 0,
progressText: "Sampling...", timestamp: State.globalOrderCounter,
progressText: "Sampling...", timestamp: State.sequenceNumber,
workflow: activeWorkspaceWorkflow, startTime: Date.now(), duration: null
});
}
renderDOMFn();
syncQueue();
});
};
api.addEventListener("progress", (e) => {
const onProgress = (e) => {
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
if (pid && promptStates.has(pid)) {
const st = promptStates.get(pid);
@@ -173,9 +172,9 @@ export function setupApiListeners() {
renderDOMFn();
}
}
});
};
api.addEventListener("executing", (e) => {
const onExecuting = (e) => {
const nodeId = e.detail;
const showWorkingNode = app.ui.settings.getSettingValue("Comfy Sidebar.Show Working Node Name") ?? true;
@@ -189,9 +188,9 @@ export function setupApiListeners() {
}
renderDOMFn();
}
});
};
api.addEventListener("b_preview", (e) => {
const onBPreview = (e) => {
const activeTasks = Array.from(promptStates.values()).filter(t => t.status === "active");
if (activeTasks.length > 0) {
const st = activeTasks[0];
@@ -202,9 +201,9 @@ export function setupApiListeners() {
st.images = [{ url: newBlobUrl }];
renderDOMFn();
}
});
};
api.addEventListener("executed", (e) => {
const onExecuted = (e) => {
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
if (pid && promptStates.has(pid)) {
const st = promptStates.get(pid);
@@ -222,14 +221,39 @@ export function setupApiListeners() {
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", () => {
const onExecutionSuccess = (e) => concludeRun(e.detail.prompt_id, "completed");
const onExecutionError = (e) => concludeRun(e.detail.prompt_id, "error");
const onExecutionInterrupted = () => {
Array.from(promptStates.values()).filter(t => t.status === "active").forEach(t => concludeRun(t.pid, "cancelled"));
syncQueue();
});
};
api.addEventListener("status", onStatus);
api.addEventListener("reconnected", onReconnected);
api.addEventListener("execution_start", onExecutionStart);
api.addEventListener("progress", onProgress);
api.addEventListener("executing", onExecuting);
api.addEventListener("b_preview", onBPreview);
api.addEventListener("executed", onExecuted);
api.addEventListener("execution_success", onExecutionSuccess);
api.addEventListener("execution_error", onExecutionError);
api.addEventListener("execution_interrupted", onExecutionInterrupted);
// Return cleanup function to unhook listeners when destroyed
return () => {
api.removeEventListener("status", onStatus);
api.removeEventListener("reconnected", onReconnected);
api.removeEventListener("execution_start", onExecutionStart);
api.removeEventListener("progress", onProgress);
api.removeEventListener("executing", onExecuting);
api.removeEventListener("b_preview", onBPreview);
api.removeEventListener("executed", onExecuted);
api.removeEventListener("execution_success", onExecutionSuccess);
api.removeEventListener("execution_error", onExecutionError);
api.removeEventListener("execution_interrupted", onExecutionInterrupted);
};
}
export async function initSessionAndHistory() {
@@ -269,10 +293,9 @@ export async function initSessionAndHistory() {
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++;
State.sequenceNumber++;
promptStates.set(pid, {
pid: pid,
status: status,
@@ -281,13 +304,13 @@ export async function initSessionAndHistory() {
nodeOutputs: outputs,
workflow: workflow,
progressText: "",
timestamp: State.globalOrderCounter,
timestamp: State.sequenceNumber,
rendered: true
});
});
pruneHistory(app);
saveStatesToLocalStorage();
scheduleStateSave();
await syncQueue();
} catch (err) {
console.error("Comfy Sidebar: Failed to initialize history from server API", err);
+13 -5
View File
@@ -1,13 +1,14 @@
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 { setupSidebarUI, applySidebarOverride, findOurSidebarButton, setSyncQueue, updateSidebarBadge, renderDOM, setupScrollListener } from "./ui.js";
import { setupApiListeners, initSessionAndHistory, syncQueue, setUIDependencies } from "./queue.js";
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings } from "./layout.js";
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings, destroyLayoutFix } from "./layout.js";
let isInitialized = false;
let activeKeydownHandler = null;
let nodeDOMObserver = null;
let cleanupFns = [];
function findHeaderByText(parent, text) {
if (!text) return null;
@@ -272,12 +273,14 @@ app.registerExtension({
setUIDependencies(renderDOM, updateSidebarBadge);
injectStyles();
setupDragAndDrop();
// Register disposable subsystems
cleanupFns.push(setupDragAndDrop());
cleanupFns.push(setupScrollListener());
cleanupFns.push(setupApiListeners());
const sidebarContainer = setupSidebarUI();
setupApiListeners();
await initSessionAndHistory();
setupVueNodeObserver();
@@ -325,6 +328,11 @@ app.registerExtension({
},
destroy() {
for (const fn of cleanupFns.splice(0)) {
try { if (typeof fn === "function") fn(); } catch (e) {}
}
destroyLayoutFix();
if (nodeDOMObserver) {
nodeDOMObserver.disconnect();
nodeDOMObserver = null;
+16 -9
View File
@@ -3,15 +3,24 @@ export const cardElements = new Map();
export const State = {
currentSearchQuery: "",
globalOrderCounter: 0,
sequenceNumber: 0,
sidebarContainer: null,
cardStack: null, // Main queue scroll container
submenuStack: null, // Submenu scroll container
cardStack: null,
submenuStack: null,
currentlyActivePromptId: null,
activeSubmenuPromptId: null, // Active run/outputs explorer pointer
activeSubmenuBatchImages: null // Active batch images explorer pointer
activeSubmenuPromptId: null,
activeSubmenuBatchImages: null
};
let saveTimer = null;
export function scheduleStateSave(delay = 250) {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
saveTimer = null;
saveStatesToLocalStorage();
}, delay);
}
export function deletePromptState(pid) {
const key = String(pid);
const state = promptStates.get(key);
@@ -32,7 +41,7 @@ export function pruneHistory(app) {
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);
tasks.sort((a, b) => (a[1].timestamp || 0) - (b[1].timestamp || 0));
if (tasks.length > maxItems) {
const deleteCount = tasks.length - maxItems;
@@ -52,8 +61,6 @@ export function saveStatesToLocalStorage() {
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({
@@ -105,7 +112,7 @@ export function loadStatesFromLocalStorage() {
list.forEach(state => {
const key = String(state.pid);
promptStates.set(key, state);
if (state.timestamp > State.globalOrderCounter) State.globalOrderCounter = state.timestamp;
if (state.timestamp > State.sequenceNumber) State.sequenceNumber = state.timestamp;
});
}
} catch (e) {}
+11 -3
View File
@@ -29,6 +29,14 @@ export function injectStyles() {
.comfy-sidebar-card.active:hover .comfy-sidebar-queue-cancel-btn {
display: flex !important;
}
/* Prevent action hover buttons (Delete/JSON/Node) from showing on pending & running cards */
.comfy-sidebar-card.pending .comfy-sidebar-hover-panel,
.comfy-sidebar-card.active .comfy-sidebar-hover-panel,
.comfy-sidebar-card.pending .comfy-sidebar-left-hover-panel,
.comfy-sidebar-card.active .comfy-sidebar-left-hover-panel {
display: none !important;
}
.comfy-sidebar-card-timer {
position: absolute; top: 6px; left: 8px; font-size: 10px;
@@ -56,14 +64,14 @@ export function injectStyles() {
color: #ffffff !important;
}
/* Hover Panels CSS Hover Control */
/* Hover Panels CSS Hover Control for Completed / Cancelled / Error cards */
.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 {
.comfy-sidebar-card:not(.pending):not(.active):hover .comfy-sidebar-hover-panel,
.comfy-sidebar-card:not(.pending):not(.active):hover .comfy-sidebar-left-hover-panel {
display: flex !important;
}
+5 -78
View File
@@ -1,6 +1,4 @@
import { State } from "./state.js";
let globalKeydownHandler = null;
import { SidebarOverlay } from "./overlay.js";
// Global helper for code block copying in Markdown view
window.moonCopyCode = function(button) {
@@ -195,63 +193,11 @@ function parseMarkdown(text) {
}
export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy = () => {}) {
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
let rawText = typeof textData === "object" ? (textData.text || "") : String(textData);
let pid = typeof textData === "object" ? textData.pid : "";
let isMarkdownMode = true;
const container = document.createElement("div");
container.className = "comfy-sidebar-comparison-overlay";
Object.assign(container.style, {
position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh",
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center", zIndex: "1000",
boxSizing: "border-box", overflow: "hidden", pointerEvents: "auto", userSelect: "none"
});
const updateOverlayBounds = () => {
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && sidebarEl.offsetWidth > 0 && sidebarEl.isConnected) {
const rect = sidebarEl.getBoundingClientRect();
if (rect.left < window.innerWidth / 2) {
const leftOffset = Math.max(0, rect.right);
container.style.left = `${leftOffset}px`;
container.style.width = `calc(100vw - ${leftOffset}px)`;
container.style.right = "0px";
} else {
const rightOffset = Math.max(0, window.innerWidth - rect.left);
container.style.left = "0px";
container.style.width = `calc(100vw - ${rightOffset}px)`;
container.style.right = `${rightOffset}px`;
}
container.style.top = `${Math.max(0, rect.top)}px`;
container.style.height = `calc(100vh - ${Math.max(0, rect.top)}px)`;
} else {
container.style.left = "0px";
container.style.width = "100vw";
container.style.top = "0px";
container.style.height = "100vh";
}
};
updateOverlayBounds();
window.addEventListener("resize", updateOverlayBounds);
const closeBtn = document.createElement("span");
closeBtn.className = "pi pi-times";
closeBtn.title = "Close (Esc)";
Object.assign(closeBtn.style, {
position: "absolute", top: "16px", right: "24px", zIndex: "30",
cursor: "pointer", fontSize: "20px", color: "#aaa", transition: "color 0.15s ease",
background: "rgba(10,10,10,0.6)", borderRadius: "50%", padding: "4px"
});
closeBtn.onmouseenter = () => closeBtn.style.color = "#fff";
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
container.appendChild(closeBtn);
const overlay = new SidebarOverlay({ onDestroy });
const readerBox = document.createElement("div");
Object.assign(readerBox.style, {
@@ -363,32 +309,13 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
updateView();
readerBox.append(headerRow, textArea);
container.appendChild(readerBox);
const destroy = () => {
window.removeEventListener("resize", updateOverlayBounds);
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
container.remove();
onDestroy();
};
closeBtn.onclick = destroy;
container.onclick = (e) => { if (e.target === container) destroy(); };
const handleKeys = (e) => { if (e.key === "Escape") destroy(); };
globalKeydownHandler = handleKeys;
document.addEventListener("keydown", globalKeydownHandler);
document.body.appendChild(container);
overlay.container.appendChild(readerBox);
return {
isText: true,
loadTarget(targetData) {
if (typeof targetData !== "object" || !targetData.text) {
destroy();
overlay.destroy();
onSwitchMedia(targetData);
return;
}
@@ -397,6 +324,6 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
title.textContent = pid ? `Text Output #${pid}` : "Text Output";
updateView();
},
destroy
destroy: () => overlay.destroy()
};
}
+35 -23
View File
@@ -1,13 +1,12 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { State, promptStates, cardElements, saveStatesToLocalStorage, deletePromptState } from "./state.js";
import { State, promptStates, cardElements, scheduleStateSave, deletePromptState } from "./state.js";
import { isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl, matchesFilter, getRunOutputs } from "./utils.js";
import { showFullscreenPreview, isAudioViewerOpen } from "./comparison.js";
export let syncQueueFn = async () => {};
export function setSyncQueue(fn) { syncQueueFn = fn; }
// Global audio playback manager: ensures only 1 audio stream plays at a time
let currentlyPlayingAudio = null;
export function stopAllAudioPlayback() {
if (currentlyPlayingAudio) {
@@ -54,11 +53,18 @@ function updateScrollTopBtnVisibility() {
}
}
document.addEventListener("scroll", () => {
const scrollListener = () => {
if (State.sidebarContainer && State.sidebarContainer.isConnected) {
updateScrollTopBtnVisibility();
}
}, { capture: true, passive: true });
};
export function setupScrollListener() {
document.addEventListener("scroll", scrollListener, { capture: true, passive: true });
return () => {
document.removeEventListener("scroll", scrollListener, { capture: true, passive: true });
};
}
function findNodeIdForImage(state, img) {
if (!state || !state.nodeOutputs || !img) return null;
@@ -223,7 +229,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();
scheduleStateSave();
renderDOM();
}
});
@@ -235,7 +241,7 @@ export function setupSidebarUI() {
}
}
try { await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ clear: true }) }); } catch (err) {}
saveStatesToLocalStorage();
scheduleStateSave();
renderDOM();
});
@@ -320,6 +326,17 @@ export function setupSidebarUI() {
function syncCardButtonVisibility(cardObj, state) {
if (!cardObj) return;
// Pending and currently running cards should never display hover action panels
const isPendingOrActive = state.status === "pending" || state.status === "active";
if (isPendingOrActive) {
if (cardObj.hoverPanel) cardObj.hoverPanel.style.setProperty("display", "none", "important");
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.setProperty("display", "none", "important");
return;
} else {
if (cardObj.hoverPanel) cardObj.hoverPanel.style.removeProperty("display");
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.removeProperty("display");
}
const isCompleted = state.status === "completed";
const hasRealImages = isCompleted && state.images && state.images.length > 0 && !state.images.some(img => img.url && img.url.startsWith("blob:"));
@@ -360,6 +377,11 @@ function syncCardButtonVisibility(cardObj, state) {
}
}
if (cardObj.btnDel) {
cardObj.btnDel.style.removeProperty("display");
cardObj.btnDel.style.display = "inline-flex";
}
if (cardObj.btnFocus) {
const currentImg = hasRealImages ? state.images[cardObj.currentImageIndex || 0] : null;
const nodeId = currentImg ? findNodeIdForImage(state, currentImg) : null;
@@ -487,7 +509,6 @@ function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
const filename = img.filename || getFilenameFromUrl(src) || "audio.wav";
const ext = filename.split('.').pop().toUpperCase();
// Solid white SVGs for exact video card style consistency
const playIconSvg = `<svg viewBox="0 0 24 24" width="14" height="14" style="margin-left: 2px; pointer-events: none;"><polygon points="6,4 20,12 6,20" fill="#ffffff"/></svg>`;
const stopIconSvg = `<svg viewBox="0 0 24 24" width="12" height="12" style="pointer-events: none;"><rect x="5" y="5" width="14" height="14" rx="2" fill="#ffffff"/></svg>`;
@@ -509,7 +530,6 @@ function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
topRow.append(title, badge);
// Center Area: Waveform + Centered Play Button
const centerArea = document.createElement("div");
Object.assign(centerArea.style, { position: "relative", width: "100%", display: "flex", alignItems: "center", justifyContent: "center", margin: "6px 0" });
@@ -537,7 +557,6 @@ function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
centerArea.append(soundwave, playBtn);
// Bottom Row: Scrubber + Centered Time Label (with side padding so corner hover buttons don't block controls)
const bottomRow = document.createElement("div");
Object.assign(bottomRow.style, { display: "flex", flexDirection: "column", gap: "4px", width: "100%", padding: "0 34px", boxSizing: "border-box" });
@@ -547,9 +566,7 @@ function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
position: "relative", cursor: "pointer"
});
const scrubberFill = document.createElement("div");
Object.assign(scrubberFill.style, {
width: "0%", height: "100%", background: "#c084fc", borderRadius: "2px"
});
Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#c084fc", borderRadius: "2px" });
scrubber.appendChild(scrubberFill);
const timeLabel = document.createElement("div");
@@ -597,7 +614,6 @@ function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
const togglePlay = (e) => {
e.stopPropagation();
// If the fullscreen audio player is open, switch the modal player to this track
if (isAudioViewerOpen()) {
stopAllAudioPlayback();
showFullscreenPreview([fullUrl]);
@@ -635,15 +651,12 @@ function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
e.stopPropagation();
const rect = scrubber.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
if (audioEl.duration > 0) {
audioEl.currentTime = pos * audioEl.duration;
}
if (audioEl.duration > 0) audioEl.currentTime = pos * audioEl.duration;
};
previewAudio.append(topRow, centerArea, bottomRow, audioEl);
wrapper.appendChild(previewAudio);
// Clicking the card stops any other audio and launches the fullscreen player
previewAudio.onclick = (e) => {
if (e.target.closest('button, input') || e.target === scrubber || e.target === scrubberFill) return;
stopAllAudioPlayback();
@@ -738,7 +751,7 @@ function renderCardImages(cardObj, state) {
const res = await fetch(src, { method: "HEAD" });
if (res.status === 404) {
deletePromptState(state.pid);
saveStatesToLocalStorage();
scheduleStateSave();
if (cardObj.element) cardObj.element.remove();
return;
}
@@ -970,7 +983,6 @@ export function renderDOM() {
const headerSearchIcon = State.sidebarContainer.querySelector(".pi-search");
const headerActions = State.sidebarContainer.querySelector(".pi-eraser")?.parentNode;
// Submenu 1: Batch Submenu View
if (State.activeSubmenuBatchImages) {
const batchInfo = State.activeSubmenuBatchImages;
@@ -1070,7 +1082,6 @@ export function renderDOM() {
return;
}
// Submenu 2: Intermediate Outputs View
if (State.activeSubmenuPromptId) {
const st = promptStates.get(State.activeSubmenuPromptId);
if (!st) {
@@ -1183,7 +1194,6 @@ export function renderDOM() {
return;
}
// Standard Queue Main View
if (headerTitle) {
headerTitle.textContent = "Queue";
headerTitle.style.cursor = "default";
@@ -1354,7 +1364,7 @@ export function renderDOM() {
resetDeleteBtn();
promptStates.delete(state.pid);
await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ delete: [state.pid] }) });
saveStatesToLocalStorage();
scheduleStateSave();
renderDOM();
}
};
@@ -1405,7 +1415,9 @@ export function renderDOM() {
return cardObj.element;
};
for (const [pid, cardObj] of cardElements.entries()) { if (pid !== "pending-summary-card" && pid !== "pending-cancel-all-standalone" && !promptStates.has(pid)) cardElements.delete(pid); }
for (const [pid] of cardElements.entries()) {
if (pid !== "pending-summary-card" && pid !== "pending-cancel-all-standalone" && !promptStates.has(pid)) cardElements.delete(pid);
}
const pendingCount = Array.from(promptStates.values()).filter(t => t.status === "pending").length;
const targetElements = [];
+17 -105
View File
@@ -1,17 +1,14 @@
import { is3DFormat } from "./utils.js";
import { State } from "./state.js";
import { SidebarOverlay } from "./overlay.js";
let globalKeydownHandler = null;
let cachedThreeLibs = null;
async function getThreeLibs() {
if (cachedThreeLibs) return cachedThreeLibs;
if (window.THREE && window.THREE.GLTFLoader && window.THREE.OrbitControls) {
cachedThreeLibs = window.THREE;
return cachedThreeLibs;
}
try {
const [three, controls, gltf, obj, stl, ply] = await Promise.all([
import("https://esm.sh/three@0.170.0"),
@@ -38,55 +35,11 @@ async function getThreeLibs() {
}
export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () => {}) {
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
const container = document.createElement("div");
container.className = "comfy-sidebar-comparison-overlay comfy-sidebar-3d-overlay-root";
Object.assign(container.style, {
position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh",
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
zIndex: "1000", boxSizing: "border-box", overflow: "hidden",
pointerEvents: "auto", userSelect: "none", "-webkit-user-select": "none"
const overlay = new SidebarOverlay({
className: "comfy-sidebar-3d-overlay-root",
onDestroy
});
const updateOverlayBounds = () => {
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && sidebarEl.offsetWidth > 0 && sidebarEl.isConnected) {
const rect = sidebarEl.getBoundingClientRect();
if (rect.left < window.innerWidth / 2) {
const leftOffset = Math.max(0, rect.right);
container.style.left = `${leftOffset}px`;
container.style.width = `calc(100vw - ${leftOffset}px)`;
container.style.right = "0px";
} else {
const rightOffset = Math.max(0, window.innerWidth - rect.left);
container.style.left = "0px";
container.style.width = `calc(100vw - ${rightOffset}px)`;
container.style.right = `${rightOffset}px`;
}
container.style.top = `${Math.max(0, rect.top)}px`;
container.style.height = `calc(100vh - ${Math.max(0, rect.top)}px)`;
} else {
container.style.left = "0px";
container.style.width = "100vw";
container.style.top = "0px";
container.style.height = "100vh";
}
};
updateOverlayBounds();
window.addEventListener("resize", updateOverlayBounds);
let resizeObserver = null;
const sidebarEl = State.sidebarContainer?.closest('.comfyui-sidebar, .comfy-sidebar, .p-sidebar, [class*="sidebar"]') || State.sidebarContainer;
if (sidebarEl && window.ResizeObserver) {
resizeObserver = new ResizeObserver(() => updateOverlayBounds());
resizeObserver.observe(sidebarEl);
}
const header = document.createElement("div");
Object.assign(header.style, {
position: "absolute", top: "16px", left: "50%", transform: "translateX(-50%)",
@@ -98,26 +51,14 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const titleText = document.createElement("span");
titleText.textContent = baseSrc.split("/").pop().split("?")[0] || "3D Model";
header.appendChild(titleText);
container.appendChild(header);
const closeBtn = document.createElement("span");
closeBtn.className = "pi pi-times";
closeBtn.title = "Close (Esc)";
Object.assign(closeBtn.style, {
position: "absolute", top: "16px", right: "24px", zIndex: "30",
cursor: "pointer", fontSize: "20px", color: "#aaa", transition: "color 0.15s ease",
background: "rgba(10,10,10,0.6)", borderRadius: "50%", padding: "4px"
});
closeBtn.onmouseenter = () => closeBtn.style.color = "#fff";
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
container.appendChild(closeBtn);
overlay.container.appendChild(header);
const canvasContainer = document.createElement("div");
Object.assign(canvasContainer.style, {
width: "100%", height: "100%", position: "relative",
display: "flex", alignItems: "center", justifyContent: "center"
});
container.appendChild(canvasContainer);
overlay.container.appendChild(canvasContainer);
const loadingSpinner = document.createElement("div");
Object.assign(loadingSpinner.style, {
@@ -127,7 +68,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
display: "flex", alignItems: "center", gap: "10px"
});
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner" style="font-size: 18px;"></span> Loading 3D Asset...`;
container.appendChild(loadingSpinner);
overlay.container.appendChild(loadingSpinner);
const settingsPanel = document.createElement("div");
Object.assign(settingsPanel.style, {
@@ -298,30 +239,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
}
};
const destroy = () => {
window.removeEventListener("resize", updateOverlayBounds);
if (resizeObserver) resizeObserver.disconnect();
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
cleanup3D();
container.remove();
onDestroy();
};
closeBtn.onclick = destroy;
container.onclick = (e) => {
if (e.target === container || e.target === canvasContainer) destroy();
};
const handleKeys = (e) => {
if (e.key === "Escape") destroy();
};
globalKeydownHandler = handleKeys;
document.addEventListener("keydown", globalKeydownHandler);
document.body.appendChild(container);
overlay.addCleanup(cleanup3D);
const initSceneAndLoad = async (srcUrl) => {
loadingSpinner.style.display = "flex";
@@ -405,11 +323,9 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const prevPos = camera.position.clone();
const prevTarget = controls.target.clone();
if (val === "orthographic") {
camera = cameraOrtho;
} else {
camera = cameraPersp;
}
if (val === "orthographic") camera = cameraOrtho;
else camera = cameraPersp;
camera.position.copy(prevPos);
controls.object = camera;
controls.target.copy(prevTarget);
@@ -455,7 +371,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
createFormGroup("FOV", fovContainer),
createFormGroup("Light Intensity", lightContainer)
);
container.appendChild(settingsPanel);
overlay.container.appendChild(settingsPanel);
const btnGrid = createToolbarBtn("pi pi-table", "Grid", true, (v) => { gridHelper.visible = v; });
const btnRotate = createToolbarBtn("pi pi-sync", "Auto-Rotate", false, (v) => { autoRotate = v; });
@@ -496,7 +412,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
exportSelect.style.color = "#93c5fd";
toolbar.append(btnGrid, btnWire, btnRotate, btnResetCam, btnSettings, exportSelect);
container.appendChild(toolbar);
overlay.container.appendChild(toolbar);
const onResize = () => {
if (!canvasContainer || !renderer || !cameraPersp || !cameraOrtho) return;
@@ -517,6 +433,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
renderer.setSize(width, height);
};
window.addEventListener("resize", onResize);
overlay.addCleanup(() => window.removeEventListener("resize", onResize));
const animate = () => {
animId = requestAnimationFrame(animate);
@@ -557,16 +474,11 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
if (loadedObj) {
currentModel = loadedObj;
currentModel.traverse((child) => {
if (child.isMesh && child.material) {
child._originalMaterial = child.material;
}
if (child.isMesh && child.material) child._originalMaterial = child.material;
});
applyMaterialMode();
applyUpDirection();
scene.add(currentModel);
fitCameraToObject(camera, currentModel, controls, THREE);
gridHelper.position.y = new THREE.Box3().setFromObject(currentModel).min.y;
@@ -605,12 +517,12 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
is3D: true,
loadTarget(targetSrc) {
if (!is3DFormat(targetSrc)) {
destroy();
overlay.destroy();
onSwitchMedia(targetSrc);
return;
}
initSceneAndLoad(targetSrc);
},
destroy
destroy: () => overlay.destroy()
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
[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.3.1"
version = "1.3.2"
license = { file = "LICENSE" }
dependencies = []