Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5074d5aec | ||
|
|
bbbbb7b686 | ||
|
|
daadd7c2ac | ||
|
|
9a7981d675 | ||
|
|
d4bc6b1849 | ||
|
|
78264c385a | ||
|
|
479ae0b73f |
@@ -6,21 +6,21 @@ A compact and powerful layout overhaul for ComfyUI that makes generation results
|
||||
|
||||
* **Compact, information-dense layout** with visual previews that let you see what is happening at a glance. Optional UI elements and sidebar tabs can be hidden when you don't need them.
|
||||
|
||||
<img src="assets/layout.png" alt="ComfySidebar layout" width="80%">
|
||||
<img src="assets/layout.png" alt="ComfySidebar layout" width="100%">
|
||||
|
||||
* **Left or right side:** Choose which side the sidebar appears on in ComfyUI settings.
|
||||
|
||||
* **Simplified mouse-friendly workflow** with drag-and-drop support and convenient actions for moving results back into the workflow, opening files, and managing outputs. Hold `Ctrl` for extra actions such as opening an image's location or deleting it directly from your device.
|
||||
|
||||
<img src="assets/controls.png" alt="ComfySidebar controls overview" width="80%">
|
||||
<img src="assets/controls.png" alt="ComfySidebar controls overview" width="100%">
|
||||
|
||||
* **Built-in comparison tool** for quickly comparing two generated results side by side.
|
||||
|
||||
<img src="assets/compare.png" alt="ComfySidebar compare" width="80%">
|
||||
<img src="assets/compare.png" alt="ComfySidebar compare" width="100%">
|
||||
|
||||
* **Runtime preview mode** combined with workflow overview makes ComfyUI practical for running and monitoring workflows in a more streamlined, queue-oriented way.
|
||||
|
||||
<img src="assets/runtime.png" alt="ComfySidebar runtime mode" width="80%">
|
||||
<img src="assets/runtime.png" alt="ComfySidebar runtime mode" width="100%">
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
|
||||
+3
-3
@@ -94,10 +94,10 @@ def open_file_in_os(target_path):
|
||||
if not os.path.exists(norm_path):
|
||||
raise FileNotFoundError(f"Path not found: {norm_path}")
|
||||
if os.path.isfile(norm_path):
|
||||
# Passed as a single argument so Windows Explorer highlights the file properly
|
||||
subprocess.Popen(["explorer", f"/select,{norm_path}"])
|
||||
# Use raw command string so Windows Explorer correctly parses /select,"path"
|
||||
subprocess.Popen(f'explorer /select,"{norm_path}"')
|
||||
else:
|
||||
subprocess.Popen(["explorer", norm_path])
|
||||
subprocess.Popen(f'explorer "{norm_path}"')
|
||||
|
||||
elif system == "Darwin": # macOS
|
||||
if not os.path.exists(target_path):
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 770 KiB After Width: | Height: | Size: 789 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
+516
-53
@@ -18,33 +18,46 @@ setRuntimePreviewStateListener(() => {
|
||||
const STYLE_ID = "comfy-sidebar-classic-layout-override";
|
||||
|
||||
const CLASSIC_LAYOUT_CSS_MEDIA = `
|
||||
/* Neutralize the leftover floating shell box to prevent empty dark square artifacts */
|
||||
.actionbar-container,
|
||||
.shadow-interface.rounded-lg.bg-comfy-menu-bg,
|
||||
.shadow-interface.rounded-lg.border-interface-stroke,
|
||||
.shadow-interface.border.rounded-lg:has([class*="actionbar-buttons"]),
|
||||
.shadow-interface.border.rounded-lg:has(.actionbar-buttons),
|
||||
div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]) {
|
||||
div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]),
|
||||
div.shadow-interface.py-1\\.75 {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Dock actionbar buttons in the topbar row with matching dark background and seamless border */
|
||||
[class*="actionbar"]:not(.actionbar),
|
||||
[class*="actionbar-buttons"],
|
||||
.actionbar-buttons {
|
||||
position: fixed !important;
|
||||
top: 3px !important;
|
||||
right: 4px !important;
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
left: auto !important;
|
||||
height: var(--topbar-height, 40px) !important;
|
||||
transform: none !important;
|
||||
z-index: 1010 !important;
|
||||
background: transparent !important;
|
||||
background: var(--comfy-menu-bg, #181818) !important;
|
||||
border: none !important;
|
||||
border-bottom: 1px solid var(--interface-stroke, var(--border-color, rgba(255, 255, 255, 0.12))) !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0 8px !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Constrain tabs boundary directly to the start of the action bar */
|
||||
.workflow-tabs-container {
|
||||
padding-right: var(--actionbar-width, 240px) !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.comfy-sidebar-extensions-override {
|
||||
@@ -67,51 +80,6 @@ div.border-interface-stroke.rounded-lg:has([class*="actionbar-buttons"]) {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
img[alt*="User Avatar"],
|
||||
img[alt*="user avatar"],
|
||||
button:has(img[alt*="User Avatar"]),
|
||||
[role="button"]:has(img[alt*="User Avatar"]),
|
||||
button:has(img[alt*="user avatar"]),
|
||||
[role="button"]:has(img[alt*="user avatar"]),
|
||||
.p-avatar,
|
||||
[data-pc-name="avatar"],
|
||||
button:has(.p-avatar),
|
||||
[role="button"]:has(.p-avatar),
|
||||
button:has([data-pc-name="avatar"]),
|
||||
[role="button"]:has([data-pc-name="avatar"]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 150px !important;
|
||||
padding-right: 580px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1599px) {
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 100px !important;
|
||||
padding-right: 420px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 40px !important;
|
||||
padding-right: 280px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
.p-tabview-nav-content,
|
||||
[class*="tabview"] {
|
||||
padding-left: 10px !important;
|
||||
padding-right: 180px !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function syncStockHistoryAndProgressSettings(enable) {
|
||||
@@ -133,6 +101,249 @@ export function syncStockHistoryAndProgressSettings(enable) {
|
||||
try { window.dispatchEvent(new Event("storage")); } catch (e) {}
|
||||
}
|
||||
|
||||
let savedButtonData = null;
|
||||
let domObserver = null;
|
||||
let syncScheduled = false;
|
||||
let actionbarResizeObserver = null;
|
||||
|
||||
function isSidebarOnLeft() {
|
||||
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
|
||||
if (!sidebar) return false;
|
||||
return sidebar.getBoundingClientRect().left < window.innerWidth / 2;
|
||||
}
|
||||
|
||||
function findSidebarBottomTarget(sidebar) {
|
||||
if (!sidebar) return null;
|
||||
|
||||
const candidates = Array.from(sidebar.querySelectorAll('button, [role="button"], a, .comfyui-sidebar-tab, .comfyui-sidebar-item'));
|
||||
const bottomBtn = candidates.find(el => {
|
||||
const text = el.textContent?.trim().toLowerCase();
|
||||
const title = (el.getAttribute('title') || el.getAttribute('aria-label') || '').toLowerCase();
|
||||
return text === 'help' || text === 'console' || text === 'shortcuts' || text === 'settings' ||
|
||||
title.includes('help') || title.includes('console') || title.includes('shortcuts') || title.includes('settings') ||
|
||||
el.querySelector('[class*="help"], [class*="terminal"], [class*="keyboard"], [class*="settings"], [class*="gear"], .pi-cog, .pi-question');
|
||||
});
|
||||
|
||||
return bottomBtn || null;
|
||||
}
|
||||
|
||||
function stripBorderRadius(canvasMenu) {
|
||||
if (!canvasMenu) return;
|
||||
const elements = canvasMenu.querySelectorAll("button, [role='button'], div, i, span");
|
||||
elements.forEach((el) => {
|
||||
el.style.setProperty("border-radius", "0px", "important");
|
||||
});
|
||||
}
|
||||
|
||||
function syncCanvasMenuPlacement(mode) {
|
||||
const HIDE_CANVAS_MENU_STYLE_ID = "comfy-sidebar-hide-canvas-menu-override";
|
||||
let hideStyleEl = document.getElementById(HIDE_CANVAS_MENU_STYLE_ID);
|
||||
|
||||
if (mode === "Hide") {
|
||||
if (!hideStyleEl) {
|
||||
hideStyleEl = document.createElement("style");
|
||||
hideStyleEl.id = HIDE_CANVAS_MENU_STYLE_ID;
|
||||
hideStyleEl.textContent = `
|
||||
.p-buttongroup.z-1200,
|
||||
.p-buttongroup.bottom-0.right-0,
|
||||
[class*="bottom-0"][class*="right-0"][class*="p-buttongroup"],
|
||||
.p-buttongroup:has([class*="w-15"]),
|
||||
.comfy-sidebar-unfloated-canvas-menu {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(hideStyleEl);
|
||||
}
|
||||
|
||||
// Return to original parent if it was docked, so the sidebar doesn't have an empty slot
|
||||
const canvasMenu = document.querySelector('.comfy-sidebar-unfloated-canvas-menu');
|
||||
if (canvasMenu && canvasMenu._originalParent) {
|
||||
canvasMenu._originalParent.insertBefore(canvasMenu, canvasMenu._originalNextSibling || null);
|
||||
canvasMenu.classList.remove("comfy-sidebar-unfloated-canvas-menu");
|
||||
const elements = canvasMenu.querySelectorAll("button, [role='button'], div, i, span");
|
||||
elements.forEach((el) => {
|
||||
el.style.removeProperty("border-radius");
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hideStyleEl) {
|
||||
hideStyleEl.remove();
|
||||
}
|
||||
|
||||
if (mode === "Dock" || mode === true) {
|
||||
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
|
||||
if (!sidebar) return;
|
||||
|
||||
const bottomTarget = findSidebarBottomTarget(sidebar);
|
||||
const targetParent = bottomTarget ? bottomTarget.parentNode : sidebar;
|
||||
|
||||
// Find any floating toolbar on the canvas
|
||||
const floatingMenu = document.querySelector(
|
||||
'.p-buttongroup.z-1200:not(.comfy-sidebar-unfloated-canvas-menu), ' +
|
||||
'.p-buttongroup.bottom-0.right-0:not(.comfy-sidebar-unfloated-canvas-menu), ' +
|
||||
'[class*="bottom-0"][class*="right-0"][class*="p-buttongroup"]:not(.comfy-sidebar-unfloated-canvas-menu), ' +
|
||||
'.p-buttongroup:has([class*="w-15"]):not(.comfy-sidebar-unfloated-canvas-menu)'
|
||||
);
|
||||
|
||||
const existingDocked = document.querySelector('.comfy-sidebar-unfloated-canvas-menu');
|
||||
|
||||
// Prevent duplication when switching sidebar sides: drop stale docked bar if a fresh floating one appeared
|
||||
if (floatingMenu && existingDocked && floatingMenu !== existingDocked) {
|
||||
existingDocked.remove();
|
||||
}
|
||||
|
||||
const canvasMenu = floatingMenu || existingDocked;
|
||||
if (!canvasMenu) return;
|
||||
|
||||
if (canvasMenu.parentNode !== targetParent) {
|
||||
if (!canvasMenu._originalParent) {
|
||||
canvasMenu._originalParent = canvasMenu.parentNode;
|
||||
canvasMenu._originalNextSibling = canvasMenu.nextSibling;
|
||||
}
|
||||
|
||||
if (bottomTarget && bottomTarget.parentNode === targetParent) {
|
||||
targetParent.insertBefore(canvasMenu, bottomTarget);
|
||||
} else {
|
||||
targetParent.appendChild(canvasMenu);
|
||||
}
|
||||
canvasMenu.classList.add("comfy-sidebar-unfloated-canvas-menu");
|
||||
|
||||
stripBorderRadius(canvasMenu);
|
||||
|
||||
if (!canvasMenu._clickGuardAttached) {
|
||||
canvasMenu._clickGuardAttached = true;
|
||||
const enforce = () => {
|
||||
requestAnimationFrame(() => stripBorderRadius(canvasMenu));
|
||||
};
|
||||
canvasMenu.addEventListener("click", enforce);
|
||||
canvasMenu.addEventListener("pointerup", enforce);
|
||||
}
|
||||
} else {
|
||||
stripBorderRadius(canvasMenu);
|
||||
}
|
||||
} else {
|
||||
const canvasMenu = document.querySelector('.comfy-sidebar-unfloated-canvas-menu');
|
||||
if (canvasMenu && canvasMenu._originalParent) {
|
||||
canvasMenu._originalParent.insertBefore(canvasMenu, canvasMenu._originalNextSibling || null);
|
||||
canvasMenu.classList.remove("comfy-sidebar-unfloated-canvas-menu");
|
||||
|
||||
const elements = canvasMenu.querySelectorAll("button, [role='button'], div, i, span");
|
||||
elements.forEach((el) => {
|
||||
el.style.removeProperty("border-radius");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips: dynamic side calculation (right for left-sidebar, left for right-sidebar)
|
||||
function setupCanvasMenuTooltipRelocator() {
|
||||
if (window._canvasTooltipRelocatorReady) return;
|
||||
window._canvasTooltipRelocatorReady = true;
|
||||
|
||||
document.addEventListener("mouseenter", (e) => {
|
||||
const targetBtn = e.target?.closest?.(
|
||||
".comfy-sidebar-unfloated-canvas-menu button, " +
|
||||
".comfy-sidebar-unfloated-canvas-menu [role='button'], " +
|
||||
".comfy-sidebar-unfloated-canvas-menu [data-pd-tooltip='true']"
|
||||
);
|
||||
if (!targetBtn) return;
|
||||
|
||||
const align = () => {
|
||||
const tooltipId = targetBtn.$_ptooltipId || targetBtn.getAttribute("aria-describedby");
|
||||
const tooltip = tooltipId ? document.getElementById(tooltipId) : document.querySelector(".p-tooltip:not(.p-tooltip-hidden)");
|
||||
if (!tooltip) return;
|
||||
|
||||
const btnRect = targetBtn.getBoundingClientRect();
|
||||
const tWidth = tooltip.offsetWidth || 80;
|
||||
const tHeight = tooltip.offsetHeight || 28;
|
||||
const onLeft = isSidebarOnLeft();
|
||||
|
||||
tooltip.classList.remove("p-tooltip-top", "p-tooltip-bottom", "p-tooltip-right", "p-tooltip-left");
|
||||
tooltip.classList.add(onLeft ? "p-tooltip-right" : "p-tooltip-left");
|
||||
|
||||
const posX = onLeft ? Math.round(btnRect.right + 8) : Math.round(btnRect.left - tWidth - 8);
|
||||
const posY = Math.round(btnRect.top + (btnRect.height - tHeight) / 2);
|
||||
|
||||
tooltip.style.setProperty("left", `${posX}px`, "important");
|
||||
tooltip.style.setProperty("top", `${posY}px`, "important");
|
||||
tooltip.style.setProperty("transform", "none", "important");
|
||||
};
|
||||
|
||||
queueMicrotask(align);
|
||||
requestAnimationFrame(align);
|
||||
}, true);
|
||||
}
|
||||
|
||||
// Popovers / Menus: positions popup on the screen side of the sidebar
|
||||
function setupCanvasMenuPopoverRelocator() {
|
||||
if (window._canvasPopoverRelocatorReady) return;
|
||||
window._canvasPopoverRelocatorReady = true;
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const btn = e.target?.closest?.(".comfy-sidebar-unfloated-canvas-menu button, .comfy-sidebar-unfloated-canvas-menu [role='button']");
|
||||
if (!btn) return;
|
||||
|
||||
const reposition = () => {
|
||||
const popovers = document.querySelectorAll(".p-popover");
|
||||
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
|
||||
if (!sidebar || !popovers.length) return;
|
||||
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
const btnRect = btn.getBoundingClientRect();
|
||||
const onLeft = sidebarRect.left < window.innerWidth / 2;
|
||||
|
||||
popovers.forEach((pop) => {
|
||||
if (pop.style.display !== "none" && !pop.classList.contains("p-overlay-hidden")) {
|
||||
const popWidth = pop.offsetWidth || 160;
|
||||
const popHeight = pop.offsetHeight || 100;
|
||||
const maxTop = window.innerHeight - popHeight - 12;
|
||||
const topPos = Math.max(12, Math.min(maxTop, Math.round(btnRect.top)));
|
||||
|
||||
const leftPos = onLeft
|
||||
? Math.round(sidebarRect.right + 8)
|
||||
: Math.round(sidebarRect.left - popWidth - 8);
|
||||
|
||||
pop.style.setProperty("left", `${leftPos}px`, "important");
|
||||
pop.style.setProperty("top", `${topPos}px`, "important");
|
||||
pop.style.setProperty("transform", "none", "important");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
[0, 15, 40, 100].forEach((ms) => setTimeout(reposition, ms));
|
||||
}, true);
|
||||
}
|
||||
|
||||
function updateTopMetrics() {
|
||||
const bar = document.querySelector('.actionbar-buttons, [class*="actionbar-buttons"]') || findTopbarContainer();
|
||||
const tabsEl = document.querySelector('.workflow-tabs-container');
|
||||
const topbar = tabsEl?.closest('header, .topbar, [class*="topbar"]') || tabsEl;
|
||||
|
||||
if (bar) {
|
||||
const rect = bar.getBoundingClientRect();
|
||||
const width = Math.ceil(rect.width);
|
||||
if (width > 80 && width < window.innerWidth * 0.8) {
|
||||
document.documentElement.style.setProperty('--actionbar-width', `${Math.round(width / 2)}px`);
|
||||
}
|
||||
|
||||
if (window.ResizeObserver && !actionbarResizeObserver) {
|
||||
actionbarResizeObserver = new ResizeObserver(() => {
|
||||
updateTopMetrics();
|
||||
});
|
||||
actionbarResizeObserver.observe(bar);
|
||||
}
|
||||
}
|
||||
|
||||
if (topbar) {
|
||||
const height = Math.round(topbar.getBoundingClientRect().height);
|
||||
if (height > 20) {
|
||||
document.documentElement.style.setProperty('--topbar-height', `${height}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyClassicLayout(enable, updateSetting = false) {
|
||||
let styleEl = document.getElementById(STYLE_ID);
|
||||
|
||||
@@ -150,8 +361,19 @@ export function applyClassicLayout(enable, updateSetting = false) {
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
styleEl.textContent = CLASSIC_LAYOUT_CSS_MEDIA;
|
||||
updateTopMetrics();
|
||||
window.removeEventListener("resize", updateTopMetrics);
|
||||
window.addEventListener("resize", updateTopMetrics);
|
||||
} else {
|
||||
if (styleEl) styleEl.remove();
|
||||
if (actionbarResizeObserver) {
|
||||
actionbarResizeObserver.disconnect();
|
||||
actionbarResizeObserver = null;
|
||||
}
|
||||
window.removeEventListener("resize", updateTopMetrics);
|
||||
document.documentElement.style.removeProperty('--actionbar-width');
|
||||
document.documentElement.style.removeProperty('--topbar-height');
|
||||
|
||||
if (updateSetting) {
|
||||
if (app.extensionManager?.setting) {
|
||||
app.extensionManager.setting.set("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
|
||||
@@ -162,9 +384,40 @@ export function applyClassicLayout(enable, updateSetting = false) {
|
||||
}
|
||||
}
|
||||
|
||||
let savedButtonData = null;
|
||||
let domObserver = null;
|
||||
let syncScheduled = false;
|
||||
function syncAvatarVisibility() {
|
||||
const hideAvatar = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Avatar") ?? false;
|
||||
const STYLE_AVATAR_ID = "comfy-sidebar-hide-avatar-override";
|
||||
let styleEl = document.getElementById(STYLE_AVATAR_ID);
|
||||
|
||||
if (hideAvatar) {
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement("style");
|
||||
styleEl.id = STYLE_AVATAR_ID;
|
||||
styleEl.textContent = `
|
||||
img[alt*="User Avatar"],
|
||||
img[alt*="user avatar"],
|
||||
button:has(img[alt*="User Avatar"]),
|
||||
[role="button"]:has(img[alt*="User Avatar"]),
|
||||
button:has(img[alt*="user avatar"]),
|
||||
[role="button"]:has(img[alt*="user avatar"]),
|
||||
.p-avatar,
|
||||
[data-pc-name="avatar"],
|
||||
button:has(.p-avatar),
|
||||
[role="button"]:has(.p-avatar),
|
||||
button:has([data-pc-name="avatar"]),
|
||||
[role="button"]:has([data-pc-name="avatar"]),
|
||||
button:has([class*="lucide--user"]),
|
||||
button:has(svg.lucide-user),
|
||||
[data-testid="user-profile-button"] {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
} else {
|
||||
if (styleEl) styleEl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function updateSidebarTabsVisibility() {
|
||||
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
|
||||
@@ -447,8 +700,21 @@ export function syncClassicLayout() {
|
||||
|
||||
try {
|
||||
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
|
||||
let canvasControlsMode = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Floating Canvas Controls");
|
||||
if (canvasControlsMode === undefined || canvasControlsMode === null) {
|
||||
const legacyDock = app.ui?.settings?.getSettingValue("Comfy Sidebar.Dock Canvas Controls");
|
||||
canvasControlsMode = legacyDock ? "Dock" : "Default";
|
||||
} else if (typeof canvasControlsMode === "boolean") {
|
||||
canvasControlsMode = canvasControlsMode ? "Dock" : "Default";
|
||||
}
|
||||
|
||||
if (isClassicLayoutEnabled) {
|
||||
updateTopMetrics();
|
||||
}
|
||||
|
||||
syncGraphButton();
|
||||
syncAvatarVisibility();
|
||||
syncCanvasMenuPlacement(canvasControlsMode);
|
||||
syncExtensionsPanel(isClassicLayoutEnabled);
|
||||
syncPropertiesButton(isClassicLayoutEnabled);
|
||||
syncActiveQueueIndicator(isClassicLayoutEnabled);
|
||||
@@ -468,6 +734,9 @@ export function syncClassicLayout() {
|
||||
}
|
||||
|
||||
export function setupPropertiesPanelToggleFix() {
|
||||
setupCanvasMenuTooltipRelocator();
|
||||
setupCanvasMenuPopoverRelocator();
|
||||
|
||||
if (!document.getElementById("comfy-sidebar-layout-fix-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "comfy-sidebar-layout-fix-styles";
|
||||
@@ -496,6 +765,187 @@ export function setupPropertiesPanelToggleFix() {
|
||||
.comfy-sidebar-custom-properties-toggle > span.comfy-sidebar-error-dot {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
/* Tooltip positioning: suppress top tooltip flash and show cleanly on left/right */
|
||||
body:has(.comfy-sidebar-unfloated-canvas-menu :hover) .p-tooltip-top {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
body:has(.comfy-sidebar-unfloated-canvas-menu :hover) .p-tooltip-left,
|
||||
body:has(.comfy-sidebar-unfloated-canvas-menu :hover) .p-tooltip-right {
|
||||
visibility: visible !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Docked canvas menu: vertical containment */
|
||||
.comfy-sidebar-unfloated-canvas-menu {
|
||||
position: static !important;
|
||||
bottom: auto !important;
|
||||
right: auto !important;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
z-index: 10 !important;
|
||||
flex-direction: column !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Strict containment: stops all children from expanding sidebar width */
|
||||
.comfy-sidebar-unfloated-canvas-menu,
|
||||
.comfy-sidebar-unfloated-canvas-menu * {
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Unconditional zero radius override across entire container and all states */
|
||||
html body .comfy-sidebar-unfloated-canvas-menu,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu *,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu *:hover,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu *:focus,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu *:active,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu button,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu button:hover,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu button:focus,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu button:active,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu [class*="hover:rounded"]:hover,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu .hover\\:rounded-lg\\!:hover,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu div,
|
||||
html body .comfy-sidebar-unfloated-canvas-menu div:hover {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Full-width rectangular button containers matching stock Help/Console */
|
||||
.comfy-sidebar-unfloated-canvas-menu > button,
|
||||
.comfy-sidebar-unfloated-canvas-menu > [role="button"],
|
||||
.comfy-sidebar-unfloated-canvas-menu > div {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: 38px !important;
|
||||
min-height: 38px !important;
|
||||
border-radius: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Full rectangular hover background across the full sidebar width */
|
||||
.comfy-sidebar-unfloated-canvas-menu > button:hover,
|
||||
.comfy-sidebar-unfloated-canvas-menu > [role="button"]:hover,
|
||||
.comfy-sidebar-unfloated-canvas-menu > div:hover,
|
||||
.comfy-sidebar-unfloated-canvas-menu button:hover {
|
||||
background: var(--interface-button-hover-surface, rgba(255, 255, 255, 0.08)) !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Neutralize CanvasModeSelector's inner selected background and padding */
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="bg-interface-panel-selected-surface"],
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="group-hover:bg-interface-button-hover-surface"] {
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Compact CanvasModeSelector padding and chevrons to eliminate extra sidebar width */
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="pr-0.5"],
|
||||
.comfy-sidebar-unfloated-canvas-menu div.flex.items-center {
|
||||
padding: 0 !important;
|
||||
gap: 2px !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Compact the Zoom button: strip w-15 (60px) and px-2 padding */
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="w-15"],
|
||||
.comfy-sidebar-unfloated-canvas-menu .w-15 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="px-2"],
|
||||
.comfy-sidebar-unfloated-canvas-menu span.px-2 {
|
||||
padding: 0 !important;
|
||||
gap: 2px !important;
|
||||
}
|
||||
|
||||
/* Compact down-arrow chevrons */
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="chevron-down"],
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="pr-1.5"] {
|
||||
padding: 0 !important;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
.comfy-sidebar-unfloated-canvas-menu button,
|
||||
.comfy-sidebar-unfloated-canvas-menu [role="button"] {
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
font-size: 11px !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Exact stock grey color #8a8a8a without opacity */
|
||||
.comfy-sidebar-unfloated-canvas-menu button,
|
||||
.comfy-sidebar-unfloated-canvas-menu [role="button"],
|
||||
.comfy-sidebar-unfloated-canvas-menu span,
|
||||
.comfy-sidebar-unfloated-canvas-menu svg {
|
||||
color: #8a8a8a !important;
|
||||
opacity: 1 !important;
|
||||
transition: color 0.15s ease !important;
|
||||
}
|
||||
|
||||
.comfy-sidebar-unfloated-canvas-menu button:hover,
|
||||
.comfy-sidebar-unfloated-canvas-menu [role="button"]:hover,
|
||||
.comfy-sidebar-unfloated-canvas-menu button:hover span,
|
||||
.comfy-sidebar-unfloated-canvas-menu [role="button"]:hover span,
|
||||
.comfy-sidebar-unfloated-canvas-menu button:hover svg,
|
||||
.comfy-sidebar-unfloated-canvas-menu [role="button"]:hover svg {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Consistent, uniform 38px horizontal separators */
|
||||
.comfy-sidebar-unfloated-canvas-menu::before,
|
||||
.comfy-sidebar-unfloated-canvas-menu::after,
|
||||
.comfy-sidebar-unfloated-canvas-menu .p-divider,
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="divider"],
|
||||
.comfy-sidebar-unfloated-canvas-menu hr,
|
||||
.comfy-sidebar-unfloated-canvas-menu > div:not(:has(*)):not(button),
|
||||
.comfy-sidebar-unfloated-canvas-menu > span:not(:has(*)):not(button),
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="w-px"],
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="w-\[1px\]"] {
|
||||
content: "" !important;
|
||||
width: 38px !important;
|
||||
height: 1px !important;
|
||||
min-height: 1px !important;
|
||||
max-height: 1px !important;
|
||||
min-width: 38px !important;
|
||||
max-width: 38px !important;
|
||||
border: none !important;
|
||||
background: var(--interface-stroke, var(--border-color, rgba(255, 255, 255, 0.15))) !important;
|
||||
margin: 4px auto !important;
|
||||
display: block !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
.comfy-sidebar-unfloated-canvas-menu .p-divider::before,
|
||||
.comfy-sidebar-unfloated-canvas-menu [class*="divider"]::before {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
@@ -541,12 +991,25 @@ export function setupPropertiesPanelToggleFix() {
|
||||
}
|
||||
|
||||
export function destroyLayoutFix() {
|
||||
syncCanvasMenuPlacement("Default");
|
||||
const hideCanvasStyle = document.getElementById("comfy-sidebar-hide-canvas-menu-override");
|
||||
if (hideCanvasStyle) hideCanvasStyle.remove();
|
||||
if (domObserver) {
|
||||
domObserver.disconnect();
|
||||
domObserver = null;
|
||||
}
|
||||
if (actionbarResizeObserver) {
|
||||
actionbarResizeObserver.disconnect();
|
||||
actionbarResizeObserver = null;
|
||||
}
|
||||
window.removeEventListener("resize", updateTopMetrics);
|
||||
document.documentElement.style.removeProperty('--actionbar-width');
|
||||
document.documentElement.style.removeProperty('--topbar-height');
|
||||
|
||||
const styleEl = document.getElementById(STYLE_ID);
|
||||
if (styleEl) styleEl.remove();
|
||||
const fixStyles = document.getElementById("comfy-sidebar-layout-fix-styles");
|
||||
if (fixStyles) fixStyles.remove();
|
||||
const avatarStyles = document.getElementById("comfy-sidebar-hide-avatar-override");
|
||||
if (avatarStyles) avatarStyles.remove();
|
||||
}
|
||||
+14
-7
@@ -132,17 +132,24 @@ export function setupVueNodeObserver() {
|
||||
};
|
||||
}
|
||||
|
||||
function getSelectedCanvasNodes() {
|
||||
const canvas = app.canvas;
|
||||
if (!canvas) return [];
|
||||
const raw = canvas.selected_nodes;
|
||||
if (raw instanceof Map || raw instanceof Set) {
|
||||
return Array.from(raw.values());
|
||||
}
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (raw && typeof raw === "object") return Object.values(raw);
|
||||
if (canvas.current_active_node) return [canvas.current_active_node];
|
||||
return [];
|
||||
}
|
||||
|
||||
export function toggleIgnoreActiveNode(onModified) {
|
||||
const canvas = app.canvas;
|
||||
if (!canvas) return;
|
||||
|
||||
let nodes = [];
|
||||
if (canvas.selected_nodes && Object.keys(canvas.selected_nodes).length > 0) {
|
||||
nodes = Object.values(canvas.selected_nodes);
|
||||
} else if (canvas.current_active_node) {
|
||||
nodes = [canvas.current_active_node];
|
||||
}
|
||||
|
||||
const nodes = getSelectedCanvasNodes();
|
||||
if (nodes.length === 0) return;
|
||||
|
||||
nodes.forEach(node => {
|
||||
|
||||
@@ -96,6 +96,11 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
const prompt = activeTasks[0];
|
||||
const newBlobUrl = URL.createObjectURL(e.detail);
|
||||
|
||||
// Clean up the previous unreferenced blob immediately to prevent browser memory leaks
|
||||
if (prompt._oldPreviewBlobUrl && prompt._oldPreviewBlobUrl !== prompt._previewBlobUrl) {
|
||||
try { URL.revokeObjectURL(prompt._oldPreviewBlobUrl); } catch (_) {}
|
||||
}
|
||||
|
||||
prompt._oldPreviewBlobUrl = prompt._previewBlobUrl;
|
||||
prompt._previewBlobUrl = newBlobUrl;
|
||||
prompt.images = [{ url: newBlobUrl }];
|
||||
@@ -142,7 +147,10 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
}
|
||||
};
|
||||
|
||||
const onExecutionSuccess = (e) => concludeRun(e.detail.prompt_id, PromptStatus.COMPLETED);
|
||||
const onExecutionSuccess = (e) => {
|
||||
const pid = e.detail?.prompt_id;
|
||||
if (pid) concludeRun(pid, PromptStatus.COMPLETED);
|
||||
};
|
||||
const onExecutionError = (e) => {
|
||||
const pid = e.detail?.prompt_id;
|
||||
const nodeId = e.detail?.node_id;
|
||||
@@ -151,7 +159,7 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
prompt.activeNodeId = String(nodeId);
|
||||
store.updatePrompt(String(pid), prompt);
|
||||
}
|
||||
concludeRun(e.detail.prompt_id, PromptStatus.ERROR);
|
||||
if (pid) concludeRun(pid, PromptStatus.ERROR);
|
||||
};
|
||||
const onExecutionInterrupted = () => {
|
||||
store.getAllPrompts()
|
||||
|
||||
@@ -104,7 +104,8 @@ export async function syncQueue() {
|
||||
if (prompt.status === PromptStatus.PENDING && !pendingIds.has(prompt.pid)) {
|
||||
store.deletePrompt(prompt.pid);
|
||||
} else if (prompt.status === PromptStatus.ACTIVE && !runningIds.has(prompt.pid)) {
|
||||
concludeRun(prompt.pid, PromptStatus.CANCELLED);
|
||||
// Do not prematurely mark as CANCELLED; let concludeRun verify via /history
|
||||
concludeRun(prompt.pid, PromptStatus.COMPLETED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,27 @@ function registerSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Avatar",
|
||||
name: "Hide Avatar",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Hide Junk.Floating Canvas Controls",
|
||||
name: "Floating Canvas Controls",
|
||||
type: "combo",
|
||||
options: ["Default", "Dock", "Hide"],
|
||||
defaultValue: "Default",
|
||||
onChange: () => {
|
||||
setTimeout(() => { syncClassicLayout(); }, 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: SettingIds.CLASSIC_LAYOUT,
|
||||
name: "Places the controls and the open workflow tabs on a single unified top bar",
|
||||
|
||||
@@ -24,6 +24,8 @@ export async function copyImageToClipboard(src) {
|
||||
}
|
||||
}
|
||||
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
export async function openFileOrFolder(img) {
|
||||
if (!img) return;
|
||||
|
||||
@@ -50,8 +52,6 @@ export async function openFileOrFolder(img) {
|
||||
a.remove();
|
||||
}
|
||||
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
export async function deleteFileOnServer(fileItem) {
|
||||
if (!fileItem || !fileItem.filename) return null;
|
||||
const isPermanent = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Permanent Delete on Disk Deletion") ?? false;
|
||||
|
||||
+117
-19
@@ -1,20 +1,35 @@
|
||||
import { isImageFormat, isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "../utils/utils.js";
|
||||
import { isImageFormat, isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl, findImagesInOutputs } from "../utils/utils.js";
|
||||
import { openFileOrFolder, removeImageFromNodeOutputs } from "./mediaActions.js";
|
||||
import { stopAllAudioPlayback, isAudioViewerOpen, setCurrentlyPlayingAudio, getCurrentlyPlayingAudio } from "./audioController.js";
|
||||
import { showFullscreenPreview } from "../utils/comparison.js";
|
||||
import { store } from "../core/store.js";
|
||||
import { PromptStatus } from "../core/constants.js";
|
||||
|
||||
export function findWorkflowBackgroundThumbnail(state, randParam = "") {
|
||||
if (!state) return null;
|
||||
// 1. Check if state.images already has a valid image asset
|
||||
const imgs = (state.images || []).filter(i => isImageFormat(i.filename || i.url) && !i.isFallback);
|
||||
let lastImg = imgs.length > 0 ? imgs[imgs.length - 1] : null;
|
||||
|
||||
// 2. Search all workflow node outputs for any generated image
|
||||
if (!lastImg && state.nodeOutputs) {
|
||||
const allOutputs = findImagesInOutputs(state.nodeOutputs, state.workflow);
|
||||
const validImgs = allOutputs.filter(i => isImageFormat(i.filename || i.url) && !i.isFallback);
|
||||
if (validImgs.length > 0) lastImg = validImgs[validImgs.length - 1];
|
||||
}
|
||||
|
||||
if (lastImg) {
|
||||
return lastImg.url || (window.location.origin + `/view?filename=${encodeURIComponent(lastImg.filename)}&type=${lastImg.type || 'output'}&subfolder=${encodeURIComponent(lastImg.subfolder || '')}${randParam}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function render3DCardPreview(cardObj, wrapper, src, img, state) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper");
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
|
||||
const imageAssets = state?.images?.filter(i => isImageFormat(i.filename || i.url)) || [];
|
||||
const lastImageAsset = imageAssets.length > 0 ? imageAssets[imageAssets.length - 1] : null;
|
||||
const bgImgSrc = lastImageAsset
|
||||
? (lastImageAsset.url || window.location.origin + `/view?filename=${encodeURIComponent(lastImageAsset.filename)}&type=${lastImageAsset.type || 'output'}&subfolder=${encodeURIComponent(lastImageAsset.subfolder || '')}`)
|
||||
: null;
|
||||
const bgImgSrc = findWorkflowBackgroundThumbnail(state, randParam);
|
||||
|
||||
if (!preview3D) {
|
||||
wrapper.innerHTML = "";
|
||||
@@ -57,6 +72,8 @@ export function render3DCardPreview(cardObj, wrapper, src, img, state) {
|
||||
newBgImg.src = bgImgSrc;
|
||||
newBgImg.alt = "3D Thumbnail Preview";
|
||||
preview3D.insertBefore(newBgImg, preview3D.firstChild);
|
||||
} else if (bgImg && !bgImgSrc) {
|
||||
bgImg.remove();
|
||||
}
|
||||
const title = preview3D.querySelector(".comfy-sidebar-3d-title");
|
||||
if (title) title.textContent = img.filename || "3D Model";
|
||||
@@ -92,11 +109,13 @@ export function render3DCardPreview(cardObj, wrapper, src, img, state) {
|
||||
}
|
||||
|
||||
export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
let previewAudio = wrapper.querySelector(".comfy-sidebar-audio-wrapper");
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
const filename = img.filename || getFilenameFromUrl(src) || "audio.wav";
|
||||
const ext = filename.split('.').pop().toUpperCase();
|
||||
const bgImgSrc = findWorkflowBackgroundThumbnail(state, randParam);
|
||||
|
||||
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>`;
|
||||
@@ -106,8 +125,16 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
previewAudio = document.createElement("div");
|
||||
previewAudio.className = "comfy-sidebar-audio-wrapper";
|
||||
|
||||
if (bgImgSrc) {
|
||||
const bgImg = document.createElement("img");
|
||||
bgImg.className = "comfy-sidebar-card-bg-img";
|
||||
bgImg.src = bgImgSrc;
|
||||
bgImg.alt = "Audio Cover Preview";
|
||||
previewAudio.appendChild(bgImg);
|
||||
}
|
||||
|
||||
const topRow = document.createElement("div");
|
||||
Object.assign(topRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", paddingLeft: "42px" });
|
||||
Object.assign(topRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", paddingLeft: "42px", position: "relative", zIndex: "2" });
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.className = "comfy-sidebar-audio-title";
|
||||
@@ -120,7 +147,7 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
topRow.append(title, badge);
|
||||
|
||||
const centerArea = document.createElement("div");
|
||||
Object.assign(centerArea.style, { position: "relative", width: "100%", display: "flex", alignItems: "center", justifyContent: "center", margin: "6px 0" });
|
||||
Object.assign(centerArea.style, { position: "relative", width: "100%", display: "flex", alignItems: "center", justifyContent: "center", margin: "6px 0", zIndex: "2" });
|
||||
|
||||
const soundwave = document.createElement("div");
|
||||
soundwave.className = "comfy-sidebar-soundwave-container";
|
||||
@@ -147,7 +174,7 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
centerArea.append(soundwave, playBtn);
|
||||
|
||||
const bottomRow = document.createElement("div");
|
||||
Object.assign(bottomRow.style, { display: "flex", flexDirection: "column", gap: "4px", width: "100%", padding: "0 34px", boxSizing: "border-box" });
|
||||
Object.assign(bottomRow.style, { display: "flex", flexDirection: "column", gap: "4px", width: "100%", padding: "0 34px", boxSizing: "border-box", position: "relative", zIndex: "2" });
|
||||
|
||||
const scrubber = document.createElement("div");
|
||||
Object.assign(scrubber.style, {
|
||||
@@ -156,7 +183,6 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
});
|
||||
const scrubberFill = document.createElement("div");
|
||||
Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#c084fc", borderRadius: "2px" });
|
||||
scrubberFill.style.borderRadius = "2px";
|
||||
scrubber.appendChild(scrubberFill);
|
||||
|
||||
const timeLabel = document.createElement("div");
|
||||
@@ -255,6 +281,21 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
}
|
||||
showFullscreenPreview([fullUrl], false, state?.pid);
|
||||
};
|
||||
} else {
|
||||
const bgImg = previewAudio.querySelector(".comfy-sidebar-card-bg-img");
|
||||
if (bgImg && bgImgSrc && bgImg.src !== bgImgSrc) {
|
||||
bgImg.src = bgImgSrc;
|
||||
} else if (!bgImg && bgImgSrc) {
|
||||
const newBgImg = document.createElement("img");
|
||||
newBgImg.className = "comfy-sidebar-card-bg-img";
|
||||
newBgImg.src = bgImgSrc;
|
||||
newBgImg.alt = "Audio Cover Preview";
|
||||
previewAudio.insertBefore(newBgImg, previewAudio.firstChild);
|
||||
} else if (bgImg && !bgImgSrc) {
|
||||
bgImg.remove();
|
||||
}
|
||||
const title = previewAudio.querySelector(".comfy-sidebar-audio-title");
|
||||
if (title) title.textContent = filename;
|
||||
}
|
||||
|
||||
cardObj.firstImgElement = previewAudio;
|
||||
@@ -285,14 +326,23 @@ export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state)
|
||||
const filename = img.filename || getFilenameFromUrl(src) || "output_file";
|
||||
const ext = (filename.split('.').pop() || "FILE").toUpperCase();
|
||||
const folderName = img.subfolder ? `${img.subfolder}/` : (img.type || "output");
|
||||
const bgImgSrc = findWorkflowBackgroundThumbnail(state, randParam);
|
||||
|
||||
if (!previewFile) {
|
||||
wrapper.innerHTML = "";
|
||||
previewFile = document.createElement("div");
|
||||
previewFile.className = "comfy-sidebar-file-wrapper";
|
||||
|
||||
if (bgImgSrc) {
|
||||
const bgImg = document.createElement("img");
|
||||
bgImg.className = "comfy-sidebar-card-bg-img";
|
||||
bgImg.src = bgImgSrc;
|
||||
bgImg.alt = "File Thumbnail Preview";
|
||||
previewFile.appendChild(bgImg);
|
||||
}
|
||||
|
||||
const topRow = document.createElement("div");
|
||||
Object.assign(topRow.style, { display: "flex", justifyContent: "flex-end", width: "100%", minHeight: "18px" });
|
||||
Object.assign(topRow.style, { display: "flex", justifyContent: "flex-end", width: "100%", minHeight: "18px", position: "relative", zIndex: "2" });
|
||||
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "comfy-sidebar-file-badge";
|
||||
@@ -300,7 +350,7 @@ export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state)
|
||||
topRow.appendChild(badge);
|
||||
|
||||
const centerArea = document.createElement("div");
|
||||
Object.assign(centerArea.style, { display: "flex", alignItems: "center", gap: "8px", width: "100%", margin: "2px 0 6px 0" });
|
||||
Object.assign(centerArea.style, { display: "flex", alignItems: "center", gap: "8px", width: "100%", margin: "2px 0 6px 0", position: "relative", zIndex: "2" });
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "pi pi-file comfy-sidebar-file-icon";
|
||||
@@ -313,7 +363,7 @@ export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state)
|
||||
centerArea.append(icon, title);
|
||||
|
||||
const bottomRow = document.createElement("div");
|
||||
Object.assign(bottomRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" });
|
||||
Object.assign(bottomRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", position: "relative", zIndex: "2" });
|
||||
|
||||
const folderInfo = document.createElement("div");
|
||||
folderInfo.className = "comfy-sidebar-file-subtext";
|
||||
@@ -328,6 +378,18 @@ export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state)
|
||||
previewFile.append(topRow, centerArea, bottomRow);
|
||||
wrapper.appendChild(previewFile);
|
||||
} else {
|
||||
const bgImg = previewFile.querySelector(".comfy-sidebar-card-bg-img");
|
||||
if (bgImg && bgImgSrc && bgImg.src !== bgImgSrc) {
|
||||
bgImg.src = bgImgSrc;
|
||||
} else if (!bgImg && bgImgSrc) {
|
||||
const newBgImg = document.createElement("img");
|
||||
newBgImg.className = "comfy-sidebar-card-bg-img";
|
||||
newBgImg.src = bgImgSrc;
|
||||
newBgImg.alt = "File Thumbnail Preview";
|
||||
previewFile.insertBefore(newBgImg, previewFile.firstChild);
|
||||
} else if (bgImg && !bgImgSrc) {
|
||||
bgImg.remove();
|
||||
}
|
||||
const title = previewFile.querySelector(".comfy-sidebar-file-title");
|
||||
if (title) { title.textContent = filename; title.title = filename; }
|
||||
const badge = previewFile.querySelector(".comfy-sidebar-file-badge");
|
||||
@@ -396,22 +458,57 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
wrapper.appendChild(ctrlOverlay);
|
||||
}
|
||||
|
||||
// Direct children check: safely disconnect any loading event listeners on previous media
|
||||
const existingTopImg = wrapper.querySelector(":scope > img, :scope > video");
|
||||
const existing3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper");
|
||||
const existingAudio = wrapper.querySelector(".comfy-sidebar-audio-wrapper");
|
||||
const existingFile = wrapper.querySelector(".comfy-sidebar-file-wrapper");
|
||||
|
||||
if (existingTopImg) {
|
||||
if (existingTopImg.tagName === "VIDEO") {
|
||||
existingTopImg.onloadedmetadata = null;
|
||||
existingTopImg.pause();
|
||||
existingTopImg.src = "";
|
||||
existingTopImg.load();
|
||||
} else {
|
||||
existingTopImg.onload = null;
|
||||
existingTopImg.src = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (is3D) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
if (existingTopImg) existingTopImg.remove();
|
||||
if (existingAudio) existingAudio.remove();
|
||||
if (existingFile) existingFile.remove();
|
||||
render3DCardPreview(cardObj, wrapper, src, img, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAudio) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
if (existingTopImg) existingTopImg.remove();
|
||||
if (existing3D) existing3D.remove();
|
||||
if (existingFile) existingFile.remove();
|
||||
renderAudioCardPreview(cardObj, wrapper, src, img, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isImage && !isVideo) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
if (existingTopImg) existingTopImg.remove();
|
||||
if (existing3D) existing3D.remove();
|
||||
if (existingAudio) existingAudio.remove();
|
||||
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
|
||||
return;
|
||||
}
|
||||
|
||||
let mediaEl = wrapper.querySelector("img, video");
|
||||
// Standard Image/Video mode: remove any 3D, audio, or file preview boxes
|
||||
if (existing3D) existing3D.remove();
|
||||
if (existingAudio) existingAudio.remove();
|
||||
if (existingFile) existingFile.remove();
|
||||
|
||||
let mediaEl = wrapper.querySelector(":scope > img, :scope > video");
|
||||
const needsRebuild = !mediaEl || (isVideo !== (mediaEl.tagName.toLowerCase() === "video"));
|
||||
|
||||
if (needsRebuild) {
|
||||
@@ -437,7 +534,6 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
return;
|
||||
}
|
||||
let activeSrc = mediaEl.currentSrc || mediaEl.src || src;
|
||||
// If it's a preview blob, snapshot the rendered pixels from the card so revoked blobs don't 404 in fullscreen
|
||||
if (activeSrc && activeSrc.startsWith("blob:") && mediaEl.naturalWidth > 0) {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -509,7 +605,6 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
||||
}
|
||||
|
||||
// If the card still has other images, persist and switch
|
||||
if (liveState.images && liveState.images.length > 0) {
|
||||
cardObj.currentImageIndex = 0;
|
||||
cardObj.lastImagesSignature = "";
|
||||
@@ -517,7 +612,6 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the card has text outputs or is in progress, persist
|
||||
if ((liveState.texts && liveState.texts.length > 0) || (liveState.status && liveState.status !== PromptStatus.COMPLETED)) {
|
||||
cardObj.lastImagesSignature = "";
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
@@ -533,8 +627,12 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
};
|
||||
|
||||
const applyDimensions = (width, height) => {
|
||||
// Strictly forbid dimension badges on 3D, Audio, or File cards
|
||||
if (is3D || isAudio || (!isImage && !isVideo)) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
if (cardObj.dimEl && width && height) {
|
||||
// Don't show the dimension badge for active/running preview frames
|
||||
if (state.status === "active" || state.status === "cancelled" || state.status === "error") {
|
||||
cardObj.dimEl.style.display = "none";
|
||||
return;
|
||||
|
||||
+16
-6
@@ -738,13 +738,17 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
|
||||
}
|
||||
};
|
||||
|
||||
mediaA.onload = () => {
|
||||
const onMediaReady = () => {
|
||||
hidePlaceholder();
|
||||
syncImageScales();
|
||||
};
|
||||
if (mediaA.complete && mediaA.naturalWidth) {
|
||||
hidePlaceholder();
|
||||
syncImageScales();
|
||||
|
||||
if (mediaA.tagName === "VIDEO") {
|
||||
mediaA.addEventListener("loadedmetadata", onMediaReady);
|
||||
if (mediaA.readyState >= 1) onMediaReady();
|
||||
} else {
|
||||
mediaA.onload = onMediaReady;
|
||||
if (mediaA.complete && mediaA.naturalWidth) onMediaReady();
|
||||
}
|
||||
|
||||
// Dynamically adjust preview scale if sidebars are resized
|
||||
@@ -912,6 +916,10 @@ export function showFullscreenPreview(imgSrcs, isShiftClick = false, pid = null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserClose = () => {
|
||||
if (isRuntimePreviewActive) setRuntimePreviewEnabled(false, false);
|
||||
};
|
||||
|
||||
// Handle Text Output
|
||||
if (typeof item === "object" && item.text) {
|
||||
if (activeComparisonViewer) {
|
||||
@@ -926,7 +934,8 @@ export function showFullscreenPreview(imgSrcs, isShiftClick = false, pid = null)
|
||||
activeComparisonViewer = createTextReader(
|
||||
item,
|
||||
(target) => showFullscreenPreview([target], false, pidStr),
|
||||
() => { activeComparisonViewer = null; }
|
||||
() => { activeComparisonViewer = null; },
|
||||
handleUserClose
|
||||
);
|
||||
if (activeComparisonViewer) activeComparisonViewer.targetPid = pidStr;
|
||||
return;
|
||||
@@ -948,7 +957,8 @@ export function showFullscreenPreview(imgSrcs, isShiftClick = false, pid = null)
|
||||
activeComparisonViewer = create3DViewer(
|
||||
src,
|
||||
(targetSrc) => showFullscreenPreview([targetSrc], false, pidStr),
|
||||
() => { activeComparisonViewer = null; }
|
||||
() => { activeComparisonViewer = null; },
|
||||
handleUserClose
|
||||
);
|
||||
if (activeComparisonViewer) activeComparisonViewer.targetPid = pidStr;
|
||||
return;
|
||||
|
||||
@@ -146,7 +146,11 @@ export async function renumberNodesTopologically() {
|
||||
|
||||
export async function setManualNodeId(targetNode = null) {
|
||||
if (!app.graph) return;
|
||||
const node = targetNode || (app.canvas?.selected_nodes ? Object.values(app.canvas.selected_nodes)[0] : null);
|
||||
const rawSelected = app.canvas?.selected_nodes;
|
||||
const selectedList = rawSelected instanceof Map || rawSelected instanceof Set
|
||||
? Array.from(rawSelected.values())
|
||||
: (Array.isArray(rawSelected) ? rawSelected : (rawSelected && typeof rawSelected === "object" ? Object.values(rawSelected) : []));
|
||||
const node = targetNode || selectedList[0] || app.canvas?.current_active_node || null;
|
||||
if (!node) {
|
||||
showNotification("Please select a node first to change its ID.", "warn");
|
||||
return;
|
||||
|
||||
@@ -339,6 +339,26 @@ export function injectStyles() {
|
||||
}
|
||||
|
||||
/* Audio Preview Cards */
|
||||
/* Generic Background Thumbnail for Non-Image Cards (Audio, 3D, File) */
|
||||
.comfy-sidebar-card-bg-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: brightness(0.35);
|
||||
transition: filter 0.2s ease, transform 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.comfy-sidebar-audio-wrapper:hover .comfy-sidebar-card-bg-img,
|
||||
.comfy-sidebar-file-wrapper:hover .comfy-sidebar-card-bg-img {
|
||||
filter: brightness(0.5);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.comfy-sidebar-audio-wrapper {
|
||||
width: 100%;
|
||||
min-height: 110px;
|
||||
@@ -425,6 +445,7 @@ export function injectStyles() {
|
||||
padding: 8px 10px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
transition: all 0.2s ease-in-out;
|
||||
user-select: none;
|
||||
|
||||
@@ -39,7 +39,7 @@ function parseInlineMarkdown(html) {
|
||||
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
|
||||
html = html.replace(/~~(.*?)~~/g, '<span style="text-decoration:line-through;color:#888;">$1</span>');
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:#61afef;text-decoration:none;border-bottom:1px dashed #61afef;cursor:pointer;">$1</a>');
|
||||
html = html.replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" style="color:#61afef;text-decoration:none;border-bottom:1px dashed #61afef;cursor:pointer;">$1</a>');
|
||||
html = html.replace(/\$(?!\s)([^\$]+?)(?<!\s)\$/g, '<span style="font-family:\'Times New Roman\',Times,serif;font-size:14px;color:#e5c07b;font-style:italic;padding:0 2px;">$1</span>');
|
||||
return html;
|
||||
}
|
||||
@@ -192,12 +192,12 @@ function parseMarkdown(text) {
|
||||
return htmlLines.join("\n");
|
||||
}
|
||||
|
||||
export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy = () => {}) {
|
||||
export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy = () => {}, onUserClose = () => {}) {
|
||||
let rawText = typeof textData === "object" ? (textData.text || "") : String(textData);
|
||||
let pid = typeof textData === "object" ? textData.pid : "";
|
||||
let isMarkdownMode = true;
|
||||
|
||||
const overlay = new SidebarOverlay({ onDestroy });
|
||||
const overlay = new SidebarOverlay({ onDestroy, onUserClose });
|
||||
|
||||
const readerBox = document.createElement("div");
|
||||
Object.assign(readerBox.style, {
|
||||
|
||||
+26
-7
@@ -292,18 +292,18 @@ export function findTextsInOutputs(outputs, rawWorkflow) {
|
||||
const val = outputs[nodeId][key];
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach(item => {
|
||||
if (typeof item === 'string' && !FILE_EXT_REGEX.test(item.trim())) {
|
||||
if (typeof item === 'string' && item.trim().length > 0 && !FILE_EXT_REGEX.test(item.trim())) {
|
||||
list.push(item);
|
||||
} else if (item && typeof item === 'object' && item.text) {
|
||||
if (Array.isArray(item.text)) list.push(...item.text);
|
||||
else if (typeof item.text === 'string') list.push(item.text);
|
||||
if (Array.isArray(item.text)) list.push(...item.text.filter(t => typeof t === 'string' && t.trim().length > 0));
|
||||
else if (typeof item.text === 'string' && item.text.trim().length > 0) list.push(item.text);
|
||||
}
|
||||
});
|
||||
} else if (typeof val === 'string' && !FILE_EXT_REGEX.test(val.trim())) {
|
||||
} else if (typeof val === 'string' && val.trim().length > 0 && !FILE_EXT_REGEX.test(val.trim())) {
|
||||
list.push(val);
|
||||
} else if (val && typeof val === 'object' && val.text) {
|
||||
if (Array.isArray(val.text)) list.push(...val.text);
|
||||
else if (typeof val.text === 'string') list.push(val.text);
|
||||
if (Array.isArray(val.text)) list.push(...val.text.filter(t => typeof t === 'string' && t.trim().length > 0));
|
||||
else if (typeof val.text === 'string' && val.text.trim().length > 0) list.push(val.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +314,26 @@ export function getRunOutputs(nodeOutputs, rawWorkflow, promptGraph = null) {
|
||||
const workflow = parseWorkflow(rawWorkflow);
|
||||
const list = [];
|
||||
if (!nodeOutputs) return list;
|
||||
for (const nodeId in nodeOutputs) {
|
||||
|
||||
// Track node execution order so the final executed node is always sorted last
|
||||
const nodeOrderMap = new Map();
|
||||
if (workflow && Array.isArray(workflow.nodes)) {
|
||||
workflow.nodes.forEach((n, idx) => {
|
||||
const order = typeof n.order === "number" ? n.order : idx;
|
||||
nodeOrderMap.set(String(n.id), order);
|
||||
});
|
||||
}
|
||||
|
||||
const nodeIds = Object.keys(nodeOutputs);
|
||||
// Sort node IDs by real execution order (lowest executes first; highest executes last)
|
||||
nodeIds.sort((a, b) => {
|
||||
const orderA = nodeOrderMap.has(String(a)) ? nodeOrderMap.get(String(a)) : Infinity;
|
||||
const orderB = nodeOrderMap.has(String(b)) ? nodeOrderMap.get(String(b)) : Infinity;
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const nodeId of nodeIds) {
|
||||
if (isNodeIgnored(nodeId, workflow)) continue;
|
||||
const nodeOut = nodeOutputs[nodeId];
|
||||
if (!nodeOut || (typeof nodeOut === "object" && Object.keys(nodeOut).length === 0)) {
|
||||
|
||||
@@ -54,10 +54,11 @@ async function getThreeLibs() {
|
||||
}
|
||||
}
|
||||
|
||||
export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () => {}) {
|
||||
export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () => {}, onUserClose = () => {}) {
|
||||
const overlay = new SidebarOverlay({
|
||||
className: "comfy-sidebar-3d-overlay-root",
|
||||
onDestroy
|
||||
onDestroy,
|
||||
onUserClose
|
||||
});
|
||||
|
||||
const header = document.createElement("div");
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfy-sidebar"
|
||||
description = "A compact and powerful layout overhaul for ComfyUI that makes generation results easier to browse, preview, compare, and manage."
|
||||
version = "1.7.0"
|
||||
version = "1.7.1"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"send2trash>=1.8.0",
|
||||
@@ -13,4 +13,4 @@ Repository = "https://github.com/m0rtus59/ComfyUI-ComfySidebar"
|
||||
[tool.comfy]
|
||||
PublisherId = "soundslikethunder"
|
||||
DisplayName = "Comfy Sidebar"
|
||||
Icon = "https://raw.githubusercontent.com/m0rtus59/ComfyUI-ComfySidebar/main/icon.png"
|
||||
Icon = "https://raw.githubusercontent.com/m0rtus59/ComfyUI-ComfySidebar/main/assets/icon.png"
|
||||
Reference in New Issue
Block a user