mirror of
https://github.com/tusharbhutt/Endless-Nodes.git
synced 2026-03-21 20:42:12 -03:00
Added minimap and node spawner
Added minimap and node spawner
This commit is contained in:
@@ -1,531 +1,469 @@
|
||||
// ComfyUI Endless 🌊✨ Fontifier - Improved Version
|
||||
// ComfyUI Endless 🌊✨ Fontifier - Fully Fixed Version
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Store original values for reset functionality
|
||||
(function waitForHelpers() {
|
||||
if (typeof window.EndlessHelpers === 'undefined') {
|
||||
console.warn("⏳ Waiting for EndlessHelpers to be ready...");
|
||||
setTimeout(waitForHelpers, 100); // Retry every 100ms
|
||||
return;
|
||||
}
|
||||
|
||||
// Load helpers from window
|
||||
const {
|
||||
registerEndlessTool,
|
||||
injectEndlessToolsButton,
|
||||
showEndlessToolMenu,
|
||||
onThemeChange,
|
||||
getComfyUIColors,
|
||||
toRGBA,
|
||||
blendColors,
|
||||
addButtonHoverEffects,
|
||||
makeDraggable
|
||||
} = window.EndlessHelpers;
|
||||
|
||||
// === ORIGINAL COMFYUI DEFAULTS ===
|
||||
const originalValues = {
|
||||
NODE_TEXT_SIZE: 14,
|
||||
NODE_SUBTEXT_SIZE: 12,
|
||||
NODE_TITLE_HEIGHT: 30,
|
||||
DEFAULT_GROUP_FONT: 24,
|
||||
NODE_FONT: 'Arial',
|
||||
NODE_SLOT_HEIGHT: 20,
|
||||
NODE_WIDGET_HEIGHT: 20
|
||||
NODE_TEXT_SIZE: LiteGraph.NODE_TEXT_SIZE || 14,
|
||||
NODE_SUBTEXT_SIZE: LiteGraph.NODE_SUBTEXT_SIZE || 12,
|
||||
NODE_TITLE_HEIGHT: LiteGraph.NODE_TITLE_HEIGHT || 30,
|
||||
DEFAULT_GROUP_FONT: LiteGraph.DEFAULT_GROUP_FONT || 24,
|
||||
NODE_FONT: LiteGraph.NODE_FONT || 'Arial',
|
||||
NODE_SLOT_HEIGHT: LiteGraph.NODE_SLOT_HEIGHT || 20,
|
||||
NODE_WIDGET_HEIGHT: LiteGraph.NODE_WIDGET_HEIGHT || 20,
|
||||
WIDGET_TEXT_SIZE: 12,
|
||||
GLOBAL_SCALE: 1
|
||||
};
|
||||
|
||||
// Current values (will be updated as user changes them)
|
||||
let currentValues = { ...originalValues };
|
||||
|
||||
// Get ComfyUI theme colors
|
||||
function getComfyUIColors() {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
return {
|
||||
background: computedStyle.getPropertyValue('--comfy-menu-bg') || '#353535',
|
||||
backgroundSecondary: computedStyle.getPropertyValue('--comfy-input-bg') || '#222',
|
||||
border: computedStyle.getPropertyValue('--border-color') || '#999',
|
||||
text: computedStyle.getPropertyValue('--input-text') || '#ddd',
|
||||
textSecondary: computedStyle.getPropertyValue('--descrip-text') || '#999',
|
||||
accent: computedStyle.getPropertyValue('--comfy-menu-bg') || '#0f0f0f'
|
||||
};
|
||||
}
|
||||
|
||||
function makeDraggable(dialog) {
|
||||
const header = dialog.querySelector('h2');
|
||||
if (!header) return;
|
||||
const saved = localStorage.getItem("endless_fontifier_defaults");
|
||||
let currentValues = saved ? JSON.parse(saved) : { ...originalValues };
|
||||
let dialogOpenValues = null;
|
||||
let currentDialog = null;
|
||||
let handlersSetup = false;
|
||||
let escHandler = null;
|
||||
let unregisterThemeCallback = null;
|
||||
let isPreviewMode = false;
|
||||
|
||||
let offsetX = 0, offsetY = 0, isDown = false;
|
||||
|
||||
header.style.cursor = 'move';
|
||||
header.style.userSelect = 'none';
|
||||
|
||||
header.onmousedown = (e) => {
|
||||
e.preventDefault();
|
||||
isDown = true;
|
||||
|
||||
// Get the actual position of the dialog
|
||||
const rect = dialog.getBoundingClientRect();
|
||||
offsetX = e.clientX - rect.left;
|
||||
offsetY = e.clientY - rect.top;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
if (!isDown) return;
|
||||
e.preventDefault();
|
||||
dialog.style.left = `${e.clientX - offsetX}px`;
|
||||
dialog.style.top = `${e.clientY - offsetY}px`;
|
||||
dialog.style.transform = 'none'; // Remove the centering transform
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
isDown = false;
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
}
|
||||
|
||||
function createFontifierDialog() {
|
||||
// Remove existing dialog if present
|
||||
const existingDialog = document.getElementById('fontifier-dialog');
|
||||
if (existingDialog) {
|
||||
existingDialog.remove();
|
||||
}
|
||||
|
||||
if (currentDialog) return;
|
||||
|
||||
const colors = getComfyUIColors();
|
||||
|
||||
// Create dialog container
|
||||
const dialog = document.createElement('div');
|
||||
dialog.id = 'fontifier-dialog';
|
||||
dialog.className = 'comfyui-dialog';
|
||||
|
||||
const dialog = document.createElement("div");
|
||||
dialog.id = "fontifier-dialog";
|
||||
dialog.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: ${colors.background};
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
z-index: 10000;
|
||||
width: 520px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
font-family: Arial, sans-serif;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
color: ${colors.text};
|
||||
`;
|
||||
|
||||
// Create backdrop
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'comfyui-backdrop';
|
||||
backdrop.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 100px;
|
||||
left: 100px;
|
||||
width: 320px;
|
||||
background: ${colors.dialogBg || colors.menu || 'rgba(20, 20, 20, 0.95)'};
|
||||
color: ${colors.inputText || '#fff'};
|
||||
font-family: sans-serif;
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 10px;
|
||||
box-shadow: ${colors.shadow || '0 0 20px rgba(0,0,0,0.5)'};
|
||||
padding: 10px;
|
||||
`;
|
||||
backdrop.onclick = () => {
|
||||
backdrop.remove();
|
||||
dialog.remove();
|
||||
};
|
||||
|
||||
|
||||
// Clean up any existing style tags
|
||||
const existingStyle = document.getElementById('fontifier-dialog-style');
|
||||
if (existingStyle) existingStyle.remove();
|
||||
|
||||
// Themed style block
|
||||
const style = document.createElement("style");
|
||||
style.id = "fontifier-dialog-style";
|
||||
style.textContent = createStyleCSS(colors);
|
||||
document.head.appendChild(style);
|
||||
|
||||
dialog.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; border-bottom: 1px solid ${colors.border}; padding-bottom: 15px;">
|
||||
<h2 style="color: ${colors.text}; margin: 0; font-size: 16px;">🌊✨ Endless Fontifier</h2>
|
||||
<button id="close-dialog" style="background: ${colors.backgroundSecondary}; border: 1px solid ${colors.border}; color: ${colors.text}; padding: 6px 12px; border-radius: 4px; cursor: pointer;">✕</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px; padding: 12px; background: ${colors.backgroundSecondary}; border-radius: 6px; border: 1px solid ${colors.border};">
|
||||
<h3 style="color: ${colors.text}; margin: 0 0 10px 0; font-size: 16px;">Global Scale</h3>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<label style="color: ${colors.textSecondary}; min-width: 80px; font-size: 12px;">Scale All:</label>
|
||||
<input type="range" id="global-scale" min="0.5" max="3" step="0.1" value="1" style="flex: 1; accent-color: ${colors.accent};">
|
||||
<input type="number" id="global-scale-num" min="0.5" max="3" step="0.1" value="1" style="width: 70px; padding: 6px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
<div id="drag-bar" style="text-align:center; padding:6px; background:${colors.menuSecondary || '#2a2a2a'}; cursor:move; border-radius:10px 10px 0 0;">Endless 🌊✨ Drag Bar</div>
|
||||
<h2 style="margin: 8px 0; text-align: center;">Fontifier Settings</h2>
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Global Scale</label>
|
||||
<div class="fontifier-row">
|
||||
<input type="range" id="global-scale" min="0.5" max="2" step="0.01" value="1" title="Overall scaling factor for all font sizes">
|
||||
<input type="number" id="global-scale-num" min="0.5" max="2" step="0.01" value="1" title="Overall scaling factor for all font sizes">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px; padding: 12px; background: ${colors.backgroundSecondary}; border-radius: 6px; border: 1px solid ${colors.border};">
|
||||
<h3 style="color: ${colors.text}; margin: 0 0 12px 0; font-size: 16px;">Font Family</h3>
|
||||
<select id="font-family" style="width: 100%; padding: 8px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Text Size</label>
|
||||
<div class="fontifier-row">
|
||||
<input type="range" id="node-text-size" min="8" max="32" value="14" title="Font size for node text content and labels">
|
||||
<input type="number" id="node-text-size-num" min="8" max="32" value="14" title="Font size for node text content and labels">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Subtext Size</label>
|
||||
<div class="fontifier-row">
|
||||
<input type="range" id="node-subtext-size" min="8" max="32" value="12" title="Font size for secondary text and descriptions">
|
||||
<input type="number" id="node-subtext-size-num" min="8" max="32" value="12" title="Font size for secondary text and descriptions">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Title Height</label>
|
||||
<div class="fontifier-row">
|
||||
<input type="range" id="title-height" min="20" max="60" value="30" title="Height of node title bars">
|
||||
<input type="number" id="title-height-num" min="20" max="60" value="30" title="Height of node title bars">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Slot Height</label>
|
||||
<div class="fontifier-row">
|
||||
<input type="range" id="slot-height" min="10" max="40" value="20" title="Height of input/output connection slots">
|
||||
<input type="number" id="slot-height-num" min="10" max="40" value="20" title="Height of input/output connection slots">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Group Font Size</label>
|
||||
<div class="fontifier-row">
|
||||
<input type="range" id="group-font-size" min="8" max="32" value="24" title="Font size for group labels and titles">
|
||||
<input type="number" id="group-font-size-num" min="8" max="32" value="24" title="Font size for group labels and titles">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Widget Font Size</label>
|
||||
<div class="fontifier-row">
|
||||
<input type="range" id="widget-text-size" min="8" max="32" value="12" title="Font size for input widgets and controls">
|
||||
<input type="number" id="widget-text-size-num" min="8" max="32" value="12" title="Font size for input widgets and controls">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fontifier-setting">
|
||||
<label>Font Family</label>
|
||||
<select id="font-family" style="width: 100%;" title="Choose the font family for all text elements">
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
<option value="Comic Sans MS">Comic Sans MS</option>
|
||||
<option value="Impact">Impact</option>
|
||||
<option value="Trebuchet MS">Trebuchet MS</option>
|
||||
<option value="Tahoma">Tahoma</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px; padding: 12px; background: ${colors.backgroundSecondary}; border-radius: 6px; border: 1px solid ${colors.border};">
|
||||
<h3 style="color: ${colors.text}; margin: 0 0 12px 0; font-size: 16px;">Text Element Sizes</h3>
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label style="color: ${colors.text}; display: block; margin-bottom: 4px; font-size: 12px; font-weight: bold;">Node Title Text</label>
|
||||
<div style="color: ${colors.textSecondary}; font-size: 10px; margin-bottom: 5px;">The main title text at the top of each node (e.g., "KSampler", "VAE Decode")</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="range" id="node-text-size" min="8" max="32" value="${currentValues.NODE_TEXT_SIZE}" style="flex: 1; accent-color: ${colors.accent};">
|
||||
<input type="number" id="node-text-size-num" min="8" max="32" value="${currentValues.NODE_TEXT_SIZE}" style="width: 60px; padding: 5px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label style="color: ${colors.text}; display: block; margin-bottom: 4px; font-size: 12px; font-weight: bold;">Widget Labels & Values</label>
|
||||
<div style="color: ${colors.textSecondary}; font-size: 10px; margin-bottom: 5px;">Text inside nodes: parameter names and values (e.g., "steps: 20", "cfg: 8.0")</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="range" id="node-subtext-size" min="6" max="24" value="${currentValues.NODE_SUBTEXT_SIZE}" style="flex: 1; accent-color: ${colors.accent};">
|
||||
<input type="number" id="node-subtext-size-num" min="6" max="24" value="${currentValues.NODE_SUBTEXT_SIZE}" style="width: 60px; padding: 5px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label style="color: ${colors.text}; display: block; margin-bottom: 4px; font-size: 12px; font-weight: bold;">Widget Text Input Size</label>
|
||||
<div style="color: ${colors.textSecondary}; font-size: 10px; margin-bottom: 5px;">Font size for text inside input boxes, dropdowns, and textareas in nodes.</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="range" id="widget-text-size" min="8" max="24" value="12" style="flex: 1; accent-color: ${colors.accent};">
|
||||
<input type="number" id="widget-text-size-num" min="8" max="24" value="12" style="width: 60px; padding: 5px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label style="color: ${colors.text}; display: block; margin-bottom: 4px; font-size: 12px; font-weight: bold;">Node Title Area Height</label>
|
||||
<div style="color: ${colors.textSecondary}; font-size: 10px; margin-bottom: 5px;">Height of the colored title bar area at the top of nodes</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="range" id="title-height" min="20" max="60" value="${currentValues.NODE_TITLE_HEIGHT}" style="flex: 1; accent-color: ${colors.accent};">
|
||||
<input type="number" id="title-height-num" min="20" max="60" value="${currentValues.NODE_TITLE_HEIGHT}" style="width: 60px; padding: 5px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label style="color: ${colors.text}; display: block; margin-bottom: 4px; font-size: 12px; font-weight: bold;">Connection Slot Height</label>
|
||||
<div style="color: ${colors.textSecondary}; font-size: 10px; margin-bottom: 5px;">Height of input/output connection points on node sides</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="range" id="slot-height" min="12" max="40" value="${currentValues.NODE_SLOT_HEIGHT}" style="flex: 1; accent-color: ${colors.accent};">
|
||||
<input type="number" id="slot-height-num" min="12" max="40" value="${currentValues.NODE_SLOT_HEIGHT}" style="width: 60px; padding: 5px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label style="color: ${colors.text}; display: block; margin-bottom: 4px; font-size: 12px; font-weight: bold;">Group Label Size</label>
|
||||
<div style="color: ${colors.textSecondary}; font-size: 10px; margin-bottom: 5px;">Text size for node group labels (when nodes are grouped together)</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="range" id="group-font-size" min="12" max="48" value="${currentValues.DEFAULT_GROUP_FONT}" style="flex: 1; accent-color: ${colors.accent};">
|
||||
<input type="number" id="group-font-size-num" min="12" max="48" value="${currentValues.DEFAULT_GROUP_FONT}" style="width: 60px; padding: 5px; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; font-size: 12px;">
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview-indicator" style="display: none; text-align: center; color: ${colors.accent}; font-size: 12px; margin: 8px 0;">
|
||||
🔍 Preview Mode - Changes not saved
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; justify-content: center; padding-top: 15px; border-top: 1px solid ${colors.border};">
|
||||
<button id="reset-btn" style="padding: 8px 16px; background: ${colors.backgroundSecondary}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; cursor: pointer; font-size: 12px; transition: border-width 0.2s ease;">Reset</button>
|
||||
<button id="preview-btn" style="padding: 8px 16px; background: ${colors.backgroundSecondary}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; cursor: pointer; font-size: 12px; transition: border-width 0.2s ease;">Preview</button>
|
||||
<button id="apply-btn" style="padding: 8px 16px; background: ${colors.backgroundSecondary}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 4px; cursor: pointer; font-size: 12px; transition: border-width 0.2s ease; background-image: linear-gradient(rgba(128, 255, 128, 0.08), rgba(128, 255, 128, 0.08));">Apply & Close</button>
|
||||
<button id="cancel-btn" style="padding: 8px 16px; background: ${colors.backgroundSecondary}; border: 1px solid ${colors.border}; color: ${colors.textSecondary}; border-radius: 4px; cursor: pointer; font-size: 12px; transition: border-width 0.2s ease; background-image: linear-gradient(rgba(255, 128, 128, 0.08), rgba(255, 128, 128, 0.08));">Cancel</button>
|
||||
|
||||
<div style="margin-top: 12px; display: flex; flex-wrap: wrap; gap: 6px; justify-content: space-between;">
|
||||
<button id="apply-btn" title="Apply changes permanently and close dialog">Apply</button>
|
||||
<button id="preview-btn" title="Preview changes temporarily without saving">Preview</button>
|
||||
<button id="reset-btn" title="Reset to ComfyUI defaults">Reset</button>
|
||||
<button id="save-defaults-btn" title="Save current settings as defaults">💾 Save as Default</button>
|
||||
<button id="cancel-btn" title="Cancel changes and close dialog">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
makeDraggable(dialog, dialog.querySelector('#drag-bar'));
|
||||
setupDialogHandlers(dialog);
|
||||
currentDialog = dialog;
|
||||
|
||||
document.body.appendChild(backdrop);
|
||||
document.body.appendChild(dialog);
|
||||
// Store the values when dialog opens for cancel functionality
|
||||
dialogOpenValues = { ...currentValues };
|
||||
|
||||
// Set current values in the dialog
|
||||
updateDialogValues(dialog);
|
||||
|
||||
// ESC key handler
|
||||
document.addEventListener('keydown', function escHandler(e) {
|
||||
if (e.key === 'Escape') {
|
||||
backdrop.remove();
|
||||
dialog.remove();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
setupDialogHandlers(dialog, backdrop);
|
||||
// Live theme updating without closing dialog
|
||||
unregisterThemeCallback = onThemeChange(() => {
|
||||
if (currentDialog) {
|
||||
updateDialogTheme();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupDialogHandlers(dialog, backdrop) {
|
||||
|
||||
// call drag function
|
||||
function createStyleCSS(colors) {
|
||||
return `
|
||||
.fontifier-setting {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fontifier-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.fontifier-row input[type="range"] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fontifier-row input[type="number"] {
|
||||
width: 50px;
|
||||
background: ${colors.inputBg || '#222'};
|
||||
border: 1px solid ${colors.border || '#999'};
|
||||
color: ${colors.inputText || '#ddd'};
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
#fontifier-dialog button {
|
||||
flex-grow: 1;
|
||||
padding: 6px;
|
||||
background: ${colors.inputBg || '#222'};
|
||||
border: 1px solid ${colors.border || '#999'};
|
||||
color: ${colors.inputText || '#ddd'};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#fontifier-dialog button:hover {
|
||||
background: ${colors.buttonHoverBg || colors.hoverBg || blendColors(colors.inputBg || '#222', '#ffffff', 0.1)};
|
||||
}
|
||||
#fontifier-dialog #apply-btn {
|
||||
background: ${toRGBA(colors.accent || '#4CAF50', 0.3)};
|
||||
border-color: ${colors.accent || '#4CAF50'};
|
||||
}
|
||||
#fontifier-dialog #cancel-btn {
|
||||
background: ${toRGBA(colors.errorText || '#f44336', 0.3)};
|
||||
border-color: ${colors.errorText || '#f44336'};
|
||||
}
|
||||
#fontifier-dialog select {
|
||||
background: ${colors.inputBg || '#222'};
|
||||
border: 1px solid ${colors.border || '#999'};
|
||||
color: ${colors.inputText || '#ddd'};
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
makeDraggable(dialog);
|
||||
function updateDialogTheme() {
|
||||
if (!currentDialog) return;
|
||||
|
||||
const newColors = getComfyUIColors();
|
||||
|
||||
// Update main dialog styling
|
||||
currentDialog.style.background = newColors.dialogBg || newColors.menu || 'rgba(20, 20, 20, 0.95)';
|
||||
currentDialog.style.color = newColors.inputText || '#fff';
|
||||
currentDialog.style.borderColor = newColors.border || '#999';
|
||||
currentDialog.style.boxShadow = newColors.shadow || '0 0 20px rgba(0,0,0,0.5)';
|
||||
|
||||
// Update drag bar
|
||||
const dragBar = currentDialog.querySelector('#drag-bar');
|
||||
if (dragBar) {
|
||||
dragBar.style.background = newColors.menuSecondary || '#2a2a2a';
|
||||
}
|
||||
|
||||
// Update preview indicator
|
||||
const previewIndicator = currentDialog.querySelector('#preview-indicator');
|
||||
if (previewIndicator) {
|
||||
previewIndicator.style.color = newColors.accent || '#4CAF50';
|
||||
}
|
||||
|
||||
// Update the style tag with new colors
|
||||
const styleTag = document.getElementById('fontifier-dialog-style');
|
||||
if (styleTag) {
|
||||
styleTag.textContent = createStyleCSS(newColors);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDialogValues(dialog) {
|
||||
dialog.querySelector('#global-scale').value = currentValues.GLOBAL_SCALE || 1;
|
||||
dialog.querySelector('#global-scale-num').value = currentValues.GLOBAL_SCALE || 1;
|
||||
dialog.querySelector('#node-text-size').value = currentValues.NODE_TEXT_SIZE;
|
||||
dialog.querySelector('#node-text-size-num').value = currentValues.NODE_TEXT_SIZE;
|
||||
dialog.querySelector('#node-subtext-size').value = currentValues.NODE_SUBTEXT_SIZE;
|
||||
dialog.querySelector('#node-subtext-size-num').value = currentValues.NODE_SUBTEXT_SIZE;
|
||||
dialog.querySelector('#title-height').value = currentValues.NODE_TITLE_HEIGHT;
|
||||
dialog.querySelector('#title-height-num').value = currentValues.NODE_TITLE_HEIGHT;
|
||||
dialog.querySelector('#slot-height').value = currentValues.NODE_SLOT_HEIGHT;
|
||||
dialog.querySelector('#slot-height-num').value = currentValues.NODE_SLOT_HEIGHT;
|
||||
dialog.querySelector('#group-font-size').value = currentValues.DEFAULT_GROUP_FONT;
|
||||
dialog.querySelector('#group-font-size-num').value = currentValues.DEFAULT_GROUP_FONT;
|
||||
dialog.querySelector('#widget-text-size').value = currentValues.WIDGET_TEXT_SIZE;
|
||||
dialog.querySelector('#widget-text-size-num').value = currentValues.WIDGET_TEXT_SIZE;
|
||||
dialog.querySelector('#font-family').value = currentValues.NODE_FONT;
|
||||
}
|
||||
|
||||
function setupDialogHandlers(dialog) {
|
||||
if (handlersSetup) return;
|
||||
handlersSetup = true;
|
||||
|
||||
addButtonHoverEffects(dialog);
|
||||
|
||||
// Sync sliders with number inputs
|
||||
const elements = [
|
||||
'global-scale',
|
||||
'node-text-size',
|
||||
'node-subtext-size',
|
||||
'title-height',
|
||||
'node-text-size',
|
||||
'node-subtext-size',
|
||||
'title-height',
|
||||
'slot-height',
|
||||
'group-font-size',
|
||||
'widget-text-size'
|
||||
];
|
||||
|
||||
|
||||
elements.forEach(id => {
|
||||
const slider = dialog.querySelector(`#${id}`);
|
||||
const numberInput = dialog.querySelector(`#${id}-num`);
|
||||
|
||||
slider.oninput = () => {
|
||||
numberInput.value = slider.value;
|
||||
// Update global scale number input properly
|
||||
if (id === 'global-scale') {
|
||||
const globalScaleNum = dialog.querySelector('#global-scale-num');
|
||||
globalScaleNum.value = slider.value;
|
||||
}
|
||||
};
|
||||
numberInput.oninput = () => {
|
||||
slider.value = numberInput.value;
|
||||
};
|
||||
if (slider && numberInput) {
|
||||
slider.oninput = () => {
|
||||
numberInput.value = slider.value;
|
||||
if (isPreviewMode) showPreviewIndicator();
|
||||
};
|
||||
numberInput.oninput = () => {
|
||||
// Enforce min/max constraints
|
||||
const min = parseFloat(numberInput.min);
|
||||
const max = parseFloat(numberInput.max);
|
||||
let value = parseFloat(numberInput.value);
|
||||
if (value < min) value = min;
|
||||
if (value > max) value = max;
|
||||
numberInput.value = value;
|
||||
slider.value = value;
|
||||
if (isPreviewMode) showPreviewIndicator();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Global scale handler
|
||||
const globalScale = dialog.querySelector('#global-scale');
|
||||
const globalScaleNum = dialog.querySelector('#global-scale-num');
|
||||
|
||||
function updateGlobalScale() {
|
||||
const scale = parseFloat(globalScale.value);
|
||||
globalScaleNum.value = scale; // Fix: Update the number input
|
||||
|
||||
// Update all individual controls
|
||||
const updates = [
|
||||
['node-text-size', originalValues.NODE_TEXT_SIZE],
|
||||
['node-subtext-size', originalValues.NODE_SUBTEXT_SIZE],
|
||||
['title-height', originalValues.NODE_TITLE_HEIGHT],
|
||||
['slot-height', originalValues.NODE_SLOT_HEIGHT],
|
||||
['group-font-size', originalValues.DEFAULT_GROUP_FONT]
|
||||
];
|
||||
|
||||
updates.forEach(([id, originalValue]) => {
|
||||
const newValue = Math.round(originalValue * scale);
|
||||
dialog.querySelector(`#${id}`).value = newValue;
|
||||
dialog.querySelector(`#${id}-num`).value = newValue;
|
||||
});
|
||||
|
||||
const saveBtn = dialog.querySelector('#save-defaults-btn');
|
||||
if (saveBtn) {
|
||||
saveBtn.onclick = () => {
|
||||
localStorage.setItem("endless_fontifier_defaults", JSON.stringify(currentValues));
|
||||
alert("🌊 Fontifier defaults saved! They'll auto-load next time.");
|
||||
};
|
||||
}
|
||||
|
||||
globalScale.oninput = updateGlobalScale;
|
||||
globalScaleNum.oninput = () => {
|
||||
globalScale.value = globalScaleNum.value;
|
||||
updateGlobalScale();
|
||||
|
||||
dialog.querySelector('#apply-btn').onclick = () => {
|
||||
applyChanges(dialog, true);
|
||||
hidePreviewIndicator();
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
// Button handlers
|
||||
dialog.querySelector('#close-dialog').onclick = () => {
|
||||
backdrop.remove();
|
||||
dialog.remove();
|
||||
dialog.querySelector('#preview-btn').onclick = () => {
|
||||
applyChanges(dialog, false);
|
||||
showPreviewIndicator();
|
||||
};
|
||||
|
||||
dialog.querySelector('#reset-btn').onclick = () => {
|
||||
dialog.querySelector('#global-scale').value = 1;
|
||||
dialog.querySelector('#global-scale-num').value = 1;
|
||||
dialog.querySelector('#node-text-size').value = originalValues.NODE_TEXT_SIZE;
|
||||
dialog.querySelector('#node-text-size-num').value = originalValues.NODE_TEXT_SIZE;
|
||||
dialog.querySelector('#node-subtext-size').value = originalValues.NODE_SUBTEXT_SIZE;
|
||||
dialog.querySelector('#node-subtext-size-num').value = originalValues.NODE_SUBTEXT_SIZE;
|
||||
dialog.querySelector('#title-height').value = originalValues.NODE_TITLE_HEIGHT;
|
||||
dialog.querySelector('#title-height-num').value = originalValues.NODE_TITLE_HEIGHT;
|
||||
dialog.querySelector('#slot-height').value = originalValues.NODE_SLOT_HEIGHT;
|
||||
dialog.querySelector('#slot-height-num').value = originalValues.NODE_SLOT_HEIGHT;
|
||||
dialog.querySelector('#group-font-size').value = originalValues.DEFAULT_GROUP_FONT;
|
||||
dialog.querySelector('#group-font-size-num').value = originalValues.DEFAULT_GROUP_FONT;
|
||||
dialog.querySelector('#font-family').value = 'Arial';
|
||||
};
|
||||
|
||||
dialog.querySelector('#preview-btn').onclick = () => applyChanges(dialog, false);
|
||||
|
||||
dialog.querySelector('#apply-btn').onclick = () => {
|
||||
applyChanges(dialog, true);
|
||||
backdrop.remove();
|
||||
dialog.remove();
|
||||
localStorage.removeItem("endless_fontifier_defaults");
|
||||
currentValues = { ...originalValues };
|
||||
applySettingsToComfyUI(originalValues);
|
||||
updateDialogValues(dialog);
|
||||
hidePreviewIndicator();
|
||||
alert("🔁 Fontifier reset to ComfyUI defaults.");
|
||||
};
|
||||
|
||||
dialog.querySelector('#cancel-btn').onclick = () => {
|
||||
backdrop.remove();
|
||||
dialog.remove();
|
||||
applySettingsToComfyUI(dialogOpenValues);
|
||||
hidePreviewIndicator();
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
// Add hover effects to buttons
|
||||
const buttons = dialog.querySelectorAll('button');
|
||||
buttons.forEach(button => {
|
||||
button.style.boxSizing = 'border-box';
|
||||
button.style.minWidth = button.offsetWidth + 'px'; // Lock the width
|
||||
button.addEventListener('mouseenter', () => {
|
||||
button.style.borderWidth = '2px';
|
||||
button.style.padding = '7px 15px';
|
||||
});
|
||||
button.addEventListener('mouseleave', () => {
|
||||
button.style.borderWidth = '1px';
|
||||
button.style.padding = '8px 16px';
|
||||
});
|
||||
});
|
||||
escHandler = e => {
|
||||
if (e.key === 'Escape') {
|
||||
applySettingsToComfyUI(dialogOpenValues);
|
||||
hidePreviewIndicator();
|
||||
closeDialog();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
|
||||
function showPreviewIndicator() {
|
||||
if (!currentDialog) return;
|
||||
isPreviewMode = true;
|
||||
const indicator = currentDialog.querySelector('#preview-indicator');
|
||||
if (indicator) indicator.style.display = 'block';
|
||||
}
|
||||
|
||||
function hidePreviewIndicator() {
|
||||
if (!currentDialog) return;
|
||||
isPreviewMode = false;
|
||||
const indicator = currentDialog.querySelector('#preview-indicator');
|
||||
if (indicator) indicator.style.display = 'none';
|
||||
}
|
||||
|
||||
function applyChanges(dialog, permanent = false) {
|
||||
const newValues = {
|
||||
const globalScale = parseFloat(dialog.querySelector('#global-scale').value);
|
||||
|
||||
const baseValues = {
|
||||
NODE_TEXT_SIZE: parseInt(dialog.querySelector('#node-text-size').value),
|
||||
NODE_SUBTEXT_SIZE: parseInt(dialog.querySelector('#node-subtext-size').value),
|
||||
NODE_TITLE_HEIGHT: parseInt(dialog.querySelector('#title-height').value),
|
||||
NODE_SLOT_HEIGHT: parseInt(dialog.querySelector('#slot-height').value),
|
||||
DEFAULT_GROUP_FONT: parseInt(dialog.querySelector('#group-font-size').value),
|
||||
FONT_FAMILY: dialog.querySelector('#font-family').value
|
||||
FONT_FAMILY: dialog.querySelector('#font-family').value,
|
||||
NODE_FONT: dialog.querySelector('#font-family').value,
|
||||
WIDGET_TEXT_SIZE: parseInt(dialog.querySelector('#widget-text-size').value),
|
||||
GLOBAL_SCALE: globalScale
|
||||
};
|
||||
|
||||
if (typeof LiteGraph !== 'undefined') {
|
||||
LiteGraph.NODE_TEXT_SIZE = newValues.NODE_TEXT_SIZE;
|
||||
LiteGraph.NODE_SUBTEXT_SIZE = newValues.NODE_SUBTEXT_SIZE;
|
||||
LiteGraph.NODE_TITLE_HEIGHT = newValues.NODE_TITLE_HEIGHT;
|
||||
LiteGraph.NODE_SLOT_HEIGHT = newValues.NODE_SLOT_HEIGHT;
|
||||
LiteGraph.NODE_WIDGET_HEIGHT = newValues.NODE_SLOT_HEIGHT;
|
||||
LiteGraph.DEFAULT_GROUP_FONT = newValues.DEFAULT_GROUP_FONT;
|
||||
LiteGraph.DEFAULT_GROUP_FONT_SIZE = newValues.DEFAULT_GROUP_FONT;
|
||||
LiteGraph.NODE_FONT = newValues.FONT_FAMILY;
|
||||
LiteGraph.DEFAULT_FONT = newValues.FONT_FAMILY;
|
||||
LiteGraph.GROUP_FONT = newValues.FONT_FAMILY;
|
||||
// Apply global scaling to font sizes
|
||||
const scaledValues = {
|
||||
...baseValues,
|
||||
NODE_TEXT_SIZE: Math.round(baseValues.NODE_TEXT_SIZE * globalScale),
|
||||
NODE_SUBTEXT_SIZE: Math.round(baseValues.NODE_SUBTEXT_SIZE * globalScale),
|
||||
DEFAULT_GROUP_FONT: Math.round(baseValues.DEFAULT_GROUP_FONT * globalScale),
|
||||
WIDGET_TEXT_SIZE: Math.round(baseValues.WIDGET_TEXT_SIZE * globalScale)
|
||||
};
|
||||
|
||||
console.log('🌊✨ Fontifier applied:', newValues);
|
||||
applySettingsToComfyUI(scaledValues);
|
||||
if (permanent) {
|
||||
currentValues = { ...baseValues }; // Store unscaled values
|
||||
isPreviewMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof app !== 'undefined' && app.canvas) {
|
||||
app.canvas.setDirty(true, true);
|
||||
if (app.canvas.draw) {
|
||||
setTimeout(() => app.canvas.draw(true, true), 100);
|
||||
}
|
||||
}
|
||||
function applySettingsToComfyUI(settings) {
|
||||
LiteGraph.NODE_TEXT_SIZE = settings.NODE_TEXT_SIZE;
|
||||
LiteGraph.NODE_SUBTEXT_SIZE = settings.NODE_SUBTEXT_SIZE;
|
||||
LiteGraph.NODE_TITLE_HEIGHT = settings.NODE_TITLE_HEIGHT;
|
||||
LiteGraph.NODE_SLOT_HEIGHT = settings.NODE_SLOT_HEIGHT;
|
||||
LiteGraph.DEFAULT_GROUP_FONT = settings.DEFAULT_GROUP_FONT;
|
||||
LiteGraph.DEFAULT_GROUP_FONT_SIZE = settings.DEFAULT_GROUP_FONT;
|
||||
LiteGraph.NODE_FONT = settings.NODE_FONT;
|
||||
LiteGraph.DEFAULT_FONT = settings.NODE_FONT;
|
||||
LiteGraph.GROUP_FONT = settings.NODE_FONT;
|
||||
|
||||
const canvases = document.querySelectorAll('canvas');
|
||||
canvases.forEach(canvas => {
|
||||
if (canvas.getContext) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const originalWidth = canvas.width;
|
||||
canvas.width = originalWidth + 1;
|
||||
canvas.width = originalWidth;
|
||||
}
|
||||
});
|
||||
if (window.app?.canvas) {
|
||||
window.app.canvas.setDirty(true, true);
|
||||
setTimeout(() => window.app.canvas.draw(true, true), 100);
|
||||
}
|
||||
|
||||
// Apply widget font size to CSS, this is DOM-only
|
||||
const widgetTextSize = parseInt(dialog.querySelector('#widget-text-size').value);
|
||||
let styleTag = document.getElementById('fontifier-widget-text-style');
|
||||
const styleId = "fontifier-widget-text-style";
|
||||
let styleTag = document.getElementById(styleId);
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style');
|
||||
styleTag.id = 'fontifier-widget-text-style';
|
||||
styleTag.id = styleId;
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
styleTag.textContent = `
|
||||
canvas ~ * .widget input, canvas ~ * .widget select, canvas ~ * .widget textarea,
|
||||
canvas ~ * .comfy-multiline-input, canvas ~ * .comfy-input,
|
||||
canvas ~ * input.comfy-multiline-input, canvas ~ * textarea.comfy-multiline-input,
|
||||
canvas ~ * [class*="comfy-input"], canvas ~ * [class*="comfy-multiline"],
|
||||
canvas ~ * .comfyui-widget input, canvas ~ * .comfyui-widget select, canvas ~ * .comfyui-widget textarea,
|
||||
canvas ~ * [class*="widget"] input, canvas ~ * [class*="widget"] select, canvas ~ * [class*="widget"] textarea,
|
||||
canvas ~ * .litegraph input, canvas ~ * .litegraph select, canvas ~ * .litegraph textarea,
|
||||
.litegraph input, .litegraph select, .litegraph textarea {
|
||||
font-size: ${widgetTextSize}px !important;
|
||||
font-family: ${newValues.FONT_FAMILY} !important;
|
||||
font-size: ${settings.WIDGET_TEXT_SIZE}px !important;
|
||||
font-family: ${settings.NODE_FONT} !important;
|
||||
}
|
||||
|
||||
/* Exclude the fontifier dialog itself */
|
||||
#fontifier-dialog input, #fontifier-dialog select, #fontifier-dialog textarea {
|
||||
font-size: 14px !important;
|
||||
font-family: Arial !important;
|
||||
}
|
||||
`;
|
||||
|
||||
if (permanent) {
|
||||
currentValues = { ...newValues };
|
||||
console.log('🌊✨ Fontifier changes applied permanently (until page refresh)');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function findToolbar() {
|
||||
// Method 1: Look for ComfyUI specific toolbar classes
|
||||
let toolbar = document.querySelector('.comfyui-menu, .comfy-menu, [class*="menu"], [class*="toolbar"]');
|
||||
|
||||
// Method 2: Look for button groups
|
||||
if (!toolbar) {
|
||||
const buttonGroups = document.querySelectorAll('[class*="button-group"], [class*="btn-group"], .comfyui-button-group');
|
||||
toolbar = Array.from(buttonGroups).find(group =>
|
||||
group.querySelectorAll('button').length > 0
|
||||
);
|
||||
function closeDialog() {
|
||||
if (currentDialog) currentDialog.remove();
|
||||
if (escHandler) document.removeEventListener('keydown', escHandler);
|
||||
if (unregisterThemeCallback) unregisterThemeCallback();
|
||||
// Clean up the style tag
|
||||
const styleTag = document.getElementById('fontifier-dialog-style');
|
||||
if (styleTag) styleTag.remove();
|
||||
currentDialog = null;
|
||||
handlersSetup = false;
|
||||
escHandler = null;
|
||||
unregisterThemeCallback = null;
|
||||
isPreviewMode = false;
|
||||
}
|
||||
|
||||
// Wait for app to be ready before initializing
|
||||
function waitForApp() {
|
||||
if (typeof window.app !== 'undefined' && window.app?.canvas) {
|
||||
// Initialize with saved defaults once app is ready
|
||||
applySettingsToComfyUI(currentValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Method 3: Look for any container with multiple buttons
|
||||
if (!toolbar) {
|
||||
const allElements = document.querySelectorAll('*');
|
||||
toolbar = Array.from(allElements).find(el => {
|
||||
const buttons = el.querySelectorAll('button');
|
||||
return buttons.length >= 2 && buttons.length <= 10; // Reasonable toolbar size
|
||||
});
|
||||
}
|
||||
|
||||
// Method 4: Fallback to the original Share button method
|
||||
if (!toolbar) {
|
||||
toolbar = Array.from(document.querySelectorAll(".comfyui-button-group")).find(div =>
|
||||
Array.from(div.querySelectorAll("button")).some(btn => btn.title === "Share")
|
||||
);
|
||||
}
|
||||
|
||||
return toolbar;
|
||||
setTimeout(waitForApp, 100);
|
||||
}
|
||||
|
||||
function injectFontifierButton() {
|
||||
const toolbar = findToolbar();
|
||||
|
||||
if (toolbar && !document.getElementById("endless-fontifier-button")) {
|
||||
const colors = getComfyUIColors();
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "endless-fontifier-button";
|
||||
btn.textContent = "🌊✨ Fontifier";
|
||||
btn.className = "comfyui-button";
|
||||
waitForApp();
|
||||
|
||||
// Function to update button colors
|
||||
function updateButtonColors() {
|
||||
const currentColors = getComfyUIColors();
|
||||
btn.style.cssText = `
|
||||
margin-left: 8px;
|
||||
background: ${currentColors.backgroundSecondary};
|
||||
border: 1px solid ${currentColors.border};
|
||||
color: ${currentColors.text};
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
`;
|
||||
|
||||
btn.onmouseover = () => {
|
||||
const hoverColors = getComfyUIColors();
|
||||
btn.style.background = hoverColors.background;
|
||||
btn.style.borderColor = hoverColors.text;
|
||||
};
|
||||
|
||||
btn.onmouseout = () => {
|
||||
const outColors = getComfyUIColors();
|
||||
btn.style.background = outColors.backgroundSecondary;
|
||||
btn.style.borderColor = outColors.border;
|
||||
};
|
||||
}
|
||||
|
||||
// Initial colors
|
||||
updateButtonColors();
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver(updateButtonColors);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'style']
|
||||
});
|
||||
|
||||
btn.onclick = createFontifierDialog;
|
||||
toolbar.appendChild(btn);
|
||||
|
||||
console.log("✅ 🌊✨ Endless Fontifier button injected successfully!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to inject immediately
|
||||
if (!injectFontifierButton()) {
|
||||
// If immediate injection fails, use observer
|
||||
const observer = new MutationObserver(() => {
|
||||
if (injectFontifierButton()) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Timeout after 30 seconds to avoid infinite observation
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
if (!document.getElementById("endless-fontifier-button")) {
|
||||
console.warn("⚠️ Could not find suitable toolbar for Fontifier button");
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
// Register into Endless Tools menu
|
||||
registerEndlessTool("Fontifier", createFontifierDialog);
|
||||
})();
|
||||
764
web/endless_minimap.js
Normal file
764
web/endless_minimap.js
Normal file
@@ -0,0 +1,764 @@
|
||||
// ComfyUI Endless 🌊✨ Minimap - Optimized Version
|
||||
|
||||
(function waitForHelpers() {
|
||||
if (typeof window.EndlessHelpers === 'undefined') {
|
||||
console.warn("⏳ Waiting for EndlessHelpers to be ready...");
|
||||
setTimeout(waitForHelpers, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
registerEndlessTool,
|
||||
onThemeChange,
|
||||
getComfyUIColors,
|
||||
toRGBA,
|
||||
makeDraggable
|
||||
} = window.EndlessHelpers;
|
||||
|
||||
console.log("✅ Endless Minimap loaded.");
|
||||
|
||||
// State variables
|
||||
let currentDialog = null;
|
||||
let animationId = null;
|
||||
let unregisterThemeCallback = null;
|
||||
let resizeObserver = null;
|
||||
|
||||
// Canvas state
|
||||
let panX = 0, panY = 0, zoom = 1;
|
||||
let isDragging = false;
|
||||
let dragStartTime = 0, dragStartX = 0, dragStartY = 0;
|
||||
const DRAG_THRESHOLD = 5;
|
||||
|
||||
// Size constants
|
||||
const BASE_WIDTH = 300, BASE_HEIGHT = 400;
|
||||
const MAX_WIDTH = 500, MAX_HEIGHT = 600;
|
||||
const MIN_WIDTH = 200, MIN_HEIGHT = 150;
|
||||
|
||||
// Node type colors - comprehensive mapping
|
||||
const NODE_COLORS = {
|
||||
// Image Processing (Blue family)
|
||||
'LoadImage': '#5DADE2', 'SaveImage': '#3498DB', 'PreviewImage': '#2E86AB',
|
||||
'ImageScale': '#85C1E9', 'ImageCrop': '#7FB3D3', 'ImageBlend': '#6BB6FF',
|
||||
|
||||
// Latent Processing (Purple family)
|
||||
'KSampler': '#8E44AD', 'KSamplerAdvanced': '#9B59B6', 'EmptyLatentImage': '#A569BD',
|
||||
'LatentUpscale': '#BB8FCE', 'LatentBlend': '#D2B4DE',
|
||||
|
||||
// VAE (Green family)
|
||||
'VAEDecode': '#27AE60', 'VAEEncode': '#2ECC71', 'VAELoader': '#58D68D',
|
||||
|
||||
// Model/Checkpoint (Teal family)
|
||||
'CheckpointLoaderSimple': '#17A2B8', 'CheckpointLoader': '#148A99',
|
||||
'ModelMergeSimple': '#1ABC9C', 'UNETLoader': '#5DADE2',
|
||||
|
||||
// CLIP/Text (Orange family)
|
||||
'CLIPTextEncode': '#E67E22', 'CLIPTextEncodeSDXL': '#F39C12',
|
||||
'CLIPLoader': '#F8C471', 'CLIPVisionEncode': '#D68910',
|
||||
|
||||
// LoRA (Yellow family)
|
||||
'LoraLoader': '#F1C40F', 'LoraLoaderModelOnly': '#F4D03F',
|
||||
|
||||
// ControlNet (Pink family)
|
||||
'ControlNetLoader': '#E91E63', 'ControlNetApply': '#F06292',
|
||||
'CannyEdgePreprocessor': '#EC407A', 'OpenposePreprocessor': '#F8BBD9',
|
||||
|
||||
// Conditioning (Coral family)
|
||||
'ConditioningAverage': '#FF6B35', 'ConditioningCombine': '#FF8C42',
|
||||
|
||||
// Utility (Gray family)
|
||||
'PrimitiveNode': '#95A5A6', 'Note': '#BDC3C7', 'Reroute': '#85929E',
|
||||
|
||||
// Upscaling (Lime family)
|
||||
'UpscaleModelLoader': '#7FB069', 'ImageUpscaleWithModel': '#8BC34A',
|
||||
|
||||
// Masks (Red family)
|
||||
'MaskComposite': '#E53935', 'MaskToImage': '#F44336', 'ImageToMask': '#EF5350',
|
||||
|
||||
'default': 'rgba(200, 200, 200, 0.7)'
|
||||
};
|
||||
|
||||
function getApp() {
|
||||
return window.app || window.comfyApp || document.querySelector('#app')?.__vue__?.$root || null;
|
||||
}
|
||||
|
||||
function getCanvasAspectRatio() {
|
||||
const mainCanvas = document.querySelector('canvas') ||
|
||||
document.querySelector('#graph-canvas') ||
|
||||
document.querySelector('.litegraph');
|
||||
|
||||
if (mainCanvas) {
|
||||
const rect = mainCanvas.getBoundingClientRect();
|
||||
return rect.width / rect.height;
|
||||
}
|
||||
|
||||
return window.innerWidth / window.innerHeight;
|
||||
}
|
||||
|
||||
function calculateDimensions() {
|
||||
const aspectRatio = getCanvasAspectRatio();
|
||||
let containerWidth, containerHeight, canvasWidth, canvasHeight;
|
||||
|
||||
if (aspectRatio > 1) {
|
||||
containerWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, BASE_WIDTH * aspectRatio));
|
||||
containerHeight = BASE_HEIGHT;
|
||||
canvasWidth = containerWidth;
|
||||
canvasHeight = BASE_HEIGHT - 50;
|
||||
} else {
|
||||
containerWidth = BASE_WIDTH;
|
||||
containerHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, BASE_HEIGHT / aspectRatio));
|
||||
canvasWidth = containerWidth;
|
||||
canvasHeight = containerHeight - 50;
|
||||
}
|
||||
|
||||
return { containerWidth, containerHeight, canvasWidth, canvasHeight };
|
||||
}
|
||||
|
||||
function getNodeColor(node) {
|
||||
const nodeType = node.type || node.constructor?.name || 'default';
|
||||
|
||||
if (NODE_COLORS[nodeType]) return NODE_COLORS[nodeType];
|
||||
|
||||
// Pattern matching for common types
|
||||
const patterns = [
|
||||
['Sampler', NODE_COLORS['KSampler']],
|
||||
['CLIP', NODE_COLORS['CLIPTextEncode']],
|
||||
['VAE', NODE_COLORS['VAEDecode']],
|
||||
['ControlNet', NODE_COLORS['ControlNetLoader']],
|
||||
['Lora', NODE_COLORS['LoraLoader']],
|
||||
['Image.*Load', NODE_COLORS['LoadImage']],
|
||||
['Image.*Save', NODE_COLORS['SaveImage']],
|
||||
['Checkpoint', NODE_COLORS['CheckpointLoaderSimple']],
|
||||
['Upscale', NODE_COLORS['UpscaleModelLoader']],
|
||||
['Mask', NODE_COLORS['MaskComposite']]
|
||||
];
|
||||
|
||||
for (const [pattern, color] of patterns) {
|
||||
if (new RegExp(pattern, 'i').test(nodeType)) return color;
|
||||
}
|
||||
|
||||
return NODE_COLORS.default;
|
||||
}
|
||||
|
||||
function getNodes() {
|
||||
const app = getApp();
|
||||
if (!app) {
|
||||
console.log("App not found, trying DOM fallback...");
|
||||
const nodeElements = document.querySelectorAll('[class*="node"], .comfy-node, .litegraph-node');
|
||||
if (nodeElements.length > 0) {
|
||||
return Array.from(nodeElements).map((el, i) => ({
|
||||
pos: [i * 150, i * 100],
|
||||
size: [100, 60],
|
||||
type: 'Unknown',
|
||||
title: `Node ${i + 1}`
|
||||
}));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodes = app.graph?._nodes ||
|
||||
app.graph?.nodes ||
|
||||
app.canvas?.graph?._nodes ||
|
||||
app.canvas?.graph?.nodes ||
|
||||
[];
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function createStyleCSS(colors) {
|
||||
return `
|
||||
#endless-minimap button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: ${colors.inputText};
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
font-size: 18px;
|
||||
border-radius: 3px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
#endless-minimap button:hover {
|
||||
background: ${toRGBA(colors.inputText, 0.1)};
|
||||
}
|
||||
#endless-minimap .drag-bar {
|
||||
padding: 4px 8px;
|
||||
background: ${toRGBA(colors.inputText, 0.05)};
|
||||
cursor: move;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#endless-minimap .legend {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
background: ${colors.menu};
|
||||
color: ${colors.inputText};
|
||||
padding: 8px;
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
#endless-minimap .pan-info {
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
background: ${colors.menuSecondary};
|
||||
color: ${colors.inputText};
|
||||
border-top: 1px solid ${colors.border};
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
if (!currentDialog) return;
|
||||
|
||||
const colors = getComfyUIColors();
|
||||
|
||||
// Update container colors only, not size
|
||||
currentDialog.style.background = colors.menu;
|
||||
currentDialog.style.color = colors.inputText;
|
||||
currentDialog.style.borderColor = colors.accent;
|
||||
|
||||
// Update style tag
|
||||
const styleTag = document.getElementById('minimap-style');
|
||||
if (styleTag) {
|
||||
styleTag.textContent = createStyleCSS(colors);
|
||||
}
|
||||
|
||||
drawMinimap();
|
||||
}
|
||||
|
||||
function updateLegend() {
|
||||
const legend = currentDialog.querySelector('.legend');
|
||||
if (!legend || legend.style.display === 'none') return;
|
||||
|
||||
const nodes = getNodes();
|
||||
if (!nodes) return;
|
||||
|
||||
const typeCounts = {};
|
||||
nodes.forEach(n => {
|
||||
const nodeType = n.type || n.constructor?.name || 'default';
|
||||
typeCounts[nodeType] = (typeCounts[nodeType] || 0) + 1;
|
||||
});
|
||||
|
||||
legend.innerHTML = Object.entries(typeCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([type, count]) => {
|
||||
const color = NODE_COLORS[type] || NODE_COLORS.default;
|
||||
return `<div style="margin: 2px 0; display: flex; align-items: center;">
|
||||
<div style="width: 12px; height: 12px; background: ${color}; margin-right: 6px; border-radius: 2px;"></div>
|
||||
<span>${type} (${count})</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Get current transform state for coordinate conversions
|
||||
function getTransformState() {
|
||||
const canvas = currentDialog?.querySelector('canvas');
|
||||
if (!canvas) return null;
|
||||
|
||||
const nodes = getNodes();
|
||||
if (!nodes?.length) return null;
|
||||
|
||||
// Calculate bounds (same as in drawMinimap)
|
||||
const bounds = nodes.reduce((acc, n) => {
|
||||
const x = n.pos?.[0] ?? n.x ?? 0;
|
||||
const y = n.pos?.[1] ?? n.y ?? 0;
|
||||
const w = n.size?.[0] ?? n.width ?? 100;
|
||||
const h = n.size?.[1] ?? n.height ?? 60;
|
||||
|
||||
return {
|
||||
minX: Math.min(acc.minX, x),
|
||||
minY: Math.min(acc.minY, y),
|
||||
maxX: Math.max(acc.maxX, x + w),
|
||||
maxY: Math.max(acc.maxY, y + h)
|
||||
};
|
||||
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||
|
||||
const width = bounds.maxX - bounds.minX;
|
||||
const height = bounds.maxY - bounds.minY;
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
|
||||
const baseScale = Math.min(canvas.width / Math.max(width, 1000), canvas.height / Math.max(height, 1000));
|
||||
const scale = baseScale * zoom;
|
||||
|
||||
return {
|
||||
bounds,
|
||||
width,
|
||||
height,
|
||||
scale,
|
||||
canvas
|
||||
};
|
||||
}
|
||||
|
||||
// Convert canvas coordinates to world coordinates
|
||||
function canvasToWorld(canvasX, canvasY) {
|
||||
const transform = getTransformState();
|
||||
if (!transform) return null;
|
||||
|
||||
const { bounds, width, height, scale, canvas } = transform;
|
||||
|
||||
// Inverse of the transform used in drawMinimap
|
||||
const worldX = (canvasX - canvas.width / 2 - panX) / scale + (bounds.minX + width / 2);
|
||||
const worldY = (canvasY - canvas.height / 2 - panY) / scale + (bounds.minY + height / 2);
|
||||
|
||||
return { x: worldX, y: worldY };
|
||||
}
|
||||
|
||||
// Convert world coordinates to canvas coordinates
|
||||
function worldToCanvas(worldX, worldY) {
|
||||
const transform = getTransformState();
|
||||
if (!transform) return null;
|
||||
|
||||
const { bounds, width, height, scale, canvas } = transform;
|
||||
|
||||
// Same transform as used in drawMinimap
|
||||
const canvasX = (worldX - (bounds.minX + width / 2)) * scale + canvas.width / 2 + panX;
|
||||
const canvasY = (worldY - (bounds.minY + height / 2)) * scale + canvas.height / 2 + panY;
|
||||
|
||||
return { x: canvasX, y: canvasY };
|
||||
}
|
||||
|
||||
function drawMinimap() {
|
||||
if (!currentDialog) return;
|
||||
|
||||
const canvas = currentDialog.querySelector('canvas');
|
||||
const panInfo = currentDialog.querySelector('.pan-info');
|
||||
if (!canvas || !panInfo) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const colors = getComfyUIColors();
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const nodes = getNodes();
|
||||
if (!nodes || !nodes.length) {
|
||||
ctx.fillStyle = colors.inputText;
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('No nodes found', canvas.width / 2, canvas.height / 2);
|
||||
ctx.fillText('or graph not loaded', canvas.width / 2, canvas.height / 2 + 15);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate bounds
|
||||
const bounds = nodes.reduce((acc, n) => {
|
||||
const x = n.pos?.[0] ?? n.x ?? 0;
|
||||
const y = n.pos?.[1] ?? n.y ?? 0;
|
||||
const w = n.size?.[0] ?? n.width ?? 100;
|
||||
const h = n.size?.[1] ?? n.height ?? 60;
|
||||
|
||||
return {
|
||||
minX: Math.min(acc.minX, x),
|
||||
minY: Math.min(acc.minY, y),
|
||||
maxX: Math.max(acc.maxX, x + w),
|
||||
maxY: Math.max(acc.maxY, y + h)
|
||||
};
|
||||
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||
|
||||
const width = bounds.maxX - bounds.minX;
|
||||
const height = bounds.maxY - bounds.minY;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
const baseScale = Math.min(canvas.width / Math.max(width, 1000), canvas.height / Math.max(height, 1000));
|
||||
const scale = baseScale * zoom;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(canvas.width / 2 + panX, canvas.height / 2 + panY);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.translate(-(bounds.minX + width/2), -(bounds.minY + height/2));
|
||||
|
||||
// Draw grid
|
||||
ctx.strokeStyle = toRGBA(colors.inputText, 0.1);
|
||||
ctx.lineWidth = 1 / scale;
|
||||
const gridSize = 100;
|
||||
for (let x = Math.floor(bounds.minX / gridSize) * gridSize; x <= bounds.maxX; x += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, bounds.minY);
|
||||
ctx.lineTo(x, bounds.maxY);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = Math.floor(bounds.minY / gridSize) * gridSize; y <= bounds.maxY; y += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bounds.minX, y);
|
||||
ctx.lineTo(bounds.maxX, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
nodes.forEach((n, index) => {
|
||||
const x = n.pos?.[0] ?? n.x ?? 0;
|
||||
const y = n.pos?.[1] ?? n.y ?? 0;
|
||||
const w = n.size?.[0] ?? n.width ?? 100;
|
||||
const h = n.size?.[1] ?? n.height ?? 60;
|
||||
|
||||
ctx.fillStyle = getNodeColor(n);
|
||||
ctx.fillRect(x, y, w, h);
|
||||
|
||||
ctx.strokeStyle = toRGBA(colors.inputText, 0.8);
|
||||
ctx.lineWidth = 1 / scale;
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
|
||||
if (scale > 0.3) {
|
||||
ctx.fillStyle = colors.inputText;
|
||||
ctx.font = `${Math.max(10, 12 / scale)}px Arial`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const title = n.title || n.type || `Node ${index + 1}`;
|
||||
ctx.fillText(title.substring(0, 15), x + w / 2, y + h / 2);
|
||||
}
|
||||
});
|
||||
|
||||
// Draw viewport indicator
|
||||
const app = getApp();
|
||||
if (app?.canvas?.ds) {
|
||||
const ds = app.canvas.ds;
|
||||
const mainCanvas = document.querySelector('canvas');
|
||||
|
||||
if (mainCanvas) {
|
||||
const viewportX = -ds.offset[0];
|
||||
const viewportY = -ds.offset[1];
|
||||
const viewportW = mainCanvas.width / ds.scale;
|
||||
const viewportH = mainCanvas.height / ds.scale;
|
||||
|
||||
ctx.fillStyle = toRGBA(colors.accent || '#4a90e2', 0.12);
|
||||
ctx.fillRect(viewportX, viewportY, viewportW, viewportH);
|
||||
|
||||
ctx.strokeStyle = toRGBA(colors.accent || '#4a90e2', 0.8);
|
||||
ctx.lineWidth = 2 / scale;
|
||||
ctx.strokeRect(viewportX, viewportY, viewportW, viewportH);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
panInfo.textContent = `Nodes: ${nodes.length} | Zoom: ${(zoom * 100).toFixed(0)}% | Pan: ${panX.toFixed(0)}, ${panY.toFixed(0)}`;
|
||||
}
|
||||
|
||||
function navigateToPosition(canvasX, canvasY) {
|
||||
const worldPos = canvasToWorld(canvasX, canvasY);
|
||||
if (!worldPos) return;
|
||||
|
||||
const app = getApp();
|
||||
const mainCanvas = document.querySelector('canvas');
|
||||
if (!app?.canvas?.ds || !mainCanvas) return;
|
||||
|
||||
try {
|
||||
// Center the main canvas on the clicked world position
|
||||
app.canvas.ds.offset[0] = -worldPos.x + mainCanvas.width / 2;
|
||||
app.canvas.ds.offset[1] = -worldPos.y + mainCanvas.height / 2;
|
||||
app.canvas.setDirty(true, true);
|
||||
|
||||
// Update minimap to reflect the change
|
||||
setTimeout(() => drawMinimap(), 50);
|
||||
} catch (err) {
|
||||
console.log("❌ Navigation error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function isClickInViewport(canvasX, canvasY) {
|
||||
const app = getApp();
|
||||
if (!app?.canvas?.ds) return false;
|
||||
|
||||
const ds = app.canvas.ds;
|
||||
const mainCanvas = document.querySelector('canvas');
|
||||
if (!mainCanvas) return false;
|
||||
|
||||
const worldPos = canvasToWorld(canvasX, canvasY);
|
||||
if (!worldPos) return false;
|
||||
|
||||
// Check if click is inside viewport rectangle in world coordinates
|
||||
const viewportX = -ds.offset[0];
|
||||
const viewportY = -ds.offset[1];
|
||||
const viewportW = mainCanvas.width / ds.scale;
|
||||
const viewportH = mainCanvas.height / ds.scale;
|
||||
|
||||
return worldPos.x >= viewportX && worldPos.x <= viewportX + viewportW &&
|
||||
worldPos.y >= viewportY && worldPos.y <= viewportY + viewportH;
|
||||
}
|
||||
|
||||
function adjustPanToKeepNodesVisible() {
|
||||
const nodes = getNodes();
|
||||
if (!nodes?.length) return;
|
||||
|
||||
const canvas = currentDialog?.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
// Calculate bounds
|
||||
const bounds = nodes.reduce((acc, n) => {
|
||||
const x = n.pos?.[0] ?? n.x ?? 0;
|
||||
const y = n.pos?.[1] ?? n.y ?? 0;
|
||||
const w = n.size?.[0] ?? n.width ?? 100;
|
||||
const h = n.size?.[1] ?? n.height ?? 60;
|
||||
|
||||
return {
|
||||
minX: Math.min(acc.minX, x),
|
||||
minY: Math.min(acc.minY, y),
|
||||
maxX: Math.max(acc.maxX, x + w),
|
||||
maxY: Math.max(acc.maxY, y + h)
|
||||
};
|
||||
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||
|
||||
const width = bounds.maxX - bounds.minX;
|
||||
const height = bounds.maxY - bounds.minY;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
const baseScale = Math.min(canvas.width / Math.max(width, 1000), canvas.height / Math.max(height, 1000));
|
||||
const scale = baseScale * zoom;
|
||||
|
||||
// If zoomed in too much, adjust pan to keep nodes centered
|
||||
if (zoom > 2) {
|
||||
const maxPanX = canvas.width / 4;
|
||||
const maxPanY = canvas.height / 4;
|
||||
panX = Math.max(-maxPanX, Math.min(maxPanX, panX));
|
||||
panY = Math.max(-maxPanY, Math.min(maxPanY, panY));
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventHandlers() {
|
||||
const canvas = currentDialog.querySelector('canvas');
|
||||
let isViewportDragging = false;
|
||||
|
||||
// Mouse handlers
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
dragStartTime = Date.now();
|
||||
dragStartX = e.clientX;
|
||||
dragStartY = e.clientY;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const canvasX = e.clientX - rect.left;
|
||||
const canvasY = e.clientY - rect.top;
|
||||
|
||||
// Check if clicking inside viewport indicator
|
||||
isViewportDragging = isClickInViewport(canvasX, canvasY);
|
||||
|
||||
if (isViewportDragging) {
|
||||
canvas.style.cursor = 'grab';
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (!dragStartTime) return;
|
||||
|
||||
const totalDelta = Math.abs(e.clientX - dragStartX) + Math.abs(e.clientY - dragStartY);
|
||||
|
||||
if (totalDelta > DRAG_THRESHOLD && !isDragging) {
|
||||
isDragging = true;
|
||||
if (isViewportDragging) {
|
||||
canvas.style.cursor = 'grabbing';
|
||||
} else {
|
||||
canvas.style.cursor = 'move';
|
||||
}
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
if (isViewportDragging) {
|
||||
// Move the viewport - convert movement to world coordinates
|
||||
const app = getApp();
|
||||
if (app?.canvas?.ds) {
|
||||
const transform = getTransformState();
|
||||
if (transform) {
|
||||
// Scale movement by the inverse of the minimap scale
|
||||
const movementScale = 1 / transform.scale;
|
||||
app.canvas.ds.offset[0] -= e.movementX * movementScale;
|
||||
app.canvas.ds.offset[1] -= e.movementY * movementScale;
|
||||
app.canvas.setDirty(true, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pan the minimap view
|
||||
panX += e.movementX;
|
||||
panY += e.movementY;
|
||||
}
|
||||
drawMinimap();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', (e) => {
|
||||
const clickDuration = Date.now() - dragStartTime;
|
||||
const totalMovement = Math.abs(e.clientX - dragStartX) + Math.abs(e.clientY - dragStartY);
|
||||
|
||||
if (!isDragging && clickDuration < 500 && totalMovement < DRAG_THRESHOLD) {
|
||||
if (!isViewportDragging) {
|
||||
// Regular click-to-navigate (only if not clicking viewport)
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
navigateToPosition(e.clientX - rect.left, e.clientY - rect.top);
|
||||
}
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
isViewportDragging = false;
|
||||
dragStartTime = 0;
|
||||
canvas.style.cursor = 'crosshair';
|
||||
});
|
||||
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const oldZoom = zoom;
|
||||
zoom = Math.max(0.1, Math.min(5, zoom * (e.deltaY > 0 ? 0.9 : 1.1)));
|
||||
|
||||
// Adjust pan to keep content centered when zooming
|
||||
if (zoom !== oldZoom) {
|
||||
adjustPanToKeepNodesVisible();
|
||||
}
|
||||
|
||||
drawMinimap();
|
||||
});
|
||||
|
||||
// Button handlers
|
||||
currentDialog.querySelector('#close-btn').onclick = () => closeDialog();
|
||||
currentDialog.querySelector('#legend-btn').onclick = () => {
|
||||
const legend = currentDialog.querySelector('.legend');
|
||||
const isVisible = legend.style.display !== 'none';
|
||||
legend.style.display = isVisible ? 'none' : 'block';
|
||||
if (!isVisible) updateLegend();
|
||||
};
|
||||
currentDialog.querySelector('#zoom-in').onclick = () => {
|
||||
zoom = Math.min(zoom * 1.2, 5);
|
||||
adjustPanToKeepNodesVisible();
|
||||
drawMinimap();
|
||||
};
|
||||
currentDialog.querySelector('#zoom-out').onclick = () => {
|
||||
zoom = Math.max(zoom / 1.2, 0.1);
|
||||
adjustPanToKeepNodesVisible();
|
||||
drawMinimap();
|
||||
};
|
||||
currentDialog.querySelector('#zoom-reset').onclick = () => {
|
||||
zoom = 1;
|
||||
panX = panY = 0;
|
||||
drawMinimap();
|
||||
};
|
||||
|
||||
// ESC key
|
||||
const escHandler = (e) => e.key === 'Escape' && closeDialog();
|
||||
document.addEventListener('keydown', escHandler);
|
||||
|
||||
return escHandler;
|
||||
}
|
||||
|
||||
function createMinimapDialog() {
|
||||
if (currentDialog) return;
|
||||
|
||||
const colors = getComfyUIColors();
|
||||
const { containerWidth, containerHeight, canvasWidth, canvasHeight } = calculateDimensions();
|
||||
|
||||
// Clean up existing style
|
||||
document.getElementById('minimap-style')?.remove();
|
||||
|
||||
// Create style tag
|
||||
const style = document.createElement('style');
|
||||
style.id = 'minimap-style';
|
||||
style.textContent = createStyleCSS(colors);
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Create container
|
||||
const container = document.createElement('div');
|
||||
container.id = 'endless-minimap';
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
right: 20px;
|
||||
width: ${containerWidth}px;
|
||||
height: ${containerHeight}px;
|
||||
background: ${colors.menu};
|
||||
color: ${colors.inputText};
|
||||
border: 1px solid ${colors.accent};
|
||||
border-radius: 8px;
|
||||
box-shadow: ${colors.shadow || '0 4px 12px rgba(0, 0, 0, 0.25)'};
|
||||
z-index: 99999;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="drag-bar">
|
||||
<span>Endless 🌊✨ Minimap</span>
|
||||
<div style="float: right; margin-top: -2px;">
|
||||
<button id="legend-btn" title="Toggle legend">🎨</button>
|
||||
<button id="zoom-out" title="Zoom out">▫️</button>
|
||||
<button id="zoom-reset" title="Reset zoom and pan">🏠</button>
|
||||
<button id="zoom-in" title="Zoom in">⬜</button>
|
||||
<button id="close-btn" title="Close minimap">❌</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1; position: relative; overflow: hidden;">
|
||||
<canvas width="${canvasWidth}" height="${canvasHeight}" style="display: block; cursor: crosshair; position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></canvas>
|
||||
<div class="legend"></div>
|
||||
</div>
|
||||
<div class="pan-info">Nodes: 0 | Zoom: 100% | Pan: 0, 0</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(container);
|
||||
currentDialog = container;
|
||||
|
||||
// Setup dragging
|
||||
makeDraggable(container, container.querySelector('.drag-bar'));
|
||||
|
||||
// Setup event handlers
|
||||
const escHandler = setupEventHandlers();
|
||||
|
||||
// Setup resize observer
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
const newAspectRatio = getCanvasAspectRatio();
|
||||
const canvas = container.querySelector('canvas');
|
||||
const currentAspectRatio = canvas.width / canvas.height;
|
||||
|
||||
if (Math.abs(newAspectRatio - currentAspectRatio) > 0.1) {
|
||||
const { containerWidth: newContainerWidth, containerHeight: newContainerHeight, canvasWidth: newCanvasWidth, canvasHeight: newCanvasHeight } = calculateDimensions();
|
||||
|
||||
// Update container size
|
||||
container.style.width = `${newContainerWidth}px`;
|
||||
container.style.height = `${newContainerHeight}px`;
|
||||
|
||||
// Update canvas size
|
||||
canvas.width = newCanvasWidth;
|
||||
canvas.height = newCanvasHeight;
|
||||
|
||||
drawMinimap();
|
||||
}
|
||||
});
|
||||
|
||||
const mainCanvas = document.querySelector('canvas');
|
||||
if (mainCanvas) resizeObserver.observe(mainCanvas);
|
||||
resizeObserver.observe(document.body);
|
||||
|
||||
// Setup theme updates
|
||||
unregisterThemeCallback = onThemeChange(updateTheme);
|
||||
|
||||
// Start animation loop
|
||||
function updateLoop() {
|
||||
drawMinimap();
|
||||
animationId = setTimeout(updateLoop, 1000);
|
||||
}
|
||||
updateLoop();
|
||||
|
||||
// Setup cleanup
|
||||
const originalRemove = container.remove.bind(container);
|
||||
container.remove = function() {
|
||||
clearTimeout(animationId);
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
resizeObserver?.disconnect();
|
||||
unregisterThemeCallback?.();
|
||||
document.getElementById('minimap-style')?.remove();
|
||||
currentDialog = null;
|
||||
animationId = null;
|
||||
unregisterThemeCallback = null;
|
||||
resizeObserver = null;
|
||||
originalRemove();
|
||||
};
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
currentDialog?.remove();
|
||||
}
|
||||
|
||||
// Register tool
|
||||
registerEndlessTool("Minimap", createMinimapDialog);
|
||||
})();
|
||||
870
web/endless_node_loader.js
Normal file
870
web/endless_node_loader.js
Normal file
@@ -0,0 +1,870 @@
|
||||
// ComfyUI Endless 🌊✨ Node Spawner - Optimized Version
|
||||
|
||||
(function waitForHelpers() {
|
||||
if (typeof window.EndlessHelpers === 'undefined') {
|
||||
console.warn("⏳ Waiting for EndlessHelpers to be ready...");
|
||||
setTimeout(waitForHelpers, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
registerEndlessTool,
|
||||
onThemeChange,
|
||||
getComfyUIColors,
|
||||
toRGBA,
|
||||
makeDraggable,
|
||||
addButtonHoverEffects
|
||||
} = window.EndlessHelpers;
|
||||
|
||||
console.log("✅ Endless Node Spawner loaded.");
|
||||
|
||||
// State management
|
||||
let currentDialog = null;
|
||||
let unregisterThemeCallback = null;
|
||||
let allNodesData = [];
|
||||
let currentFilter = '';
|
||||
let searchTimeout = null;
|
||||
let hoverTimeout = null;
|
||||
|
||||
// Persistent data
|
||||
let recentlyUsedNodes = JSON.parse(localStorage.getItem('endlessNodeLoader_recentlyUsed') || '[]');
|
||||
let searchHistory = JSON.parse(localStorage.getItem('endlessNodeLoader_searchHistory') || '[]');
|
||||
|
||||
// Constants
|
||||
const MAX_RECENT = 15;
|
||||
const MAX_HISTORY = 15;
|
||||
const DEFAULT_SPACING = { x: 300, y: 150 };
|
||||
const NODE_PADDING = 20;
|
||||
|
||||
function createStyleCSS(colors) {
|
||||
return `
|
||||
.dialog-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 60vh;
|
||||
width: 35vw;
|
||||
min-width: 400px;
|
||||
min-height: 300px;
|
||||
background: ${colors.menu};
|
||||
color: ${colors.inputText};
|
||||
padding: 10px;
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 8px;
|
||||
box-shadow: ${colors.shadow || '0 4px 20px rgba(0,0,0,0.5)'};
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dialog-title {
|
||||
margin: 0 0 15px 0;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
padding: 6px;
|
||||
background: ${colors.menuSecondary};
|
||||
color: ${colors.inputText};
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
}
|
||||
.filter-section {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
}
|
||||
.filter-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
background: ${colors.inputBg};
|
||||
color: ${colors.inputText};
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.filter-input:focus {
|
||||
outline: none;
|
||||
border-color: ${colors.accent};
|
||||
}
|
||||
.expand-btn {
|
||||
background: ${colors.inputBg};
|
||||
color: ${colors.inputText};
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.expand-btn:hover {
|
||||
background: ${colors.hoverBg};
|
||||
border-color: ${colors.accent};
|
||||
}
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 80px;
|
||||
background: ${colors.menu};
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 4px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
}
|
||||
.search-item {
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
}
|
||||
.search-item:last-child { border-bottom: none; }
|
||||
.search-item:hover { background: ${colors.hoverBg}; }
|
||||
.counters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
color: ${colors.descriptionText};
|
||||
}
|
||||
.counter-selected {
|
||||
color: ${colors.accent};
|
||||
font-weight: bold;
|
||||
}
|
||||
.recent-section {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-height: 30px;
|
||||
max-height: 15%;
|
||||
overflow-y: auto;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.recent-chip {
|
||||
background: ${toRGBA(colors.accent, 0.1)};
|
||||
color: ${colors.inputText};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.recent-chip:hover {
|
||||
border-color: ${colors.accent};
|
||||
background: ${toRGBA(colors.accent, 0.2)};
|
||||
}
|
||||
.node-list {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.category {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.category > summary {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
list-style: none;
|
||||
color: ${colors.inputText};
|
||||
}
|
||||
.category > summary::-webkit-details-marker { display: none; }
|
||||
.category > summary::before {
|
||||
content: "▶";
|
||||
width: 12px;
|
||||
text-align: center;
|
||||
color: ${colors.descriptionText};
|
||||
font-size: 10px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.category[open] > summary::before {
|
||||
transform: rotate(90deg);
|
||||
color: ${colors.inputText};
|
||||
}
|
||||
.category > summary:hover {
|
||||
background: ${colors.hoverBg};
|
||||
border-radius: 4px;
|
||||
}
|
||||
.category ul {
|
||||
margin: 4px 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.category li:hover {
|
||||
background: ${colors.hoverBg};
|
||||
border-radius: 4px;
|
||||
}
|
||||
.category input[type="checkbox"] {
|
||||
accent-color: ${colors.accent};
|
||||
}
|
||||
.cat-btn {
|
||||
background: ${colors.inputBg};
|
||||
color: ${colors.inputText};
|
||||
border: 1px solid ${colors.border};
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.cat-btn:hover {
|
||||
background: ${colors.hoverBg};
|
||||
border-color: ${colors.accent};
|
||||
}
|
||||
.cat-btn.select { background: ${toRGBA('#4CAF50', 0.1)}; }
|
||||
.cat-btn.deselect { background: ${toRGBA('#f44336', 0.1)}; }
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.dialog-btn {
|
||||
background: ${colors.inputBg};
|
||||
color: ${colors.inputText};
|
||||
border: 1px solid ${colors.border};
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.dialog-btn:hover {
|
||||
background: ${colors.hoverBg};
|
||||
border-color: ${colors.accent};
|
||||
}
|
||||
.dialog-btn.primary {
|
||||
background: ${toRGBA(colors.accent || '#4CAF50', 0.2)};
|
||||
border-color: ${colors.accent || '#4CAF50'};
|
||||
}
|
||||
.dialog-btn.secondary {
|
||||
background: ${toRGBA(colors.errorText || '#f44336', 0.2)};
|
||||
border-color: ${colors.errorText || '#f44336'};
|
||||
}
|
||||
.hidden { display: none !important; }
|
||||
`;
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
if (!currentDialog) return;
|
||||
|
||||
const colors = getComfyUIColors();
|
||||
const styleTag = document.getElementById('node-loader-style');
|
||||
if (styleTag) {
|
||||
styleTag.textContent = createStyleCSS(colors);
|
||||
}
|
||||
}
|
||||
|
||||
function getApp() {
|
||||
return window.app || window.comfyApp;
|
||||
}
|
||||
|
||||
function getExistingNodePositions() {
|
||||
const app = getApp();
|
||||
const positions = [];
|
||||
|
||||
if (app?.graph?.nodes) {
|
||||
app.graph.nodes.forEach(node => {
|
||||
if (node.pos) {
|
||||
positions.push({
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
width: node.size?.[0] || 200,
|
||||
height: node.size?.[1] || 100
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
function findNonOverlappingPosition(startX, startY, width, height, existingPositions, spacingX, spacingY) {
|
||||
let x = startX;
|
||||
let y = startY;
|
||||
|
||||
while (true) {
|
||||
let overlaps = false;
|
||||
|
||||
for (const pos of existingPositions) {
|
||||
if (!(x + width + NODE_PADDING < pos.x ||
|
||||
x - NODE_PADDING > pos.x + pos.width ||
|
||||
y + height + NODE_PADDING < pos.y ||
|
||||
y - NODE_PADDING > pos.y + pos.height)) {
|
||||
overlaps = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overlaps) return { x, y };
|
||||
|
||||
x += spacingX;
|
||||
if (x > startX + spacingX * 5) {
|
||||
x = startX;
|
||||
y += spacingY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function spawnNodes(types, spacingX = DEFAULT_SPACING.x, spacingY = DEFAULT_SPACING.y) {
|
||||
const app = getApp();
|
||||
if (!app?.graph?.add) {
|
||||
alert("ComfyUI graph not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
const startX = -app.canvas.ds.offset[0] + 50;
|
||||
const startY = -app.canvas.ds.offset[1] + 50;
|
||||
const existingPositions = getExistingNodePositions();
|
||||
|
||||
const spawnedNodes = [];
|
||||
|
||||
types.forEach((type, i) => {
|
||||
const node = LiteGraph.createNode(type);
|
||||
if (node) {
|
||||
const nodeWidth = node.size?.[0] || 200;
|
||||
const nodeHeight = node.size?.[1] || 100;
|
||||
|
||||
const position = findNonOverlappingPosition(
|
||||
startX + (i % 5) * spacingX,
|
||||
startY + Math.floor(i / 5) * spacingY,
|
||||
nodeWidth,
|
||||
nodeHeight,
|
||||
existingPositions,
|
||||
spacingX,
|
||||
spacingY
|
||||
);
|
||||
|
||||
node.pos = [position.x, position.y];
|
||||
app.graph.add(node);
|
||||
spawnedNodes.push(type);
|
||||
|
||||
existingPositions.push({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
width: nodeWidth,
|
||||
height: nodeHeight
|
||||
});
|
||||
} else {
|
||||
console.warn(`Could not create node: ${type}`);
|
||||
}
|
||||
});
|
||||
|
||||
updateRecentlyUsedNodes(spawnedNodes);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
function updateRecentlyUsedNodes(newNodes) {
|
||||
newNodes.forEach(nodeType => {
|
||||
const index = recentlyUsedNodes.indexOf(nodeType);
|
||||
if (index > -1) recentlyUsedNodes.splice(index, 1);
|
||||
recentlyUsedNodes.unshift(nodeType);
|
||||
});
|
||||
|
||||
recentlyUsedNodes = recentlyUsedNodes.slice(0, MAX_RECENT);
|
||||
localStorage.setItem('endlessNodeLoader_recentlyUsed', JSON.stringify(recentlyUsedNodes));
|
||||
|
||||
if (currentDialog) updateRecentChips();
|
||||
}
|
||||
|
||||
function updateRecentChips() {
|
||||
const recentSection = currentDialog.querySelector('.recent-section');
|
||||
if (!recentSection) return;
|
||||
|
||||
recentSection.innerHTML = '';
|
||||
|
||||
recentlyUsedNodes.forEach(nodeType => {
|
||||
const chip = document.createElement('button');
|
||||
chip.className = 'recent-chip';
|
||||
|
||||
const nodeClass = LiteGraph.registered_node_types[nodeType];
|
||||
const displayName = nodeClass?.title || nodeClass?.name || nodeType.split("/").pop();
|
||||
chip.textContent = displayName;
|
||||
chip.title = nodeType;
|
||||
|
||||
chip.onclick = () => {
|
||||
const checkbox = Array.from(currentDialog.querySelectorAll('.node-checkbox')).find(cb => {
|
||||
return cb.closest('li').dataset.nodeType === nodeType;
|
||||
});
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
updateSelectedCounter();
|
||||
}
|
||||
};
|
||||
|
||||
recentSection.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function addToSearchHistory(searchTerm) {
|
||||
if (!searchTerm.trim() || searchHistory.includes(searchTerm)) return;
|
||||
|
||||
searchHistory.unshift(searchTerm);
|
||||
searchHistory = searchHistory.slice(0, MAX_HISTORY);
|
||||
localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory));
|
||||
}
|
||||
|
||||
function showSearchHistory(inputElement) {
|
||||
const dropdown = inputElement.parentElement.querySelector('.search-dropdown');
|
||||
if (!dropdown || searchHistory.length === 0) {
|
||||
if (dropdown) dropdown.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.innerHTML = '';
|
||||
searchHistory.forEach(term => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-item';
|
||||
item.textContent = term;
|
||||
item.onclick = () => {
|
||||
inputElement.value = term;
|
||||
applyFilter(term, true);
|
||||
hideSearchHistory(dropdown);
|
||||
};
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
dropdown.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
if (dropdown.style.display === 'block') {
|
||||
hideSearchHistory(dropdown);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function hideSearchHistory(dropdown) {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
|
||||
function applyFilter(filterText, saveToHistory = true) {
|
||||
currentFilter = filterText.toLowerCase();
|
||||
const nodeList = currentDialog.querySelector('.node-list');
|
||||
|
||||
if (!currentFilter) {
|
||||
nodeList.querySelectorAll('.category, .category li').forEach(el => {
|
||||
el.classList.remove('hidden');
|
||||
});
|
||||
updateTotalCounter();
|
||||
return;
|
||||
}
|
||||
|
||||
nodeList.querySelectorAll('.category').forEach(details => {
|
||||
const categoryName = details.querySelector('summary span').textContent.toLowerCase();
|
||||
const categoryMatches = categoryName.includes(currentFilter);
|
||||
|
||||
let hasMatchingNodes = false;
|
||||
const nodeItems = details.querySelectorAll('li');
|
||||
|
||||
nodeItems.forEach(li => {
|
||||
const nodeText = li.textContent.toLowerCase();
|
||||
const nodeType = li.dataset.nodeType?.toLowerCase() || '';
|
||||
const matches = nodeText.includes(currentFilter) || nodeType.includes(currentFilter);
|
||||
|
||||
if (matches) {
|
||||
li.classList.remove('hidden');
|
||||
hasMatchingNodes = true;
|
||||
} else {
|
||||
li.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
if (categoryMatches || hasMatchingNodes) {
|
||||
details.classList.remove('hidden');
|
||||
if (hasMatchingNodes && !categoryMatches) {
|
||||
details.open = true;
|
||||
}
|
||||
} else {
|
||||
details.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
updateTotalCounter();
|
||||
}
|
||||
|
||||
function updateSelectedCounter() {
|
||||
const counter = currentDialog.querySelector('.counter-selected');
|
||||
if (!counter) return;
|
||||
|
||||
const checkedBoxes = currentDialog.querySelectorAll('.node-checkbox:checked');
|
||||
counter.textContent = `Selected: ${checkedBoxes.length}`;
|
||||
}
|
||||
|
||||
function updateTotalCounter() {
|
||||
const counter = currentDialog.querySelector('.counter-total');
|
||||
if (!counter) return;
|
||||
|
||||
const visibleNodes = currentDialog.querySelectorAll('.category li:not(.hidden)');
|
||||
counter.textContent = `Total: ${visibleNodes.length}/${allNodesData.length}`;
|
||||
}
|
||||
|
||||
function toggleAllCategories(expand) {
|
||||
const details = currentDialog.querySelectorAll('.category:not(.hidden)');
|
||||
details.forEach(detail => {
|
||||
detail.open = expand;
|
||||
});
|
||||
}
|
||||
|
||||
function buildHierarchy(nodes) {
|
||||
const root = {};
|
||||
nodes.forEach(n => {
|
||||
let current = root;
|
||||
n.pathParts.forEach((part, idx) => {
|
||||
if (!current[part]) {
|
||||
current[part] = { _nodes: [], _subcategories: {} };
|
||||
}
|
||||
if (idx === n.pathParts.length - 1) {
|
||||
current[part]._nodes.push(n);
|
||||
} else {
|
||||
current = current[part]._subcategories;
|
||||
}
|
||||
});
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
function countNodesInCategory(categoryObj) {
|
||||
let count = categoryObj._nodes?.length || 0;
|
||||
if (categoryObj._subcategories) {
|
||||
Object.values(categoryObj._subcategories).forEach(sub => {
|
||||
count += countNodesInCategory(sub);
|
||||
});
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function selectAllInCategory(categoryDetails, select = true) {
|
||||
const checkboxes = categoryDetails.querySelectorAll("input[type='checkbox']");
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = select;
|
||||
});
|
||||
updateSelectedCounter();
|
||||
}
|
||||
|
||||
function renderCategory(categoryObj, depth = 0) {
|
||||
return Object.entries(categoryObj)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([cat, obj]) => {
|
||||
const totalNodes = countNodesInCategory(obj);
|
||||
|
||||
const details = document.createElement("details");
|
||||
details.className = "category";
|
||||
details.style.paddingLeft = `${depth * 1.2}em`;
|
||||
|
||||
const summary = document.createElement("summary");
|
||||
|
||||
const categoryName = document.createElement("span");
|
||||
categoryName.textContent = `${cat} (${totalNodes})`;
|
||||
|
||||
const selectAllBtn = document.createElement("button");
|
||||
selectAllBtn.textContent = "All";
|
||||
selectAllBtn.className = "cat-btn select";
|
||||
selectAllBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
selectAllInCategory(details, true);
|
||||
};
|
||||
|
||||
const selectNoneBtn = document.createElement("button");
|
||||
selectNoneBtn.textContent = "None";
|
||||
selectNoneBtn.className = "cat-btn deselect";
|
||||
selectNoneBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
selectAllInCategory(details, false);
|
||||
};
|
||||
|
||||
summary.appendChild(categoryName);
|
||||
summary.appendChild(selectAllBtn);
|
||||
summary.appendChild(selectNoneBtn);
|
||||
details.appendChild(summary);
|
||||
|
||||
const list = document.createElement("ul");
|
||||
(obj._nodes || []).forEach(node => {
|
||||
const li = document.createElement("li");
|
||||
li.dataset.nodeType = node.type;
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.className = "node-checkbox";
|
||||
checkbox.onchange = updateSelectedCounter;
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = node.displayName;
|
||||
|
||||
li.appendChild(checkbox);
|
||||
li.appendChild(label);
|
||||
list.appendChild(li);
|
||||
});
|
||||
|
||||
details.appendChild(list);
|
||||
|
||||
const subCategories = renderCategory(obj._subcategories || {}, depth + 1);
|
||||
subCategories.forEach(sub => details.appendChild(sub));
|
||||
|
||||
return details;
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedNodeTypes() {
|
||||
const selected = [];
|
||||
currentDialog.querySelectorAll('.node-checkbox:checked').forEach(checkbox => {
|
||||
const nodeType = checkbox.closest('li').dataset.nodeType;
|
||||
if (nodeType) selected.push(nodeType);
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
function clearSelectedNodes() {
|
||||
currentDialog.querySelectorAll('.node-checkbox:checked').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
updateSelectedCounter();
|
||||
}
|
||||
|
||||
function setupEventHandlers() {
|
||||
const filterInput = currentDialog.querySelector('.filter-input');
|
||||
const searchDropdown = currentDialog.querySelector('.search-dropdown');
|
||||
const expandBtn = currentDialog.querySelector('.expand-btn');
|
||||
|
||||
let isExpanded = false;
|
||||
|
||||
// Filter input handlers
|
||||
filterInput.oninput = (e) => applyFilter(e.target.value, false);
|
||||
|
||||
filterInput.onkeydown = (e) => {
|
||||
if (e.key === 'Enter' && e.target.value.trim()) {
|
||||
addToSearchHistory(e.target.value.trim());
|
||||
hideSearchHistory(searchDropdown);
|
||||
} else if (e.key === 'ArrowDown' && searchHistory.length > 0) {
|
||||
showSearchHistory(filterInput);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
filterInput.onfocus = (e) => {
|
||||
if (!e.target.value.trim()) {
|
||||
showSearchHistory(filterInput);
|
||||
}
|
||||
};
|
||||
|
||||
filterInput.onblur = (e) => {
|
||||
if (e.target.value.trim()) {
|
||||
addToSearchHistory(e.target.value.trim());
|
||||
}
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
setTimeout(() => hideSearchHistory(searchDropdown), 150);
|
||||
};
|
||||
|
||||
filterInput.onmouseenter = () => {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = null;
|
||||
}
|
||||
|
||||
hoverTimeout = setTimeout(() => {
|
||||
if (searchHistory.length > 0) {
|
||||
showSearchHistory(filterInput);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
filterInput.onmouseleave = () => {
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
hideSearchHistory(searchDropdown);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
// Expand button
|
||||
expandBtn.onclick = () => {
|
||||
isExpanded = !isExpanded;
|
||||
toggleAllCategories(isExpanded);
|
||||
expandBtn.textContent = isExpanded ? "Collapse All" : "Expand All";
|
||||
};
|
||||
|
||||
// Button handlers
|
||||
currentDialog.querySelector('#spawn-btn').onclick = () => {
|
||||
const selectedTypes = getSelectedNodeTypes();
|
||||
if (selectedTypes.length === 0) {
|
||||
alert("Please select at least one node to spawn.");
|
||||
return;
|
||||
}
|
||||
spawnNodes(selectedTypes);
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
currentDialog.querySelector('#clear-btn').onclick = clearSelectedNodes;
|
||||
currentDialog.querySelector('#cancel-btn').onclick = closeDialog;
|
||||
currentDialog.querySelector('#clear-history-btn').onclick = () => {
|
||||
searchHistory = [];
|
||||
localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory));
|
||||
const dropdown = currentDialog.querySelector('.search-dropdown');
|
||||
if (dropdown) dropdown.style.display = 'none';
|
||||
};
|
||||
currentDialog.querySelector('#clear-recent-btn').onclick = () => {
|
||||
recentlyUsedNodes = [];
|
||||
localStorage.setItem('endlessNodeLoader_recentlyUsed', JSON.stringify(recentlyUsedNodes));
|
||||
updateRecentChips(); // This will clear the chips
|
||||
};
|
||||
|
||||
// ESC key handler
|
||||
const escHandler = (e) => e.key === 'Escape' && closeDialog();
|
||||
document.addEventListener('keydown', escHandler);
|
||||
|
||||
return escHandler;
|
||||
}
|
||||
|
||||
function createNodeLoaderDialog() {
|
||||
if (currentDialog) return;
|
||||
|
||||
const colors = getComfyUIColors();
|
||||
|
||||
// Clean up existing style
|
||||
document.getElementById('node-loader-style')?.remove();
|
||||
|
||||
// Create style tag
|
||||
const style = document.createElement('style');
|
||||
style.id = 'node-loader-style';
|
||||
style.textContent = createStyleCSS(colors);
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Create container
|
||||
const container = document.createElement("div");
|
||||
// Calculate max size based on viewport
|
||||
const maxWidth = Math.min(window.innerWidth * 0.4, 1536); // 40% of window width, max 1536px
|
||||
const maxHeight = Math.min(window.innerHeight * 0.6, 1296); // 60% of window height, max 1296px
|
||||
|
||||
container.className = "dialog-container";
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: ${maxWidth}px;
|
||||
max-height: ${maxHeight}px;
|
||||
`;
|
||||
|
||||
container.innerHTML = `
|
||||
<h3 class="dialog-title"> Endless 🌊✨ Node Spawner Drag Bar</h3>
|
||||
|
||||
<div class="filter-section">
|
||||
<div class="filter-row">
|
||||
<input type="text" class="filter-input" placeholder="Filter nodes..." title="Type to filter nodes, ↓ for history">
|
||||
<div class="search-dropdown"></div>
|
||||
<button class="expand-btn" title="Expand/collapse all categories">Expand All</button>
|
||||
</div>
|
||||
<div class="counters">
|
||||
<span class="counter-selected">Selected: 0</span>
|
||||
<span class="counter-total">Total: 0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recent-section"></div>
|
||||
<div class="node-list"></div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="btn-group">
|
||||
<button id="spawn-btn" class="dialog-btn primary" title="Spawn selected nodes">🌊 Spawn Nodes</button>
|
||||
<button id="clear-btn" class="dialog-btn" title="Clear all selections">Clear Selected</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button id="clear-history-btn" class="dialog-btn" title="Clear search history">Clear Search</button>
|
||||
<button id="clear-recent-btn" class="dialog-btn" title="Clear recent nodes">Clear Recent</button>
|
||||
<button id="cancel-btn" class="dialog-btn secondary" title="Close dialog">❌ Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(container);
|
||||
currentDialog = container;
|
||||
|
||||
// Setup dragging
|
||||
makeDraggable(container, container.querySelector('.dialog-title'));
|
||||
|
||||
// Build node data
|
||||
const nodes = Object.entries(LiteGraph.registered_node_types)
|
||||
.filter(([key, value]) => key && value)
|
||||
.map(([type, nodeClass]) => {
|
||||
const category = nodeClass.category || "Other";
|
||||
const displayName = nodeClass.title || nodeClass.name || type.split("/").pop();
|
||||
return {
|
||||
type,
|
||||
category,
|
||||
pathParts: category.split("/"),
|
||||
displayName,
|
||||
description: nodeClass.desc || nodeClass.description || "",
|
||||
fullPath: category + "/" + displayName
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.category.localeCompare(b.category) || a.displayName.localeCompare(b.displayName));
|
||||
|
||||
allNodesData = nodes;
|
||||
|
||||
// Render hierarchy
|
||||
const hierarchy = buildHierarchy(nodes);
|
||||
const tree = renderCategory(hierarchy);
|
||||
const nodeList = container.querySelector('.node-list');
|
||||
tree.forEach(section => nodeList.appendChild(section));
|
||||
|
||||
// Add hover effects
|
||||
addButtonHoverEffects(container);
|
||||
|
||||
// Setup event handlers
|
||||
const escHandler = setupEventHandlers();
|
||||
|
||||
// Setup theme updates
|
||||
unregisterThemeCallback = onThemeChange(updateTheme);
|
||||
|
||||
// Initialize
|
||||
updateRecentChips();
|
||||
updateSelectedCounter();
|
||||
updateTotalCounter();
|
||||
|
||||
// Focus filter input
|
||||
container.querySelector('.filter-input').focus();
|
||||
|
||||
// Setup cleanup
|
||||
const originalRemove = container.remove.bind(container);
|
||||
container.remove = function() {
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
unregisterThemeCallback?.();
|
||||
document.getElementById('node-loader-style')?.remove();
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
if (hoverTimeout) clearTimeout(hoverTimeout);
|
||||
currentDialog = null;
|
||||
unregisterThemeCallback = null;
|
||||
searchTimeout = null;
|
||||
hoverTimeout = null;
|
||||
originalRemove();
|
||||
};
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
currentDialog?.remove();
|
||||
}
|
||||
|
||||
// Register tool
|
||||
registerEndlessTool("Node Spawner", createNodeLoaderDialog);
|
||||
})();
|
||||
276
web/endless_ui_helpers.js
Normal file
276
web/endless_ui_helpers.js
Normal file
@@ -0,0 +1,276 @@
|
||||
// === Endless 🌊✨ Tools UI Helper ===
|
||||
|
||||
const endlessToolsRegistry = [];
|
||||
|
||||
export function registerEndlessTool(name, callback) {
|
||||
endlessToolsRegistry.push({ name, callback });
|
||||
}
|
||||
|
||||
export function injectEndlessToolsButton() {
|
||||
const toolbar = findToolbar();
|
||||
if (!toolbar || document.getElementById("endless-tools-button")) return;
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "endless-tools-button";
|
||||
btn.textContent = "Endless 🌊✨ Tools";
|
||||
btn.className = "comfyui-button";
|
||||
btn.style.marginLeft = "8px";
|
||||
btn.onclick = showEndlessToolMenu;
|
||||
toolbar.appendChild(btn);
|
||||
}
|
||||
|
||||
export function showEndlessToolMenu() {
|
||||
document.getElementById("endless-tools-float")?.remove();
|
||||
|
||||
const colors = getComfyUIColors();
|
||||
|
||||
const menu = document.createElement("div");
|
||||
menu.id = "endless-tools-float";
|
||||
menu.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${colors.menu};
|
||||
color: ${colors.inputText};
|
||||
padding: 12px;
|
||||
border: 1px solid ${colors.accent};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
z-index: 99999;
|
||||
opacity: 1;
|
||||
width: fit-content;
|
||||
transition: opacity 0.2s ease;
|
||||
`;
|
||||
|
||||
const dragBar = document.createElement("div");
|
||||
dragBar.textContent = "Endless 🌊✨ Tools Drag Bar";
|
||||
dragBar.style.cssText = `
|
||||
padding: 4px;
|
||||
background: ${toRGBA(colors.inputText, 0.05)};
|
||||
cursor: move;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
`;
|
||||
menu.appendChild(dragBar);
|
||||
|
||||
endlessToolsRegistry.sort((a, b) => a.name.localeCompare(b.name)).forEach(tool => {
|
||||
const btn = document.createElement("div");
|
||||
btn.textContent = `🌊✨ ${tool.name}`;
|
||||
btn.style.cssText = `
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
`;
|
||||
btn.onmouseover = () => btn.style.background = toRGBA(colors.inputText, 0.1);
|
||||
btn.onmouseout = () => btn.style.background = "transparent";
|
||||
btn.onclick = () => {
|
||||
tool.callback();
|
||||
menu.remove();
|
||||
};
|
||||
menu.appendChild(btn);
|
||||
});
|
||||
|
||||
makeDraggable(menu, dragBar);
|
||||
|
||||
// Live theme updater
|
||||
function updateMenuTheme(newColors = getComfyUIColors()) {
|
||||
menu.style.background = newColors.menu;
|
||||
menu.style.color = newColors.inputText;
|
||||
menu.style.borderColor = newColors.accent;
|
||||
menu.style.boxShadow = newColors.shadow;
|
||||
dragBar.style.background = toRGBA(newColors.inputText, 0.05);
|
||||
dragBar.style.borderBottomColor = newColors.border;
|
||||
}
|
||||
|
||||
const unregister = onThemeChange(updateMenuTheme);
|
||||
menu.remove = ((orig => function () {
|
||||
unregister();
|
||||
orig.call(this);
|
||||
})(menu.remove));
|
||||
|
||||
document.body.appendChild(menu);
|
||||
}
|
||||
|
||||
// === Hotkeys ===
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'e') {
|
||||
showEndlessToolMenu();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
document.getElementById("endless-tools-float")?.remove();
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Endless 🌊✨ Tools menu: press Ctrl+Alt+E if toolbar button is missing.");
|
||||
|
||||
function waitForToolbarAndInject() {
|
||||
if (document.querySelector('.comfyui-menu')) {
|
||||
injectEndlessToolsButton();
|
||||
return;
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
if (document.querySelector('.comfyui-menu')) {
|
||||
injectEndlessToolsButton();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
waitForToolbarAndInject();
|
||||
|
||||
function findToolbar() {
|
||||
return (
|
||||
document.querySelector('.comfyui-menu, .comfy-menu, [class*="menu"], [class*="toolbar"]') ||
|
||||
Array.from(document.querySelectorAll('[class*="button-group"], [class*="btn-group"], .comfyui-button-group'))
|
||||
.find(g => g.querySelectorAll('button').length > 0) ||
|
||||
Array.from(document.querySelectorAll('*'))
|
||||
.find(el => {
|
||||
const buttons = el.querySelectorAll('button');
|
||||
return buttons.length >= 2 && buttons.length <= 10;
|
||||
}) ||
|
||||
Array.from(document.querySelectorAll(".comfyui-button-group"))
|
||||
.find(div => Array.from(div.querySelectorAll("button")).some(btn => btn.title === "Share"))
|
||||
);
|
||||
}
|
||||
|
||||
// === Live Theme Monitoring ===
|
||||
let themeObserver = null;
|
||||
const themeCallbacks = new Set();
|
||||
|
||||
export function onThemeChange(callback) {
|
||||
themeCallbacks.add(callback);
|
||||
if (themeCallbacks.size === 1) startThemeObserver();
|
||||
return () => {
|
||||
themeCallbacks.delete(callback);
|
||||
if (themeCallbacks.size === 0) stopThemeObserver();
|
||||
};
|
||||
}
|
||||
|
||||
function startThemeObserver() {
|
||||
if (themeObserver) return;
|
||||
themeObserver = new MutationObserver(() => {
|
||||
clearTimeout(window.themeChangeTimeout);
|
||||
window.themeChangeTimeout = setTimeout(() => {
|
||||
const newColors = getComfyUIColors();
|
||||
themeCallbacks.forEach(cb => cb(newColors));
|
||||
}, 100);
|
||||
});
|
||||
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
|
||||
themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
|
||||
}
|
||||
|
||||
function stopThemeObserver() {
|
||||
if (themeObserver) {
|
||||
themeObserver.disconnect();
|
||||
themeObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getComfyUIColors() {
|
||||
const computed = getComputedStyle(document.documentElement);
|
||||
const getVar = name => computed.getPropertyValue(name).trim() || null;
|
||||
return {
|
||||
fg: getVar("--fg-color") || "#ddd",
|
||||
bg: getVar("--bg-color") || "#353535",
|
||||
menu: getVar("--comfy-menu-bg") || "#353535",
|
||||
menuSecondary: getVar("--comfy-menu-secondary-bg") || "#222",
|
||||
inputBg: getVar("--comfy-input-bg") || "#222",
|
||||
inputText: getVar("--input-text") || "#ddd",
|
||||
descriptionText: getVar("--descrip-text") || "#999",
|
||||
dragText: getVar("--drag-text") || "#ddd",
|
||||
errorText: getVar("--error-text") || "#f44336",
|
||||
border: getVar("--border-color") || "#999",
|
||||
accent: getVar("--comfy-accent") || getVar("--comfy-accent-color") || "#4a90e2",
|
||||
hoverBg: getVar("--content-hover-bg") || "rgba(255,255,255,0.1)",
|
||||
hoverFg: getVar("--content-hover-fg") || "#fff",
|
||||
shadow: getVar("--bar-shadow") || "0 2px 10px rgba(0,0,0,0.3)",
|
||||
dialogBg: getVar("--comfy-menu-bg") || getVar("--bg-color") || "#353535",
|
||||
buttonHoverBg: getVar("--content-hover-bg") || "rgba(255,255,255,0.1)"
|
||||
};
|
||||
}
|
||||
|
||||
export function toRGBA(color, alpha = 0.2) {
|
||||
if (!color) return `rgba(128,128,128,${alpha})`;
|
||||
color = color.trim();
|
||||
if (color.startsWith('#')) {
|
||||
const hex = color.slice(1);
|
||||
const fullHex = hex.length === 3 ? hex.split('').map(c => c + c).join('') : hex;
|
||||
const bigint = parseInt(fullHex, 16);
|
||||
const r = (bigint >> 16) & 255, g = (bigint >> 8) & 255, b = bigint & 255;
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
if (color.startsWith('rgb')) {
|
||||
const rgb = color.match(/\d+/g);
|
||||
if (rgb?.length >= 3) return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
|
||||
}
|
||||
return `rgba(128,128,128,${alpha})`;
|
||||
}
|
||||
|
||||
export function blendColors(color1, color2, ratio) {
|
||||
const c1 = toRGBA(color1, 1).match(/\d+/g);
|
||||
const c2 = toRGBA(color2, 1).match(/\d+/g);
|
||||
if (!c1 || !c2) return color1;
|
||||
const r = Math.round(c1[0] * (1 - ratio) + c2[0] * ratio);
|
||||
const g = Math.round(c1[1] * (1 - ratio) + c2[1] * ratio);
|
||||
const b = Math.round(c1[2] * (1 - ratio) + c2[2] * ratio);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
export function addButtonHoverEffects(container) {
|
||||
container?.querySelectorAll('button').forEach(button => {
|
||||
button.addEventListener('mouseenter', () => {
|
||||
button.style.boxShadow = '0 0 0 1px currentColor';
|
||||
button.style.filter = 'brightness(1.1)';
|
||||
button.style.transform = 'translateY(-1px)';
|
||||
});
|
||||
button.addEventListener('mouseleave', () => {
|
||||
button.style.boxShadow = 'none';
|
||||
button.style.filter = 'brightness(1)';
|
||||
button.style.transform = 'translateY(0px)';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function makeDraggable(element, handle = element) {
|
||||
let offsetX = 0, offsetY = 0, isDown = false;
|
||||
handle.onmousedown = (e) => {
|
||||
isDown = true;
|
||||
if (element.style.position !== 'fixed') {
|
||||
element.style.position = 'fixed';
|
||||
element.style.right = 'auto';
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
offsetX = e.clientX - rect.left;
|
||||
offsetY = e.clientY - rect.top;
|
||||
element.style.cursor = 'move';
|
||||
document.onmousemove = (e) => {
|
||||
if (!isDown) return;
|
||||
element.style.left = `${e.clientX - offsetX}px`;
|
||||
element.style.top = `${e.clientY - offsetY}px`;
|
||||
element.style.transform = 'none';
|
||||
};
|
||||
document.onmouseup = () => {
|
||||
isDown = false;
|
||||
element.style.cursor = 'default';
|
||||
document.onmousemove = null;
|
||||
document.onmouseup = null;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// === Global exposure for F12 ===
|
||||
window.EndlessHelpers = {
|
||||
registerEndlessTool,
|
||||
injectEndlessToolsButton,
|
||||
showEndlessToolMenu,
|
||||
onThemeChange,
|
||||
getComfyUIColors,
|
||||
toRGBA,
|
||||
blendColors,
|
||||
addButtonHoverEffects,
|
||||
makeDraggable
|
||||
};
|
||||
Reference in New Issue
Block a user