Compare commits

...
12 Commits
Author SHA1 Message Date
m0rtus59 717bb901e5 add locales 2026-10-17 09:53:10 +05:00
m0rtus59 426c7d5bd0 better compatibility with the recent frontend UI update 2026-10-14 22:56:51 +05:00
m0rtus59 4707921f81 improve ghost cards cleaning 2026-10-14 22:42:02 +05:00
m0rtus59 01b5b36522 improve 'comfy layout' toggle logic 2026-10-09 20:53:18 +05:00
m0rtus59 5068f155bc improved canvas toolbar docking logic 2026-10-09 20:10:20 +05:00
m0rtus59 f5074d5aec Fix preview memory leaks, false cancelled runs, and double-decker cards
- Fixed memory leak by revoking live preview blob URLs

- Fixed race condition that falsely marked completed runs as Cancelled

- Fixed double-decker card layout: 3D, Audio, and File cards now embed workflow images as background thumbnails

- Fixed Runtime Preview mode failing to deactivate after closing Text or 3D viewers

- Prevented video dimensions from leaking onto 3D and Audio cards

- Sorted workflow outputs by canvas execution order so final node output is always prioritized

- Fixed Windows Explorer failing to highlight files when paths have spaces
2026-10-02 22:45:24 +05:00
m0rtus59 bbbbb7b686 add hide canvas controls panel toggle 2026-10-02 21:09:34 +05:00
m0rtus59 daadd7c2ac Improvement 2026-10-01 20:54:33 +05:00
m0rtus59 9a7981d675 Add a toggle to unfloat the bottom floating toolbar 2026-09-28 00:05:49 +05:00
m0rtus59 d4bc6b1849 Improved tab stacking layout. Restored 'avatar' button with an optional toggle 2026-09-27 22:28:51 +05:00
m0rtus59 78264c385a Update readme 2026-09-26 20:57:13 +05:00
m0rtus59 479ae0b73f Update readme 2026-09-26 20:55:43 +05:00
35 changed files with 2507 additions and 253 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
__pycache__/
*.pyc
*.pyc
Thumbs.db
.DS_Store
+4 -4
View File
@@ -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
View File
@@ -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

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

+612 -60
View File
@@ -18,33 +18,45 @@ 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.rounded-lg.bg-comfy-menu-bg:not(.floating-panel):not([role="toolbar"]),
.shadow-interface.rounded-lg.border-interface-stroke:not(.floating-panel):not([role="toolbar"]),
.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 transparent background */
[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;
border: none !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 +79,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 +100,304 @@ 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;
canvasMenu.style.setProperty("border-radius", "0px", "important");
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 = `
[role="toolbar"]:has([data-testid="zoom-controls-button"]):has([data-testid="toggle-minimap-button"]),
.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");
canvasMenu.style.removeProperty("border-radius");
const elements = canvasMenu.querySelectorAll("*");
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;
// Stamp sidebar position and width for instant CSS positioning of modals
document.documentElement.dataset.sidebarPosition = isSidebarOnLeft() ? "left" : "right";
const sidebarWidth = Math.round(sidebar.getBoundingClientRect().width) || 60;
document.documentElement.style.setProperty('--comfy-sidebar-dock-width', `${sidebarWidth}px`);
const bottomTarget = findSidebarBottomTarget(sidebar);
const targetParent = bottomTarget ? bottomTarget.parentNode : sidebar;
// Find floating canvas toolbar by semantic role and test IDs, with class fallback
const floatingMenu = Array.from(
document.querySelectorAll('[role="toolbar"]')
).find((el) =>
!el.classList.contains("comfy-sidebar-unfloated-canvas-menu") &&
el.querySelector('[data-testid="zoom-controls-button"]') &&
el.querySelector('[data-testid="toggle-minimap-button"]')
) || 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 = () => {
if (!canvasMenu.classList.contains("comfy-sidebar-unfloated-canvas-menu")) return;
requestAnimationFrame(() => stripBorderRadius(canvasMenu));
};
canvasMenu.addEventListener("click", enforce);
canvasMenu.addEventListener("pointerup", enforce);
canvasMenu.addEventListener("mouseenter", enforce, true);
}
} else {
stripBorderRadius(canvasMenu);
}
} else {
// Return to floating mode: completely restore stock parent and enforce 8px rounded corners
const canvasMenu = document.querySelector('.comfy-sidebar-unfloated-canvas-menu') ||
document.querySelector('[role="toolbar"]') ||
document.querySelector('.floating-panel');
if (canvasMenu && canvasMenu.classList.contains("comfy-sidebar-unfloated-canvas-menu")) {
if (canvasMenu._originalParent) {
canvasMenu._originalParent.insertBefore(canvasMenu, canvasMenu._originalNextSibling || null);
}
canvasMenu.classList.remove("comfy-sidebar-unfloated-canvas-menu");
}
if (canvasMenu) {
canvasMenu.style.setProperty("border-radius", "8px", "important");
canvasMenu.querySelectorAll("*").forEach((el) => {
el.style.removeProperty("border-radius");
});
}
delete document.documentElement.dataset.sidebarPosition;
document.documentElement.style.removeProperty('--comfy-sidebar-dock-width');
}
}
// 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 popups and modals next to the docked sidebar button
function setupCanvasMenuPopoverRelocator() {
if (window._canvasPopoverRelocatorReady) return;
window._canvasPopoverRelocatorReady = true;
// Instantly pre-calculate coordinates on pointerdown so the Zoom modal renders in place with ZERO flash
const preAnchorZoomModal = (e) => {
const btn = e.target?.closest?.(
'.comfy-sidebar-unfloated-canvas-menu [data-testid="zoom-controls-button"], ' +
'.comfy-sidebar-unfloated-canvas-menu button[title*="Zoom"], ' +
'.comfy-sidebar-unfloated-canvas-menu button[aria-label*="Zoom"], ' +
'.comfy-sidebar-unfloated-canvas-menu [class*="w-15"]'
);
if (!btn) return;
const btnRect = btn.getBoundingClientRect();
const onLeft = btnRect.left < window.innerWidth / 2;
const modalWidth = 250;
const modalHeight = 165;
// Anchor 8px away from the button's edge
const leftPos = onLeft
? Math.round(btnRect.right + 8)
: Math.round(btnRect.left - modalWidth - 8);
// Center vertically with the button, clamped to viewport bounds
const maxTop = window.innerHeight - modalHeight - 12;
const topPos = Math.max(12, Math.min(maxTop, Math.round(btnRect.top - (modalHeight / 2) + (btnRect.height / 2))));
document.documentElement.style.setProperty('--zoom-modal-left', `${leftPos}px`);
document.documentElement.style.setProperty('--zoom-modal-top', `${topPos}px`);
};
document.addEventListener("pointerdown", preAnchorZoomModal, true);
document.addEventListener("click", preAnchorZoomModal, 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,21 +415,55 @@ 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 (updateSetting) {
if (app.extensionManager?.setting) {
app.extensionManager.setting.set("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
} else if (app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
}
if (actionbarResizeObserver) {
actionbarResizeObserver.disconnect();
actionbarResizeObserver = null;
}
window.removeEventListener("resize", updateTopMetrics);
document.documentElement.style.removeProperty('--actionbar-width');
document.documentElement.style.removeProperty('--topbar-height');
}
}
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 +746,35 @@ export function syncClassicLayout() {
try {
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
const tabsPos = app.extensionManager?.setting?.get?.("Comfy.Workflow.WorkflowTabsPosition") ??
app.ui?.settings?.getSettingValue("Comfy.Workflow.WorkflowTabsPosition");
// Real-time safeguard: if tabs are in Sidebar, disable Comfy Layout immediately
if (isClassicLayoutEnabled && tabsPos === "Sidebar") {
applyClassicLayout(false, false);
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set("Comfy Sidebar.Comfy Layout", false); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue("Comfy Sidebar.Comfy Layout", false); } catch (e) {}
}
return;
}
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 +794,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 +825,212 @@ export function setupPropertiesPanelToggleFix() {
.comfy-sidebar-custom-properties-toggle > span.comfy-sidebar-error-dot {
display: inline-flex !important;
}
/* Ensure floating canvas toolbar keeps its native rounded corners when undocked */
.floating-panel:not(.comfy-sidebar-unfloated-canvas-menu),
[role="toolbar"]:not(.comfy-sidebar-unfloated-canvas-menu),
.p-buttongroup:not(.comfy-sidebar-unfloated-canvas-menu) {
border-radius: 8px !important;
}
/* When docked, instantly anchor Zoom modal right beside the Zoom button with zero flash */
html:has(.comfy-sidebar-unfloated-canvas-menu) div.z-1300,
html:has(.comfy-sidebar-unfloated-canvas-menu) [class*="bottom-[62px]"],
html:has(.comfy-sidebar-unfloated-canvas-menu) [class*="bottom-\\[62px\\]"] {
position: fixed !important;
left: var(--zoom-modal-left, -9999px) !important;
top: var(--zoom-modal-top, -9999px) !important;
right: auto !important;
bottom: auto !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 inner selected backgrounds so idle mode is clean and transparent */
.comfy-sidebar-unfloated-canvas-menu > div div,
.comfy-sidebar-unfloated-canvas-menu > div button,
.comfy-sidebar-unfloated-canvas-menu > div [role="button"],
.comfy-sidebar-unfloated-canvas-menu [class*="bg-interface-panel-selected-surface"],
.comfy-sidebar-unfloated-canvas-menu [class*="group-hover:bg-interface-button-hover-surface"] {
background: transparent !important;
border-radius: 0 !important;
box-shadow: none !important;
}
/* Compact CanvasModeSelector padding and chevrons to eliminate extra sidebar width and ensure full-width highlight */
.comfy-sidebar-unfloated-canvas-menu [class*="pr-0.5"],
.comfy-sidebar-unfloated-canvas-menu div.flex.items-center,
.comfy-sidebar-unfloated-canvas-menu > div,
.comfy-sidebar-unfloated-canvas-menu > div > button {
padding: 0 !important;
margin: 0 !important;
gap: 2px !important;
justify-content: center !important;
width: 100% !important;
border-radius: 0 !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 +1076,29 @@ export function setupPropertiesPanelToggleFix() {
}
export function destroyLayoutFix() {
delete document.documentElement.dataset.sidebarPosition;
document.documentElement.style.removeProperty('--comfy-sidebar-dock-width');
document.documentElement.style.removeProperty('--zoom-modal-left');
document.documentElement.style.removeProperty('--zoom-modal-top');
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
View File
@@ -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 => {
+83
View File
@@ -0,0 +1,83 @@
import { app } from "/scripts/app.js";
let currentLocale = "en";
let translations = {};
let fallbackTranslations = {};
/**
* Initializes the localization engine by reading ComfyUI's language setting
* and dynamically loading the corresponding JSON dictionary.
*/
export async function initI18n() {
try {
// ComfyUI's internal setting ID is "Comfy.Locale"
let settingVal = app?.extensionManager?.setting?.get?.("Comfy.Locale")
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale")
|| app?.extensionManager?.setting?.get?.("Comfy.Locale.Language")
|| app?.ui?.settings?.getSettingValue?.("Comfy.Locale.Language");
// Fallback check in localStorage in case settings API hasn't populated yet
if (!settingVal) {
try {
settingVal = localStorage.getItem("Comfy.Settings.Comfy.Locale")
|| localStorage.getItem("Comfy.Locale");
} catch (e) {}
}
const raw = (settingVal || "en").toLowerCase().replace("_", "-");
// Check regional locale first (e.g. "zh-tw", "pt-br"), then base ("zh", "pt")
const candidates = [raw];
const base = raw.split("-")[0];
if (base !== raw) candidates.push(base);
currentLocale = candidates[0];
console.log("[ComfySidebar] Detected locale:", currentLocale);
// 1. Load English fallback dictionary
try {
const enUrl = new URL("../locales/en.json", import.meta.url).href;
const enRes = await fetch(enUrl);
if (enRes.ok) {
fallbackTranslations = await enRes.json();
}
} catch (e) {
console.warn("[ComfySidebar] Failed to load en.json fallback", e);
}
// 2. If language is not English, load target locale
if (base !== "en") {
let loaded = false;
for (const cand of candidates) {
try {
const locUrl = new URL(`../locales/${cand}.json`, import.meta.url).href;
const locRes = await fetch(locUrl);
if (locRes.ok) {
translations = await locRes.json();
console.log(`[ComfySidebar] Loaded ${cand}.json with`, Object.keys(translations).length, "translations");
loaded = true;
break;
}
} catch (e) {}
}
if (!loaded) {
console.warn(`[ComfySidebar] No translation file found for: ${candidates.join(" / ")}`);
}
}
} catch (e) {
console.warn("[ComfySidebar] i18n initialization failed", e);
}
}
/**
* Translate a phrase.
* If translation is missing in current language, falls back to English.
* If missing in English too, returns the original text.
*/
export function t(key, vars = {}) {
if (!key) return "";
let str = translations[key] || fallbackTranslations[key] || key;
for (const [k, v] of Object.entries(vars)) {
str = str.replaceAll(`{${k}}`, v);
}
return str;
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "Queue",
"Search History": "Search History",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "No nodes selected. Click a node on the canvas first to toggle ignore.",
"Renumber All Nodes by Execution Order (Alt+R)": "Renumber All Nodes by Execution Order (Alt+R)",
"Clear Cancelled & Failed": "Clear Cancelled & Failed",
"Clear All History": "Clear All History",
"Filter by text, images, nodes...": "Filter by text, images, nodes...",
"Clear & Close Search": "Clear & Close Search",
"Scroll to Top": "Scroll to Top",
"Batch of #": "Batch of #",
"Go Back": "Go Back",
"Go Back to Queue": "Go Back to Queue",
"Outputs of #": "Outputs of #",
"No separate intermediate outputs found.": "No separate intermediate outputs found.",
"Cancel All Pending": "Cancel All Pending",
"Pending Queue:": "Pending Queue:",
"tasks": "tasks",
"Download Object": "Download Object",
"Copy to Clipboard": "Copy to Clipboard",
"Download JSON": "Download JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "Delete Card (Hold Ctrl to delete file from disk)",
"Delete Card": "Delete Card",
"Delete File": "Delete File",
"Delete Files": "Delete Files",
"Show Node": "Show Node",
"Show Stopped/Failed Node": "Show Stopped/Failed Node",
"View all intermediate outputs": "View all intermediate outputs",
"Delete Card (Ctrl+Click to delete from disk)": "Delete Card (Ctrl+Click to delete from disk)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+Click again to delete file from DISK / TRASH",
"Click again to confirm removing card": "Click again to confirm removing card",
"Remove from batch (Hold Ctrl to delete file from disk)": "Remove from batch (Hold Ctrl to delete file from disk)",
"Click again to confirm removing from batch": "Click again to confirm removing from batch",
"Delete node output (Hold Ctrl to delete file from disk)": "Delete node output (Hold Ctrl to delete file from disk)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Click again to delete this node's files from DISK / TRASH",
"Click again to confirm removing output": "Click again to confirm removing output",
"Moved file to Recycle Bin.": "Moved file to Recycle Bin.",
"Permanently deleted file.": "Permanently deleted file.",
"Cancelled": "Cancelled",
"Error": "Error",
"Warning": "Warning",
"Cancel Queued Task": "Cancel Queued Task",
"Interrupt Execution": "Interrupt Execution",
"Click to read full text": "Click to read full text",
"No Outputs": "No Outputs",
"Processing...": "Processing...",
"Finishing...": "Finishing...",
"No workflow metadata found in this file.": "No workflow metadata found in this file.",
"Open Folder": "Open Folder",
"Open Location": "Open Location",
"View all images in this batch": "View all images in this batch",
"No nodes found on canvas to renumber.": "No nodes found on canvas to renumber.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).",
"Failed to renumber nodes: ": "Failed to renumber nodes: ",
"Please select a node first to change its ID.": "Please select a node first to change its ID.",
"Enter new ID for node #{id} ({name}):": "Enter new ID for node #{id} ({name}):",
"Please enter a valid positive number.": "Please enter a valid positive number.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "Node ID #{id} is already used by \"{name}\". Swap their IDs?",
"Assigned node ID #{id} to \"{name}\".": "Assigned node ID #{id} to \"{name}\".",
"Failed to set node ID: ": "Failed to set node ID: ",
"Video playback. Press Esc to close.": "Video playback. Press Esc to close.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Click image to zoom (100%/Fit) | Shift+Click another card to compare.",
"Toggle Loop": "Toggle Loop",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "Click image to zoom | Drag image to compare slider | Esc to close.",
"Copy": "Copy",
"Copied!": "Copied!",
"Text Output": "Text Output",
"Text Output #": "Text Output #",
"Raw Text": "Raw Text",
"{wc} words • {chars} chars": "{wc} words • {chars} chars",
"3D Model": "3D Model",
"Loading 3D Asset...": "Loading 3D Asset...",
"Exporting {format}...": "Exporting {format}...",
"Failed to load 3D engine.": "Failed to load 3D engine.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.",
"Background Color": "Background Color",
"Up Direction": "Up Direction",
"Material Mode": "Material Mode",
"Camera Type": "Camera Type",
"FOV": "FOV",
"Light Intensity": "Light Intensity",
"Original (Y-Up)": "Original (Y-Up)",
"Z-Up (+90°)": "Z-Up (+90°)",
"Inverted (-90°)": "Inverted (-90°)",
"Original / Textured": "Original / Textured",
"Clay / Studio Matte": "Clay / Studio Matte",
"Normals": "Normals",
"Perspective": "Perspective",
"Orthographic": "Orthographic",
"Grid": "Grid",
"Auto-Rotate": "Auto-Rotate",
"Wireframe": "Wireframe",
"Reset View": "Reset View",
"Settings": "Settings",
"Export As...": "Export As...",
"Failed to load 3D format": "Failed to load 3D format"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "Cola",
"Search History": "Buscar en el historial",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nodo(s) seleccionado(s) (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "No hay nodos seleccionados. Haz clic primero en un nodo del lienzo.",
"Renumber All Nodes by Execution Order (Alt+R)": "Renumerar nodos por orden de ejecución (Alt+R)",
"Clear Cancelled & Failed": "Limpiar cancelados y fallidos",
"Clear All History": "Limpiar todo el historial",
"Filter by text, images, nodes...": "Filtrar por texto, imágenes, nodos...",
"Clear & Close Search": "Limpiar y cerrar búsqueda",
"Scroll to Top": "Volver arriba",
"Batch of #": "Lote de #",
"Go Back": "Atrás",
"Go Back to Queue": "Volver a la cola",
"Outputs of #": "Salidas de #",
"No separate intermediate outputs found.": "No se encontraron salidas intermedias separadas.",
"Cancel All Pending": "Cancelar todos los pendientes",
"Pending Queue:": "Cola pendiente:",
"tasks": "tareas",
"Download Object": "Descargar objeto",
"Copy to Clipboard": "Copiar al portapapeles",
"Download JSON": "Descargar JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "Eliminar tarjeta (Ctrl para borrar del disco)",
"Delete Card": "Eliminar tarjeta",
"Delete File": "Eliminar archivo",
"Delete Files": "Eliminar archivos",
"Show Node": "Mostrar nodo",
"Show Stopped/Failed Node": "Mostrar nodo detenido o fallido",
"View all intermediate outputs": "Ver todas las salidas intermedias",
"Delete Card (Ctrl+Click to delete from disk)": "Eliminar tarjeta (Ctrl+Clic para borrar del disco)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+Clic de nuevo para borrar del DISCO",
"Click again to confirm removing card": "Haz clic de nuevo para confirmar la eliminación",
"Remove from batch (Hold Ctrl to delete file from disk)": "Quitar del lote (Ctrl para borrar del disco)",
"Click again to confirm removing from batch": "Haz clic de nuevo para quitar del lote",
"Delete node output (Hold Ctrl to delete file from disk)": "Eliminar salida (Ctrl para borrar del disco)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Clic de nuevo para borrar del DISCO",
"Click again to confirm removing output": "Haz clic de nuevo para eliminar la salida",
"Moved file to Recycle Bin.": "Archivo movido a la papelera.",
"Permanently deleted file.": "Archivo eliminado permanentemente.",
"Cancelled": "Cancelado",
"Error": "Error",
"Warning": "Advertencia",
"Cancel Queued Task": "Cancelar tarea en cola",
"Interrupt Execution": "Interrumpir ejecución",
"Click to read full text": "Haz clic para leer el texto completo",
"No Outputs": "Sin salidas",
"Processing...": "Procesando...",
"Finishing...": "Finalizando...",
"No workflow metadata found in this file.": "No se encontraron metadatos de flujo en este archivo.",
"Open Folder": "Abrir carpeta",
"Open Location": "Abrir ubicación",
"View all images in this batch": "Ver todas las imágenes de este lote",
"No nodes found on canvas to renumber.": "No se encontraron nodos para renumerar en el lienzo.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "Renumerar reescribirá los ID de todos los nodos según el orden de ejecución.\n\nEsto arregla salidas intermedias mostradas como resultado final, pero puede afectar a scripts externos.\n\n¿Deseas continuar?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "{count} nodos renumerados por orden de ejecución. Último nodo: #{id} ({name}).",
"Failed to renumber nodes: ": "Error al renumerar nodos: ",
"Please select a node first to change its ID.": "Selecciona primero un nodo para cambiar su ID.",
"Enter new ID for node #{id} ({name}):": "Introduce nuevo ID para el nodo #{id} ({name}):",
"Please enter a valid positive number.": "Introduce un número entero positivo válido.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "El ID #{id} ya está en uso por \"{name}\". ¿Intercambiar sus ID?",
"Assigned node ID #{id} to \"{name}\".": "Asignado ID #{id} al nodo \"{name}\".",
"Failed to set node ID: ": "Error al asignar ID de nodo: ",
"Video playback. Press Esc to close.": "Reproducción de video. Pulsa Esc para cerrar.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Clic para zoom (100%/Ajustar) | Shift+Clic en otra tarjeta para comparar.",
"Toggle Loop": "Bucle continuo",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "Proporciones incompatibles ({wA}x{hA} vs {wB}x{hB}). Las imágenes deben tener la misma relación de aspecto para compararse.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "Clic para zoom | Arrastra para comparar | Esc para cerrar.",
"Copy": "Copiar",
"Copied!": "¡Copiado!",
"Text Output": "Salida de texto",
"Text Output #": "Salida de texto #",
"Raw Text": "Texto sin formato",
"{wc} words • {chars} chars": "{wc} palabras • {chars} car.",
"3D Model": "Modelo 3D",
"Loading 3D Asset...": "Cargando modelo 3D...",
"Exporting {format}...": "Exportando a {format}...",
"Failed to load 3D engine.": "Error al cargar el motor 3D.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "No se pudieron descargar las bibliotecas 3D. Comprueba tu conexión a Internet.",
"Background Color": "Color de fondo",
"Up Direction": "Orientación vertical",
"Material Mode": "Modo de material",
"Camera Type": "Tipo de cámara",
"FOV": "Campo de visión (FOV)",
"Light Intensity": "Intensidad de luz",
"Original (Y-Up)": "Original (Y arriba)",
"Z-Up (+90°)": "Z arriba (+90°)",
"Inverted (-90°)": "Invertido (-90°)",
"Original / Textured": "Original / Textura",
"Clay / Studio Matte": "Arcilla / Mate",
"Normals": "Normales",
"Perspective": "Perspectiva",
"Orthographic": "Ortográfica",
"Grid": "Cuadrícula",
"Auto-Rotate": "Rotación automática",
"Wireframe": "Malla / Alambre",
"Reset View": "Restablecer vista",
"Settings": "Ajustes",
"Export As...": "Exportar como...",
"Failed to load 3D format": "Error al cargar el formato 3D"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "قائمة الانتظار",
"Search History": "البحث في السجل",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "تجاهل/تضمين العقد المحددة (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "لم يتم تحديد أي عقدة. انقر فوق عقدة في اللوحة أولاً.",
"Renumber All Nodes by Execution Order (Alt+R)": "إعادة ترقيم العقد وفقاً لترتيب التنفيذ (Alt+R)",
"Clear Cancelled & Failed": "مسح الملغاة والفاشلة",
"Clear All History": "مسح كل السجل",
"Filter by text, images, nodes...": "تصفية حسب النص، الصور، العقد...",
"Clear & Close Search": "مسح وإغلاق البحث",
"Scroll to Top": "التمرير إلى الأعلى",
"Batch of #": "دفعة #",
"Go Back": "رجوع",
"Go Back to Queue": "العودة إلى القائمة",
"Outputs of #": "مخرجات #",
"No separate intermediate outputs found.": "لم يتم العثور على مخرجات وسيطة منفصلة.",
"Cancel All Pending": "إلغاء جميع المهام المعلقة",
"Pending Queue:": "في الانتظار:",
"tasks": "مهام",
"Download Object": "تنزيل العنصر",
"Copy to Clipboard": "نسخ إلى الحافظة",
"Download JSON": "تنزيل JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "حذف البطاقة (اضغط Ctrl للحذف من القرص)",
"Delete Card": "حذف البطاقة",
"Delete File": "حذف الملف",
"Delete Files": "حذف الملفات",
"Show Node": "إظهار العقدة",
"Show Stopped/Failed Node": "إظهار العقدة المتوقفة أو الفاشلة",
"View all intermediate outputs": "عرض جميع المخرجات الوسيطة",
"Delete Card (Ctrl+Click to delete from disk)": "حذف البطاقة (Ctrl+نقر للحذف من القرص)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+نقر مجدداً للحذف النهائي من القرص",
"Click again to confirm removing card": "انقر مجدداً لتأكيد الإزالة",
"Remove from batch (Hold Ctrl to delete file from disk)": "إزالة من الدفعة (Ctrl للحذف من القرص)",
"Click again to confirm removing from batch": "انقر مجدداً لتأكيد الإزالة من الدفعة",
"Delete node output (Hold Ctrl to delete file from disk)": "حذف مخرج العقدة (Ctrl للحذف من القرص)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+نقر مجدداً لحذف ملفات العقدة من القرص",
"Click again to confirm removing output": "انقر مجدداً لتأكيد إزالة المخرج",
"Moved file to Recycle Bin.": "تم نقل الملف إلى سلة المحذوفات.",
"Permanently deleted file.": "تم حذف الملف نهائياً.",
"Cancelled": "ملغى",
"Error": "خطأ",
"Warning": "تحذير",
"Cancel Queued Task": "إلغاء المهمة في القائمة",
"Interrupt Execution": "إيقاف التنفيذ",
"Click to read full text": "انقر لقراءة النص كاملاً",
"No Outputs": "لا توجد مخرجات",
"Processing...": "جارٍ المعالجة...",
"Finishing...": "جارٍ الإنهاء...",
"No workflow metadata found in this file.": "لم يتم العثور على بيانات سير العمل في هذا الملف.",
"Open Folder": "فتح المجلد",
"Open Location": "فتح المسار",
"View all images in this batch": "عرض جميع صور هذه الدفعة",
"No nodes found on canvas to renumber.": "لم يتم العثور على عقد لإعادة ترقيمها.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "ستؤدي إعادة الترقيم إلى إعادة كتابة معرفات العقد حسب ترتيب التنفيذ.\n\nيحل هذا مشكلة المخرجات الوسيطة المعروضة كنتائج نهائية، ولكنه قد يؤثر على النصوص الخارجية المعتمدة على المعرفات.\n\nهل تريد المتابعة؟",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "تمت إعادة ترقيم {count} عقدة وفقاً للترتيب. آخر عقدة: #{id} ({name}).",
"Failed to renumber nodes: ": "فشلت إعادة ترقيم العقد: ",
"Please select a node first to change its ID.": "يرجى تحديد عقدة أولاً لتغيير معرّفها.",
"Enter new ID for node #{id} ({name}):": "أدخل المعرف الجديد للعقدة #{id} ({name}):",
"Please enter a valid positive number.": "يرجى إدخال رقم صحيح موجب.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "المعرف #{id} مستخدم بالفعل بواسطة \"{name}\". هل تريد تبديل المعرفات؟",
"Assigned node ID #{id} to \"{name}\".": "تم تعيين المعرف #{id} للعقدة \"{name}\".",
"Failed to set node ID: ": "فشل تعيين معرف العقدة: ",
"Video playback. Press Esc to close.": "تشغيل الفيديو. اضغط Esc للإغلاق.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "انقر للتكبير (100%/ملاءمة) | Shift+نقر على بطاقة أخرى للمقارنة.",
"Toggle Loop": "تبديل التكرار",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "عدم تطابق في الأبعاد ({wA}x{hA} مقابل {wB}x{hB}). يجب أن تكون الصور بنفس النسبة للمقارنة.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "انقر للتكبير | اسحب شريط التمرير للمقارنة | Esc للإغلاق.",
"Copy": "نسخ",
"Copied!": "تم النسخ!",
"Text Output": "مخرج نصي",
"Text Output #": "مخرج نصي #",
"Raw Text": "النص الأصلي",
"{wc} words • {chars} chars": "{wc} كلمة • {chars} حرف",
"3D Model": "نموذج 3D",
"Loading 3D Asset...": "جارٍ تحميل العنصر ثلاثي الأبعاد...",
"Exporting {format}...": "جارٍ التصدير بتنسيق {format}...",
"Failed to load 3D engine.": "فشل تحميل محرك 3D.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "تعذر تنزيل مكتبات 3D. تأكد من اتصال جهازك بالإنترنت.",
"Background Color": "لون الخلفية",
"Up Direction": "الاتجاه للأعلى",
"Material Mode": "وضع الخامة",
"Camera Type": "نوع الكاميرا",
"FOV": "مجال الرؤية (FOV)",
"Light Intensity": "شدة الإضاءة",
"Original (Y-Up)": "أصلي (Y للأعلى)",
"Z-Up (+90°)": "Z للأعلى (+90°)",
"Inverted (-90°)": "معكوس (-90°)",
"Original / Textured": "أصلي / إكساء",
"Clay / Studio Matte": "طين / استوديو غير لامع",
"Normals": "النواظم (Normals)",
"Perspective": "منظور",
"Orthographic": "إسقاط متوازي",
"Grid": "الشبكة",
"Auto-Rotate": "تدوير تلقائي",
"Wireframe": "إطار شبكي",
"Reset View": "إعادة ضبط المنظور",
"Settings": "الإعدادات",
"Export As...": "تصدير كـ...",
"Failed to load 3D format": "فشل تحميل تنسيق 3D"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "File d'attente",
"Search History": "Rechercher dans l'historique",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorer/Activer les nœuds sélectionnés (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "Aucun nœud sélectionné. Cliquez d'abord sur un nœud du canevas.",
"Renumber All Nodes by Execution Order (Alt+R)": "Renuméroter tous les nœuds selon l'ordre d'exécution (Alt+R)",
"Clear Cancelled & Failed": "Effacer annulés et échoués",
"Clear All History": "Effacer tout l'historique",
"Filter by text, images, nodes...": "Filtrer par texte, image, nœud...",
"Clear & Close Search": "Effacer et fermer la recherche",
"Scroll to Top": "Haut de page",
"Batch of #": "Lot de #",
"Go Back": "Retour",
"Go Back to Queue": "Retour à la file",
"Outputs of #": "Sorties de #",
"No separate intermediate outputs found.": "Aucune sortie intermédiaire distincte trouvée.",
"Cancel All Pending": "Tout annuler en attente",
"Pending Queue:": "File d'attente:",
"tasks": "tâches",
"Download Object": "Télécharger l'objet",
"Copy to Clipboard": "Copier dans le presse-papier",
"Download JSON": "Télécharger le JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "Supprimer la carte (Ctrl pour supprimer du disque)",
"Delete Card": "Supprimer la carte",
"Delete File": "Supprimer le fichier",
"Delete Files": "Supprimer les fichiers",
"Show Node": "Afficher le nœud",
"Show Stopped/Failed Node": "Afficher le nœud arrêté/échoué",
"View all intermediate outputs": "Voir toutes les sorties intermédiaires",
"Delete Card (Ctrl+Click to delete from disk)": "Supprimer la carte (Ctrl+Clic pour supprimer du disque)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+Clic à nouveau pour supprimer du DISQUE",
"Click again to confirm removing card": "Cliquez à nouveau pour confirmer la suppression",
"Remove from batch (Hold Ctrl to delete file from disk)": "Retirer du lot (Ctrl pour supprimer du disque)",
"Click again to confirm removing from batch": "Cliquez à nouveau pour confirmer le retrait",
"Delete node output (Hold Ctrl to delete file from disk)": "Supprimer la sortie (Ctrl pour supprimer du disque)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Clic à nouveau pour supprimer les fichiers du DISQUE",
"Click again to confirm removing output": "Cliquez à nouveau pour supprimer la sortie",
"Moved file to Recycle Bin.": "Fichier déplacé dans la corbeille.",
"Permanently deleted file.": "Fichier définitivement supprimé.",
"Cancelled": "Annulé",
"Error": "Erreur",
"Warning": "Avertissement",
"Cancel Queued Task": "Annuler la tâche",
"Interrupt Execution": "Interrompre l'exécution",
"Click to read full text": "Cliquer pour lire le texte complet",
"No Outputs": "Aucune sortie",
"Processing...": "Traitement...",
"Finishing...": "Finalisation...",
"No workflow metadata found in this file.": "Aucune métadonnée de workflow trouvée dans ce fichier.",
"Open Folder": "Ouvrir le dossier",
"Open Location": "Emplacement du fichier",
"View all images in this batch": "Voir toutes les images de ce lot",
"No nodes found on canvas to renumber.": "Aucun nœud à renuméroter sur le canevas.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "La renumérotation réécrira tous les identifiants de nœuds selon l'ordre d'exécution.\n\nCela corrige l'affichage des sorties intermédiaires, mais peut affecter les scripts externes dépendant d'identifiants précis.\n\nVoulez-vous continuer ?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "{count} nœuds renumérotés par ordre d'exécution. Dernier nœud : #{id} ({name}).",
"Failed to renumber nodes: ": "Échec de la renumérotation : ",
"Please select a node first to change its ID.": "Veuillez d'abord sélectionner un nœud pour modifier son ID.",
"Enter new ID for node #{id} ({name}):": "Entrez le nouvel ID pour le nœud #{id} ({name}) :",
"Please enter a valid positive number.": "Veuillez entrer un entier positif valide.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "L'ID #{id} est déjà utilisé par \"{name}\". Échanger leurs ID ?",
"Assigned node ID #{id} to \"{name}\".": "ID #{id} assigné au nœud \"{name}\".",
"Failed to set node ID: ": "Échec de l'assignation de l'ID : ",
"Video playback. Press Esc to close.": "Lecture vidéo. Appuyez sur Échap pour fermer.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Cliquer pour zoomer (100%/Ajuster) | Shift+Clic sur une autre carte pour comparer.",
"Toggle Loop": "Lecture en boucle",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "Proportions incompatibles ({wA}x{hA} vs {wB}x{hB}). Les images doivent avoir le même ratio pour être comparées.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "Cliquer pour zoomer | Glisser le curseur pour comparer | Échap pour fermer.",
"Copy": "Copier",
"Copied!": "Copié !",
"Text Output": "Sortie texte",
"Text Output #": "Sortie texte #",
"Raw Text": "Texte brut",
"{wc} words • {chars} chars": "{wc} mots • {chars} car.",
"3D Model": "Modèle 3D",
"Loading 3D Asset...": "Chargement de l'élément 3D...",
"Exporting {format}...": "Exportation au format {format}...",
"Failed to load 3D engine.": "Échec du chargement du moteur 3D.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "Impossible de télécharger les bibliothèques 3D. Vérifiez votre connexion Internet.",
"Background Color": "Couleur d'arrière-plan",
"Up Direction": "Orientation vers le haut",
"Material Mode": "Mode de rendu",
"Camera Type": "Type de caméra",
"FOV": "Champ de vision (FOV)",
"Light Intensity": "Intensité de la lumière",
"Original (Y-Up)": "Original (Y-Haut)",
"Z-Up (+90°)": "Z-Haut (+90°)",
"Inverted (-90°)": "Inversé (-90°)",
"Original / Textured": "Original / Texturé",
"Clay / Studio Matte": "Argile / Mat",
"Normals": "Normales",
"Perspective": "Perspective",
"Orthographic": "Orthographique",
"Grid": "Grille",
"Auto-Rotate": "Rotation auto",
"Wireframe": "Filaire",
"Reset View": "Réinitialiser la vue",
"Settings": "Paramètres",
"Export As...": "Exporter sous...",
"Failed to load 3D format": "Échec du chargement du format 3D"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "תור",
"Search History": "חיפוש בהיסטוריה",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "התעלם/כלול צמתים נבחרים בתור (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "לא נבחר צומת. לחץ תחילה על צומת בלוח.",
"Renumber All Nodes by Execution Order (Alt+R)": "מספר מחדש את כל הצמתים לפי סדר ביצוע (Alt+R)",
"Clear Cancelled & Failed": "נקה מבוטלים ונכשלים",
"Clear All History": "נקה את כל ההיסטוריה",
"Filter by text, images, nodes...": "סנן לפי טקסט, תמונה, צומת...",
"Clear & Close Search": "נקה וסגור חיפוש",
"Scroll to Top": "גלול למעלה",
"Batch of #": "אצווה #",
"Go Back": "חזור",
"Go Back to Queue": "חזור לתור",
"Outputs of #": "תוצרים של #",
"No separate intermediate outputs found.": "לא נמצאו תוצרי ביניים נפרדים.",
"Cancel All Pending": "בטל את כל המשימות הממתינות",
"Pending Queue:": "ממתינים בתור:",
"tasks": "משימות",
"Download Object": "הורד קובץ",
"Copy to Clipboard": "העתק ללוח",
"Download JSON": "הורד JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "מחק כרטיס (Ctrl למחיקה מהדיסק)",
"Delete Card": "מחק כרטיס",
"Delete File": "מחק קובץ",
"Delete Files": "מחק קבצים",
"Show Node": "הצג צומת",
"Show Stopped/Failed Node": "הצג צומת שנעצר או נכשל",
"View all intermediate outputs": "הצג את כל תוצרי הביניים",
"Delete Card (Ctrl+Click to delete from disk)": "מחק כרטיס (Ctrl+לחיצה למחיקה מהדיסק)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+לחיצה שוב למחיקה לצמיתות מהדיסק",
"Click again to confirm removing card": "לחץ שוב לאישור ההסרה",
"Remove from batch (Hold Ctrl to delete file from disk)": "הסר מהאצווה (Ctrl למחיקה מהדיסק)",
"Click again to confirm removing from batch": "לחץ שוב לאישור הסרה מהאצווה",
"Delete node output (Hold Ctrl to delete file from disk)": "מחק תוצר צומת (Ctrl למחיקה מהדיסק)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+לחיצה שוב למחיקת קבצי הצומת מהדיסק",
"Click again to confirm removing output": "לחץ שוב לאישור הסרת התוצר",
"Moved file to Recycle Bin.": "הקובץ הועבר לסל המיחזור.",
"Permanently deleted file.": "הקובץ נמחק לצמיתות.",
"Cancelled": "בוטל",
"Error": "שגיאה",
"Warning": "אזהרה",
"Cancel Queued Task": "בטל משימה בתור",
"Interrupt Execution": "הפסק ביצוע",
"Click to read full text": "לחץ לקריאת הטקסט המלא",
"No Outputs": "אין תוצרים",
"Processing...": "מעבד...",
"Finishing...": "מסיים...",
"No workflow metadata found in this file.": "לא נמצאו מטא-נתוני תהליך עבודה בקובץ זה.",
"Open Folder": "פתח תיקייה",
"Open Location": "פתח מיקום קובץ",
"View all images in this batch": "הצג את כל התמונות באצווה זו",
"No nodes found on canvas to renumber.": "לא נמצאו צמתים למספור מחדש בלוח.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "מספור מחדש ישכתב את מזהי כל הצמתים לפי סדר הביצוע.\n\nפעולה זו מתקנת תוצרי ביניים שמופיעים כתוצאה סופית, אך עלולה לפגוע בסקריפטים חיצוניים המסתמכים על מזהים קבועים.\n\nהאם להמשיך?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "מוספרו מחדש {count} צמתים לפי סדר ביצוע. צומת אחרון: #{id} ({name}).",
"Failed to renumber nodes: ": "נכשל מספור מחדש של צמתים: ",
"Please select a node first to change its ID.": "אנא בחר צומת תחילה כדי לשנות את המזהה שלו.",
"Enter new ID for node #{id} ({name}):": "הזן מזהה חדש לצומת #{id} ({name}):",
"Please enter a valid positive number.": "אנא הזן מספר חיובי שלם תקין.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "המזהה #{id} כבר בשימוש על ידי \"{name}\". האם להחליף ביניהם?",
"Assigned node ID #{id} to \"{name}\".": "המזהה #{id} הוקצה לצומת \"{name}\".",
"Failed to set node ID: ": "נכשל בהגדרת מזהה צומת: ",
"Video playback. Press Esc to close.": "הפעלת וידאו. לחץ Esc לסגירה.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "לחץ להגדלה (100%/התאמה) | Shift+לחיצה על כרטיס אחר להשוואה.",
"Toggle Loop": "הפעל/השבת חזרה",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "חוסר התאמה ביחס הגבהים ({wA}x{hA} מול {wB}x{hB}). התמונות חייבות להיות באותו יחס כדי להשוות.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "לחץ להגדלה | גרור את המחוון להשוואה | Esc לסגירה.",
"Copy": "העתק",
"Copied!": "הועתק!",
"Text Output": "תוצר טקסט",
"Text Output #": "תוצר טקסט #",
"Raw Text": "טקסט גולמי",
"{wc} words • {chars} chars": "{wc} מילים • {chars} תווים",
"3D Model": "דגם 3D",
"Loading 3D Asset...": "טוען אלמנט תלת-ממדי...",
"Exporting {format}...": "מייצא בפורמט {format}...",
"Failed to load 3D engine.": "טעינת מנוע 3D נכשלה.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "לא ניתן להוריד ספריות 3D. ודא שהמחשב מחובר לאינטרנט.",
"Background Color": "צבע רקע",
"Up Direction": "כיוון למעלה",
"Material Mode": "מצב חומר",
"Camera Type": "סוג מצלמה",
"FOV": "שדה ראייה (FOV)",
"Light Intensity": "עוצמת תאורה",
"Original (Y-Up)": "מקורי (Y למעלה)",
"Z-Up (+90°)": "Z למעלה (+90°)",
"Inverted (-90°)": "הפוך (-90°)",
"Original / Textured": "מקורי / טקסטורה",
"Clay / Studio Matte": "חימר / מט",
"Normals": "נורמלים",
"Perspective": "פרספקטיבה",
"Orthographic": "אורתוגרפי",
"Grid": "רשת",
"Auto-Rotate": "סיבוב אוטומטי",
"Wireframe": "שלד קווי (Wireframe)",
"Reset View": "אפס תצוגה",
"Settings": "הגדרות",
"Export As...": "ייצא כ...",
"Failed to load 3D format": "טעינת פורמט 3D נכשלה"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "Coda",
"Search History": "Cerca nella cronologia",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignora/Attiva nodo/i selezionato/i (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "Nessun nodo selezionato. Clicca prima su un nodo nell'area di lavoro.",
"Renumber All Nodes by Execution Order (Alt+R)": "Rinumera i nodi per ordine di esecuzione (Alt+R)",
"Clear Cancelled & Failed": "Cancella annullati e falliti",
"Clear All History": "Cancella tutta la cronologia",
"Filter by text, images, nodes...": "Filtra per testo, immagini, nodi...",
"Clear & Close Search": "Cancella e chiudi ricerca",
"Scroll to Top": "Torna in cima",
"Batch of #": "Lotto di #",
"Go Back": "Indietro",
"Go Back to Queue": "Torna alla coda",
"Outputs of #": "Uscite di #",
"No separate intermediate outputs found.": "Nessuna uscita intermedia separata trovata.",
"Cancel All Pending": "Annulla tutti i task in attesa",
"Pending Queue:": "In attesa:",
"tasks": "attività",
"Download Object": "Scarica elemento",
"Copy to Clipboard": "Copia negli appunti",
"Download JSON": "Scarica JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "Elimina scheda (Ctrl per eliminare dal disco)",
"Delete Card": "Elimina scheda",
"Delete File": "Elimina file",
"Delete Files": "Elimina file",
"Show Node": "Mostra nodo",
"Show Stopped/Failed Node": "Mostra nodo fermato o fallito",
"View all intermediate outputs": "Visualizza tutte le uscite intermedie",
"Delete Card (Ctrl+Click to delete from disk)": "Elimina scheda (Ctrl+Clic per cancellare dal disco)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+Clic di nuovo per eliminare definitivamente",
"Click again to confirm removing card": "Clicca di nuovo per confermare l'eliminazione",
"Remove from batch (Hold Ctrl to delete file from disk)": "Rimuovi dal lotto (Ctrl per eliminare dal disco)",
"Click again to confirm removing from batch": "Clicca di nuovo per rimuovere dal lotto",
"Delete node output (Hold Ctrl to delete file from disk)": "Elimina uscita nodo (Ctrl per eliminare dal disco)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Clic di nuovo per eliminare i file dal disco",
"Click again to confirm removing output": "Clicca di nuovo per confermare l'eliminazione",
"Moved file to Recycle Bin.": "File spostato nel cestino.",
"Permanently deleted file.": "File eliminato definitivamente.",
"Cancelled": "Annullato",
"Error": "Errore",
"Warning": "Avviso",
"Cancel Queued Task": "Annulla task in coda",
"Interrupt Execution": "Interrompi esecuzione",
"Click to read full text": "Clicca per leggere il testo completo",
"No Outputs": "Nessuna uscita",
"Processing...": "Elaborazione...",
"Finishing...": "Completamento...",
"No workflow metadata found in this file.": "Nessun metadato di workflow trovato in questo file.",
"Open Folder": "Apri cartella",
"Open Location": "Apri percorso",
"View all images in this batch": "Mostra tutte le immagini del lotto",
"No nodes found on canvas to renumber.": "Nessun nodo trovato da rinumerare.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "La rinumerazione riscriverà gli ID di tutti i nodi nell'ordine di esecuzione.\n\nQuesto risolverà le uscite intermedie mostrate come risultato finale, ma potrebbe interferire con script esterni.\n\nVuoi continuare?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "Rinumerati {count} nodi in ordine di esecuzione. Ultimo nodo: #{id} ({name}).",
"Failed to renumber nodes: ": "Errore nella rinumerazione dei nodi: ",
"Please select a node first to change its ID.": "Seleziona prima un nodo per cambiarne l'ID.",
"Enter new ID for node #{id} ({name}):": "Inserisci il nuovo ID per il nodo #{id} ({name}):",
"Please enter a valid positive number.": "Inserisci un numero intero positivo valido.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "L'ID #{id} è già utilizzato da \"{name}\". Vuoi scambiarli?",
"Assigned node ID #{id} to \"{name}\".": "Assegnato ID #{id} al nodo \"{name}\".",
"Failed to set node ID: ": "Errore nell'impostazione dell'ID: ",
"Video playback. Press Esc to close.": "Riproduzione video. Premi Esc per chiudere.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Clicca per ingrandire (100%/Adatta) | Shift+Clic su un'altra scheda per confrontare.",
"Toggle Loop": "Riproduzione continua",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "Proporzioni diverse ({wA}x{hA} vs {wB}x{hB}). Le immagini devono avere lo stesso formato per essere confrontate.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "Clicca per ingrandire | Trascina per confrontare | Esc per chiudere.",
"Copy": "Copia",
"Copied!": "Copiato!",
"Text Output": "Uscita testo",
"Text Output #": "Uscita testo #",
"Raw Text": "Testo non formattato",
"{wc} words • {chars} chars": "{wc} parole • {chars} car.",
"3D Model": "Modello 3D",
"Loading 3D Asset...": "Caricamento modello 3D...",
"Exporting {format}...": "Esportazione in {format}...",
"Failed to load 3D engine.": "Caricamento motore 3D non riuscito.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "Impossibile scaricare le librerie 3D. Verifica la connessione Internet.",
"Background Color": "Colore di sfondo",
"Up Direction": "Direzione verso l'alto",
"Material Mode": "Modalità materiale",
"Camera Type": "Tipo di telecamera",
"FOV": "Campo visivo (FOV)",
"Light Intensity": "Intensità luce",
"Original (Y-Up)": "Originale (Y verso l'alto)",
"Z-Up (+90°)": "Z verso l'alto (+90°)",
"Inverted (-90°)": "Invertito (-90°)",
"Original / Textured": "Originale / Texture",
"Clay / Studio Matte": "Argilla / Opaco",
"Normals": "Normali",
"Perspective": "Prospettiva",
"Orthographic": "Ortografica",
"Grid": "Griglia",
"Auto-Rotate": "Rotazione automatica",
"Wireframe": "Reticolo / Wireframe",
"Reset View": "Ripristina visuale",
"Settings": "Impostazioni",
"Export As...": "Esporta come...",
"Failed to load 3D format": "Caricamento formato 3D non riuscito"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "キュー",
"Search History": "履歴を検索",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "選択したノードをキューで無視/解除 (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "ノードが選択されていません。キャンバス上のノードをクリックしてください。",
"Renumber All Nodes by Execution Order (Alt+R)": "実行順にすべてのノード番号を振り直す (Alt+R)",
"Clear Cancelled & Failed": "キャンセルと失敗をクリア",
"Clear All History": "すべての履歴を消去",
"Filter by text, images, nodes...": "テキスト、画像、ノードで絞り込み...",
"Clear & Close Search": "検索をクリアして閉じる",
"Scroll to Top": "トップへスクロール",
"Batch of #": "バッチ #",
"Go Back": "戻る",
"Go Back to Queue": "キューに戻る",
"Outputs of #": "出力 #",
"No separate intermediate outputs found.": "個別のノード中間出力が見つかりません。",
"Cancel All Pending": "待機中のタスクをすべてキャンセル",
"Pending Queue:": "待機キュー:",
"tasks": "件のタスク",
"Download Object": "ファイルをダウンロード",
"Copy to Clipboard": "クリップボードにコピー",
"Download JSON": "JSONをダウンロード",
"Delete Card (Hold Ctrl to delete file from disk)": "カードを削除 (Ctrl長押しでディスクから削除)",
"Delete Card": "カードを削除",
"Delete File": "ファイルを削除",
"Delete Files": "ファイルを削除",
"Show Node": "ノードを表示",
"Show Stopped/Failed Node": "停止・失敗したノードを表示",
"View all intermediate outputs": "すべての中間出力を表示",
"Delete Card (Ctrl+Click to delete from disk)": "カードを削除 (Ctrl+クリックでディスクから削除)",
"Ctrl+Click again to delete file from DISK / TRASH": "ファイルをディスクから削除するには再度Ctrl+クリック",
"Click again to confirm removing card": "再度クリックして削除を確認",
"Remove from batch (Hold Ctrl to delete file from disk)": "バッチから削除 (Ctrlでディスクから削除)",
"Click again to confirm removing from batch": "再度クリックしてバッチからの削除を確認",
"Delete node output (Hold Ctrl to delete file from disk)": "ノード出力を削除 (Ctrlでディスクから削除)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "このノードのファイルを削除するには再度Ctrl+クリック",
"Click again to confirm removing output": "再度クリックして出力を削除",
"Moved file to Recycle Bin.": "ファイルをゴミ箱に移動しました。",
"Permanently deleted file.": "ファイルを完全に削除しました。",
"Cancelled": "キャンセル済み",
"Error": "エラー",
"Warning": "警告",
"Cancel Queued Task": "タスクをキャンセル",
"Interrupt Execution": "実行を中断",
"Click to read full text": "クリックして全文を読む",
"No Outputs": "出力なし",
"Processing...": "処理中...",
"Finishing...": "完了中...",
"No workflow metadata found in this file.": "このファイルにワークフローのメタデータが見つかりません。",
"Open Folder": "フォルダを開く",
"Open Location": "ファイルの場所を開く",
"View all images in this batch": "このバッチのすべての画像を表示",
"No nodes found on canvas to renumber.": "キャンバス上に番号を振り直すノードが見つかりません。",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "ノードの再番号付けは、トポロジカルな実行順序ですべてのIDを書き換えます。\n\nこれにより中間出力が最終結果として表示される問題が修正されますが、特定のIDに依存するスクリプトが動作しなくなる可能性があります。\n\n続行しますか?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "実行順に {count} 個のノードを再番号付けしました。最新ノード: #{id} ({name})。",
"Failed to renumber nodes: ": "ノードの再番号付けに失敗しました: ",
"Please select a node first to change its ID.": "IDを変更するノードを1つ選択してください。",
"Enter new ID for node #{id} ({name}):": "ノード #{id} ({name}) の新しいIDを入力してください:",
"Please enter a valid positive number.": "正の整数を入力してください。",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "ノードID #{id} はすでに \"{name}\" で使用されています。IDを入れ替えますか?",
"Assigned node ID #{id} to \"{name}\".": "ノード \"{name}\" にID #{id} を割り当てました。",
"Failed to set node ID: ": "ノードIDの設定に失敗しました: ",
"Video playback. Press Esc to close.": "動画再生。Escキーで閉じる。",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "画像クリックでズーム (100%/フィット) | Shift+クリックで他のカードと比較",
"Toggle Loop": "ループ再生の切り替え",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "アスペクト比が一致しません ({wA}x{hA} と {wB}x{hB})。比較するには同じ縦横比である必要があります。",
"Click image to zoom | Drag image to compare slider | Esc to close.": "クリックでズーム | ドラッグでスライダー操作 | Escキーで閉じる",
"Copy": "コピー",
"Copied!": "コピーしました!",
"Text Output": "テキスト出力",
"Text Output #": "テキスト出力 #",
"Raw Text": "プレーンテキスト",
"{wc} words • {chars} chars": "{wc} 単語 • {chars} 文字",
"3D Model": "3Dモデル",
"Loading 3D Asset...": "3Dアセットを読み込み中...",
"Exporting {format}...": "{format} 形式でエクスポート中...",
"Failed to load 3D engine.": "3Dエンジンの読み込みに失敗しました。",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "3Dライブラリをダウンロードできません。インターネット接続を確認してください。",
"Background Color": "背景色",
"Up Direction": "上方向の軸",
"Material Mode": "マテリアルモード",
"Camera Type": "カメラの種類",
"FOV": "視野角 (FOV)",
"Light Intensity": "光の強さ",
"Original (Y-Up)": "標準 (Y軸が上)",
"Z-Up (+90°)": "Z軸が上 (+90°)",
"Inverted (-90°)": "反転 (-90°)",
"Original / Textured": "オリジナル / テクスチャ",
"Clay / Studio Matte": "クレイ / スタジオマット",
"Normals": "法線 (ノーマル)",
"Perspective": "透視投影 (Perspective)",
"Orthographic": "正投影 (Orthographic)",
"Grid": "グリッド",
"Auto-Rotate": "自動回転",
"Wireframe": "ワイヤーフレーム",
"Reset View": "視点をリセット",
"Settings": "設定",
"Export As...": "エクスポート...",
"Failed to load 3D format": "3D形式の読み込みに失敗しました"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "대기열",
"Search History": "기록 검색",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "대기열에서 선택한 노드 무시/해제 (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "선택된 노드가 없습니다. 캔버스에서 노드를 먼저 클릭하세요.",
"Renumber All Nodes by Execution Order (Alt+R)": "실행 순서대로 모든 노드 재번호 매기기 (Alt+R)",
"Clear Cancelled & Failed": "취소 및 실패 항목 삭제",
"Clear All History": "모든 기록 삭제",
"Filter by text, images, nodes...": "텍스트, 이미지, 노드로 필터링...",
"Clear & Close Search": "검색 지우기 및 닫기",
"Scroll to Top": "맨 위로 이동",
"Batch of #": "배치 #",
"Go Back": "뒤로 가기",
"Go Back to Queue": "대기열로 돌아가기",
"Outputs of #": "출력 #",
"No separate intermediate outputs found.": "별도의 중간 노드 출력이 없습니다.",
"Cancel All Pending": "모든 대기 작업 취소",
"Pending Queue:": "대기열:",
"tasks": "개의 작업",
"Download Object": "파일 다운로드",
"Copy to Clipboard": "클립보드에 복사",
"Download JSON": "JSON 다운로드",
"Delete Card (Hold Ctrl to delete file from disk)": "카드 삭제 (Ctrl 누른 채 클릭 시 디스크에서 삭제)",
"Delete Card": "카드 삭제",
"Delete File": "파일 삭제",
"Delete Files": "파일 삭제",
"Show Node": "노드 표시",
"Show Stopped/Failed Node": "중단/실패한 노드 표시",
"View all intermediate outputs": "모든 중간 출력 보기",
"Delete Card (Ctrl+Click to delete from disk)": "카드 삭제 (Ctrl+클릭으로 디스크에서 삭제)",
"Ctrl+Click again to delete file from DISK / TRASH": "디스크에서 파일을 영구 삭제하려면 다시 Ctrl+클릭",
"Click again to confirm removing card": "카드를 제거하려면 다시 클릭",
"Remove from batch (Hold Ctrl to delete file from disk)": "배치에서 제거 (Ctrl 누른 채 디스크에서 삭제)",
"Click again to confirm removing from batch": "배치에서 제거하려면 다시 클릭",
"Delete node output (Hold Ctrl to delete file from disk)": "노드 출력 삭제 (Ctrl 누른 채 디스크에서 삭제)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "이 노드의 파일을 디스크에서 삭제하려면 다시 Ctrl+클릭",
"Click again to confirm removing output": "출력을 제거하려면 다시 클릭",
"Moved file to Recycle Bin.": "휴지통으로 파일을 이동했습니다.",
"Permanently deleted file.": "파일을 영구 삭제했습니다.",
"Cancelled": "취소됨",
"Error": "오류",
"Warning": "경고",
"Cancel Queued Task": "작업 취소",
"Interrupt Execution": "실행 중단",
"Click to read full text": "전체 텍스트 읽기",
"No Outputs": "출력 없음",
"Processing...": "처리 중...",
"Finishing...": "마무리 중...",
"No workflow metadata found in this file.": "이 파일에서 워크플로 메타데이터를 찾을 수 없습니다.",
"Open Folder": "폴더 열기",
"Open Location": "파일 위치 열기",
"View all images in this batch": "이 배치의 모든 이미지 보기",
"No nodes found on canvas to renumber.": "캔버스에 재번호를 매길 노드가 없습니다.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "노드 재번호 매기기는 실행 순서에 따라 캔버스의 모든 노드 ID를 다시 씁니다.\n\n중간 노드 출력이 최종 결과로 표시되는 문제를 해결하지만, 특정 ID에 의존하는 외부 스크립트가 손상될 수 있습니다.\n\n계속하시겠습니까?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "실행 순서대로 {count}개의 노드 번호를 다시 매겼습니다. 최신 노드: #{id} ({name}).",
"Failed to renumber nodes: ": "노드 재번호 매기기 실패: ",
"Please select a node first to change its ID.": "ID를 변경할 노드를 먼저 선택하세요.",
"Enter new ID for node #{id} ({name}):": "노드 #{id} ({name})의 새 ID 입력:",
"Please enter a valid positive number.": "올바른 양의 정수를 입력하세요.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "노드 ID #{id}는 이미 \"{name}\"에서 사용 중입니다. 두 노드의 ID를 맞바꾸시겠습니까?",
"Assigned node ID #{id} to \"{name}\".": "\"{name}\"에 노드 ID #{id}를 할당했습니다.",
"Failed to set node ID: ": "노드 ID 설정 실패: ",
"Video playback. Press Esc to close.": "동영상 재생 중. Esc를 눌러 닫기.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "이미지 클릭 시 확대 (100%/맞춤) | Shift+클릭으로 다른 카드와 비교",
"Toggle Loop": "반복 재생 전환",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "가로세로 비율이 다릅니다 ({wA}x{hA} vs {wB}x{hB}). 비교하려면 비율이 같아야 합니다.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "클릭하여 확대 | 슬라이더를 드래그하여 비교 | Esc로 닫기",
"Copy": "복사",
"Copied!": "복사됨!",
"Text Output": "텍스트 출력",
"Text Output #": "텍스트 출력 #",
"Raw Text": "원본 텍스트",
"{wc} words • {chars} chars": "{wc} 단어 • {chars} 글자",
"3D Model": "3D 모델",
"Loading 3D Asset...": "3D 에셋 로드 중...",
"Exporting {format}...": "{format} 내보내는 중...",
"Failed to load 3D engine.": "3D 엔진 로드 실패.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "3D 라이브러리를 다운로드할 수 없습니다. 인터넷 연결을 확인하세요.",
"Background Color": "배경색",
"Up Direction": "상향 축",
"Material Mode": "머티리얼 모드",
"Camera Type": "카메라 종류",
"FOV": "시야각 (FOV)",
"Light Intensity": "조명 강도",
"Original (Y-Up)": "기본 (Y축 위)",
"Z-Up (+90°)": "Z축 위 (+90°)",
"Inverted (-90°)": "반전 (-90°)",
"Original / Textured": "기본 / 텍스처",
"Clay / Studio Matte": "클레이 / 스튜디오 무광",
"Normals": "노멀 (법선)",
"Perspective": "원근 투영",
"Orthographic": "직교 투영",
"Grid": "격자",
"Auto-Rotate": "자동 회전",
"Wireframe": "와이어프레임",
"Reset View": "시점 초기화",
"Settings": "설정",
"Export As...": "내보내기...",
"Failed to load 3D format": "3D 형식 로드 실패"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "Fila",
"Search History": "Pesquisar histórico",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Ignorar/Incluir nó(s) selecionado(s) na fila (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "Nenhum nó selecionado. Clique primeiro em um nó na tela.",
"Renumber All Nodes by Execution Order (Alt+R)": "Renumerar todos os nós por ordem de execução (Alt+R)",
"Clear Cancelled & Failed": "Limpar cancelados e com falha",
"Clear All History": "Limpar todo o histórico",
"Filter by text, images, nodes...": "Filtrar por texto, imagens, nós...",
"Clear & Close Search": "Limpar e fechar busca",
"Scroll to Top": "Voltar ao topo",
"Batch of #": "Lote de #",
"Go Back": "Voltar",
"Go Back to Queue": "Voltar para a fila",
"Outputs of #": "Saídas de #",
"No separate intermediate outputs found.": "Nenhuma saída intermediária separada encontrada.",
"Cancel All Pending": "Cancelar todas as tarefas pendentes",
"Pending Queue:": "Fila pendente:",
"tasks": "tarefas",
"Download Object": "Baixar objeto",
"Copy to Clipboard": "Copiar para a área de transferência",
"Download JSON": "Baixar JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "Excluir cartão (Ctrl para apagar do disco)",
"Delete Card": "Excluir cartão",
"Delete File": "Excluir arquivo",
"Delete Files": "Excluir arquivos",
"Show Node": "Mostrar nó",
"Show Stopped/Failed Node": "Mostrar nó interrompido ou com falha",
"View all intermediate outputs": "Ver todas as saídas intermediárias",
"Delete Card (Ctrl+Click to delete from disk)": "Excluir cartão (Ctrl+Clique para apagar do disco)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+Clique novamente para excluir permanentemente",
"Click again to confirm removing card": "Clique novamente para confirmar a remoção",
"Remove from batch (Hold Ctrl to delete file from disk)": "Remover do lote (Ctrl para apagar do disco)",
"Click again to confirm removing from batch": "Clique novamente para remover do lote",
"Delete node output (Hold Ctrl to delete file from disk)": "Excluir saída (Ctrl para apagar do disco)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Clique novamente para apagar do disco",
"Click again to confirm removing output": "Clique novamente para confirmar a exclusão",
"Moved file to Recycle Bin.": "Arquivo movido para a Lixeira.",
"Permanently deleted file.": "Arquivo excluído permanentemente.",
"Cancelled": "Cancelado",
"Error": "Erro",
"Warning": "Aviso",
"Cancel Queued Task": "Cancelar tarefa da fila",
"Interrupt Execution": "Interromper execução",
"Click to read full text": "Clique para ler o texto completo",
"No Outputs": "Sem saídas",
"Processing...": "Processando...",
"Finishing...": "Finalizando...",
"No workflow metadata found in this file.": "Nenhum metadado de fluxo de trabalho encontrado.",
"Open Folder": "Abrir pasta",
"Open Location": "Abrir local do arquivo",
"View all images in this batch": "Ver todas as imagens deste lote",
"No nodes found on canvas to renumber.": "Nenhum nó encontrado na tela para renumerar.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "A renumeração reescreverá os IDs de todos os nós de acordo com a ordem de execução.\n\nIsso corrige saídas intermediárias mostradas como resultado final, mas pode interferir em scripts externos.\n\nDeseja continuar?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "{count} nós renumerados por ordem de execução. Último nó: #{id} ({name}).",
"Failed to renumber nodes: ": "Falha ao renumerar nós: ",
"Please select a node first to change its ID.": "Selecione primeiro um nó para alterar o ID.",
"Enter new ID for node #{id} ({name}):": "Digite o novo ID para o nó #{id} ({name}):",
"Please enter a valid positive number.": "Insira um número inteiro positivo válido.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "O ID #{id} já está em uso por \"{name}\". Deseja inverter os IDs?",
"Assigned node ID #{id} to \"{name}\".": "ID #{id} atribuído ao nó \"{name}\".",
"Failed to set node ID: ": "Falha ao definir o ID do nó: ",
"Video playback. Press Esc to close.": "Reprodução de vídeo. Pressione Esc para fechar.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Clique na imagem para zoom (100%/Ajustar) | Shift+Clique em outro cartão para comparar.",
"Toggle Loop": "Repetição contínua",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "Proporção incompatível ({wA}x{hA} vs {wB}x{hB}). Imagens devem ter o mesmo formato para comparação.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "Clique para zoom | Arraste para comparar | Esc para fechar.",
"Copy": "Copiar",
"Copied!": "Copiado!",
"Text Output": "Saída de texto",
"Text Output #": "Saída de texto #",
"Raw Text": "Texto puro",
"{wc} words • {chars} chars": "{wc} palavras • {chars} car.",
"3D Model": "Modelo 3D",
"Loading 3D Asset...": "Carregando modelo 3D...",
"Exporting {format}...": "Exportando para {format}...",
"Failed to load 3D engine.": "Falha ao carregar o motor 3D.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "Não foi possível baixar bibliotecas 3D. Verifique sua conexão com a Internet.",
"Background Color": "Cor de fundo",
"Up Direction": "Eixo superior",
"Material Mode": "Modo de material",
"Camera Type": "Tipo de câmera",
"FOV": "Campo de visão (FOV)",
"Light Intensity": "Intensidade da luz",
"Original (Y-Up)": "Original (Y para cima)",
"Z-Up (+90°)": "Z para cima (+90°)",
"Inverted (-90°)": "Invertido (-90°)",
"Original / Textured": "Original / Texturizado",
"Clay / Studio Matte": "Argila / Fosco",
"Normals": "Normais",
"Perspective": "Perspectiva",
"Orthographic": "Ortográfica",
"Grid": "Grade",
"Auto-Rotate": "Rotação automática",
"Wireframe": "Aramado",
"Reset View": "Redefinir visão",
"Settings": "Configurações",
"Export As...": "Exportar como...",
"Failed to load 3D format": "Falha ao carregar formato 3D"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "Очередь",
"Search History": "Поиск по истории",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Игнорировать выбранные ноды в очереди (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "Ноды не выбраны. Сначала выберите ноду на холсте.",
"Renumber All Nodes by Execution Order (Alt+R)": "Перенумеровать ноды по порядку выполнения (Alt+R)",
"Clear Cancelled & Failed": "Очистить отменённые и с ошибкой",
"Clear All History": "Очистить всю историю",
"Filter by text, images, nodes...": "Поиск по тексту, файлам, нодам...",
"Clear & Close Search": "Очистить и закрыть поиск",
"Scroll to Top": "Наверх",
"Batch of #": "Пакет #",
"Go Back": "Назад",
"Go Back to Queue": "Назад в очередь",
"Outputs of #": "Результаты #",
"No separate intermediate outputs found.": "Промежуточные результаты не найдены.",
"Cancel All Pending": "Отменить все в очереди",
"Pending Queue:": "Очередь ожидания:",
"tasks": "задач",
"Download Object": "Скачать объект",
"Copy to Clipboard": "Скопировать в буфер",
"Download JSON": "Скачать JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "Удалить карточку (Ctrl для удаления с диска)",
"Delete Card": "Удалить карточку",
"Delete File": "Удалить файл",
"Delete Files": "Удалить файлы",
"Show Node": "Показать ноду",
"Show Stopped/Failed Node": "Показать ноду с ошибкой",
"View all intermediate outputs": "Показать промежуточные результаты",
"Delete Card (Ctrl+Click to delete from disk)": "Удалить карточку (Ctrl+Клик для удаления с диска)",
"Ctrl+Click again to delete file from DISK / TRASH": "Ctrl+Клик еще раз для удаления с ДИСКА",
"Click again to confirm removing card": "Нажмите еще раз для подтверждения удаления",
"Remove from batch (Hold Ctrl to delete file from disk)": "Удалить из пакета (Ctrl для удаления с диска)",
"Click again to confirm removing from batch": "Нажмите еще раз для удаления из пакета",
"Delete node output (Hold Ctrl to delete file from disk)": "Удалить результат ноды (Ctrl для удаления с диска)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Ctrl+Клик еще раз для удаления с ДИСКА",
"Click again to confirm removing output": "Нажмите еще раз для подтверждения удаления",
"Moved file to Recycle Bin.": "Файл перемещен в корзину.",
"Permanently deleted file.": "Файл удален навсегда.",
"Cancelled": "Отменено",
"Error": "Ошибка",
"Warning": "Предупреждение",
"Cancel Queued Task": "Отменить задачу в очереди",
"Interrupt Execution": "Прервать выполнение",
"Click to read full text": "Нажмите, чтобы прочитать полный текст",
"No Outputs": "Нет результатов",
"Processing...": "Обработка...",
"Finishing...": "Завершение...",
"No workflow metadata found in this file.": "В этом файле не найдены метаданные рабочего процесса.",
"Open Folder": "Открыть папку",
"Open Location": "Открыть папку",
"View all images in this batch": "Все изображения этого пакета",
"No nodes found on canvas to renumber.": "Ноды для перенумерации на холсте не найдены.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "Перенумерация перезапишет ID всех нод на холсте по порядку выполнения.\n\nЭто исправит отображение промежуточных нод как финального результата, но может нарушить работу внешних скриптов, зависящих от конкретных ID.\n\nПродолжить?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "Перенумеровано {count} нод по порядку выполнения. Последняя нода: #{id} ({name}).",
"Failed to renumber nodes: ": "Не удалось перенумеровать ноды: ",
"Please select a node first to change its ID.": "Сначала выберите ноду для изменения её ID.",
"Enter new ID for node #{id} ({name}):": "Введите новый ID для ноды #{id} ({name}):",
"Please enter a valid positive number.": "Пожалуйста, введите положительное число.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "ID ноды #{id} уже используется нодой \"{name}\". Поменять их местами?",
"Assigned node ID #{id} to \"{name}\".": "Ноде \"{name}\" присвоен ID #{id}.",
"Failed to set node ID: ": "Не удалось установить ID ноды: " ,
"Video playback. Press Esc to close.": "Воспроизведение видео. Нажмите Esc для закрытия.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Клик для масштабирования (100%/Вписать) | Shift+Клик по другой карточке для сравнения.",
"Toggle Loop": "Зациклить",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "Несовпадение пропорций ({wA}x{hA} против {wB}x{hB}). Изображения должны иметь одинаковые пропорции для сравнения.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "Клик для масштабирования | Перетащите для сдвига слайдера | Esc для закрытия.",
"Copy": "Копировать",
"Copied!": "Скопировано!",
"Text Output": "Текстовый результат",
"Text Output #": "Текстовый результат #",
"Raw Text": "Исходный текст",
"{wc} words • {chars} chars": "{wc} слов • {chars} симв.",
"3D Model": "3D-модель",
"Loading 3D Asset...": "Загрузка 3D-модели...",
"Exporting {format}...": "Экспорт в {format}...",
"Failed to load 3D engine.": "Не удалось загрузить 3D-движок.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "Не удалось скачать библиотеки 3D. Убедитесь, что компьютер подключен к интернету, или проверьте, не блокирует ли флаг <code>--disable-api-nodes</code> сетевые запросы.",
"Background Color": "Цвет фона",
"Up Direction": "Направление вверх",
"Material Mode": "Режим материала",
"Camera Type": "Тип камеры",
"FOV": "Угол обзора (FOV)",
"Light Intensity": "Интенсивность света",
"Original (Y-Up)": "Оригинал (Y-Вверх)",
"Z-Up (+90°)": "Z-Вверх (+90°)",
"Inverted (-90°)": "Инвертировано (-90°)",
"Original / Textured": "Оригинал / Текстуры",
"Clay / Studio Matte": "Глина / Студийный",
"Normals": "Нормали",
"Perspective": "Перспектива",
"Orthographic": "Ортогональная",
"Grid": "Сетка",
"Auto-Rotate": "Автоповорот",
"Wireframe": "Каркас",
"Reset View": "Сброс вида",
"Settings": "Настройки",
"Export As...": "Экспорт...",
"Failed to load 3D format": "Не удалось загрузить 3D-формат"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "Kuyruk",
"Search History": "Geçmişte ara",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "Seçili düğümleri kuyrukta yoksay/etkinleştir (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "Düğüm seçilmedi. Önce tuvalde bir düğüme tıklayın.",
"Renumber All Nodes by Execution Order (Alt+R)": "Düğümleri çalıştırma sırasına göre yeniden numaralandır (Alt+R)",
"Clear Cancelled & Failed": "İptal edilenleri ve hatalıları temizle",
"Clear All History": "Tüm geçmişi temizle",
"Filter by text, images, nodes...": "Metin, görsel, düğüme göre filtrele...",
"Clear & Close Search": "Aramayı temizle ve kapat",
"Scroll to Top": "Yukarı kaydır",
"Batch of #": "Toplu işlem #",
"Go Back": "Geri",
"Go Back to Queue": "Kuyruğa dön",
"Outputs of #": "Çıktıları #",
"No separate intermediate outputs found.": "Ayrı bir ara çıktı bulunamadı.",
"Cancel All Pending": "Bekleyen tüm görevleri iptal et",
"Pending Queue:": "Bekleyen kuyruk:",
"tasks": "görev",
"Download Object": "Dosyayı indir",
"Copy to Clipboard": "Panoya kopyala",
"Download JSON": "JSON indir",
"Delete Card (Hold Ctrl to delete file from disk)": "Kartı sil (Diskten silmek için Ctrl'ye basılı tutun)",
"Delete Card": "Kartı sil",
"Delete File": "Dosyayı sil",
"Delete Files": "Dosyaları sil",
"Show Node": "Düğümü göster",
"Show Stopped/Failed Node": "Durdurulan/hatalı düğümü göster",
"View all intermediate outputs": "Tüm ara çıktıları görüntüle",
"Delete Card (Ctrl+Click to delete from disk)": "Kartı sil (Diskten silmek için Ctrl+Tıklama)",
"Ctrl+Click again to delete file from DISK / TRASH": "Dosyayı diskten tamamen silmek için tekrar Ctrl+Tıklama yapın",
"Click again to confirm removing card": "Kaldırmayı onaylamak için tekrar tıklayın",
"Remove from batch (Hold Ctrl to delete file from disk)": "Gruptan çıkar (Diskten silmek için Ctrl)",
"Click again to confirm removing from batch": "Gruptan çıkarmayı onaylamak için tekrar tıklayın",
"Delete node output (Hold Ctrl to delete file from disk)": "Çıktıyı sil (Diskten silmek için Ctrl)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "Bu düğümün dosyalarını diskten silmek için tekrar Ctrl+Tıklayın",
"Click again to confirm removing output": "Çıktıyı kaldırmayı onaylamak için tekrar tıklayın",
"Moved file to Recycle Bin.": "Dosya Geri Dönüşüm Kutusu'na taşındı.",
"Permanently deleted file.": "Dosya kalıcı olarak silindi.",
"Cancelled": "İptal edildi",
"Error": "Hata",
"Warning": "Uyarı",
"Cancel Queued Task": "Kuyruktaki görevi iptal et",
"Interrupt Execution": "Çalıştırmayı durdur",
"Click to read full text": "Metnin tamamını okumak için tıklayın",
"No Outputs": "Çıktı yok",
"Processing...": "İşleniyor...",
"Finishing...": "Tamamlanıyor...",
"No workflow metadata found in this file.": "Bu dosyada iş akışı meta verisi bulunamadı.",
"Open Folder": "Klasörü aç",
"Open Location": "Dosya konumunu aç",
"View all images in this batch": "Bu gruptaki tüm resimleri görüntüle",
"No nodes found on canvas to renumber.": "Tuvalde numaralandırılacak düğüm bulunamadı.",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "Yeniden numaralandırma, tüm düğüm kimliklerini çalıştırma sırasına göre yeniden yazar.\n\nBu işlem ara çıktıların nihai sonuç olarak görünmesini düzeltir ancak belirli kimliklere bağımlı harici betikleri bozabilir.\n\nDevam etmek istiyor musunuz?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "{count} düğüm çalıştırma sırasına göre numaralandırıldı. Son düğüm: #{id} ({name}).",
"Failed to renumber nodes: ": "Düğümler yeniden numaralandırılamadı: ",
"Please select a node first to change its ID.": "Kimliğini değiştirmek için lütfen önce bir düğüm seçin.",
"Enter new ID for node #{id} ({name}):": "#{id} ({name}) düğümü için yeni kimlik girin:",
"Please enter a valid positive number.": "Lütfen geçerli bir pozitif tamsayı girin.",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "#{id} kimliği zaten \"{name}\" tarafından kullanılıyor. Değiştirilsin mi?",
"Assigned node ID #{id} to \"{name}\".": "\"{name}\" düğümüne #{id} kimliği atandı.",
"Failed to set node ID: ": "Düğüm kimliği ayarlanamadı: ",
"Video playback. Press Esc to close.": "Video oynatılıyor. Kapatmak için Esc'ye basın.",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "Yakınlaştırmak için tıkla (100%/Sığdır) | Karşılaştırmak için başka bir karta Shift+Tıkla.",
"Toggle Loop": "Döngüyü aç/kapat",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "En-boy oranı uyumsuzluğu ({wA}x{hA} ile {wB}x{hB}). Karşılaştırma için görseller aynı oranda olmalıdır.",
"Click image to zoom | Drag image to compare slider | Esc to close.": "Yakınlaştırmak için tıkla | Karşılaştırmak için kaydır | Kapatmak için Esc.",
"Copy": "Kopyala",
"Copied!": "Kopyalandı!",
"Text Output": "Metin çıktısı",
"Text Output #": "Metin çıktısı #",
"Raw Text": "Ham metin",
"{wc} words • {chars} chars": "{wc} kelime • {chars} karakter",
"3D Model": "3D Model",
"Loading 3D Asset...": "3D model yükleniyor...",
"Exporting {format}...": "{format} formatında dışa aktarılıyor...",
"Failed to load 3D engine.": "3D motoru yüklenemedi.",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "3D kütüphaneleri indirilemedi. İnternet bağlantınızı kontrol edin.",
"Background Color": "Arka plan rengi",
"Up Direction": "Yukarı yön",
"Material Mode": "Materyal modu",
"Camera Type": "Kamera türü",
"FOV": "Görüş açısı (FOV)",
"Light Intensity": "Işık yoğunluğu",
"Original (Y-Up)": "Orijinal (Y yukarı)",
"Z-Up (+90°)": "Z yukarı (+90°)",
"Inverted (-90°)": "Ters (-90°)",
"Original / Textured": "Orijinal / Dokulu",
"Clay / Studio Matte": "Kil / Mat",
"Normals": "Normaller",
"Perspective": "Perspektif",
"Orthographic": "Ortografik",
"Grid": "Izgara",
"Auto-Rotate": "Otomatik döndür",
"Wireframe": "Tel kafes",
"Reset View": "Görünümü sıfırla",
"Settings": "Ayarlar",
"Export As...": "Farklı dışa aktar...",
"Failed to load 3D format": "3D formatı yüklenemedi"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "隊列",
"Search History": "搜尋歷史",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在隊列中忽略選取節點 (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "未選取任何節點。請先在畫布上點選節點。",
"Renumber All Nodes by Execution Order (Alt+R)": "按執行順序重新編號所有節點 (Alt+R)",
"Clear Cancelled & Failed": "清除已取消與失敗任務",
"Clear All History": "清除所有歷史",
"Filter by text, images, nodes...": "按文字、影像、節點篩選...",
"Clear & Close Search": "清除並關閉搜尋",
"Scroll to Top": "回到頂部",
"Batch of #": "批次 #",
"Go Back": "返回",
"Go Back to Queue": "返回隊列",
"Outputs of #": "輸出 #",
"No separate intermediate outputs found.": "未找到獨立的中間輸出。",
"Cancel All Pending": "取消所有等待任務",
"Pending Queue:": "等待隊列:",
"tasks": "個任務",
"Download Object": "下載檔案",
"Copy to Clipboard": "複製到剪貼簿",
"Download JSON": "下載 JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "刪除卡片 (按住 Ctrl 從磁碟刪除檔案)",
"Delete Card": "刪除卡片",
"Delete File": "刪除檔案",
"Delete Files": "刪除檔案",
"Show Node": "顯示節點",
"Show Stopped/Failed Node": "顯示停止/失敗的節點",
"View all intermediate outputs": "檢視所有中間輸出",
"Delete Card (Ctrl+Click to delete from disk)": "刪除卡片 (Ctrl+點擊 從磁碟刪除)",
"Ctrl+Click again to delete file from DISK / TRASH": "再次 Ctrl+點擊 以徹底刪除檔案",
"Click again to confirm removing card": "再次點擊確認移除卡片",
"Remove from batch (Hold Ctrl to delete file from disk)": "從批次中移除 (按住 Ctrl 從磁碟刪除)",
"Click again to confirm removing from batch": "再次點擊確認從批次中移除",
"Delete node output (Hold Ctrl to delete file from disk)": "刪除節點輸出 (按住 Ctrl 從磁碟刪除)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "再次 Ctrl+點擊 從磁碟刪除此節點的檔案",
"Click again to confirm removing output": "再次點擊確認移除輸出",
"Moved file to Recycle Bin.": "檔案已移至資源回收筒。",
"Permanently deleted file.": "檔案已永久刪除。",
"Cancelled": "已取消",
"Error": "錯誤",
"Warning": "警告",
"Cancel Queued Task": "取消排隊任務",
"Interrupt Execution": "中斷執行",
"Click to read full text": "點擊檢視全文",
"No Outputs": "無輸出",
"Processing...": "處理中...",
"Finishing...": "完成中...",
"No workflow metadata found in this file.": "此檔案中未找到工作流元資料。",
"Open Folder": "開啟資料夾",
"Open Location": "開啟檔案位置",
"View all images in this batch": "檢視此批次中的所有影像",
"No nodes found on canvas to renumber.": "畫布上未找到可重新編號的節點。",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "重新編號將按拓撲執行順序重寫畫布上的所有節點 ID。\n\n這將修復中間節點輸出顯示為最終結果的問題,但可能會影響依賴特定節點 ID 的外部腳本。\n\n是否繼續?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "已按執行順序對 {count} 個節點重新編號。最新節點: #{id} ({name})。",
"Failed to renumber nodes: ": "節點重新編號失敗: ",
"Please select a node first to change its ID.": "請先選擇一個節點以更改其 ID。",
"Enter new ID for node #{id} ({name}):": "輸入節點 #{id} ({name}) 的新 ID:",
"Please enter a valid positive number.": "請輸入有效的正整數。",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "節點 ID #{id} 已被 \"{name}\" 使用。是否對調它們的 ID?",
"Assigned node ID #{id} to \"{name}\".": "已將節點 ID #{id} 分配給 \"{name}\"。",
"Failed to set node ID: ": "設定節點 ID 失敗: ",
"Video playback. Press Esc to close.": "影片播放。按 Esc 關閉。",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "點擊影像縮放 (100%/適應) | Shift+點擊 另一張卡片進行對比。",
"Toggle Loop": "循環播放",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "長寬比不符合 ({wA}x{hA} 對比 {wB}x{hB})。影像必須具有相同比例才能對比。",
"Click image to zoom | Drag image to compare slider | Esc to close.": "點擊縮放 | 拖曳滑塊對比 | Esc 關閉。",
"Copy": "複製",
"Copied!": "已複製!",
"Text Output": "文字輸出",
"Text Output #": "文字輸出 #",
"Raw Text": "原始文字",
"{wc} words • {chars} chars": "{wc} 個詞 • {chars} 個字元",
"3D Model": "3D 模型",
"Loading 3D Asset...": "正在載入 3D 資產...",
"Exporting {format}...": "正在匯出 {format}...",
"Failed to load 3D engine.": "載入 3D 引擎失敗。",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "無法下載 3D 函式庫。請確保連線正常,或檢查是否啟用了 <code>--disable-api-nodes</code>。",
"Background Color": "背景顏色",
"Up Direction": "朝上方向",
"Material Mode": "材質模式",
"Camera Type": "相機類型",
"FOV": "視野 (FOV)",
"Light Intensity": "光照強度",
"Original (Y-Up)": "原始 (Y 軸向上)",
"Z-Up (+90°)": "Z 軸向上 (+90°)",
"Inverted (-90°)": "反轉 (-90°)",
"Original / Textured": "原始 / 貼圖",
"Clay / Studio Matte": "黏土 / 啞光",
"Normals": "法線",
"Perspective": "透視",
"Orthographic": "正交",
"Grid": "格線",
"Auto-Rotate": "自動旋轉",
"Wireframe": "線框",
"Reset View": "重設視角",
"Settings": "設定",
"Export As...": "匯出為...",
"Failed to load 3D format": "載入 3D 格式失敗"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Queue": "队列",
"Search History": "搜索历史",
"Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)": "在队列中忽略所选节点 (Ctrl+Q)",
"No nodes selected. Click a node on the canvas first to toggle ignore.": "未选择任何节点。请先在画布上点击节点。",
"Renumber All Nodes by Execution Order (Alt+R)": "按执行顺序重新编号所有节点 (Alt+R)",
"Clear Cancelled & Failed": "清除已取消和失败的任务",
"Clear All History": "清除所有历史",
"Filter by text, images, nodes...": "按文本、图像、节点过滤...",
"Clear & Close Search": "清空并关闭搜索",
"Scroll to Top": "回到顶部",
"Batch of #": "批次 #",
"Go Back": "返回",
"Go Back to Queue": "返回队列",
"Outputs of #": "输出 #",
"No separate intermediate outputs found.": "未找到单独的中间输出。",
"Cancel All Pending": "取消所有等待任务",
"Pending Queue:": "等待队列:",
"tasks": "个任务",
"Download Object": "下载文件",
"Copy to Clipboard": "复制到剪贴板",
"Download JSON": "下载 JSON",
"Delete Card (Hold Ctrl to delete file from disk)": "删除卡片 (按住 Ctrl 从磁盘删除文件)",
"Delete Card": "删除卡片",
"Delete File": "删除文件",
"Delete Files": "删除文件",
"Show Node": "显示节点",
"Show Stopped/Failed Node": "显示停止/失败的节点",
"View all intermediate outputs": "查看所有中间输出",
"Delete Card (Ctrl+Click to delete from disk)": "删除卡片 (Ctrl+点击 从磁盘删除)",
"Ctrl+Click again to delete file from DISK / TRASH": "再次 Ctrl+点击 以彻底删除文件",
"Click again to confirm removing card": "再次点击确认移除卡片",
"Remove from batch (Hold Ctrl to delete file from disk)": "从批次中移除 (按住 Ctrl 从磁盘删除)",
"Click again to confirm removing from batch": "再次点击确认从批次中移除",
"Delete node output (Hold Ctrl to delete file from disk)": "删除节点输出 (按住 Ctrl 从磁盘删除)",
"Ctrl+Click again to delete this node's files from DISK / TRASH": "再次 Ctrl+点击 从磁盘删除此节点的文件",
"Click again to confirm removing output": "再次点击确认移除输出",
"Moved file to Recycle Bin.": "文件已移至回收站。",
"Permanently deleted file.": "文件已永久删除。",
"Cancelled": "已取消",
"Error": "错误",
"Warning": "警告",
"Cancel Queued Task": "取消排队任务",
"Interrupt Execution": "中断执行",
"Click to read full text": "点击查看全文",
"No Outputs": "无输出",
"Processing...": "处理中...",
"Finishing...": "完成中...",
"No workflow metadata found in this file.": "此文件中未找到工作流元数据。",
"Open Folder": "打开文件夹",
"Open Location": "打开位置",
"View all images in this batch": "查看此批次中的所有图像",
"No nodes found on canvas to renumber.": "画布上未找到可重新编号的节点。",
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?": "重新编号将按拓扑执行顺序重写画布上的所有节点 ID。\n\n这将修复中间节点输出被显示为最终结果的问题,但可能会破坏依赖于特定节点 ID 的外部脚本。\n\n是否继续?",
"Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).": "已按执行顺序对 {count} 个节点重新编号。最新节点: #{id} ({name})。",
"Failed to renumber nodes: ": "节点重新编号失败: ",
"Please select a node first to change its ID.": "请先选择一个节点以更改其 ID。",
"Enter new ID for node #{id} ({name}):": "输入节点 #{id} ({name}) 的新 ID:",
"Please enter a valid positive number.": "请输入有效的正整数。",
"Node ID #{id} is already used by \"{name}\". Swap their IDs?": "节点 ID #{id} 已被 \"{name}\" 使用。是否交换它们的 ID?",
"Assigned node ID #{id} to \"{name}\".": "已将节点 ID #{id} 分配给 \"{name}\"。",
"Failed to set node ID: ": "设置节点 ID 失败: ",
"Video playback. Press Esc to close.": "视频播放。按 Esc 关闭。",
"Click image to zoom (100%/Fit) | Shift+Click another card to compare.": "点击图像缩放 (100%/适应) | Shift+点击 另一张卡片进行对比。",
"Toggle Loop": "循环播放",
"Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.": "宽高比不匹配 ({wA}x{hA} 对比 {wB}x{hB})。图像必须具有相同的比例才能进行对比。",
"Click image to zoom | Drag image to compare slider | Esc to close.": "点击缩放 | 拖动滑块对比 | Esc 关闭。",
"Copy": "复制",
"Copied!": "已复制!",
"Text Output": "文本输出",
"Text Output #": "文本输出 #",
"Raw Text": "原始文本",
"{wc} words • {chars} chars": "{wc} 个词 • {chars} 个字符",
"3D Model": "3D 模型",
"Loading 3D Asset...": "正在加载 3D 资产...",
"Exporting {format}...": "正在导出 {format}...",
"Failed to load 3D engine.": "加载 3D 引擎失败。",
"Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.": "无法下载 3D 库。请确保网络连接正常,或检查是否开启了 <code>--disable-api-nodes</code>。",
"Background Color": "背景颜色",
"Up Direction": "向上方向",
"Material Mode": "材质模式",
"Camera Type": "相机类型",
"FOV": "视场角 (FOV)",
"Light Intensity": "光照强度",
"Original (Y-Up)": "原始 (Y 轴向上)",
"Z-Up (+90°)": "Z 轴向上 (+90°)",
"Inverted (-90°)": "反转 (-90°)",
"Original / Textured": "原始 / 纹理",
"Clay / Studio Matte": "黏土 / 哑光",
"Normals": "法线",
"Perspective": "透视",
"Orthographic": "正交",
"Grid": "网格",
"Auto-Rotate": "自动旋转",
"Wireframe": "线框",
"Reset View": "重置视角",
"Settings": "设置",
"Export As...": "导出为...",
"Failed to load 3D format": "加载 3D 格式失败"
}
+10 -2
View File
@@ -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()
+2 -1
View File
@@ -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);
}
}
+139 -3
View File
@@ -9,6 +9,7 @@ import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, s
import { initSessionAndHistory, syncQueue } from "./queue/queueService.js";
import { setupExecutionTracker } from "./queue/executionTracker.js";
import { setupSidebarView, renderSidebar, teardownSidebarView } from "./ui/sidebarView.js";
import { initI18n, t } from "./core/i18n.js";
import { updateCardProgressTargeted, cardPool } from "./ui/cardRenderer.js";
import { store } from "./core/store.js";
@@ -87,6 +88,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",
@@ -134,6 +156,105 @@ function setupKeydownShortcuts() {
document.addEventListener("keydown", activeKeydownHandler, true);
}
function setupWorkflowTabsPositionWatcher() {
let isSyncing = false;
const TARGET_SETTING = "Comfy.Workflow.WorkflowTabsPosition";
const checkAndDisableIfSidebar = (val) => {
if (isSyncing) return;
if (val === "Sidebar") {
const isClassicEnabled = app.ui?.settings?.getSettingValue(SettingIds.CLASSIC_LAYOUT) ?? false;
if (isClassicEnabled) {
isSyncing = true;
try {
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
}
applyClassicLayout(false, false);
} finally {
isSyncing = false;
}
}
}
};
const cleanups = [];
// 1. Hook the setting's native onChange callback directly (instant execution on click)
const attachNativeOnChange = (settingObj) => {
if (!settingObj || settingObj._classicWatcherAttached) return;
settingObj._classicWatcherAttached = true;
const origOnChange = settingObj.onChange;
settingObj.onChange = (val) => {
if (origOnChange) origOnChange(val);
checkAndDisableIfSidebar(val);
};
cleanups.push(() => {
settingObj.onChange = origOnChange;
delete settingObj._classicWatcherAttached;
});
};
// Check existing registered setting in legacy UI and modern extension manager
attachNativeOnChange(app.ui?.settings?.settings?.[TARGET_SETTING]);
attachNativeOnChange(app.extensionManager?.setting?.getSetting?.(TARGET_SETTING));
attachNativeOnChange(app.extensionManager?.setting?.settings?.[TARGET_SETTING]);
// 2. Hook Vue/Pinia reactive store subscription if available (zero polling, synchronous)
const settingStore = app.extensionManager?.setting?.settingStore;
if (settingStore?.$subscribe) {
const unsubscribe = settingStore.$subscribe(() => {
const val = app.extensionManager?.setting?.get?.(TARGET_SETTING) ??
app.ui?.settings?.getSettingValue(TARGET_SETTING);
checkAndDisableIfSidebar(val);
});
cleanups.push(unsubscribe);
}
// 3. Hook addSetting in case core registers/re-registers the setting later
if (app.ui?.settings?.addSetting) {
const origAdd = app.ui.settings.addSetting.bind(app.ui.settings);
app.ui.settings.addSetting = (setting) => {
if (setting?.id === TARGET_SETTING) {
attachNativeOnChange(setting);
}
return origAdd(setting);
};
cleanups.push(() => {
app.ui.settings.addSetting = origAdd;
});
}
// 4. Hook API setters as fallback
if (app.ui?.settings?.setSettingValue) {
const origSetSettingValue = app.ui.settings.setSettingValue.bind(app.ui.settings);
app.ui.settings.setSettingValue = (id, value) => {
origSetSettingValue(id, value);
if (id === TARGET_SETTING) checkAndDisableIfSidebar(value);
};
cleanups.push(() => {
app.ui.settings.setSettingValue = origSetSettingValue;
});
}
if (app.extensionManager?.setting?.set) {
const origExtSet = app.extensionManager.setting.set.bind(app.extensionManager.setting);
app.extensionManager.setting.set = (id, value) => {
origExtSet(id, value);
if (id === TARGET_SETTING) checkAndDisableIfSidebar(value);
};
cleanups.push(() => {
app.extensionManager.setting.set = origExtSet;
});
}
return () => {
cleanups.forEach(fn => fn());
};
}
app.registerExtension({
name: "ComfySidebar.ClassicRestore",
@@ -203,8 +324,22 @@ app.registerExtension({
async setup() {
if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return;
await initI18n();
const tabsPos = app.extensionManager?.setting?.get?.("Comfy.Workflow.WorkflowTabsPosition") ??
app.ui.settings.getSettingValue("Comfy.Workflow.WorkflowTabsPosition");
let isClassicLayoutEnabled = app.ui.settings.getSettingValue(SettingIds.CLASSIC_LAYOUT) ?? false;
// Startup safeguard: if workflows are set to Sidebar, disable Comfy Layout
if (isClassicLayoutEnabled && tabsPos === "Sidebar") {
isClassicLayoutEnabled = false;
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue(SettingIds.CLASSIC_LAYOUT, false); } catch (e) {}
}
}
const isClassicLayoutEnabled = app.ui.settings.getSettingValue(SettingIds.CLASSIC_LAYOUT) ?? false;
applyClassicLayout(isClassicLayoutEnabled, false);
const isHistoryOverrideEnabled = app.ui.settings.getSettingValue(SettingIds.OVERRIDE_STOCK_HISTORY) ?? false;
@@ -217,6 +352,7 @@ app.registerExtension({
setupPropertiesPanelToggleFix();
injectStyles();
cleanupFns.push(setupWorkflowTabsPositionWatcher());
cleanupFns.push(setupDragAndDrop());
cleanupFns.push(setupVueNodeObserver());
@@ -242,8 +378,8 @@ app.registerExtension({
app.extensionManager.registerSidebarTab({
id: "classic-comfy-sidebar",
icon: "pi pi-images",
title: "Queue",
tooltip: "Comfy Queue (Q)",
title: t("Queue"),
tooltip: `${t("Queue")} (Q)`,
type: "custom",
render: (el) => { el.appendChild(sidebarContainer); }
});
+23 -22
View File
@@ -1,5 +1,6 @@
import { app } from "/scripts/app.js";
import { PromptStatus } from "../core/constants.js";
import { t } from "../core/i18n.js";
import { store } from "../core/store.js";
import { renderCardImages } from "./mediaPreview.js";
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage, removeImageFromNodeOutputs } from "./mediaActions.js";
@@ -96,7 +97,7 @@ export function syncCardButtonVisibility(cardObj, state) {
if (wf) state.workflow = wf;
}
if (!wf) {
alert("No workflow metadata found in this file.");
alert(t("No workflow metadata found in this file."));
return;
}
const blob = new Blob([JSON.stringify(wf, null, 2)], { type: "application/json" });
@@ -121,8 +122,8 @@ export function syncCardButtonVisibility(cardObj, state) {
cardObj.btnDelLabel.style.display = hasRealDiskFiles ? "" : "none";
}
cardObj.btnDel.title = hasRealDiskFiles
? "Delete Card (Hold Ctrl to delete file from disk)"
: "Delete Card";
? t("Delete Card (Hold Ctrl to delete file from disk)")
: t("Delete Card");
}
if (cardObj.btnFocus) {
@@ -130,7 +131,7 @@ export function syncCardButtonVisibility(cardObj, state) {
if (nodeId) {
cardObj.btnFocus.style.removeProperty("display");
cardObj.btnFocus.style.display = "inline-flex";
cardObj.btnFocus.title = (!currentImg && state.activeNodeId) ? "Show Stopped/Failed Node" : "Show Node";
cardObj.btnFocus.title = (!currentImg && state.activeNodeId) ? t("Show Stopped/Failed Node") : t("Show Node");
cardObj.btnFocus.onclick = (ev) => {
ev.stopPropagation();
centerAndSelectCanvasNode(nodeId);
@@ -322,7 +323,7 @@ export function updateCardProgressTargeted(cardObj, progress, activeNodeName, sh
}
if (cardObj.statusText && showWorkingNode) {
cardObj.statusText.style.display = "block";
const nodeText = activeNodeName ? (activeNodeName === "Finishing..." ? "Finishing..." : `[${activeNodeName}]`) : "Processing...";
const nodeText = activeNodeName ? (activeNodeName === "Finishing..." ? t("Finishing...") : `[${activeNodeName}]`) : t("Processing...");
cardObj.statusText.textContent = `${nodeText} ${progress}%`;
}
@@ -408,23 +409,23 @@ export function getOrCreateCard(state, callbacks = {}) {
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
btnImg.title = t("Download Object");
const btnCopy = document.createElement("span");
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.title = "Copy to Clipboard";
btnCopy.title = t("Copy to Clipboard");
btnCopy.style.display = "none";
const btnJson = document.createElement("span");
btnJson.className = "pi pi-file comfy-sidebar-card-action-btn";
btnJson.title = "Download JSON";
btnJson.title = t("Download JSON");
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
btnDel.title = "Delete Card (Hold Ctrl to delete file from disk)";
btnDel.title = t("Delete Card (Hold Ctrl to delete file from disk)");
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = "Delete File";
btnDelLabel.textContent = t("Delete File");
btnDel.appendChild(btnDelLabel);
const leftHoverPanel = document.createElement("div");
@@ -435,11 +436,11 @@ export function getOrCreateCard(state, callbacks = {}) {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
btnFocus.title = t("Show Node");
const leftHoverBtn = document.createElement("span");
leftHoverBtn.className = "pi pi-images comfy-sidebar-card-action-btn";
leftHoverBtn.title = "View all intermediate outputs";
leftHoverBtn.title = t("View all intermediate outputs");
hoverPanel.append(btnImg, btnJson, btnDel);
leftHoverPanel.append(btnCopy, btnFocus, leftHoverBtn);
@@ -507,7 +508,7 @@ export function getOrCreateCard(state, callbacks = {}) {
isDeletePending = false;
isDiskDelete = false;
cardObj.btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
cardObj.btnDel.title = "Delete Card (Ctrl+Click to delete from disk)";
cardObj.btnDel.title = t("Delete Card (Ctrl+Click to delete from disk)");
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
@@ -520,8 +521,8 @@ export function getOrCreateCard(state, callbacks = {}) {
isDiskDelete = wantsDiskDelete;
cardObj.btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
cardObj.btnDel.title = wantsDiskDelete
? "Ctrl+Click again to delete file from DISK / TRASH"
: "Click again to confirm removing card";
? t("Ctrl+Click again to delete file from DISK / TRASH")
: t("Click again to confirm removing card");
deleteTimeout = setTimeout(resetDeleteBtn, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
@@ -600,7 +601,7 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
color: "#000",
zIndex: "30"
});
cardObj.statusBadge.textContent = "Cancelled";
cardObj.statusBadge.textContent = t("Cancelled");
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
} else if (state.status === PromptStatus.ERROR) {
Object.assign(cardObj.statusBadge.style, {
@@ -609,7 +610,7 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
color: "#fff",
zIndex: "30"
});
cardObj.statusBadge.textContent = "Error";
cardObj.statusBadge.textContent = t("Error");
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
} else {
cardObj.statusBadge.style.display = "none";
@@ -617,14 +618,14 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
if (state.status === PromptStatus.PENDING && !showPendingSummary) {
cardObj.cancelBtn.style.display = "flex";
cardObj.cancelBtn.title = "Cancel Queued Task";
cardObj.cancelBtn.title = t("Cancel Queued Task");
cardObj.cancelBtn.onclick = async (ev) => {
ev.stopPropagation();
cancelPendingTask(state.pid);
};
} else if (state.status === PromptStatus.ACTIVE) {
cardObj.cancelBtn.style.display = "flex";
cardObj.cancelBtn.title = "Interrupt Execution";
cardObj.cancelBtn.title = t("Interrupt Execution");
cardObj.cancelBtn.onclick = async (ev) => {
ev.stopPropagation();
interruptActive();
@@ -663,7 +664,7 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
if (state.texts && state.texts.length > 0) {
const fullText = state.texts.join("\n\n");
cardObj.placeholder.textContent = fullText;
cardObj.placeholder.title = "Click to read full text";
cardObj.placeholder.title = t("Click to read full text");
cardObj.placeholder.className = "comfy-sidebar-text-clamp";
cardObj.placeholder.style.cssText = "";
cardObj.placeholder.style.display = "-webkit-box";
@@ -673,7 +674,7 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
showFullscreenPreview([{ text: fullText, pid: state.pid }], false, state.pid);
};
} else {
cardObj.placeholder.textContent = state.progressText || "No Outputs";
cardObj.placeholder.textContent = state.progressText || t("No Outputs");
cardObj.placeholder.className = "";
Object.assign(cardObj.placeholder.style, {
fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px",
@@ -686,7 +687,7 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
if (state.status === PromptStatus.ACTIVE) {
if (showWorkingNode) {
cardObj.statusText.style.display = "block";
const nodeText = state.activeNodeName ? (state.activeNodeName === "Finishing..." ? "Finishing..." : `[${state.activeNodeName}]`) : "Processing...";
const nodeText = state.activeNodeName ? (state.activeNodeName === "Finishing..." ? t("Finishing...") : `[${state.activeNodeName}]`) : t("Processing...");
cardObj.statusText.textContent = `${nodeText}${state.progress ? ` ${state.progress}%` : ""}`;
} else {
cardObj.statusText.style.display = "none";
+2 -2
View File
@@ -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;
+131 -25
View File
@@ -1,20 +1,37 @@
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";
import { t } from "../core/i18n.js";
import { deleteHistoryItem } from "../queue/queueService.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 +74,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 +111,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 +127,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 +149,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 +176,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 +185,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 +283,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 +328,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 +352,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 +365,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";
@@ -321,13 +373,25 @@ export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state)
const openHint = document.createElement("div");
openHint.className = "comfy-sidebar-file-subtext";
openHint.innerHTML = `<i class="pi pi-external-link" style="font-size: 9px;"></i><span>Open Folder</span>`;
openHint.innerHTML = `<i class="pi pi-external-link" style="font-size: 9px;"></i><span>${t("Open Folder")}</span>`;
bottomRow.append(folderInfo, openHint);
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");
@@ -392,26 +456,61 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
if (!ctrlOverlay) {
ctrlOverlay = document.createElement("div");
ctrlOverlay.className = "comfy-sidebar-ctrl-overlay";
ctrlOverlay.innerHTML = `<i class="pi pi-folder-open"></i><span>Open Location</span>`;
ctrlOverlay.innerHTML = `<i class="pi pi-folder-open"></i><span>${t("Open Location")}</span>`;
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 +536,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");
@@ -487,7 +585,11 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
if (parentState.nodeOutputs) {
removeImageFromNodeOutputs(parentState.nodeOutputs, img);
}
store.updatePrompt(parentPid, parentState);
if ((!parentState.images || parentState.images.length === 0) && parentState.status === PromptStatus.COMPLETED) {
deleteHistoryItem(parentPid);
} else {
store.updatePrompt(parentPid, parentState);
}
}
cardObj.element?.remove();
const remaining = store.ui.cardStack?.querySelectorAll('.comfy-sidebar-card');
@@ -509,7 +611,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,14 +618,15 @@ 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)) {
// If the job is still actively generating/running, keep it
if (liveState.status && liveState.status !== PromptStatus.COMPLETED) {
cardObj.lastImagesSignature = "";
store.updatePrompt(liveState.pid, liveState);
return;
}
store.deletePrompt(liveState.pid);
// Temp image preview was deleted and only intermediate prompt text remains: delete card for good
deleteHistoryItem(liveState.pid);
cardObj.element?.remove();
return;
}
@@ -533,8 +635,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;
@@ -686,7 +792,7 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
label.className = "comfy-sidebar-batch-label";
label.textContent = `${idx + 1}/${state.images.length}`;
label.style.cursor = "pointer";
label.title = "View all images in this batch";
label.title = t("View all images in this batch");
label.onclick = (ev) => {
ev.stopPropagation();
+39 -38
View File
@@ -1,6 +1,7 @@
import { app } from "/scripts/app.js";
import { PromptStatus, StoreEvents, SettingIds } from "../core/constants.js";
import { store } from "../core/store.js";
import { t } from "../core/i18n.js";
import { getOrCreateCard, updateCardDOM, resetAllCardHoverStates, cardPool } from "./cardRenderer.js";
import { clearCancelledOrFailed, clearAllHistory, cancelAllPending } from "../queue/queueService.js";
import { updateSidebarBadge, centerAndSelectCanvasNode } from "../comfy/adapter.js";
@@ -177,10 +178,10 @@ export function setupSidebarView() {
const searchIcon = document.createElement("span");
searchIcon.className = "pi pi-search comfy-sidebar-icon-btn";
searchIcon.title = "Search History";
searchIcon.title = t("Search History");
const title = document.createElement("h3");
title.textContent = "Queue";
title.textContent = t("Queue");
Object.assign(title.style, { margin: "0", fontSize: "14px", fontWeight: "bold", opacity: "0.9", color: "var(--fg-color, #eee)" });
titleGroup.appendChild(searchIcon);
@@ -235,7 +236,7 @@ export function setupSidebarView() {
return btn;
};
const btnToggleIgnore = createInstantBtn("pi pi-eye-slash", "Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)", () => {
const btnToggleIgnore = createInstantBtn("pi pi-eye-slash", t("Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)"), () => {
const raw = app.canvas?.selected_nodes;
const selected = raw instanceof Map || raw instanceof Set
? Array.from(raw.values())
@@ -243,14 +244,14 @@ export function setupSidebarView() {
const selectedNodes = selected.length > 0 ? selected : (app.canvas?.selected_node ? [app.canvas.selected_node] : []);
if (selectedNodes.length === 0) {
showNotification("No nodes selected. Click a node on the canvas first to toggle ignore.", "warn");
showNotification(t("No nodes selected. Click a node on the canvas first to toggle ignore."), "warn");
return;
}
toggleIgnoreActiveNode(() => renderSidebar());
});
const btnRenumber = createInstantBtn("pi pi-sort-numeric-down", "Renumber All Nodes by Execution Order (Alt+R)", async () => {
const btnRenumber = createInstantBtn("pi pi-sort-numeric-down", t("Renumber All Nodes by Execution Order (Alt+R)"), async () => {
await renumberNodesTopologically();
});
@@ -260,12 +261,12 @@ export function setupSidebarView() {
margin: "0 2px", opacity: "0.6"
});
const btnClearInterrupted = createActionBtn("pi pi-eraser", "Clear Cancelled & Failed", "#ffc107", async () => {
const btnClearInterrupted = createActionBtn("pi pi-eraser", t("Clear Cancelled & Failed"), "#ffc107", async () => {
await clearCancelledOrFailed();
renderSidebar();
});
const btnClearAll = createActionBtn("pi pi-trash", "Clear All History", "#dc3545", async () => {
const btnClearAll = createActionBtn("pi pi-trash", t("Clear All History"), "#dc3545", async () => {
await clearAllHistory();
renderSidebar();
});
@@ -288,14 +289,14 @@ export function setupSidebarView() {
searchInputIcon.className = "pi pi-search";
Object.assign(searchInputIcon.style, { fontSize: "11px", opacity: "0.5", marginRight: "6px" });
const searchInput = document.createElement("input");
Object.assign(searchInput, { type: "text", placeholder: "Filter by text, images, nodes..." });
Object.assign(searchInput, { type: "text", placeholder: t("Filter by text, images, nodes...") });
Object.assign(searchInput.style, {
flex: "1", background: "transparent", border: "none", outline: "none",
color: "var(--comfy-input-color, var(--fg-color, #eee))", fontSize: "11px", padding: "0"
});
const clearSearchBtn = document.createElement("span");
clearSearchBtn.className = "pi pi-times comfy-sidebar-icon-btn";
clearSearchBtn.title = "Clear & Close Search";
clearSearchBtn.title = t("Clear & Close Search");
Object.assign(clearSearchBtn.style, { marginLeft: "6px" });
searchContainer.appendChild(searchInputIcon);
@@ -341,7 +342,7 @@ export function setupSidebarView() {
scrollToTopBtnEl = document.createElement("button");
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
scrollToTopBtnEl.title = "Scroll to Top";
scrollToTopBtnEl.title = t("Scroll to Top");
scrollToTopBtnEl.addEventListener("click", (e) => {
e.stopPropagation();
@@ -406,9 +407,9 @@ export function renderSidebar() {
const batchInfo = store.ui.activeSubmenuBatchImages;
if (headerTitle) {
headerTitle.textContent = `Batch of #${batchInfo.pid}`;
headerTitle.textContent = `${t("Batch of #")}${batchInfo.pid}`;
headerTitle.style.cursor = "pointer";
headerTitle.title = "Go Back";
headerTitle.title = t("Go Back");
headerTitle.onclick = () => {
if (batchInfo.parentPromptId) {
store.openOutputsSubmenu(batchInfo.parentPromptId);
@@ -469,7 +470,7 @@ export function renderSidebar() {
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
btnImg.title = t("Download Object");
btnImg.onclick = (ev) => {
ev.stopPropagation();
const a = document.createElement("a");
@@ -480,10 +481,10 @@ export function renderSidebar() {
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
btnDel.title = "Remove from batch (Hold Ctrl to delete file from disk)";
btnDel.title = t("Remove from batch (Hold Ctrl to delete file from disk)");
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = "Delete File";
btnDelLabel.textContent = t("Delete File");
btnDel.appendChild(btnDelLabel);
let isDeletePending = false, isDiskDelete = false, deleteTimeout = null;
@@ -491,7 +492,7 @@ export function renderSidebar() {
isDeletePending = false;
isDiskDelete = false;
btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
btnDel.title = "Remove from batch (Hold Ctrl to delete file from disk)";
btnDel.title = t("Remove from batch (Hold Ctrl to delete file from disk)");
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
@@ -504,8 +505,8 @@ export function renderSidebar() {
isDiskDelete = wantsDiskDelete;
cardObj.btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
cardObj.btnDel.title = wantsDiskDelete
? "Ctrl+Click again to delete file from DISK / TRASH"
: "Click again to confirm removing from batch";
? t("Ctrl+Click again to delete file from DISK / TRASH")
: t("Click again to confirm removing from batch");
deleteTimeout = setTimeout(resetDelete, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
@@ -514,8 +515,8 @@ export function renderSidebar() {
if (shouldDeleteFromDisk && img && img.filename) {
const action = await deleteFileOnServer(img);
if (action) {
const msg = action === "trashed" ? "Moved file to Recycle Bin." : "Permanently deleted file.";
showNotification(msg, "success", "Delete File");
const msg = action === "trashed" ? t("Moved file to Recycle Bin.") : t("Permanently deleted file.");
showNotification(msg, "success", t("Delete File"));
}
}
@@ -565,7 +566,7 @@ export function renderSidebar() {
const btnCopy = document.createElement("span");
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.title = "Copy to Clipboard";
btnCopy.title = t("Copy to Clipboard");
const isCopyable = isImageFormat(img.filename || img.url);
if (!isCopyable) {
btnCopy.style.display = "none";
@@ -587,7 +588,7 @@ export function renderSidebar() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
btnFocus.title = t("Show Node");
const nodeId = findNodeIdForImage({ nodeOutputs: batchInfo.nodeOutputs }, img);
if (nodeId) {
btnFocus.onclick = (ev) => {
@@ -649,9 +650,9 @@ export function renderSidebar() {
}
if (headerTitle) {
headerTitle.textContent = `Outputs of #${store.ui.activeSubmenuPromptId}`;
headerTitle.textContent = `${t("Outputs of #")}${store.ui.activeSubmenuPromptId}`;
headerTitle.style.cursor = "pointer";
headerTitle.title = "Go Back to Queue";
headerTitle.title = t("Go Back to Queue");
headerTitle.onclick = () => {
store.closeSubmenu();
resetAllCardHoverStates();
@@ -691,7 +692,7 @@ export function renderSidebar() {
padding: "16px", textAlign: "center", color: "var(--desc-color, #aaa)", fontSize: "12px",
gridColumn: "1 / -1"
});
emptyCard.textContent = "No separate intermediate outputs found.";
emptyCard.textContent = t("No separate intermediate outputs found.");
targetElements.push(emptyCard);
} else {
outputs.forEach((out) => {
@@ -738,7 +739,7 @@ export function renderSidebar() {
const btnImg = document.createElement("span");
btnImg.className = "pi pi-image comfy-sidebar-card-action-btn";
btnImg.title = "Download Object";
btnImg.title = t("Download Object");
btnImg.onclick = (ev) => {
ev.stopPropagation();
out.images.forEach(img => {
@@ -751,10 +752,10 @@ export function renderSidebar() {
const btnDel = document.createElement("span");
btnDel.className = "pi pi-trash comfy-sidebar-card-action-btn comfy-sidebar-btn-del";
btnDel.title = "Delete node output (Hold Ctrl to delete file from disk)";
btnDel.title = t("Delete node output (Hold Ctrl to delete file from disk)");
const btnDelLabel = document.createElement("span");
btnDelLabel.className = "comfy-sidebar-del-label";
btnDelLabel.textContent = "Delete File";
btnDelLabel.textContent = t("Delete File");
btnDel.appendChild(btnDelLabel);
let isDeletePending = false, isDiskDelete = false, deleteTimeout = null;
@@ -762,7 +763,7 @@ export function renderSidebar() {
isDeletePending = false;
isDiskDelete = false;
btnDel.classList.remove("confirm-delete", "confirm-delete-disk");
btnDel.title = "Delete node output (Hold Ctrl to delete file from disk)";
btnDel.title = t("Delete node output (Hold Ctrl to delete file from disk)");
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
};
@@ -775,8 +776,8 @@ export function renderSidebar() {
isDiskDelete = wantsDiskDelete;
btnDel.classList.add(wantsDiskDelete ? "confirm-delete-disk" : "confirm-delete");
btnDel.title = wantsDiskDelete
? "Ctrl+Click again to delete this node's files from DISK / TRASH"
: "Click again to confirm removing output";
? t("Ctrl+Click again to delete this node's files from DISK / TRASH")
: t("Click again to confirm removing output");
deleteTimeout = setTimeout(resetDelete, 2000);
} else {
const shouldDeleteFromDisk = isDiskDelete || wantsDiskDelete;
@@ -825,7 +826,7 @@ export function renderSidebar() {
const btnCopy = document.createElement("span");
btnCopy.className = "pi pi-copy comfy-sidebar-card-action-btn";
btnCopy.title = "Copy to Clipboard";
btnCopy.title = t("Copy to Clipboard");
const firstImg = out.images[0];
const isCopyable = firstImg && isImageFormat(firstImg.filename || firstImg.url);
if (!isCopyable) {
@@ -848,7 +849,7 @@ export function renderSidebar() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye comfy-sidebar-card-action-btn";
btnFocus.title = "Show Node";
btnFocus.title = t("Show Node");
btnFocus.onclick = (ev) => {
ev.stopPropagation();
centerAndSelectCanvasNode(out.nodeId);
@@ -901,7 +902,7 @@ export function renderSidebar() {
});
} else {
cardObj.placeholder.style.display = "block";
cardObj.placeholder.textContent = "No Outputs";
cardObj.placeholder.textContent = t("No Outputs");
}
targetElements.push(cardObj.element);
@@ -920,7 +921,7 @@ export function renderSidebar() {
// Main Queue View
if (headerTitle) {
headerTitle.textContent = "Queue";
headerTitle.textContent = t("Queue");
headerTitle.style.cursor = "default";
headerTitle.onclick = null;
}
@@ -962,7 +963,7 @@ export function renderSidebar() {
const textDiv = document.createElement("div");
el.appendChild(textDiv);
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancel All Pending";
cancelBtn.textContent = t("Cancel All Pending");
Object.assign(cancelBtn.style, {
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "4px",
cursor: "pointer", fontSize: "11px", fontWeight: "bold"
@@ -974,13 +975,13 @@ export function renderSidebar() {
pCard = { element: el, textDiv };
cardPool.set("pending-summary-card", pCard);
}
pCard.textDiv.textContent = `Pending Queue: ${pendingCount} tasks`;
pCard.textDiv.textContent = `${t("Pending Queue:")} ${pendingCount} ${t("tasks")}`;
bannerElements.push(pCard.element);
} else {
let btnCard = cardPool.get("pending-cancel-all-standalone");
if (!btnCard) {
const btn = document.createElement("button");
btn.textContent = "Cancel All Pending";
btn.textContent = t("Cancel All Pending");
Object.assign(btn.style, {
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "6px",
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%"
+24 -13
View File
@@ -5,6 +5,7 @@ import { stopAllAudioPlayback } from "../ui/audioController.js";
import { SidebarOverlay } from "./overlay.js";
import { store } from "../core/store.js";
import { PromptStatus } from "../core/constants.js";
import { t } from "../core/i18n.js";
let activeComparisonViewer = null;
let isRuntimePreviewActive = false;
@@ -213,7 +214,7 @@ function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () =>
borderRadius: "4px", padding: "4px 8px", fontSize: "11px", cursor: "pointer"
});
loopBtn.innerHTML = `<i class="pi pi-replay"></i>`;
loopBtn.title = "Toggle Loop";
loopBtn.title = t("Toggle Loop");
const speedSelect = document.createElement("select");
Object.assign(speedSelect.style, {
@@ -501,8 +502,8 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
});
const infoText = document.createElement("span");
infoText.textContent = isBaseVideo
? "Video playback. Press Esc to close."
: "Click image to zoom (100%/Fit) | Shift+Click another card to compare.";
? t("Video playback. Press Esc to close.")
: t("Click image to zoom (100%/Fit) | Shift+Click another card to compare.");
header.appendChild(infoText);
overlay.container.appendChild(header);
@@ -667,12 +668,12 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
const arDiff = Math.abs(arA - arB) / Math.max(arA, arB);
if (arDiff > 0.05) {
infoText.textContent = `Aspect ratio mismatch (${wA}x${hA} vs ${wB}x${hB}). Images must have matching proportions to compare.`;
infoText.textContent = t("Aspect ratio mismatch ({wA}x{hA} vs {wB}x{hB}). Images must have matching proportions to compare.", { wA, hA, wB, hB });
infoText.style.color = "#f87171";
setTimeout(() => {
if (infoText) {
infoText.style.color = "#aaa";
infoText.textContent = "Click image to zoom (100%/Fit) | Shift+Click another card to compare.";
infoText.textContent = t("Click image to zoom (100%/Fit) | Shift+Click another card to compare.");
}
}, 3500);
@@ -689,7 +690,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
if (mediaA) mediaA.style.pointerEvents = "none";
if (mediaB) mediaB.style.pointerEvents = "none";
infoText.style.color = "#aaa";
infoText.textContent = "Click image to zoom | Drag image to compare slider | Esc to close.";
infoText.textContent = t("Click image to zoom | Drag image to compare slider | Esc to close.");
updateSliderPosition(50);
const maxW = Math.max(wA, wB);
@@ -738,13 +739,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
@@ -887,7 +892,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
mediaA.onload = () => {
hidePlaceholder();
infoText.style.color = "#aaa";
infoText.textContent = "Click image to zoom (100%/Fit) | Shift+Click another card to compare.";
infoText.textContent = t("Click image to zoom (100%/Fit) | Shift+Click another card to compare.");
syncImageScales();
};
}
@@ -912,6 +917,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 +935,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 +958,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;
+17 -14
View File
@@ -1,8 +1,9 @@
import { app } from "/scripts/app.js";
import { t } from "../core/i18n.js";
import { syncAllNodeBadges } from "../comfy/nodes.js";
export function showNotification(message, type = "success", title = null) {
const summary = title || (type === "error" ? "Error" : type === "warn" ? "Warning" : "Comfy Sidebar");
const summary = title || (type === "error" ? t("Error") : type === "warn" ? t("Warning") : "Comfy Sidebar");
if (app.extensionManager?.toast?.add) {
app.extensionManager.toast.add({
@@ -57,14 +58,12 @@ export function showNotification(message, type = "success", title = null) {
export async function renumberNodesTopologically() {
if (!app.graph || !app.graph._nodes || app.graph._nodes.length === 0) {
showNotification("No nodes found on canvas to renumber.", "warn");
showNotification(t("No nodes found on canvas to renumber."), "warn");
return;
}
const confirmed = confirm(
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\n" +
"This will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\n" +
"Do you want to proceed?"
t("Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?")
);
if (!confirmed) return;
@@ -137,28 +136,32 @@ export async function renumberNodesTopologically() {
const lastNode = sortedNodes[sortedNodes.length - 1];
const lastNodeName = lastNode ? (lastNode.title || lastNode.type) : "SaveImage";
showNotification(`Renumbered ${sortedNodes.length} nodes by execution order. Latest node: #${lastNode.id} (${lastNodeName}).`, "success");
showNotification(t("Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).", { count: sortedNodes.length, id: lastNode ? lastNode.id : "", name: lastNodeName }), "success");
} catch (err) {
console.error("ComfySidebar: Failed to renumber nodes", err);
showNotification("Failed to renumber nodes: " + err.message, "error");
showNotification(t("Failed to renumber nodes: ") + err.message, "error");
}
}
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");
showNotification(t("Please select a node first to change its ID."), "warn");
return;
}
const currentId = node.id;
const input = prompt(`Enter new ID for node #${currentId} (${node.title || node.type}):`, String(currentId));
const input = prompt(t("Enter new ID for node #{id} ({name}):", { id: currentId, name: node.title || node.type }), String(currentId));
if (!input) return;
const newId = parseInt(input.trim(), 10);
if (isNaN(newId) || newId <= 0) {
showNotification("Please enter a valid positive number.", "error");
showNotification(t("Please enter a valid positive number."), "error");
return;
}
if (newId === currentId) return;
@@ -166,7 +169,7 @@ export async function setManualNodeId(targetNode = null) {
try {
const existingNode = app.graph.getNodeById ? app.graph.getNodeById(newId) : null;
if (existingNode && existingNode !== node) {
const confirmSwap = confirm(`Node ID #${newId} is already used by "${existingNode.title || existingNode.type}". Swap their IDs?`);
const confirmSwap = confirm(t("Node ID #{id} is already used by \"{name}\". Swap their IDs?", { id: newId, name: existingNode.title || existingNode.type }));
if (!confirmSwap) return;
}
@@ -205,9 +208,9 @@ export async function setManualNodeId(targetNode = null) {
app.canvas.draw(true, true);
}
syncAllNodeBadges();
showNotification(`Assigned node ID #${newId} to "${node.title || node.type}".`, "success");
showNotification(t("Assigned node ID #{id} to \"{name}\".", { id: newId, name: node.title || node.type }), "success");
} catch (err) {
console.error("ComfySidebar: Failed to set node ID", err);
showNotification("Failed to set node ID: " + err.message, "error");
showNotification(t("Failed to set node ID: ") + err.message, "error");
}
}
+21
View File
@@ -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;
+20 -19
View File
@@ -1,4 +1,5 @@
import { SidebarOverlay } from "./overlay.js";
import { t } from "../core/i18n.js";
// Global helper for code block copying in Markdown view
window.moonCopyCode = function(button) {
@@ -10,23 +11,23 @@ window.moonCopyCode = function(button) {
const text = codeEl.innerText;
navigator.clipboard.writeText(text).then(() => {
button.innerText = "Copied!";
button.innerText = t("Copied!");
button.style.backgroundColor = "#4CAF50";
button.style.borderColor = "#45a049";
button.style.color = "#ffffff";
setTimeout(() => {
button.innerText = "Copy";
button.innerText = t("Copy");
button.style.backgroundColor = "#333333";
button.style.borderColor = "#555555";
button.style.color = "#aaaaaa";
}, 1500);
}).catch(() => {
button.innerText = "Error";
setTimeout(() => button.innerText = "Copy", 1500);
button.innerText = t("Error");
setTimeout(() => button.innerText = t("Copy"), 1500);
});
} catch (e) {
button.innerText = "Error";
setTimeout(() => button.innerText = "Copy", 1500);
button.innerText = t("Error");
setTimeout(() => button.innerText = t("Copy"), 1500);
}
};
@@ -39,7 +40,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;
}
@@ -91,7 +92,7 @@ function parseMarkdown(text) {
const rCode = codeContent.join("\n");
const langBadge = codeLang ? `<div style="position:absolute;top:6px;left:10px;color:#85c5ec;font-size:10px;font-family:sans-serif;font-weight:bold;text-transform:uppercase;opacity:0.8;user-select:none;">${codeLang}</div>` : '';
const btnStyle = "position:absolute;top:6px;right:6px;background:#333;color:#aaa;border:1px solid #555;border-radius:3px;padding:3px 8px;font-size:10px;cursor:pointer;";
htmlLines.push(`<div class="code-block-container" style="position:relative;margin:8px 0;">${langBadge}<button onclick="window.moonCopyCode(this)" style="${btnStyle}">Copy</button><pre style="background:#1e1e1e;padding:10px;padding-top:28px;border-radius:4px;border:1px solid #333;font-family:monospace;overflow-x:auto;margin:0;color:#85c5ec;"><code>${rCode}</code></pre></div>`);
htmlLines.push(`<div class="code-block-container" style="position:relative;margin:8px 0;">${langBadge}<button onclick="window.moonCopyCode(this)" style="${btnStyle}">${t("Copy")}</button><pre style="background:#1e1e1e;padding:10px;padding-top:28px;border-radius:4px;border:1px solid #333;font-family:monospace;overflow-x:auto;margin:0;color:#85c5ec;"><code>${rCode}</code></pre></div>`);
codeContent = []; codeLang = ""; inCodeBlock = false;
} else {
inCodeBlock = true; codeLang = trimmed.slice(3).trim();
@@ -192,12 +193,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, {
@@ -213,9 +214,9 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
});
const formatTitle = (id) => {
if (!id) return "Text Output";
if (!id) return t("Text Output");
const shortId = id.length > 10 ? `${id.slice(0, 8)}…` : id;
return `Text Output #${shortId}`;
return `${t("Text Output #")}${shortId}`;
};
const headerRow = document.createElement("div");
@@ -233,7 +234,7 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const title = document.createElement("span");
title.textContent = formatTitle(pid);
title.title = pid ? `Text Output #${pid}` : "Text Output";
title.title = pid ? `${t("Text Output #")}${pid}` : t("Text Output");
Object.assign(title.style, {
fontSize: "14px", fontWeight: "bold", color: "#f8fafc",
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flexShrink: "1"
@@ -267,7 +268,7 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "6px",
whiteSpace: "nowrap", height: "28px", boxSizing: "border-box"
});
copyBtn.innerHTML = `<i class="pi pi-copy"></i><span>Copy</span>`;
copyBtn.innerHTML = `<i class="pi pi-copy"></i><span>${t("Copy")}</span>`;
const downloadBtn = document.createElement("button");
Object.assign(downloadBtn.style, {
@@ -296,7 +297,7 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
const updateView = () => {
const wc = rawText.trim() ? rawText.trim().split(/\s+/).length : 0;
statsBadge.textContent = `${wc} words • ${rawText.length} chars`;
statsBadge.textContent = t("{wc} words • {chars} chars", { wc, chars: rawText.length });
if (isMarkdownMode) {
modeBtn.innerHTML = `<i class="pi pi-code"></i><span>Markdown</span>`;
@@ -306,7 +307,7 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
textArea.style.whiteSpace = "normal";
textArea.innerHTML = parseMarkdown(rawText);
} else {
modeBtn.innerHTML = `<i class="pi pi-align-left"></i><span>Raw Text</span>`;
modeBtn.innerHTML = `<i class="pi pi-align-left"></i><span>${t("Raw Text")}</span>`;
modeBtn.style.background = "#334155";
modeBtn.style.borderColor = "#64748b";
textArea.style.fontFamily = "monospace";
@@ -322,8 +323,8 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
copyBtn.onclick = () => {
navigator.clipboard.writeText(rawText);
copyBtn.innerHTML = `<i class="pi pi-check" style="color:#10b981;"></i><span style="color:#10b981;">Copied!</span>`;
setTimeout(() => { copyBtn.innerHTML = `<i class="pi pi-copy"></i><span>Copy</span>`; }, 1800);
copyBtn.innerHTML = `<i class="pi pi-check" style="color:#10b981;"></i><span style="color:#10b981;">${t("Copied!")}</span>`;
setTimeout(() => { copyBtn.innerHTML = `<i class="pi pi-copy"></i><span>${t("Copy")}</span>`; }, 1800);
};
downloadBtn.onclick = () => {
@@ -352,7 +353,7 @@ export function createTextReader(textData, onSwitchMedia = () => {}, onDestroy =
rawText = targetData.text || "";
pid = targetData.pid || "";
title.textContent = formatTitle(pid);
title.title = pid ? `Text Output #${pid}` : "Text Output";
title.title = pid ? `${t("Text Output #")}${pid}` : t("Text Output");
updateView();
},
destroy: () => overlay.destroy()
+26 -7
View File
@@ -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)) {
+32 -30
View File
@@ -1,5 +1,6 @@
import { is3DFormat, getFilenameFromUrl } from "./utils.js";
import { SidebarOverlay } from "./overlay.js";
import { t } from "../core/i18n.js";
let cachedThreeLibs = null;
@@ -54,10 +55,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");
@@ -69,7 +71,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
boxShadow: "0 2px 6px rgba(0,0,0,0.4)", maxWidth: "85%", textAlign: "center"
});
const titleText = document.createElement("span");
titleText.textContent = getFilenameFromUrl(baseSrc) || "3D Model";
titleText.textContent = getFilenameFromUrl(baseSrc) || t("3D Model");
header.appendChild(titleText);
overlay.container.appendChild(header);
@@ -87,7 +89,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
background: "rgba(0,0,0,0.8)", padding: "10px 18px", borderRadius: "8px",
display: "flex", alignItems: "center", gap: "10px"
});
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner" style="font-size: 18px;"></span> Loading 3D Asset...`;
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner" style="font-size: 18px;"></span> ${t("Loading 3D Asset...")}`;
overlay.container.appendChild(loadingSpinner);
const settingsPanel = document.createElement("div");
@@ -220,7 +222,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
try {
loadingSpinner.style.display = "flex";
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner"></span> Exporting ${format}...`;
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner"></span> ${t("Exporting {format}...", { format })}`;
const base = window.__COMFY_SIDEBAR_THREE_BASE || "https://esm.sh/";
if (format === "OBJ") {
@@ -267,16 +269,16 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
const initSceneAndLoad = async (srcUrl) => {
loadingSpinner.style.display = "flex";
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner" style="font-size: 18px;"></span> Loading 3D Asset...`;
titleText.textContent = getFilenameFromUrl(srcUrl) || "3D Model";
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner" style="font-size: 18px;"></span> ${t("Loading 3D Asset...")}`;
titleText.textContent = getFilenameFromUrl(srcUrl) || t("3D Model");
THREE = await getThreeLibs();
if (!THREE) {
loadingSpinner.innerHTML = `
<div style="text-align: center; max-width: 340px; line-height: 1.4; padding: 6px;">
<span style="color: #f87171; font-weight: bold; font-size: 13px;">Failed to load 3D engine.</span><br>
<span style="color: #f87171; font-weight: bold; font-size: 13px;">${t("Failed to load 3D engine.")}</span><br>
<span style="font-size: 11px; color: #cbd5e1; display: inline-block; margin-top: 4px;">
Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.
${t("Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.")}
</span>
</div>
`;
@@ -330,26 +332,26 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
bgColorInput.oninput = (e) => { scene.background.set(e.target.value); };
const upDirSelect = createSelect([
{ value: "y-up", label: "Original (Y-Up)" },
{ value: "z-up", label: "Z-Up (+90°)" },
{ value: "z-down", label: "Inverted (-90°)" }
{ value: "y-up", label: t("Original (Y-Up)") },
{ value: "z-up", label: t("Z-Up (+90°)") },
{ value: "z-down", label: t("Inverted (-90°)") }
], currentUpDirection, (val) => {
currentUpDirection = val;
applyUpDirection();
});
const matModeSelect = createSelect([
{ value: "original", label: "Original / Textured" },
{ value: "clay", label: "Clay / Studio Matte" },
{ value: "normal", label: "Normals" }
{ value: "original", label: t("Original / Textured") },
{ value: "clay", label: t("Clay / Studio Matte") },
{ value: "normal", label: t("Normals") }
], currentMaterialMode, (val) => {
currentMaterialMode = val;
applyMaterialMode();
});
const camTypeSelect = createSelect([
{ value: "perspective", label: "Perspective" },
{ value: "orthographic", label: "Orthographic" }
{ value: "perspective", label: t("Perspective") },
{ value: "orthographic", label: t("Orthographic") }
], "perspective", (val) => {
const prevPos = camera.position.clone();
const prevTarget = controls.target.clone();
@@ -395,18 +397,18 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
lightContainer.append(lightSlider, lightVal);
settingsPanel.append(
createFormGroup("Background Color", bgColorInput),
createFormGroup("Up Direction", upDirSelect),
createFormGroup("Material Mode", matModeSelect),
createFormGroup("Camera Type", camTypeSelect),
createFormGroup("FOV", fovContainer),
createFormGroup("Light Intensity", lightContainer)
createFormGroup(t("Background Color"), bgColorInput),
createFormGroup(t("Up Direction"), upDirSelect),
createFormGroup(t("Material Mode"), matModeSelect),
createFormGroup(t("Camera Type"), camTypeSelect),
createFormGroup(t("FOV"), fovContainer),
createFormGroup(t("Light Intensity"), lightContainer)
);
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; });
const btnWire = createToolbarBtn("pi pi-box", "Wireframe", false, (v) => {
const btnGrid = createToolbarBtn("pi pi-table", t("Grid"), true, (v) => { gridHelper.visible = v; });
const btnRotate = createToolbarBtn("pi pi-sync", t("Auto-Rotate"), false, (v) => { autoRotate = v; });
const btnWire = createToolbarBtn("pi pi-box", t("Wireframe"), false, (v) => {
showWireframe = v;
applyMaterialMode();
});
@@ -417,18 +419,18 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
borderRadius: "4px", padding: "4px 10px", fontSize: "11px", cursor: "pointer",
display: "flex", alignItems: "center", gap: "6px"
});
btnResetCam.innerHTML = `<i class="pi pi-compass"></i><span>Reset View</span>`;
btnResetCam.innerHTML = `<i class="pi pi-compass"></i><span>${t("Reset View")}</span>`;
btnResetCam.onclick = (e) => {
e.stopPropagation();
if (currentModel) fitCameraToObject(camera, currentModel, controls, THREE);
};
const btnSettings = createToolbarBtn("pi pi-sliders-h", "Settings", false, (v) => {
const btnSettings = createToolbarBtn("pi pi-sliders-h", t("Settings"), false, (v) => {
settingsPanel.style.display = v ? "flex" : "none";
});
const exportSelect = createSelect([
{ value: "", label: "Export As..." },
{ value: "", label: t("Export As...") },
{ value: "OBJ", label: "OBJ (.obj)" },
{ value: "GLB", label: "GLB / GLTF" },
{ value: "STL", label: "STL (.stl)" }
@@ -517,7 +519,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
}
} catch (err) {
console.error("Comfy Sidebar: Failed to parse 3D model", err);
loadingSpinner.innerHTML = `<span style="color:#f87171;">Failed to load 3D format</span>`;
loadingSpinner.innerHTML = `<span style="color:#f87171;">${t("Failed to load 3D format")}</span>`;
return;
}
+2 -2
View File
@@ -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.2"
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"