Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
108ac61bec | ||
|
|
b31cfb5f8a | ||
|
|
fe154b7787 |
+65
-267
@@ -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, {
|
||||
@@ -525,23 +391,8 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
|
||||
});
|
||||
scrollContainer.appendChild(wrapper);
|
||||
|
||||
const hintPrompt = document.createElement("div");
|
||||
Object.assign(hintPrompt.style, {
|
||||
position: "absolute", bottom: "16px", left: "50%", transform: "translateX(-50%)",
|
||||
zIndex: "30", color: "#888", fontSize: "12px", fontFamily: "sans-serif",
|
||||
pointerEvents: "none", background: "rgba(10,10,10,0.75)", padding: "4px 10px",
|
||||
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);
|
||||
|
||||
let mediaA = createMediaElement(baseSrc, false);
|
||||
wrapper.appendChild(mediaA);
|
||||
|
||||
let mediaB = null;
|
||||
let isZoomed = false;
|
||||
|
||||
const slider = document.createElement("div");
|
||||
@@ -557,16 +408,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 +422,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 +443,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 +454,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 +465,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;
|
||||
@@ -667,17 +518,15 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
|
||||
wrapper.style.maxHeight = "85%";
|
||||
wrapper.style.width = "auto";
|
||||
wrapper.style.height = "auto";
|
||||
wrapper.style.aspectRatio = `${maxW} / ${maxH}`;
|
||||
wrapper.style.cursor = "zoom-in";
|
||||
|
||||
[mediaA, mediaB].forEach(el => {
|
||||
if (el) {
|
||||
el.style.maxWidth = "100%";
|
||||
el.style.maxHeight = "80vh";
|
||||
el.style.width = "100%";
|
||||
el.style.height = "100%";
|
||||
el.style.objectFit = "fill";
|
||||
el.style.aspectRatio = `${maxW} / ${maxH}`;
|
||||
el.style.width = "auto";
|
||||
el.style.height = "auto";
|
||||
el.style.objectFit = "contain";
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -696,10 +545,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 +559,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 +572,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 +620,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 +631,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 +651,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
|
||||
};
|
||||
}
|
||||
},
|
||||
destroy
|
||||
destroy: () => overlay.destroy()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Vendored
+133
-103
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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.3"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user