From a884d519774195ba07829c0517019bd9f838b759 Mon Sep 17 00:00:00 2001 From: "Alexander G. Morano" Date: Sun, 2 Mar 2025 04:15:25 -0500 Subject: [PATCH] first pass --- .gitattributes | 2 + .gitignore | 24 ++ LICENSE | 23 ++ NOTICE | 20 ++ README.md | 72 +++++- __init__.py | 24 ++ pyproject.toml | 29 +++ requirements.txt | 0 web/core_color.js | 639 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 831 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 __init__.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 web/core_color.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7288237 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Python-specific +__pycache__/ +*.pyc + +# Virtual environments +.env/ +venv/ + +# IDE files +.idea/ +.vscode/ +*.code-workspace + +# Build artifacts +build/ +dist/ +*.egg-info/ + +# Other +.DS_Store + +# user junk +_support/ +.coverage \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f5db99a --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2025 Alexander G. Morano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +GO NUTS; JUST TRY NOT TO DO IT IN YOUR HEAD. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..4e0c6d5 --- /dev/null +++ b/NOTICE @@ -0,0 +1,20 @@ +This project includes code concepts from the MTB Nodes project (MIT) +https://github.com/melMass/comfy_mtb + +This project includes code concepts from the ComfyUI-Custom-Scripts project (MIT) +https://github.com/pythongosssss/ComfyUI-Custom-Scripts + +This project includes code concepts from the KJNodes for ComfyUI project (GPL 3.0) +https://github.com/kijai/ComfyUI-KJNodes + +This project includes code concepts from the UE Nodes project (Apache 2.0) +https://github.com/chrisgoringe/cg-use-everywhere + +This project includes code concepts from the WAS Node Suite project (MIT) +https://github.com/WASasquatch/was-node-suite-comfyui + +This project includes code concepts from the rgthree-comfy project (MIT) +https://github.com/rgthree/rgthree-comfy + +This project includes code concepts from the FizzNodes project (MIT) +https://github.com/FizzleDorf/ComfyUI_FizzNodes \ No newline at end of file diff --git a/README.md b/README.md index c351a51..98ac212 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# Jovi_Colorizer -Colorize nodes in comfy with custom defaults per node, node category or through regex filtering +
+ + + + Colorize ComfyUI nodes with defaults per node, node category or via regex filtering + + +
+ +
+ +COMFYUI Colorize ComfyUI nodes with defaults per node, node category or via regex filtering + +
+ +
+ +![KNIVES!](https://badgen.net/github/open-issues/Amorano/Jovi_Colorizer) +![FORKS!](https://badgen.net/github/forks/Amorano/Jovi_Colorizer) + +
+ + + +# SPONSORSHIP + +Please consider sponsoring me if you enjoy the results of my work, code or documentation or otherwise. A good way to keep code development open and free is through sponsorship. + +
+ + | | |  +-|-|-|- +[![BE A GITHUB SPONSOR ❤️](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/Amorano) | [![DIRECTLY SUPPORT ME VIA PAYPAL](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/onarom) | [![PATREON SUPPORTER](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/joviex) | [![SUPPORT ME ON KO-FI!](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/alexandermorano) +
+ +## HIGHLIGHTS + +
+Jovi Streaming Node Family +
+ +* + +## UPDATES + +**2025/03/02** @1.0.0: +* intial breakout from Jovimetrix + +# INSTALLATION + +## COMFYUI MANAGER + +If you have [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) installed, simply search for Jovi_Colorizer and install from the manager's database. + +## MANUAL INSTALL +Clone the repository into your ComfyUI custom_nodes directory. You can clone the repository with the command: +``` +git clone https://github.com/Amorano/Jovi_Colorizer.git +``` +You can then install the requirements by using the command: +``` +.\python_embed\python.exe -s -m pip install -r requirements.txt +``` +If you are using a virtual environment (venv), make sure it is activated before installation. Then install the requirements with the command: +``` +pip install -r requirements.txt +``` +# WHERE TO FIND ME + +You can find me on [![DISCORD](https://dcbadge.vercel.app/api/server/62TJaZ3Z5r?style=flat-square)](https://discord.gg/62TJaZ3Z5r). diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..db65705 --- /dev/null +++ b/__init__.py @@ -0,0 +1,24 @@ +""" + ██╗ ██████╗ ██╗ ██╗██╗ + ██║██╔═══██╗██║ ██║██║ + ██║██║ ██║██║ ██║██║ + ██ ██║██║ ██║╚██╗ ██╔╝██║ + ╚█████╔╝╚██████╔╝ ╚████╔╝ ██║ + ╚════╝ ╚═════╝ ╚═══╝ ╚═╝ + + ██████╗ ██████╗ ██╗ ██████╗ ██████╗ ██╗███████╗███████╗██████╗ +██╔════╝██╔═══██╗██║ ██╔═══██╗██╔══██╗██║╚══███╔╝██╔════╝██╔══██╗ +██║ ██║ ██║██║ ██║ ██║██████╔╝██║ ███╔╝ █████╗ ██████╔╝ +██║ ██║ ██║██║ ██║ ██║██╔══██╗██║ ███╔╝ ██╔══╝ ██╔══██╗ +╚██████╗╚██████╔╝███████╗╚██████╔╝██║ ██║██║███████╗███████╗██║ ██║ + ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ + +Colorize ComfyUI nodes with defaults per node, node category or via regex filtering + http://www.github.com/amorano/Jovi_Colorizer +""" + +__author__ = "Alexander G. Morano" +__email__ = "amorano@gmail.com" + +WEB_DIRECTORY = "./web" +NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = {}, {} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fc3df21 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "jovi_colorizer" +description = "Colorize ComfyUI nodes with defaults per node, node category or via regex filtering." +version = "1.0.0" +license = { file = "LICENSE" } +readme = "README.md" +authors = [{ name = "Alexander G. Morano", email = "amorano@gmail.com" }] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Intended Audience :: Developers", +] +requires-python = ">=3.11" +dependencies = [] + +[project.urls] +Homepage = "https://github.com/Amorano/Jovi_Colorizer" +Documentation = "https://github.com/Amorano/Jovi_Colorizer/wiki" +Repository = "https://github.com/Amorano/Jovi_Colorizer" +Issues = "https://github.com/Amorano/Jovi_Colorizer/issues" + +[tool.comfy] +PublisherId = "amorano" +DisplayName = "Jovi Colorizer" +Icon = "https://raw.githubusercontent.com/Amorano/Jovimetrix-examples/refs/heads/master/res/logo-jovi_colorizer.png" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/web/core_color.js b/web/core_color.js new file mode 100644 index 0000000..2690ade --- /dev/null +++ b/web/core_color.js @@ -0,0 +1,639 @@ +/** + * File: core_color.js + */ + +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js" +import { $el } from "../../scripts/ui.js"; + +const DEFAULT_THEME = { + "JOV_CAPTURE 📸": { title: "#661f66" }, + "JOV_GL 🌈": { title: "#1f661f" }, + "JOV_MEASURE 📐": { title: "#993838" }, + "JOV_MIDI 🎛️": { title: "#66661f" }, + "JOV_SPOUT 📺": { title: "#1f6666" }, + "JOVIMETRIX 🔺🟩🔵": { title: "#a23da2" }, + "JOVIMETRIX 🔺🟩🔵/CREATE": { title: "#1b871b" }, + "JOVIMETRIX 🔺🟩🔵/COMPOSE": { title: "#5c1f9a" }, + "JOVIMETRIX 🔺🟩🔵/CALC": { title: "#993838" }, + "JOVIMETRIX 🔺🟩🔵/DEVICE": { title: "#1f9999" }, + "JOVIMETRIX 🔺🟩🔵/UTILITY": { title: "#0a0a0a" } +}; + +const SETTING_REGEX = 'jovi.color.regex'; +const SETTING_THEME = 'jovi.color.theme'; + +let PANEL_COLORIZE, NODE_LIST; + +function normalizeHex(hex) { + if (!hex.startsWith('#')) { + hex = '#' + hex; + } + + // If shorthand (e.g., #333), expand it to full form (#333333) + if (hex.length === 4) { + hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]; + } + return hex.toLowerCase(); +} + +function colorHex2RGB(hex) { + hex = hex.replace(/^#/, ''); + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return [r, g, b]; +} + +function colorContrast(hexColor) { + const rgb = colorHex2RGB(hexColor); + const normalizedRGB = rgb.map(value => value / 255); + const L = 0.2126 * normalizedRGB[0] + 0.7152 * normalizedRGB[1] + 0.0722 * normalizedRGB[2]; + return L > 0.420 ? "#000" : "#CCC"; +} + +function getColor(node) { + // regex overrides first + const CONFIG_REGEX = app.extensionManager.setting.get(SETTING_REGEX); + if (Array.isArray(CONFIG_REGEX)) { + for (const { regex, ...colors } of CONFIG_REGEX) { + if (regex && node.type.match(new RegExp(regex, "i"))) { + return colors; + } + } + } + + // explicit color set first... + const CONFIG_THEME = app.extensionManager.setting.get(SETTING_THEME); + const newColor = CONFIG_THEME?.[node.type] + ?? (function() { + const color = NODE_LIST[node.type]; + let k = color?.category; + while (k) { + if (CONFIG_THEME?.[k]) { + return CONFIG_THEME[k]; + } + k = k.substring(0, k.lastIndexOf("/")); + } + })(); + + return newColor; +} + +const origDrawNode = LGraphCanvas.prototype.drawNode; +LGraphCanvas.prototype.drawNode = function (node, ctx) { + // STASH THE CURRENT COLOR STATE + const origTitle = node.constructor.title_text_color; + const origSelectedTitleColor = LiteGraph.NODE_SELECTED_TITLE_COLOR; + const origNodeTextColor = LiteGraph.NODE_TEXT_COLOR; + const origWidgetSecondaryTextColor = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + const origWidgetTextColor = LiteGraph.WIDGET_TEXT_COLOR; + const origNodeTitleColor = LiteGraph.NODE_TITLE_COLOR; + const origWidgetBGColor = LiteGraph.WIDGET_BGCOLOR; + + const new_color = getColor(node); + + if (new_color) { + // Title text when node is selected + //LiteGraph.NODE_SELECTED_TITLE_COLOR = '#FF00FF' + + if (new_color?.text) { + node.constructor.title_text_color = new_color.text; + } else { + const color = node.constructor.title_text_color; + if (color) { + node.constructor.title_text_color = colorContrast(color); + } + } + + // Slot label text + //LiteGraph.NODE_TEXT_COLOR = '#7777FF' + + // Widget Text + //LiteGraph.WIDGET_SECONDARY_TEXT_COLOR = "#FFFFFF" + + // Widget controls + field text + //LiteGraph.WIDGET_TEXT_COLOR = '#FF0000'; + + // Widget control BG color + // LiteGraph.WIDGET_BGCOLOR + + // node's title bar background color + if (new_color?.title) { + node.color = new_color.title; + } + + // node's body background color + if (new_color?.body) { + node.bgcolor = new_color.body; + } + } + + const res = origDrawNode.apply(this, arguments); + + // Default back to last pushed state ComfyUI colors + if (new_color) { + node.constructor.title_text_color = origTitle; + LiteGraph.NODE_SELECTED_TITLE_COLOR = origSelectedTitleColor; + LiteGraph.NODE_TEXT_COLOR = origNodeTextColor; + LiteGraph.WIDGET_SECONDARY_TEXT_COLOR = origWidgetSecondaryTextColor; + LiteGraph.WIDGET_TEXT_COLOR = origWidgetTextColor; + LiteGraph.NODE_TITLE_COLOR = origNodeTitleColor; + LiteGraph.WIDGET_BGCOLOR = origWidgetBGColor; + } + + return res; +} + +function applyTheme(theme) { + const majorElements = document.querySelectorAll('.jov-panel-color-cat_major'); + const minorElements = document.querySelectorAll('.jov-panel-color-cat_minor'); + majorElements.forEach(el => el.classList.remove('light', 'dark')); + minorElements.forEach(el => el.classList.remove('light', 'dark')); + majorElements.forEach(el => el.classList.add(theme)); + minorElements.forEach(el => el.classList.add(theme)); +} + +class JovimetrixPanelColorize { + constructor() { + this.content = null; + this.picker = null; + this.pickerWrapper = null; + this.buttonCurrent = null; + this.recentColors = []; + this.title_content = "HI!" + this.searchInput = null; + this.tbody = null; + } + + createSearchInput() { + this.searchInput = $el("input", { + type: "text", + placeholder: "Filter nodes...", + className: "jov-search-input", + oninput: (e) => this.filterItems(e.target.value) + }); + return this.searchInput; + } + + filterItems(searchTerm) { + if (!this.tbody) return; + + const searchLower = searchTerm.toLowerCase(); + const rows = this.tbody.querySelectorAll('tr'); + + rows.forEach(row => { + const nameCell = row.querySelector('td:last-child'); + if (!nameCell) return; + + const text = nameCell.textContent.toLowerCase(); + const categoryMatch = row.classList.contains('jov-panel-color-cat_major') || + row.classList.contains('jov-panel-color-cat_minor'); + + // Show categories if they or their children match + if (categoryMatch) { + const siblingRows = this.getNextSiblingRowsUntilCategory(row); + const hasVisibleChildren = siblingRows.some(sibling => { + const siblingText = sibling.querySelector('td:last-child')?.textContent.toLowerCase() || ''; + return siblingText.includes(searchLower); + }); + + row.style.display = hasVisibleChildren || text.includes(searchLower) ? '' : 'none'; + } else { + row.style.display = text.includes(searchLower) ? '' : 'none'; + } + }); + } + + getNextSiblingRowsUntilCategory(categoryRow) { + const siblings = []; + let currentRow = categoryRow.nextElementSibling; + + while (currentRow && + !currentRow.classList.contains('jov-panel-color-cat_major') && + !currentRow.classList.contains('jov-panel-color-cat_minor')) { + siblings.push(currentRow); + currentRow = currentRow.nextElementSibling; + } + + return siblings; + } + + createColorButton(type, name, color, idx) { + const label = type == "title" ? "T" : type == "body" ? "B" : "X"; + const button = $el('button.color-button', { + style: { backgroundColor: color }, + dataset: { + type: type, + name: name, + color: color, + colorOld: color, + idx: idx + }, + value: label, + content: label, + textContent: label, + label: label + }); + + button.addEventListener('mousedown', (event) => { + event.stopPropagation(); + if (this.buttonCurrent) { + this.buttonCurrent.dataset.colorOld = normalizeHex(this.buttonCurrent.dataset.color); + } + this.buttonCurrent = event.target; + this.showPicker(event.target); + }); + + return button; + } + + async updateConfig() { + const cb = this.buttonCurrent.dataset; + const color = normalizeHex(cb.color) + if (cb.idx && cb.idx !== "undefined") { + const CONFIG_REGEX = app.extensionManager.setting.get(SETTING_REGEX); + CONFIG_REGEX[cb.idx][cb.type] = color; + await app.extensionManager.setting.set(SETTING_REGEX, CONFIG_REGEX); + } else { + const CONFIG_THEME = app.extensionManager.setting.get(SETTING_THEME); + CONFIG_THEME[cb.name] = CONFIG_THEME[cb.name] || (CONFIG_THEME[cb.name] = {}); + + let colorCheck = LiteGraph.NODE_DEFAULT_BGCOLOR; + if (cb.type === "title") { + colorCheck = LiteGraph.NODE_DEFAULT_COLOR; + } else if (cb.type === "text") { + colorCheck = LiteGraph.NODE_TEXT_COLOR; + } + colorCheck = normalizeHex(colorCheck); + if (color === colorCheck && CONFIG_THEME[cb.name] && CONFIG_THEME[cb.name].hasOwnProperty(cb.type)) { + delete CONFIG_THEME[cb.name][cb.type]; + } else if (color !== colorCheck) { + CONFIG_THEME[cb.name][cb.type] = color; + } + await app.extensionManager.setting.set(SETTING_THEME, CONFIG_THEME); + } + + this.buttonCurrent.style.backgroundColor = color; + app.canvas.setDirty(true); + } + + async pickerCancel() { + this.buttonCurrent.dataset.colorOld = normalizeHex(this.buttonCurrent.dataset.colorOld); + this.buttonCurrent.dataset.color = this.buttonCurrent.dataset.colorOld; + this.pickerWrapper.style.display = 'none'; + await this.updateConfig(); + this.buttonCurrent = null; + } + + async pickerReset() { + let colorCheck = LiteGraph.NODE_DEFAULT_BGCOLOR; + if (this.buttonCurrent.dataset.type === "title") { + colorCheck = LiteGraph.NODE_DEFAULT_COLOR; + } else if (this.buttonCurrent.dataset.type === "text") { + colorCheck = LiteGraph.NODE_TEXT_COLOR; + } + this.buttonCurrent.dataset.color = colorCheck; + await this.updateConfig(); + await this.picker.color.set(colorCheck); + } + + async pickerColorChange(color) { + this.buttonCurrent.style.backgroundColor = color; + this.buttonCurrent.dataset.color = color; + await this.updateConfig(); + } + + showPicker(button) { + if (!this.picker) { + + this.pickerWrapper = $el('div.picker-wrapper', { + style: { + position: 'absolute', + zIndex: '9999', + backgroundColor: '#fff', + padding: '5px', + borderRadius: '5px', + boxShadow: '0 0 10px rgba(0,0,0,0.2)', + display: 'none' + } + }); + + const pickerElement = $el('div.picker'); + const recentColorsElement = $el('div.recent-colors'); + const buttonWrapper = $el('div.button-wrapper', { + style: { + display: 'flex', + justifyContent: 'space-between', + marginTop: '10px' + } + }); + const cancelButton = $el('button', { + textContent: 'Cancel', + onclick: async () => this.pickerCancel() + }); + + const resetButton = $el('button', { + textContent: 'Reset', + onclick: async () => this.pickerReset() + }); + + buttonWrapper.appendChild(cancelButton); + buttonWrapper.appendChild(resetButton); + + this.pickerWrapper.appendChild(pickerElement); + this.pickerWrapper.appendChild(recentColorsElement); + this.pickerWrapper.appendChild(buttonWrapper); + + document.body.appendChild(this.pickerWrapper); + + this.picker = new iro.ColorPicker(pickerElement, { + width: 200, + color: '#ffffff', + display: 'block', + layout: [ + { + component: iro.ui.Slider, + options: { sliderType: 'hue' } + }, + { + component: iro.ui.Slider, + options: { sliderType: 'value' } + }, + { + component: iro.ui.Slider, + options: { sliderType: 'saturation' } + }, + ] + }); + + this.picker.on('color:change', async (color) => this.pickerColorChange(color.hexString)); + } + + this.picker.color.set(button.dataset.color || '#ffffff'); + const buttonRect = button.getBoundingClientRect(); + const pickerRect = this.pickerWrapper.getBoundingClientRect(); + + let left = buttonRect.left; + let top = buttonRect.bottom + 5; + + if (left + pickerRect.width > window.innerWidth) { + left = window.innerWidth - pickerRect.width - 5; + } + + if (top + pickerRect.height > window.innerHeight) { + top = buttonRect.top - pickerRect.height - 5; + } + + this.pickerWrapper.style.left = `${left}px`; + this.pickerWrapper.style.top = `${top}px`; + this.pickerWrapper.style.display = 'block'; + this.updateRecentColors(button); + } + + updateRecentColors(button) { + const color = button.dataset.color; + if (!this.recentColors.includes(color)) { + this.recentColors.unshift(color); + if (this.recentColors.length > 5) { + this.recentColors.pop(); + } + } + + const recentColorsElement = this.pickerWrapper.querySelector('.recent-colors'); + recentColorsElement.innerHTML = ''; + this.recentColors.forEach(recentColor => { + const colorSwatch = $el('div', { + style: { + width: '20px', + height: '20px', + backgroundColor: recentColor, + display: 'inline-block', + margin: '2px', + cursor: 'pointer' + }, + onclick: () => this.picker.color.set(recentColor) + }); + recentColorsElement.appendChild(colorSwatch); + }); + } + + templateColorRow(data, type, classList = "jov-panel-color-category") { + const titleColor = data.title || LiteGraph.NODE_DEFAULT_COLOR; + const bodyColor = data.body || LiteGraph.NODE_DEFAULT_BGCOLOR; + const textColor = data.text || LiteGraph.NODE_TEXT_COLOR; + + // Determine background color based on class + let rowClass = classList; + let style = {}; + + if (classList === "jov-panel-color-cat_major") { + // Darker background for major categories + style.backgroundColor = "var(--border-color)"; + } else if (classList === "jov-panel-color-cat_minor") { + style.backgroundColor = "var(--tr-odd-bg-color)"; + } + + const element = $el("tr", { className: rowClass, style }, [ + $el("td", {}, [this.createColorButton("title", data.name, titleColor, data.idx)]), + $el("td", {}, [this.createColorButton("body", data.name, bodyColor, data.idx)]), + $el("td", {}, [this.createColorButton("text", data.name, textColor, data.idx)]), + (type === "regex") ? $el("td", [ + $el("input", { + value: data.name + }) + ]) + : $el("td", { textContent: data.name }) + ]); + + return element; + } + + createRegexPalettes() { + const table = $el("table.flexible-table"); + this.tbody = $el("tbody"); + + let CONFIG_REGEX = app.extensionManager.setting.get(SETTING_REGEX) || []; + if (!Array.isArray(CONFIG_REGEX)) { + CONFIG_REGEX = [] + } + CONFIG_REGEX.forEach((entry, idx) => { + const data = { + idx: idx, + name: entry.regex, + title: entry.title || LiteGraph.NODE_TITLE_COLOR, + body: entry.body || LiteGraph.WIDGET_BGCOLOR, + text: entry.body || LiteGraph.NODE_TEXT_COLOR + }; + this.tbody.appendChild(this.templateColorRow(data, "regex")); + }); + + table.appendChild(this.tbody); + return table; + } + + createColorPalettes() { + let background_index = 0; + const categories = []; + const CONFIG_THEME = app.extensionManager.setting.get(SETTING_THEME); + + Object.entries(NODE_LIST).forEach(([nodeName, node]) => { + const category = node.category; + const majorCategory = category.split("/")?.[0]; + + if (!categories.includes(majorCategory)) { + background_index = (background_index + 1) % 2; + const element = { + name: majorCategory, + title: CONFIG_THEME?.[majorCategory]?.title, + body: CONFIG_THEME?.[majorCategory]?.body, + text: CONFIG_THEME?.[majorCategory]?.text + }; + this.tbody.appendChild(this.templateColorRow(element, null, "jov-panel-color-cat_major")); + categories.push(majorCategory); + } + + if (!categories.includes(category)) { + background_index = (background_index + 1) % 2; + const element = { + name: category, + title: CONFIG_THEME?.[category]?.title, + body: CONFIG_THEME?.[category]?.body, + text: CONFIG_THEME?.[category]?.text + }; + this.tbody.appendChild(this.templateColorRow(element, null, "jov-panel-color-cat_minor")); + categories.push(category); + } + + const nodeConfig = CONFIG_THEME[nodeName] || {}; + const data = { + name: nodeName, + title: nodeConfig.title, + body: nodeConfig.body, + text: nodeConfig.text + }; + this.tbody.appendChild(this.templateColorRow(data)); + }); + } + + getRandomTitle() { + const TITLES = [ + "COLOR CONFIGURATION", "COLOR CALIBRATION", "COLOR CUSTOMIZATION", + "CHROMA CALIBRATION", "CHROMA CONFIGURATION", "CHROMA CUSTOMIZATION", + "CHROMATIC CALIBRATION", "CHROMATIC CONFIGURATION", "CHROMATIC CUSTOMIZATION", + "HUE HOMESTEAD", "PALETTE PREFERENCES", "PALETTE PERSONALIZATION", + "PALETTE PICKER", "PIGMENT PREFERENCES", "PIGMENT PERSONALIZATION", + "PIGMENT PICKER", "SPECTRUM STYLING", "TINT TAILORING", "TINT TWEAKING" + ]; + return TITLES[Math.floor(Math.random() * TITLES.length)]; + } + + createContent() { + if (!this.content) { + const table = this.createRegexPalettes(); + this.createColorPalettes(); + + this.title_content = $el("div.jov-title-header", { textContent: "EMPTY" }); + this.content = $el("div.jov-panel-color", [ + $el("div.jov-title", [this.title_content]), + this.createSearchInput(), // Add search input + $el("div.jov-config-color", [table]), + $el("div.button", []), + ]); + + // hide the picker when clicking outside + document.addEventListener('click', async (event) => { + if (this.picker && !this.pickerWrapper.contains(event.target) && !event.target.classList.contains('color-button')) { + this.pickerWrapper.style.display = 'none'; + if (this.buttonCurrent) { + this.buttonCurrent.dataset.colorOld = normalizeHex(this.buttonCurrent.dataset.color); + } + } + }); + } + applyTheme('light'); + this.title_content.textContent = this.getRandomTitle(); + return this.content; + } +} + +app.extensionManager.registerSidebarTab({ + id: "jovimetrix.sidebar.colorizer", + icon: "pi pi-palette", + title: "JOVIMETRIX COLORIZER 🔺🟩🔵", + tooltip: "Color node title and body via unique name, group and regex filtering\nJOVIMETRIX 🔺🟩🔵", + type: "custom", + render: async (el) => { + el.innerHTML = ""; + if (!PANEL_COLORIZE) { + PANEL_COLORIZE = new JovimetrixPanelColorize(); + } + const content = PANEL_COLORIZE.createContent(); + el.appendChild(content); + } +}); + +app.registerExtension({ + name: "jovimetrix.color", + settings: [ + { + id: SETTING_REGEX, + name: "Regex Entries for Jovimetrix Colorizer", + type: "hidden", + defaultValue: {} + }, + { + id: SETTING_THEME, + name: "Node theme entries for Jovimetrix Colorizer", + type: "hidden", + defaultValue: {} + }, + ], + async init() { + document.head.appendChild(Object.assign(document.createElement('script'), { + src: "https://cdn.jsdelivr.net/npm/@jaames/iro@5" + })); + }, + async setup() { + + var response = await api.fetchApi("/object_info", { cache: "no-store" }); + const all_nodes = await response.json(); + + NODE_LIST = Object.entries(all_nodes).sort((a, b) => { + const categoryA = a[1].category.toLowerCase(); + const categoryB = b[1].category.toLowerCase(); + + // First, sort by category + if (categoryA < categoryB) return -1; + if (categoryA > categoryB) return 1; + + // If categories are equal, sort by key name + return a[0].toLowerCase().localeCompare(b[0].toLowerCase()); + }); + NODE_LIST = Object.fromEntries(NODE_LIST); + + let CONFIG_REGEX = app.extensionManager.setting.get(SETTING_REGEX) || []; + if (!Array.isArray(CONFIG_REGEX)) { + CONFIG_REGEX = []; + } + while (CONFIG_REGEX.length < 5) { + CONFIG_REGEX.push({ "regex": "" }); + } + await app.extensionManager.setting.set(SETTING_REGEX, CONFIG_REGEX); + + let CONFIG_THEME = app.extensionManager.setting.get(SETTING_THEME) || {}; + Object.keys(DEFAULT_THEME).forEach(key => { + if (!(key in CONFIG_THEME)) { + CONFIG_THEME[key] = DEFAULT_THEME[key]; + } + }); + + try { + await app.extensionManager.setting.set(SETTING_THEME, CONFIG_THEME); + console.info("wrote new Jovi_Colorizer defaults") + } catch (error) { + console.error("Failed to update settings:", error); + } + } +});