diff --git a/.github/workflows/publish_action.yml b/.github/workflows/publish_action.yml new file mode 100644 index 0000000..e3c5c78 --- /dev/null +++ b/.github/workflows/publish_action.yml @@ -0,0 +1,20 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. diff --git a/README.md b/README.md index ba5cd10..2a30560 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,94 @@ -# Endless-Buttons -Some basic custom JavaScript files for the ComfyUI user interface +# Endless Sea of Stars Buttons + +Some basic custom JavaScript files for the ComfyUI user interface. Features: + ++ Fontifier: a script that allows you to change fonts and font sizes for ComfyUI nodes ++ More to come (maybe) + +When using the [ComfyUI](https://github.com/comfyanonymous/ComfyUI) interface for AI art generation, I sometimes find that the standard nodes and the many custom nodes out there don't work the way I want them to, or how I think they should. + +Rightly or wrongly, I was teaching myself a bit of Python back in 2023 to get some nodes up and running to do what I'd like, and I now I am doing the same for JavaScript. Yes, I am using ChatGPT, Copilot, Claude and others, and yes, I am a glutton for punishment. There are no promises that these scripts will work for you or that I will maintain them. Feel free to do with them as you wish, according to the license model. + +*** +**UPDATE: JUL 20, 2025** + ++ **Cloned the scripts from [Endless Sea of Stars Nodes](https://github.com/tusharbhutt/Endless-Nodes) so you can just download the scripts if you want** ++ Added the Endless 🌊✨ Tools button to taskbar. This acts as a centralized repository of all my scripts ++ Introduced the Endless 🌊✨ Fontifier, a script that allows you to dynamically change fonts and sizes. + + No need to dive into CSS to change text size + + Allows changes to the title areas, connector text, widgets, and more + + Adjust the height of the title bar and other areas too, to accommodate the new font size + +## Installation Instructions + +It’s preferable to install from the ComfyUI Node Manager, but for direct installation, do this: + ++ Navigate to your /ComfyUI/custom_nodes/ folder ++ In Windows, you can then right-click to start a command prompt and type: + +`git clone https://github.com/tusharbhutt/Endless-Buttons` + +If installed correctly, you should see a button in the main ComfyUI taskbar that look like this: + +Endless 🌊✨ Tools + +and when you click it, a floating toolbar with available scripts will be enabled. **DON'T SEE THE BUTTON IN THE TOOLBAR? PRESS CTRL-ALT-E TO BRING THE TOOLBAR** + +*** +## Assistance Requests + +I am not a programmer, nor do I care to be. I have a fulltime job that eats up 50-60 hours a week and I made these scripts for myself. I’ve tested the scripts on my system and uploaded them as they seem to work. They may or may not work for you or on your system. + +If you have issues, ask me **nicely** for help. Your tone matters; I'm too old and tired to pay attention to people who think I blew up their machines, and if how I react to you if you are difficult bothers you, some self-reflection is in order on your part. You are not "forthright" or "honest" or "direct", you're merely an ass if you think badgering people is justifiable to get what you want. The world has too many assholes, don't make me think you're another one. +*** + +## Button List + + A helper script places a button on your task bar called "Endless 🌊✨ Tools": + +![endlesstools](./img/endlesstools.png) + +**IF YOU DO NOT SEE IT, PRESS CTRL-ALT-E TO BRING UP A FLOATING TOOLBAR THAT CAN BE MOVED AROUND** This is the central toolbar that contains all the scripts I have made for this collection + + +### Endless 🌊✨ Fontifier + +I always found it odd that in the early days of ComfyUI, you could not change the font size for various node elements. Sure you could manually go into the CSS styling in a user file, but that is not user friendly. Later versions have allowed you to change the widget text size, but that's it. Yes, you can zoom in, but... now you've lost your larger view of the workflow. If you have a 4K monitor and old eyes, too bad so sad for you. Click the Endless 🌊✨ Tools button to bring up the floating toolbar and you should see a choice for "🌊✨ Fontifier". Clicking it shows the dialog box below: + + +![fontifierbox](./img/fontifierbox.png) + + +Drag the box around by clicking and holding the title. To cancel, you can simply click outside the dialog box or press the escape key. With this dialog box, you can do the following: + ++ Globally change the font size for all text elements ++ Change the fonts themselves ++ Instead of a global change, select various elements to resize ++ Adjust the height of the title bar or connectors and other input areas + + +Once you make your changes, you can preview them and then choose to apply or cancel. Changed your mind? Load the box again and press the reset key. + +![fontifiernode](./img/fontifiernode.png) + + +*** +## Usage License and Restrictions + +See [GPL Licensing V3](https://www.gnu.org/licenses/gpl-3.0.en.html#license-text) for usage. You may modify this code as long as you keep the credits for this repository and for those noted in the credit section below. **YOU ARE EXPRESSLY FORBIDDEN FROM USING THESE SCRIPTS TO CREATE ANY IMAGES OR ARTWORK THAT VIOLATES THE STABLE DIFFUSION [ACCEPTABLE USE POLICY](https://stability.ai/use-policy) OR THE USAGE POLICY FROM [BLACK FOREST LABS](https://bfl.ai/legal/usage-policy).** + +For example, don't be a mouth-breather who creates fake nudes or sexual content of **anyone, even if you have their consent**. JUST. DON’T. BE. AN. ASSHOLE. I call this my "Don't Be an Assholeβ„’" licensing model (I'm kidding about the trademark but not about the message). + +The author expressly disclaims any liability for any images you create using these nodes. +*** +## Disclaimer + +These scripts may or may not be maintained. They work on my system but may not on yours. Feel free to send in a bug report if you find one! +*** +## Credits + ++ Inspiration for the Fontifier came from having old eyes and a 4K screen ++ Anthropic Claude and OpenAI ChatGPT are actively used for coding creation/assistance ++ Thanks to all the node and script creators out there, most who toil away with little or no appreciation + +# \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..76a0fe6 --- /dev/null +++ b/__init__.py @@ -0,0 +1,16 @@ +# buttons/__init__.py + + +# ========================= +# πŸ’‰ Set JavaScript +# ========================= + +WEB_DIRECTORY = "./web/" + +# Version info +__version__ = "1.0.2" + +print("\n===============================") +print(f"Endless Sea of Stars Buttons v{__version__} loaded successfully! 🌠") +print("Endless 🌊✨ Tools menu: Ctrl+Alt+E if toolbar button is missing") +print("===============================\n") \ No newline at end of file diff --git a/changlelog.md b/changlelog.md new file mode 100644 index 0000000..6b21c87 --- /dev/null +++ b/changlelog.md @@ -0,0 +1 @@ +July 20/25, V1.0.2: Introducing the Endless Fontifier, a JavaScript file that adds allows the user to change font sizes and fonts for various text elements on the ComfyUI interface. diff --git a/img/endlesstools.png b/img/endlesstools.png new file mode 100644 index 0000000..32bbbcc Binary files /dev/null and b/img/endlesstools.png differ diff --git a/img/fontifierbox.png b/img/fontifierbox.png new file mode 100644 index 0000000..deb8ebf Binary files /dev/null and b/img/fontifierbox.png differ diff --git a/img/fontifiernode.png b/img/fontifiernode.png new file mode 100644 index 0000000..4158775 Binary files /dev/null and b/img/fontifiernode.png differ diff --git a/img/icon.png b/img/icon.png new file mode 100644 index 0000000..40f439b Binary files /dev/null and b/img/icon.png differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2418de2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "endless-buttons" +description = "A small set of JavaScript files I created for myself. The scripts provide Quality of Life enhancements to the ComfyUI interface, such as changing fonts and font sizes." +version = "1.0.2" +license = { file = "LICENSE" } +dependencies = "" + +[project.urls] +Repository = "https://github.com/tusharbhutt/Endless-Buttons" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "tusharbhutt" +DisplayName = "Endless 🌊✨ Butons" +Icon = "https://raw.githubusercontent.com/tusharbhutt/Endless-Buttons/main/img/icon.png" diff --git a/web/endless_fontifier.js b/web/endless_fontifier.js new file mode 100644 index 0000000..6a790b2 --- /dev/null +++ b/web/endless_fontifier.js @@ -0,0 +1,460 @@ +// ComfyUI Endless 🌊✨ Fontifier - Fixed Version +import { registerEndlessTool } from './endless_ui_helpers.js'; + +(function() { + 'use strict'; + + // Store original ComfyUI default values for reset functionality + 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, + WIDGET_TEXT_SIZE: 12 // Added widget text size to original values + }; + + // Current values (will be updated as user changes them) + let currentValues = { ...originalValues }; + + // Values when dialog opens (for cancel/preview rollback) + let dialogOpenValues = {}; + + // 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; + + let offsetX = 0, offsetY = 0, isDown = false; + + header.style.cursor = 'move'; + header.style.userSelect = 'none'; + + header.onmousedown = (e) => { + e.preventDefault(); + isDown = true; + + 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'; + }; + + const onMouseUp = () => { + isDown = false; + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + } + + // Function to capture current ComfyUI settings + function captureCurrentSettings() { + const settings = { + NODE_TEXT_SIZE: (typeof LiteGraph !== 'undefined' && LiteGraph.NODE_TEXT_SIZE) ? LiteGraph.NODE_TEXT_SIZE : originalValues.NODE_TEXT_SIZE, + NODE_SUBTEXT_SIZE: (typeof LiteGraph !== 'undefined' && LiteGraph.NODE_SUBTEXT_SIZE) ? LiteGraph.NODE_SUBTEXT_SIZE : originalValues.NODE_SUBTEXT_SIZE, + NODE_TITLE_HEIGHT: (typeof LiteGraph !== 'undefined' && LiteGraph.NODE_TITLE_HEIGHT) ? LiteGraph.NODE_TITLE_HEIGHT : originalValues.NODE_TITLE_HEIGHT, + DEFAULT_GROUP_FONT: (typeof LiteGraph !== 'undefined' && LiteGraph.DEFAULT_GROUP_FONT) ? LiteGraph.DEFAULT_GROUP_FONT : originalValues.DEFAULT_GROUP_FONT, + NODE_FONT: (typeof LiteGraph !== 'undefined' && LiteGraph.NODE_FONT) ? LiteGraph.NODE_FONT : originalValues.NODE_FONT, + NODE_SLOT_HEIGHT: (typeof LiteGraph !== 'undefined' && LiteGraph.NODE_SLOT_HEIGHT) ? LiteGraph.NODE_SLOT_HEIGHT : originalValues.NODE_SLOT_HEIGHT, + WIDGET_TEXT_SIZE: currentValues.WIDGET_TEXT_SIZE // Get current widget text size + }; + + console.log('πŸ“Έ Captured current settings:', settings); + return settings; + } + + function createFontifierDialog() { + // Remove existing dialog if present + const existingDialog = document.getElementById('fontifier-dialog'); + if (existingDialog) { + existingDialog.remove(); + } + + // Capture current settings when dialog opens + currentValues = captureCurrentSettings(); + dialogOpenValues = { ...currentValues }; + + const colors = getComfyUIColors(); + + // Create dialog container + const dialog = document.createElement('div'); + dialog.id = 'fontifier-dialog'; + dialog.className = 'comfyui-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 - REMOVE the onclick handler to prevent accidental cancellation + 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); + z-index: 9999; + `; + // Removed the onclick handler to prevent accidental cancellation + + dialog.innerHTML = ` +
+

🌊✨ Endless Fontifier

+ +
+ +
+

Global Scale

+
+ + + +
+
+ +
+

Font Family

+ +
+ +
+

Text Element Sizes

+ +
+ +
The main title text at the top of each node (e.g., "KSampler", "VAE Decode")
+
+ + +
+
+ +
+ +
Text inside nodes: parameter names and values (e.g., "steps: 20", "cfg: 8.0")
+
+ + +
+
+
+ +
Font size for text inside input boxes, dropdowns, and textareas in nodes.
+
+ + +
+
+ +
+ +
Height of the colored title bar area at the top of nodes
+
+ + +
+
+ +
+ +
Height of input/output connection points on node sides
+
+ + +
+
+ +
+ +
Text size for node group labels (when nodes are grouped together)
+
+ + +
+
+
+ +
+ + + + +
+ `; + + document.body.appendChild(backdrop); + document.body.appendChild(dialog); + + // ESC key handler - restore original values on escape + function escHandler(e) { + if (e.key === 'Escape') { + restoreSettings(dialogOpenValues); + backdrop.remove(); + dialog.remove(); + document.removeEventListener('keydown', escHandler); + } + } + document.addEventListener('keydown', escHandler); + + // Set up event handlers + setupDialogHandlers(dialog, backdrop); + + // Set font family to current value + dialog.querySelector('#font-family').value = currentValues.NODE_FONT; + } + + // Function to restore settings + function restoreSettings(settings) { + applySettingsToComfyUI(settings); + console.log('πŸ”„ Settings restored:', settings); + } + + // Separated function to apply settings to ComfyUI + function applySettingsToComfyUI(settings) { + if (typeof LiteGraph !== 'undefined') { + 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.NODE_WIDGET_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 || settings.FONT_FAMILY; + LiteGraph.DEFAULT_FONT = settings.NODE_FONT || settings.FONT_FAMILY; + LiteGraph.GROUP_FONT = settings.NODE_FONT || settings.FONT_FAMILY; + + if (typeof app !== 'undefined' && app.canvas) { + app.canvas.setDirty(true, true); + if (app.canvas.draw) { + setTimeout(() => app.canvas.draw(true, true), 100); + } + } + + 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; + } + }); + } + + // Apply widget font size to CSS + let styleTag = document.getElementById('fontifier-widget-text-style'); + if (!styleTag) { + styleTag = document.createElement('style'); + styleTag.id = 'fontifier-widget-text-style'; + 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: ${settings.WIDGET_TEXT_SIZE}px !important; + font-family: ${settings.NODE_FONT || settings.FONT_FAMILY} !important; + } + + /* Exclude the fontifier dialog itself */ + #fontifier-dialog input, #fontifier-dialog select, #fontifier-dialog textarea { + font-size: 14px !important; + font-family: Arial !important; + } + `; + } + + function setupDialogHandlers(dialog, backdrop) { + makeDraggable(dialog); + + // Sync sliders with number inputs + const elements = [ + 'global-scale', + '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; + if (id === 'global-scale') { + const globalScaleNum = dialog.querySelector('#global-scale-num'); + globalScaleNum.value = slider.value; + } + }; + numberInput.oninput = () => { + slider.value = numberInput.value; + }; + }); + + // Global scale handler - now includes widget text size + const globalScale = dialog.querySelector('#global-scale'); + const globalScaleNum = dialog.querySelector('#global-scale-num'); + + function updateGlobalScale() { + const scale = parseFloat(globalScale.value); + globalScaleNum.value = scale; + + // Update all individual controls including widget text size + 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], + ['widget-text-size', originalValues.WIDGET_TEXT_SIZE] // Added widget text size to global scaling + ]; + + updates.forEach(([id, originalValue]) => { + const newValue = Math.round(originalValue * scale); + dialog.querySelector(`#${id}`).value = newValue; + dialog.querySelector(`#${id}-num`).value = newValue; + }); + } + + globalScale.oninput = updateGlobalScale; + globalScaleNum.oninput = () => { + globalScale.value = globalScaleNum.value; + updateGlobalScale(); + }; + + // Button handlers + dialog.querySelector('#close-dialog').onclick = () => { + restoreSettings(dialogOpenValues); + backdrop.remove(); + dialog.remove(); + }; + + 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('#widget-text-size').value = originalValues.WIDGET_TEXT_SIZE; + dialog.querySelector('#widget-text-size-num').value = originalValues.WIDGET_TEXT_SIZE; + 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(); + }; + + dialog.querySelector('#cancel-btn').onclick = () => { + restoreSettings(dialogOpenValues); + backdrop.remove(); + dialog.remove(); + }; + + // Add hover effects to buttons + const buttons = dialog.querySelectorAll('button'); + buttons.forEach(button => { + button.style.boxSizing = 'border-box'; + button.style.minWidth = button.offsetWidth + 'px'; + button.addEventListener('mouseenter', () => { + button.style.borderWidth = '2px'; + button.style.padding = '7px 15px'; + }); + button.addEventListener('mouseleave', () => { + button.style.borderWidth = '1px'; + button.style.padding = '8px 16px'; + }); + }); + } + + function applyChanges(dialog, permanent = false) { + const newValues = { + 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, + NODE_FONT: dialog.querySelector('#font-family').value, + WIDGET_TEXT_SIZE: parseInt(dialog.querySelector('#widget-text-size').value) + }; + + applySettingsToComfyUI(newValues); + console.log('🌊✨ Fontifier applied:', newValues); + + if (permanent) { + currentValues = { ...newValues }; + console.log('🌊✨ Fontifier changes applied permanently (until page refresh)'); + } + } + + registerEndlessTool("Fontifier", createFontifierDialog); +})(); \ No newline at end of file diff --git a/web/endless_node_loader.js b/web/endless_node_loader.js new file mode 100644 index 0000000..67c62db --- /dev/null +++ b/web/endless_node_loader.js @@ -0,0 +1,1020 @@ +import { registerEndlessTool } from './endless_ui_helpers.js'; + +console.log("βœ… Endless Node Loader loaded."); + +let dialogInstance = null; + +// Set search and recent Nodes + +let recentlyUsedNodes = JSON.parse(localStorage.getItem('endlessNodeLoader_recentlyUsed') || '[]'); +let searchHistory = JSON.parse(localStorage.getItem('endlessNodeLoader_searchHistory') || '[]'); + +// Timeout for search history drop down +let searchTimeout; +let hoverTimeout; + +// Dragging state +let isDragging = false; +let dragOffset = { x: 0, y: 0 }; + +// Filter state +let currentFilter = ''; +let allNodesData = []; + +function handleDrag(e) { + if (!isDragging) return; + const container = dialogInstance; + if (!container) return; + const x = e.clientX - dragOffset.x; + const y = e.clientY - dragOffset.y; + container.style.left = `${Math.max(0, Math.min(window.innerWidth - container.offsetWidth, x))}px`; + container.style.top = `${Math.max(0, Math.min(window.innerHeight - container.offsetHeight, y))}px`; +} + +function handleDragEnd() { + isDragging = false; + document.removeEventListener("mousemove", handleDrag); + document.removeEventListener("mouseup", handleDragEnd); + document.body.style.userSelect = ""; +} + +// Get ComfyUI themes // + +function getComfyUIThemeColors() { + const computed = getComputedStyle(document.documentElement); + + const getVar = name => computed.getPropertyValue(name).trim() || null; + + return { + background: getVar("--bg-color"), + surface: getVar("--comfy-menu-bg"), + surfaceSecondary: getVar("--comfy-menu-secondary-bg"), + inputBg: getVar("--comfy-input-bg"), + inputText: getVar("--input-text"), + textSecondary: getVar("--descrip-text"), + border: getVar("--border-color"), + accent: getVar("--comfy-accent") || "#4a90e2", + hoverBg: getVar("--content-hover-bg"), + hoverFg: getVar("--content-hover-fg"), + shadow: getVar("--bar-shadow") + }; +} + +function addDialogBoxParts() { + const styleId = 'endless-node-loader-styles'; + if (document.getElementById(styleId)) return; + + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + .dialog-container { + display: flex; + flex-direction: column; + height: 60vh; + width: 35vw; + background-color: var(--comfy-menu-bg); + color: var(--input-text); + padding: 10px; + border: 2px solid var(--border-color); + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + border-radius: 8px; + box-shadow: var(--bar-shadow, 0 4px 20px rgba(0,0,0,0.5)); + pointer-events: auto; + overflow: hidden; + box-sizing: border-box; + } + .dialog-title { + margin: 0 0 15px 0; + cursor: move; + user-select: none; + padding: 4px; + background-color: var(--comfy-menu-secondary-bg); + color: var(--input-text); + border-radius: 4px; + font-size: 14px; + border-bottom: 1px solid var(--border-color); + padding-bottom: 6px; + margin-bottom: 6px; + } + .dialog-filter-section { + flex: 0 0 auto; + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-color); + } + .filter-input-container { + position: relative; + display: flex; + gap: 8px; + align-items: center; + } + .filter-input { + flex: 1; + background-color: var(--comfy-input-bg); + color: var(--input-text); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 6px 8px; + font-size: 12px; + } + .filter-input:focus { + outline: none; + border-color: var(--comfy-accent); + } + .filter-expand-btn { + background-color: var(--comfy-input-bg); + color: var(--input-text); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + white-space: nowrap; + } + .filter-expand-btn:hover { + background-color: var(--content-hover-bg); + } + .search-history-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: var(--comfy-menu-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + max-height: 1050px; + overflow-y: auto; + z-index: 10000; + display: none; + } + .search-history-item { + padding: 6px 8px; + cursor: pointer; + font-size: 12px; + border-bottom: 1px solid var(--border-color); + } + .search-history-item:last-child { + border-bottom: none; + } + .search-history-item:hover { + background-color: var(--comfy-input-bg); + } + .node-counters { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--descrip-text); + } + .counter-selected { + color: var(--comfy-accent); + font-weight: bold; + } + .dialog-recent { + flex: 0 0 auto; + display: flex; + flex-wrap: wrap; + gap: 6px; + min-height: 30px; + max-height: 15%; + overflow-y: auto; + box-sizing: border-box; + border-bottom: 1px solid var(--border-color); + padding-bottom: 6px; + margin-bottom: 6px; + + } + .recent-chip { + display: inline-block; + background-color: var(--comfy-menu-secondary-bg); + color: var(--input-text); + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + cursor: pointer; + white-space: nowrap; + flex: 0 0 auto; + border: 1px solid transparent; + background-image: linear-gradient(rgba(128, 128, 255, 0.08), rgba(128, 128, 255, 0.08)); + } + .recent-chip:hover { + border-color: var(--comfy-accent); + background-color: var(--comfy-input-bg); + background-image: linear-gradient(rgba(128, 128, 255, 0.12), rgba(128, 128, 255, 0.12)); + transition: border-color 0.2s ease; + } + .dialog-list { + flex: 1 1 auto; + min-height: 40%; + overflow-y: auto; + box-sizing: border-box; + border-bottom: 1px solid var(--border-color); + padding-bottom: 6px; + margin-bottom: 6px; + } + .node-details { + margin-bottom: 4px; + } + + .node-details > summary { + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + list-style: none; + position: relative; + color: var(--input-text); + } + + .node-details > summary::-webkit-details-marker { + display: none; + } + + .node-details > summary::before { + content: "β–Ά"; + display: inline-block; + width: 12px; + text-align: center; + color: var(--descrip-text); + font-size: 10px; + transition: transform 0.2s ease; + margin-right: 4px; + } + + .node-details[open] > summary::before { + transform: rotate(90deg); + color: var(--input-text); + } + + .node-details > summary:hover::before { + color: var(--input-text); + } + + .node-details > summary:hover { + background-color: var(--comfy-input-bg); + border-radius: 4px; + } + + .node-details ul { + margin: 4px 0; + padding-left: 1em; + } + + .node-details li:hover { + background-color: var(--comfy-menu-secondary-bg); + border-radius: 4px; + } + + .node-details input[type="checkbox"] { + accent-color: var(--comfy-accent); + } + + .node-details button { + background-color: var(--comfy-input-bg); + color: var(--input-text); + border: 1px solid var(--border-color); + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + } + + .node-details button:hover { + background-color: var(--content-hover-bg); + } + + .node-details .select-all-btn { + background-color: var(--comfy-input-bg); + color: var(--input-text); + border: 1px solid var(--border-color); + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + background-image: linear-gradient(rgba(128, 255, 128, 0.08), rgba(128, 255, 128, 0.08)); + } + + .node-details .select-all-btn:hover { + background-color: var(--content-hover-bg); + border-color: var(--comfy-accent); + background-image: linear-gradient(rgba(128, 255, 128, 0.12), rgba(128, 255, 128, 0.12)); + transition: border-color 0.2s ease; + } + + .node-details .select-none-btn { + background-color: var(--comfy-input-bg); + color: var(--input-text); + border: 1px solid var(--border-color); + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + background-image: linear-gradient(rgba(255, 128, 128, 0.08), rgba(255, 128, 128, 0.08)); + } + + .node-details .select-none-btn:hover { + background-color: var(--content-hover-bg); + border-color: var(--comfy-accent); + background-image: linear-gradient(rgba(255, 128, 128, 0.12), rgba(255, 128, 128, 0.12)); + transition: border-color 0.2s ease; + } + + .dialog-footer { + display: flex; + justify-content: space-between; + padding: 8px; + gap: 8px; + } + .dialog-btn { + background-color: var(--comfy-input-bg); + color: var(--input-text); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + } + .dialog-btn:hover { + background-color: var(--content-hover-bg); + border-color: var(--comfy-accent); + transition: border-color 0.2s ease; + } + .dialog-btn.primary { + background-color: var(--comfy-input-bg); + color: var(--input-text); + background-image: linear-gradient(rgba(128, 255, 128, 0.08), rgba(128, 255, 128, 0.08)); + border: 1px solid rgba(128, 255, 128, 0.12); + } + .dialog-btn.primary:hover { + opacity: 0.9; + background-color: var(--content-hover-bg); + border-color: var(--comfy-accent); + transition: border-color 0.2s ease; + background-image: linear-gradient(rgba(128, 255, 128, 0.12), rgba(128, 255, 128, 0.12)); + } + + .dialog-btn.sec { + background-color: var(--comfy-input-bg); + color: var(--input-text); + background-image: linear-gradient(rgba(255, 128, 128, 0.08), rgba(255, 128, 128, 0.08)); + border: 1px solid rgba(255, 128, 128, 0.12); + } + .dialog-btn.sec:hover { + opacity: 0.9; + background-color: var(--content-hover-bg); + border-color: var(--comfy-accent); + transition: border-color 0.2s ease; + background-image: linear-gradient(rgba(255, 128, 128, 0.12), rgba(255, 128, 128, 0.12)); + } + + .hidden { + display: none !important; + } + `; + document.head.appendChild(style); +} + +// Node spawning functions +function spawnNodes(types, spacingX = 300, spacingY = 150) { + if (!window.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 ? node.size[0] : 200; + const nodeHeight = node.size ? node.size[1] : 100; + + // Use collision detection + 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); + + // Add this node's position to existing positions for next iteration + existingPositions.push({ + x: position.x, + y: position.y, + width: nodeWidth, + height: nodeHeight + }); + } else { + console.warn(`Could not create node: ${type}`); + } + }); + + // Update recently used nodes + updateRecentlyUsedNodes(spawnedNodes); + + app.graph.setDirtyCanvas(true, true); +} + +function getExistingNodePositions() { + const positions = []; + if (window.app?.graph?.nodes) { + window.app.graph.nodes.forEach(node => { + if (node.pos) { + positions.push({ + x: node.pos[0], + y: node.pos[1], + width: node.size ? node.size[0] : 200, + height: node.size ? node.size[1] : 100 + }); + } + }); + } + return positions; +} + +function findNonOverlappingPosition(startX, startY, width, height, existingPositions, spacingX, spacingY) { + const padding = 20; + let x = startX; + let y = startY; + + while (true) { + let overlaps = false; + + for (const pos of existingPositions) { + if (!(x + width + padding < pos.x || + x - padding > pos.x + pos.width || + y + height + padding < pos.y || + y - padding > pos.y + pos.height)) { + overlaps = true; + break; + } + } + + if (!overlaps) { + return { x, y }; + } + + // Move to next position + x += spacingX; + if (x > startX + spacingX * 5) { // After 5 columns, move to next row + x = startX; + y += spacingY; + } + } +} + +// Recently used nodes management +function updateRecentlyUsedNodes(newNodes) { + newNodes.forEach(nodeType => { + // Remove if already exists + const index = recentlyUsedNodes.indexOf(nodeType); + if (index > -1) { + recentlyUsedNodes.splice(index, 1); + } + // Add to beginning + recentlyUsedNodes.unshift(nodeType); + }); + + // Keep only last 15 + recentlyUsedNodes = recentlyUsedNodes.slice(0, 15); + localStorage.setItem('endlessNodeLoader_recentlyUsed', JSON.stringify(recentlyUsedNodes)); + + // Update chips if dialog is open + if (dialogInstance) { + updateRecentChips(); + } +} + +function updateRecentChips() { + const recentSection = dialogInstance.querySelector('.dialog-recent'); + if (!recentSection) return; + + recentSection.innerHTML = ''; + + recentlyUsedNodes.forEach(nodeType => { + const chip = document.createElement('button'); + chip.className = 'recent-chip'; + + // Get display name + const nodeClass = LiteGraph.registered_node_types[nodeType]; + const displayName = nodeClass?.title || nodeClass?.name || nodeType.split("/").pop(); + chip.textContent = displayName; + chip.title = nodeType; // Full type as tooltip + + chip.onclick = () => { + // Check this node type in the list + const checkbox = Array.from(dialogInstance.querySelectorAll('.node-checkbox')).find(cb => { + return cb.closest('li').dataset.nodeType === nodeType; + }); + if (checkbox) { + checkbox.checked = true; + updateSelectedCounter(); + } + }; + + recentSection.appendChild(chip); + }); +} + +// Search history management +function addToSearchHistory(searchTerm) { + if (!searchTerm.trim() || searchHistory.includes(searchTerm)) return; + + searchHistory.unshift(searchTerm); + searchHistory = searchHistory.slice(0, 15); + localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory)); +} + +function showSearchHistory(inputElement) { + const dropdown = inputElement.parentElement.querySelector('.search-history-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-history-item'; + item.textContent = term; + item.onclick = () => { + inputElement.value = term; + applyFilter(term, true); // Save this selection to history + hideSearchHistory(dropdown); + }; + dropdown.appendChild(item); + }); + + dropdown.style.display = 'block'; + + // Auto-hide after 10 seconds if no interaction + setTimeout(() => { + if (dropdown.style.display === 'block') { + hideSearchHistory(dropdown); + } + }, 10000); +} + +function hideSearchHistory(dropdown) { + dropdown.style.display = 'none'; +} + +// Filter functionality +function applyFilter(filterText, saveToHistory = true) { + currentFilter = filterText.toLowerCase(); + const nodeListSection = dialogInstance.querySelector('.dialog-list'); + + if (!currentFilter) { + // Show all + nodeListSection.querySelectorAll('.node-details, .node-details li').forEach(el => { + el.classList.remove('hidden'); + }); + updateTotalCounter(); + return; + } + + // Filter logic + nodeListSection.querySelectorAll('.node-details').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'); + } + }); + + // Show category if it matches or has matching nodes + if (categoryMatches || hasMatchingNodes) { + details.classList.remove('hidden'); + if (hasMatchingNodes && !categoryMatches) { + details.open = true; // Auto-expand if contains matches + } + } else { + details.classList.add('hidden'); + } + }); + + updateTotalCounter(); +} + +// Counter management +function updateSelectedCounter() { + const counter = dialogInstance.querySelector('.counter-selected'); + if (!counter) return; + + const checkedBoxes = dialogInstance.querySelectorAll('.node-checkbox:checked'); + counter.textContent = `Selected: ${checkedBoxes.length}`; +} + +function updateTotalCounter() { + const counter = dialogInstance.querySelector('.counter-total'); + if (!counter) return; + + const visibleNodes = dialogInstance.querySelectorAll('.node-details li:not(.hidden)'); + const totalNodes = allNodesData.length; + counter.textContent = `Total: ${visibleNodes.length}/${totalNodes}`; +} + +// Expand/collapse functionality +function toggleAllCategories(expand) { + const details = dialogInstance.querySelectorAll('.node-details:not(.hidden)'); + details.forEach(detail => { + detail.open = expand; + }); +} + +// Build node list and hierarchy // + +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 = 0; + count += categoryObj._nodes ? categoryObj._nodes.length : 0; + if (categoryObj._subcategories) { + Object.values(categoryObj._subcategories).forEach(sub => { + count += countNodesInCategory(sub); + }); + } + return count; +} + +function selectAllInCategory(categoryDetails, select = true) { + const items = categoryDetails.querySelectorAll("input[type='checkbox']"); + items.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.open = false; + details.className = "node-details"; + 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 = "select-all-btn"; + selectAllBtn.onclick = (e) => { + e.stopPropagation(); + selectAllInCategory(details, true); + }; + + const selectNoneBtn = document.createElement("button"); + selectNoneBtn.textContent = "None"; + selectNoneBtn.className = "select-none-btn"; + 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"); + list.className = "node-list"; + + (obj._nodes || []).forEach(node => { + const li = document.createElement("li"); + li.dataset.nodeType = node.type; // Store node type for filtering + + 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 = []; + dialogInstance.querySelectorAll('.node-checkbox:checked').forEach(checkbox => { + const nodeType = checkbox.closest('li').dataset.nodeType; + if (nodeType) selected.push(nodeType); + }); + return selected; +} + +function clearSelectedNodes() { + dialogInstance.querySelectorAll('.node-checkbox:checked').forEach(checkbox => { + checkbox.checked = false; + }); + updateSelectedCounter(); +} + +function showNodeLoaderDialog() { + //Overall box // + if (dialogInstance) return; + addDialogBoxParts(); + + const container = document.createElement("div"); + container.className = "dialog-container"; + const theme = getComfyUIThemeColors(); + +// ESC key to close + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + closeDialog(); + } + }; + + const closeDialog = () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.removeChild(container); + dialogInstance = null; + }; + +document.addEventListener('keydown', handleKeyDown); + + //Title Section // + const title = document.createElement("h3"); + title.textContent = "Endless 🌊✨ Node Loader"; + title.className = "dialog-title"; + container.appendChild(title); + + title.addEventListener("mousedown", (e) => { + isDragging = true; + container.style.transform = "none"; // 🚨 Disable center alignment during drag + const rect = container.getBoundingClientRect(); + dragOffset.x = e.clientX - rect.left; + dragOffset.y = e.clientY - rect.top; + document.addEventListener("mousemove", handleDrag); + document.body.style.userSelect = "none"; + document.addEventListener("mouseup", handleDragEnd); + e.preventDefault(); + }); + + // Filter Section + const filterSection = document.createElement("div"); + filterSection.className = "dialog-filter-section"; + + const filterInputContainer = document.createElement("div"); + filterInputContainer.className = "filter-input-container"; + + const filterInput = document.createElement("input"); + filterInput.type = "text"; + filterInput.className = "filter-input"; + filterInput.placeholder = "Filter nodes..."; + + const searchHistoryDropdown = document.createElement("div"); + searchHistoryDropdown.className = "search-history-dropdown"; + + filterInput.oninput = (e) => applyFilter(e.target.value, false); // Don't save to history on input + + filterInput.onkeydown = (e) => { + if (e.key === 'Enter' && e.target.value.trim()) { + addToSearchHistory(e.target.value.trim()); + hideSearchHistory(searchHistoryDropdown); + } else if (e.key === 'ArrowDown' && searchHistory.length > 0) { + // Show history when user presses down arrow + showSearchHistory(filterInput); + e.preventDefault(); + } + }; + + filterInput.onfocus = (e) => { + // Only show history if input is empty + if (!e.target.value.trim()) { + showSearchHistory(filterInput); + } + }; + + filterInput.onblur = (e) => { + // Save to history when leaving the field (if it has content) + if (e.target.value.trim()) { + addToSearchHistory(e.target.value.trim()); + } + // Clear any pending hover timeout + if (hoverTimeout) { + clearTimeout(hoverTimeout); + hoverTimeout = null; + } + // Delay hiding to allow clicks on dropdown items + setTimeout(() => hideSearchHistory(searchHistoryDropdown), 150); + }; + + filterInput.onmouseenter = () => { + // Clear auto-hide timer when mouse enters + if (searchTimeout) { + clearTimeout(searchTimeout); + searchTimeout = null; + } + + // Start hover timer - show history after 1 second of hovering + hoverTimeout = setTimeout(() => { + if (searchHistory.length > 0) { + showSearchHistory(filterInput); + } + }, 1000); + }; + + filterInput.onmouseleave = (e) => { + // Clear hover timeout when mouse leaves + if (hoverTimeout) { + clearTimeout(hoverTimeout); + hoverTimeout = null; + } + + // Start auto-hide timer for the dropdown + searchTimeout = setTimeout(() => { + hideSearchHistory(searchHistoryDropdown); + }, 10000); + }; + + const expandBtn = document.createElement("button"); + expandBtn.className = "filter-expand-btn"; + expandBtn.textContent = "Expand All"; + let isExpanded = false; + expandBtn.onclick = () => { + isExpanded = !isExpanded; + toggleAllCategories(isExpanded); + expandBtn.textContent = isExpanded ? "Collapse All" : "Expand All"; + }; + + filterInputContainer.appendChild(filterInput); + filterInputContainer.appendChild(searchHistoryDropdown); + filterInputContainer.appendChild(expandBtn); + + const nodeCounters = document.createElement("div"); + nodeCounters.className = "node-counters"; + nodeCounters.innerHTML = ` + Selected: 0 + Total: 0 + `; + + filterSection.appendChild(filterInputContainer); + filterSection.appendChild(nodeCounters); + container.appendChild(filterSection); + + // Recent Section // + const recentSection = document.createElement("div"); + recentSection.className = "dialog-recent"; + container.appendChild(recentSection); + + // Node Section // + const nodeListSection = document.createElement("div"); + nodeListSection.className = "dialog-list"; + + 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; // Store for filtering + + const hierarchy = buildHierarchy(nodes); + const tree = renderCategory(hierarchy); + tree.forEach(section => { + nodeListSection.appendChild(section); + }); + container.appendChild(nodeListSection); + +// Footer Section // +const footerSection = document.createElement("div"); +footerSection.className = "dialog-footer"; + +const leftButtons = document.createElement("div"); +leftButtons.style.display = "flex"; +leftButtons.style.gap = "8px"; + +const spawnBtn = document.createElement("button"); +spawnBtn.textContent = "Spawn Nodes"; +spawnBtn.className = "dialog-btn primary"; +spawnBtn.onclick = () => { + const selectedTypes = getSelectedNodeTypes(); + if (selectedTypes.length === 0) { + alert("Please select at least one node to spawn."); + return; + } + spawnNodes(selectedTypes); + closeDialog(); // Use the shared closeDialog function +}; + +const clearBtn = document.createElement("button"); +clearBtn.textContent = "Clear Selected"; +clearBtn.className = "dialog-btn"; +clearBtn.onclick = clearSelectedNodes; + +const cancelBtn = document.createElement("button"); +cancelBtn.textContent = "Cancel"; +cancelBtn.className = "dialog-btn sec"; +cancelBtn.onclick = closeDialog; // Use the shared closeDialog function + +leftButtons.appendChild(spawnBtn); +leftButtons.appendChild(clearBtn); + +const rightButtons = document.createElement("div"); +rightButtons.style.display = "flex"; +rightButtons.style.gap = "8px"; + +const clearHistoryBtn = document.createElement("button"); +clearHistoryBtn.textContent = "Clear History"; +clearHistoryBtn.className = "dialog-btn"; +clearHistoryBtn.onclick = () => { + searchHistory = []; + localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory)); + // Hide dropdown if it's currently showing + const dropdown = dialogInstance.querySelector('.search-history-dropdown'); + if (dropdown) dropdown.style.display = 'none'; +}; + +rightButtons.appendChild(clearHistoryBtn); +rightButtons.appendChild(cancelBtn); + +footerSection.appendChild(leftButtons); +footerSection.appendChild(rightButtons); +container.appendChild(footerSection); + + document.body.appendChild(container); + dialogInstance = container; + + // Initialize + updateRecentChips(); + updateSelectedCounter(); + updateTotalCounter(); + + // Focus filter input + filterInput.focus(); +} + +registerEndlessTool("Node Loader", showNodeLoaderDialog); \ No newline at end of file diff --git a/web/endless_ui_helpers.js b/web/endless_ui_helpers.js new file mode 100644 index 0000000..11a83a1 --- /dev/null +++ b/web/endless_ui_helpers.js @@ -0,0 +1,164 @@ +// === 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.background}; + color: ${colors.text}; + padding: 12px; + border: 1px solid ${colors.accent}; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + z-index: 99999; + transition: opacity 0.2s ease; + opacity: 1; + min-width: 180px; + `; + + const dragBar = document.createElement("div"); + dragBar.textContent = "β†’ Drag Me With This ←"; + dragBar.style.cssText = ` + padding: 4px; + background: rgba(255, 255, 255, 0.05); + cursor: move; + font-size: 12px; + text-align: center; + user-select: none; + border-bottom: 1px solid ${colors.border}; + `; + menu.appendChild(dragBar); + + endlessToolsRegistry.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 = "rgba(255, 255, 255, 0.1)"; + btn.onmouseout = () => btn.style.background = "transparent"; + btn.onclick = () => { + tool.callback(); + menu.remove(); + }; + menu.appendChild(btn); + }); + + makeDraggable(menu, dragBar); + document.body.appendChild(menu); +} + +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() { + let toolbar = document.querySelector('.comfyui-menu, .comfy-menu, [class*="menu"], [class*="toolbar"]'); + 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); + } + if (!toolbar) { + const allElements = document.querySelectorAll('*'); + toolbar = Array.from(allElements).find(el => { + const buttons = el.querySelectorAll('button'); + return buttons.length >= 2 && buttons.length <= 10; + }); + } + if (!toolbar) { + toolbar = Array.from(document.querySelectorAll(".comfyui-button-group")).find(div => + Array.from(div.querySelectorAll("button")).some(btn => btn.title === "Share") + ); + } + return toolbar; +} + +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', + accent: computedStyle.getPropertyValue('--comfy-accent-color') || '#5fc0e9' // fallback soft blue + }; +} + +function makeDraggable(element, handle = element) { + let offsetX = 0, offsetY = 0, isDown = false; + + handle.onmousedown = (e) => { + isDown = true; + const rect = element.getBoundingClientRect(); + offsetX = e.clientX - rect.left; + offsetY = e.clientY - rect.top; + element.style.cursor = 'move'; + element.style.position = 'fixed'; + element.style.right = 'auto'; + + 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; + }; + }; +} +