diff --git a/README.md b/README.md index e90cd72..8dfb423 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,21 @@ For what i know nobody did it, so i did it. Maybe you don't need it. I need it >.< # Updates +- 25-05-2024 + - Fixed some bugs + - Added Templates support** + - Added ability categorize custom categories + - Added preview (list of used nodes) + + - Added Workflow support! + - Added ability to rename workflows + - Added ability to remove workflows + - Added ability to categorize workflows + - Added preview (list of used nodes), (disabled by default) + +** i did not enabled the **rename** and **delete** because it's conflicing with the TemplateManager + + - 21-04-2024 - Rewritten a lot of the code to improve performance and readability, and slightly adjusted the UI to ensure compatibility with future updates. - ADDED CUSTOM CATEGORIES @@ -80,7 +95,6 @@ This feature is enabled by default, you can disable it in the settings. I've use ![Custom Categories](./images/custom_categories.gif) - ### ➕Expand/Collapse Categories/Sidebar➖ ![Expand/Collapse Categories and Sidebar](./images/expand_collapse.gif) @@ -92,14 +106,21 @@ This feature is enabled by default, you can disable it in the settings. I've use ### 👁 Preview Node 👁 ![Preview Node](./images/preview.gif) +### 📄 Templates Support 📄 +![Templates Support](./images/templates.gif) + +#### 📄 Workflow Support 📄 +![Workflow Support](./images/workflows.gif) + ### 🎨 ComfyUI Themes Support 🎨 ![Themes Support ](./images/theme.gif) ### 🎨 New Settings Panel 🎨 ![ComfyUI Settings](./images/settings.png) -### Additional Features -- Width and Minimized State of the sidebar are saved in cookies +PS: Workflow Path + + ### Todo: diff --git a/__init__.py b/__init__.py index 300c7b9..a96b533 100644 --- a/__init__.py +++ b/__init__.py @@ -3,6 +3,8 @@ import folder_paths from aiohttp import web import os import json +from collections import OrderedDict +from pathlib import Path @@ -14,7 +16,8 @@ class JSONConfigManager: "menuctx_options": [], "menuctx_subOptions": [], "menuctx_opt_callback": [], - "menuctx_sub_opt_callback": [] + "menuctx_sub_opt_callback": [], + "sb_wf_path": "" }""" if os.path.isfile(file_name): self.data = self.read_json_file() @@ -67,7 +70,7 @@ else: -file_path = os.path.join(folder_paths.get_folder_paths("custom_nodes")[0],"ComfyUI-N-Sidebar","app","settings.json") +file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"app","settings.json") config_manager = JSONConfigManager(file_path) @@ -75,8 +78,27 @@ config_manager = JSONConfigManager(file_path) NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} -list_panels = os.path.join(folder_paths.get_folder_paths("custom_nodes")[0],"ComfyUI-N-Sidebar","app","panels") +default_path = os.path.join(folder_paths.base_path, "workflows") +try: + list_workflows_dirs = config_manager.read_item("sb_wf_path").replace("\n\r", "\n").split('\n') +except: + print("No 'sb_wf_path' in settings.json. Using default path.") + list_workflows_dirs = [] + +workflow_dirs = [] + +for list_workflows in list_workflows_dirs: + if os.path.isdir(list_workflows.strip()): + workflow_dirs.append(list_workflows.strip()) + + +workflow_dirs.insert(0, default_path) + + +list_panels = os.path.join(os.path.dirname(os.path.realpath(__file__)),"app","panels") + +list_workflows_array = [] list_panels_array = [] #list folders folders = os.listdir(list_panels) @@ -86,6 +108,78 @@ for folder in folders: +@server.PromptServer.instance.routes.get("/sidebar/workflows" ) +async def s_get(request): + workflows_dict = {} + existing_names = set() + + for list_workflows in workflow_dirs: + for entry in os.scandir(list_workflows): + if entry.is_file() and entry.name.endswith('.json'): + base_name = entry.name[:-5] # Remove the '.json' extension + unique_name = base_name + + if unique_name in workflows_dict: + folder_name = os.path.basename(list_workflows) + unique_name = f"{base_name} (from {folder_name})" + + workflows_dict[unique_name] = entry.path + existing_names.add(unique_name) + sorted_workflows_dict = dict(sorted(workflows_dict.items())) + + return web.json_response(sorted_workflows_dict, content_type='application/json') + + + +@server.PromptServer.instance.routes.post("/sidebar/workflow") +async def s_get(request): + + data = await request.json() + + action = data["action"] + + if action == "delete": + path = Path(data["path"]) + os.remove(path) + result = {"result": "OK"} + return web.json_response(result, content_type='application/json') + + elif action == "rename": + path = Path(data["path"]) + newName = data["newName"] + + if not path.exists(): + raise FileNotFoundError(f"The system cannot find the path specified: {path}") + + # Ottieni la directory e il nuovo percorso + new_path = path.with_name(newName).with_suffix('.json') + if new_path.exists(): + result = {"result": "File already exists!"} + else: + #rename + os.rename(path, new_path) + result = {"result": "OK"} + return web.json_response(result, content_type='application/json') + else: + file = data["path"] + + # if file exists + if os.path.isfile(file): + + return web.FileResponse(file) + else: + return web.Response(status=403) + + + + + + + + + + + @server.PromptServer.instance.routes.get("/sidebar/panels" ) async def s_get(request): result = {"panels": list_panels_array} diff --git a/app/css/base_style.css b/app/css/base_style.css index 5b76604..6cbb667 100644 --- a/app/css/base_style.css +++ b/app/css/base_style.css @@ -84,7 +84,7 @@ } - .clearIcon, .searchCategoryIcon, .clearIconCustomCategory, .searchCategoryIconCustomCategory { + .clearIcon, .searchCategoryIcon, .clearIconCustomCategory, .searchCategoryIconCustomCategory, .clearIconCustomTemplate, .searchTemplateIconCustomTemplate, .clearIconCustomWorkflow, .searchWorkflowIconCustomWorkflow { position: absolute; padding: 5px; right: 30px; @@ -98,11 +98,11 @@ } - .clearIcon:hover, .clearIconCustomCategory:hover { + .clearIcon:hover, .clearIconCustomCategory:hover, .clearIconCustomTemplate:hover, .clearIconCustomWorkflow:hover { opacity: 1.0; } - .searchCategoryIcon, .searchCategoryIconCustomCategory { + .searchCategoryIcon, .searchCategoryIconCustomCategory, .searchTemplateIconCustomTemplate, .searchWorkflowIconCustomWorkflow { right: 0px; padding: 3px; font-size: larger; @@ -117,7 +117,7 @@ } - .searchCategoryIcon:hover, .searchCategoryIconCustomCategory:hover { + .searchCategoryIcon:hover, .searchCategoryIconCustomCategory:hover, .searchTemplateIconCustomTemplate:hover, .searchWorkflowIconCustomWorkflow:hover { opacity: 0.5; } @@ -528,6 +528,7 @@ border-radius: 5px; margin-left: 7px; + } .panel_sidebar label{ @@ -648,6 +649,27 @@ } + #sb_settingsDiv .setting input[type="text"]{ + background: var(--comfy-menu-bg); + color: var(--drag-text); + padding: 5px; + float: right; + width: 129px; + margin-right: 47px; + border-radius: 5px; + border: solid 1px var(--descrip-text); + } + #sb_settingsDiv .setting textarea{ + background: var(--comfy-menu-bg); + color: var(--drag-text); + padding: 5px; + float: right; + width: calc(100% - 60px); + resize: vertical; + margin-right: 47px; + border-radius: 5px; + border: solid 1px var(--descrip-text); + } #sb_settingsDiv .setting input[type="checkbox"]{ width: 18px; height: 18px; @@ -756,4 +778,9 @@ #switch_sidebar{ margin-top: 5px; + } + + #sbModal{ + display: none; + z-index: 9999; } \ No newline at end of file diff --git a/app/html/sidebar.html b/app/html/sidebar.html index 58876d9..65dce86 100644 --- a/app/html/sidebar.html +++ b/app/html/sidebar.html @@ -1,49 +1,61 @@ \ No newline at end of file diff --git a/app/js/functions/settings.js b/app/js/functions/settings.js index fa44945..b63369b 100644 --- a/app/js/functions/settings.js +++ b/app/js/functions/settings.js @@ -4,30 +4,36 @@ let sbPosition = "left"; let defaultSearchToggle = "fuzzy"; let defaultSearchOrder = "comfyui"; -async function getConfiguration(name) { +async function getConfiguration(name,local = false) { //console.log("getStyles called " + name); try { - const response = await fetch('/sidebar/settings', { - cache: "no-store", - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - action : "read", - parameter: name - - }), - - }); - - const data = await response.json(); + if (local) { + return localStorage.getItem(name); + } else { + + + const response = await fetch('/sidebar/settings', { + cache: "no-store", + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + action : "read", + parameter: name + + }), + + }); + + const data = await response.json(); - return data["value"]; - } catch (error) { - console.log(error); - return null; - } + return data["value"]; + } + } catch (error) { + console.log(error); + return null; + } } function addConfiguration(name,value) { @@ -45,7 +51,11 @@ async function getConfiguration(name) { }) } - function updateConfiguration(name,value) { + function updateConfiguration(name,value,local = false) { + if (local) { + return localStorage.setItem(name,value) + } else { + return fetch('/sidebar/settings', { method: 'POST', headers: { @@ -57,6 +67,8 @@ async function getConfiguration(name) { value: value }), }) + + } } function removeConfiguration(name) { @@ -91,9 +103,9 @@ function factoryResetSettings() { -function settingsSetup(app,$el) { +async function settingsSetup(app,$el) { - + app.ui.settings.addSetting({ id: "0_sidebar", name: "[Sidebar] Better ComfyUI Settings Style", @@ -118,6 +130,33 @@ function settingsSetup(app,$el) { }); + addSBSetting( "sb_settingsDiv",{ + id: "workflow_preview", + name: "Enable Workflow Node List", + defaultValue: "false", + type: "boolean", + local: true, + info: "This is disabled by default since workflows preview can be slow.", + + + onChange(value) { + + }, + }); + + addSBSetting( "sb_settingsDiv",{ + id: "workflow_replace", + name: "Workflow Replace Popup", + defaultValue: "true", + type: "boolean", + local: true, + + + onChange(value) { + + }, + }); + addSBSetting( "sb_settingsDiv",{ id: "font", name: "Font Size", @@ -147,7 +186,7 @@ function settingsSetup(app,$el) { addDynamicCSSRule('.sidebar li', 'padding', value + 'px'); }, }); - + addSBSetting( "sb_settingsDiv", { id: "bartop", name: "Space Top", @@ -395,7 +434,7 @@ function settingsSetup(app,$el) { }, }); - addSBSetting( "sb_settingsDiv", { + await addSBSetting( "sb_settingsDiv", { id: "order_type", name: "Nodes Order Type", defaultValue: "comfyui", @@ -416,6 +455,23 @@ function settingsSetup(app,$el) { }); + addSBSetting( "sb_settingsDiv", { + id: "wf_path", + name: "Workflow Paths", + defaultValue: "", + type: "textarea", + info: "Paths must be row separated (only one per row).\nThe default ComfyUI workflows path has already been set.\nReboot ComfyUI to apply changes!", + + onChange(value) { + + NaN + + + }, + }); + + + @@ -446,18 +502,22 @@ function settingsSetup(app,$el) { factoryReset(); }) + + + + } async function addSBSetting(fieldLocation, setting) { - setting.id = "sb_" + setting.id; // Aggiunge il prefisso del campo + setting.id = "sb_" + setting.id; - // Crea l'elemento HTML per il campo di impostazione + const settingElement = document.createElement("div"); settingElement.classList.add("setting"); - // Aggiungi il nome del campo + const nameLabel = document.createElement("label"); nameLabel.textContent = setting.name; settingElement.appendChild(nameLabel); @@ -471,20 +531,27 @@ async function addSBSetting(fieldLocation, setting) { nameLabel.appendChild(infoIcon); } - // Aggiungi il tipo appropriato di input per il campo + let inputElement; if (setting.type === "boolean") { inputElement = document.createElement("input"); inputElement.type = "checkbox"; - inputElement.checked = await getConfiguration(setting.id) || setting.defaultValue; + + let configValue = await getConfiguration(setting.id, setting.local) || setting.defaultValue; + if (configValue == "true") { + inputElement.checked = true; + } else if (configValue == "false") { + inputElement.checked = false; + } + setting.onChange(inputElement.checked); inputElement.addEventListener("change", function() { setting.onChange(inputElement.checked); - updateConfiguration(setting.id,inputElement.checked) + updateConfiguration(setting.id,inputElement.checked, setting.local) }); } else if (setting.type === "slider") { - // Crea l'elemento input per lo slider + const sliderContainer = document.createElement("div"); sliderContainer.classList.add("slider-container"); @@ -497,19 +564,20 @@ async function addSBSetting(fieldLocation, setting) { inputElement.value = await getConfiguration(setting.id) || setting.defaultValue || setting.defaultValue; setting.onChange(inputElement.value); inputElement.addEventListener("input", function() { - // Aggiorna il valore del campo testuale quando lo slider viene spostato + valueText.textContent = inputElement.value; setting.onChange(inputElement.value); updateConfiguration(setting.id,inputElement.value) }); - // Crea l'elemento per il valore testuale dello slider + const valueText = document.createElement("span"); valueText.textContent = await getConfiguration(setting.id) || setting.defaultValue; valueText.id = setting.id+"_span"; sliderContainer.appendChild(inputElement); sliderContainer.appendChild(valueText); settingElement.appendChild(sliderContainer); + } else if (setting.type === "text") { inputElement = document.createElement("input"); inputElement.type = "text"; @@ -519,7 +587,18 @@ async function addSBSetting(fieldLocation, setting) { setting.onChange(inputElement.value); updateConfiguration(setting.id,inputElement.value) }); - } else if (setting.type === "dropdown") { + } + else if (setting.type === "textarea") { + inputElement = document.createElement("textarea"); + inputElement.rows = 3; + inputElement.value = await getConfiguration(setting.id) || setting.defaultValue; + setting.onChange(inputElement.value); + inputElement.addEventListener("input", function() { + setting.onChange(inputElement.value); + updateConfiguration(setting.id,inputElement.value) + }); + } + else if (setting.type === "dropdown") { inputElement = document.createElement("select"); const defaultValue = await getConfiguration(setting.id) || setting.defaultValue; setting.options.forEach(async (option) => { @@ -541,7 +620,7 @@ async function addSBSetting(fieldLocation, setting) { settingElement.appendChild(inputElement); - // Aggiungi il campo di impostazione al luogo specificato + const targetElement = document.getElementById(fieldLocation); if (targetElement) { targetElement.appendChild(settingElement); @@ -552,12 +631,9 @@ async function addSBSetting(fieldLocation, setting) { } - - - function resetSettings() { - let ok = confirm("This option will reset the following settings: \n-Search Type\n-Position\n-Nodes Order Type\n-Font Size\n-Node Size\n-Space Top\n-Space Bottom\n-Node Radius Border\n-Blur Intesity\n-Opacity\nAre you sure you want to reset these settings?"); + let ok = confirm("This option will reset the following settings: \n-Search Type\n-Position\n-Nodes Order Type\n-Font Size\n-Node Size\n-Space Top\n-Space Bottom\n-Node Radius Border\n-Blur Intesity\n-Opacity\nsidebarWidth\nsb_pinned_collapsed\nsb_minimized\nsb_current_tab\nsb_workflow_preview\nsb_workflow_replace\nAre you sure you want to reset these settings?"); if (!ok) { return; } @@ -566,6 +642,16 @@ function resetSettings() { removeConfiguration(setting); }); + const localSettings = ["sidebarWidth","sb_pinned_collapsed","sb_minimized","sb_current_tab","sb_workflow_preview","sb_workflow_replace"]; + + localSettings.forEach(setting => { + try{ + removeVar(setting); + }catch(err){ + console.log(err) + } + }) + window.location.reload(); } @@ -577,7 +663,7 @@ function factoryReset() { } factoryResetSettings(); - const localSettings = ["sidebarWidth","sb_pinned_collapsed","sb_minimized","sb_current_tab","sb_current_tab"]; + const localSettings = ["sidebarWidth","sb_pinned_collapsed","sb_minimized","sb_current_tab","sb_workflow_preview","sb_workflow_replace"]; localSettings.forEach(setting => { try{ diff --git a/app/js/functions/utils.js b/app/js/functions/utils.js index 588e8b1..8b81e7b 100644 --- a/app/js/functions/utils.js +++ b/app/js/functions/utils.js @@ -93,7 +93,6 @@ function addJavascriptVar(name, value) { const scripta = document.createElement('script'); scripta.type = 'text/javascript'; - console.log("var "+name+" = "+value) scripta.innerHTML = "var "+name+" = "+value; document.head.appendChild(scripta); @@ -212,16 +211,37 @@ function createContextMenu(event, subMenus,settingsData) { showSubMenu(subMenus[index], li); }); try { + + + + if (settingsData.menuctx_opt_callback[index].indexOf(".") !== -1) { + + // Handle sub menu option + var mainfunc = settingsData.menuctx_opt_callback[index].split(".")[0]; + var subfunc = settingsData.menuctx_opt_callback[index].split(".")[1]; + if (typeof (window[mainfunc][subfunc]) === 'function') { + + + li.addEventListener('click', () => { + // Handle sub menu click + window[mainfunc][subfunc](event); + }); + + } + } + else{ + if (typeof (window[settingsData.menuctx_opt_callback[index]]) === 'function') { li.addEventListener('click', () => { // Handle main option click - //alert(`Selected option: ${option}`); + window[settingsData.menuctx_opt_callback[index]](event); }); } + } } catch (error) { console.log(error); } @@ -368,9 +388,16 @@ window.addEventListener("click", function(event) { // Function to check for special characters -function containsSpecialCharacters(name) { +function containsSpecialCharacters(name, level=0) { // Define a regular expression pattern to match special characters - var specialCharacters = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/; + if (level == 0) { + var specialCharacters = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/; + } else if (level == 1) { + var specialCharacters = /[\*':"\\|,<>\/?]/g; + } else{ + var specialCharacters = /['"]/gi; + } + // Return true if the name contains any special characters, false otherwise return specialCharacters.test(name); } @@ -442,7 +469,11 @@ function openModal(addon=null) { closeModalButton.addEventListener('click', closeModal); try{ // Focus the input field - document.getElementById('sb-categoryName').focus(); + inputElement = document.getElementById('sb-categoryName'); + inputElement.focus(); + const length = inputElement.value.length; + inputElement.setSelectionRange(length, length); + }catch(err){ } @@ -454,12 +485,17 @@ try{ function closeModal() { const modalBackdrop = document.querySelector('.sb-modal-backdrop'); const modal = document.querySelector('.sb-category-dialog'); - + try{ // Remove the modal from the DOM modalBackdrop.parentNode.removeChild(modalBackdrop); modal.parentNode.removeChild(modal); + } + catch(err){ + console.log(err); + } } + function rgbToHex(rgb) { // Verifica se il colore è nel formato "rgb()" if (rgb.indexOf('rgb') !== -1) { @@ -522,3 +558,10 @@ function isBottomEdgeVisible(el) { return false; } + +function convertCanvasToOffset(canvas, pos, out) { + out = out || [0, 0]; + out[0] = pos[0] / canvas.scale - canvas.offset[0]; + out[1] = pos[1] / canvas.scale - canvas.offset[1]; + return out; +}; diff --git a/app/js/sidebar_node.js b/app/js/sidebar_node.js index e3b5d56..ed5cf6d 100644 --- a/app/js/sidebar_node.js +++ b/app/js/sidebar_node.js @@ -3,7 +3,9 @@ import { api } from "../../../../scripts/api.js" import { app } from "../../../../scripts/app.js"; import { $el } from "../../../../scripts/ui.js"; import { fuzzyMatch } from "./fts_fuzzy_match.js"; +import { GroupNodeConfig, GroupNodeHandler } from "../../../../extensions/core/groupNode.js"; +const gnc = GroupNodeConfig; //import {settingsSetup} from './functions/settings.jsb'; console.time('execution time'); const cnPath = "../extensions/ComfyUI-N-Sidebar/" @@ -93,7 +95,7 @@ function postPinned() { previewDivTop = (itemPosition.top + this.offsetHeight) - previewDiv.offsetHeight; } - let sidebar_width = parseInt(getVar("sidebarWidth")) || 300; + let sidebar_width = parseInt(getVar("sidebarWidth")) || 500; previewDiv.style.top = `${previewDivTop}px`; const previewDivLeft = sidebar_width - sidebad_view_width; @@ -112,14 +114,8 @@ function postPinned() { previewDiv.style.display = 'none'; }) - - - reloadCtxMenu() - - - }); pinnedElement.addEventListener("dragover", function (event) { @@ -228,8 +224,11 @@ async function loadPinnedItemsAndAddToBookmarks() { if (item) { + try{ let addedItem = sidebarBookmarks.appendChild(item.cloneNode(true)); addedItem.lastChild.lastChild.classList.add("pinned"); + + }catch(err){ } } }); @@ -579,7 +578,7 @@ async function createCategoryList() { previewDivTop = (itemPosition.top + this.offsetHeight) - previewDiv.offsetHeight; } - let sidebar_width = parseInt(getVar("sidebarWidth")) || 300; + let sidebar_width = parseInt(getVar("sidebarWidth")) || 500; previewDiv.style.top = `${previewDivTop}px`; @@ -755,13 +754,7 @@ async function addSidebar() { }); - function convertCanvasToOffset(canvas, pos, out) { - out = out || [0, 0]; - out[0] = pos[0] / canvas.scale - canvas.offset[0]; - out[1] = pos[1] / canvas.scale - canvas.offset[1]; - return out; - }; - + function handleDrop(event) { event.preventDefault(); @@ -973,7 +966,7 @@ function processViews(viewsData, settingsData) { div_panel_title.classList = "panel_sidebar_title sb_label"; div_panel_title.id = "panel_title_" + panel; - div_panel_title.innerHTML = panel.replace("_", " "); + div_panel_title.innerHTML = view.id.replace("_", " "); @@ -987,6 +980,7 @@ function processViews(viewsData, settingsData) { div_view.insertAdjacentElement('beforeend', div_panel); jsloader(`${cnPath}panels/${panel}/${panel}.jsb`); + //window.custom_templates(); console.log("LOADING " + cnPath + `panels/${panel}/${panel}.js`) } } catch (error) { diff --git a/app/panels/custom_categories/custom_categories.jsb b/app/panels/custom_categories/custom_categories.jsb index 7b4cf2b..a438c61 100644 --- a/app/panels/custom_categories/custom_categories.jsb +++ b/app/panels/custom_categories/custom_categories.jsb @@ -512,6 +512,13 @@ async function renameCategory(oldCategoryName, newCategoryName) { } let categoryNodeMap = await getNodeMap(); + + if (categoryNodeMap[newCategoryName]) { + alert("Category already exists!"); + return; + } + + let categoryNodeStatus = getNodesStatus(); if (categoryNodeMap[oldCategoryName]) { // Categoty Current Order diff --git a/app/panels/custom_templates/custom_templates.html b/app/panels/custom_templates/custom_templates.html new file mode 100644 index 0000000..9bcd28a --- /dev/null +++ b/app/panels/custom_templates/custom_templates.html @@ -0,0 +1,12 @@ +
+ +
+
+
\ No newline at end of file diff --git a/app/panels/custom_templates/custom_templates.jsb b/app/panels/custom_templates/custom_templates.jsb new file mode 100644 index 0000000..2ebfb9e --- /dev/null +++ b/app/panels/custom_templates/custom_templates.jsb @@ -0,0 +1,1231 @@ +var custom_templates = (function () { + let currentElement = null; + + function createTemplatePreview(nodeID, jsondata) { + const jsonData = JSON.parse(jsondata); + const nodeTypes = [...new Set(jsonData.nodes.map(node => node.type))]; + + //const nodeTypes = jsonData.nodes.map(node => node.type); + + let rows = ""; + nodeTypes.forEach(type => { + rows += `
  • ${type}
  • ` + + }) + return `
    +
    ${nodeID}
    +
    USED NODES
    + +
    `; + + } + // MODAL + function createCustomHtml(type, name, elem = null) { + if (type == "rename") { + return `

    Rename Template Category

    +
    + + +
    + + + ` + } else if (type == "color") { + currentElement = elem; + return `

    Color ${name}

    +
    + +
    + + + + + ` + } + else if(type == "renameTemplate"){ + return `

    Rename Template

    +
    + + +
    + + + ` + } + } + + + + function setModalContext(addon = null) { + if (addon == null) { + return ` +

    Add New Template

    +
    + + +
    + + `; + } + else { + return addon + } + } + + + + function openModal(addon = null) { + // Create HTML elements for the modal + const modalBackdrop = document.createElement('div'); + modalBackdrop.classList.add('sb-modal-backdrop'); + + const modal = document.createElement('div'); + modal.classList.add('sb-category-dialog'); + modal.innerHTML = setModalContext(addon) + + // Add the modal to the DOM + document.body.appendChild(modalBackdrop); + document.body.appendChild(modal); + + // Add an event listener for the close button + const closeModalButton = modal.querySelector('#closeModalButton'); + closeModalButton.addEventListener('click', closeModal); + try { + // Focus the input field + inputElement = document.getElementById('sb-templateName'); + inputElement.focus(); + const length = inputElement.value.length; + inputElement.setSelectionRange(length, length); + } catch (err) { + + } + } + + + // Function to open the modal + + //insert button in label with id panel_title_custom_templates + let templateSearchToggleCustom = false; + const panel_title_custom_templates = document.getElementById("panel_title_custom_templates"); + + const button_custom_templates = document.createElement("button"); + button_custom_templates.classList = "expand_node"; + button_custom_templates.innerHTML = ` + + + + + +`; + + panel_title_custom_templates.appendChild(button_custom_templates); + + button_custom_templates.addEventListener("click", (event) => { + const clickedElement = event.target; + const pinButton = clickedElement.tagName; + if ((pinButton == "rect") && clickedElement.classList.contains("expand_node")) { + + sdExpandAll(); + } + + }) + const clearIconCustomTemplate = document.querySelector(".clearIconCustomTemplate"); + const itemSearchInput = document.getElementById("searchTemplateInput"); + clearIconCustomTemplate.addEventListener("click", async function () { + try { + + itemSearchInput.value = ""; + renderList("custom_templates_main"); + + } catch (error) { + console.error("Error occurred during search:", error); + } + }); + + + + + function afterRender() { + + customMenuTemplate = ` + { + "menuctx_template": "Context Menu", + "menuctx_options": [ + "Rename Category", + "Delete Category", + "Change Color" + ], + "menuctx_subOptions": [ + [ + ] + ], + "menuctx_opt_callback": [ + "custom_templates.renameCategoryCallback", + "custom_templates.deleteCategory", + "custom_templates.colorTemplateCallback" + + ], + "menuctx_sub_opt_callback": [ + [ + ], + [ + ] + ] + }`; + /* + customMenuTemplateItem = ` + { + "menuctx_template": "Context Menu", + "menuctx_options": [ + "Delete from Template", + "Rename Template", + "Delete Template" + ], + "menuctx_subOptions": [ + [ + ] + ], + "menuctx_opt_callback": [ + "custom_templates.removeNodeFromTemplateCallback", + "custom_templates.renameTemplateCallback", + "custom_templates.removeTemplateCallback" + + ], + "menuctx_sub_opt_callback": [ + [ + ], + [ + ] + ] + }`;*/ + customMenuTemplateItem = ` + { + "menuctx_template": "Context Menu", + "menuctx_options": [ + "Delete from Template" + ], + "menuctx_subOptions": [ + [ + ] + ], + "menuctx_opt_callback": [ + "custom_templates.removeNodeFromTemplateCallback" + + ], + "menuctx_sub_opt_callback": [ + [ + ], + [ + ] + ] + }`; + + customMenuTemplateItemOut = ` + { + "menuctx_template": "Context Menu", + "menuctx_options": [ + "Rename Template", + "Delete Template" + ], + "menuctx_subOptions": [ + [ + ] + ], + "menuctx_opt_callback": [ + "custom_templates.renameTemplateCallback", + "custom_templates.removeTemplateCallback" + + ], + "menuctx_sub_opt_callback": [ + [ + ], + [ + ] + ] + }`; + customMenuJSON = JSON.parse(customMenuTemplate); + customMenuItemJSON = JSON.parse(customMenuTemplateItem); + customMenuItemJSONOut = JSON.parse(customMenuTemplateItemOut); + + setContextMenu(customMenuJSON, "#custom_templates_main .sidebarCategory", 0, "sidebarItem", "sidebarCategory"); + + //setContextMenu(customMenuItemJSONOut, "#custom_templates_main .sidebarItem"); + + setContextMenu(customMenuItemJSON, "#custom_templates_main .displayNamesList .sidebarItem"); + // ORDER CUSTOM CATEGORIES + const custom_templates_main = document.getElementById("custom_templates_main"); + + custom_templates_main.querySelectorAll(".sidebarCategory").forEach(function (item) { + item.addEventListener("dragstart", handleTemplateDragStart); + }); + + custom_templates_main.removeEventListener("dragover", handleTemplateDragOver); + custom_templates_main.addEventListener("dragover", handleTemplateDragOver); + + custom_templates_main.removeEventListener("drop", handleCustomTemplatesMainDrop); + custom_templates_main.addEventListener("drop", handleCustomTemplatesMainDrop); + + // Template drag and drop + custom_templates_main.querySelectorAll(".sidebarItem").forEach(function (item) { + item.addEventListener("dragstart", handleTemplateDragStart); + }); + + sidebar.removeEventListener("dragstart", handleSidebarTemplateDragStart); + sidebar.addEventListener("dragstart", handleSidebarTemplateDragStart); + + document.removeEventListener("drop", handleDocumentTemplateDropEvent); + document.addEventListener("drop", handleDocumentTemplateDropEvent); + + // ORDER CUSTOM NODES + custom_templates_main.querySelectorAll(".sidebarItem").forEach(function (item) { + item.addEventListener("dragstart", handleTemplateDragStart); + }); + } + + function handleTemplateDragStart(event) { + dragItem = event.target; + } + + function handleTemplateDragOver(event) { + event.preventDefault(); + } + + function handleCustomTemplatesMainDrop(event) { + event.preventDefault(); + if (dragItem) { + if (draggedElement.id !== 'sidebarItem') { + if (dragItem.parentElement === event.target.parentElement) { + const custom_templates_main = document.getElementById("custom_templates_main"); + custom_templates_main.insertBefore(dragItem, event.target); + reorderTemplates(getCustomTemplateOrder()); + } + } + dragItem = null; + } + } + + function handleSidebarTemplateDragStart(event) { + draggedElement = event.target; + } + + function handleDocumentTemplateDropEvent(event) { + event.preventDefault(); + if (draggedElement.id === 'sidebarItem' && event.target.id === 'graph-canvas') { + clipboardAction(async () => { + const data = JSON.parse(draggedElement.dataset.data); + localStorage.setItem("litegrapheditor_clipboard", draggedElement.dataset.data); + const coord = convertCanvasToOffset(app.canvasEl.data.ds, [event.clientX, event.clientY]); + + app.canvas.graph_mouse[0] = coord[0]; + app.canvas.graph_mouse[1] = coord[1]; + app.canvas.pasteFromClipboard(); + }); + } else if (draggedElement.id === 'sidebarItem' && (event.target.classList.contains('sidebarCategory') || event.target.classList.contains('displayNamesList'))) { + const ulElement = event.target.querySelector('ul'); + if (ulElement) { + ulElement.appendChild(draggedElement); + } else { + event.target.appendChild(draggedElement); + } + updateConfiguration('sb_templateNodeMap', JSON.stringify(getCustomTemplateStructure())); + renderList("custom_templates_main"); + } else if (draggedElement.id === 'sidebarItem' && event.target.parentElement.parentElement.classList.contains('sidebarCategory')) { + event.target.parentElement.insertBefore(draggedElement, event.target); + updateConfiguration('sb_templateNodeMap', JSON.stringify(getCustomTemplateStructure())); + renderList("custom_templates_main"); + } + } + + const clipboardAction = async (cb) => { + const old = localStorage.getItem("litegrapheditor_clipboard"); + await cb(); + localStorage.setItem("litegrapheditor_clipboard", old); + }; + + + function getCustomTemplateOrder() { + let cat = []; + const custom_templates_main = document.getElementById("custom_templates_main"); + custom_templates_main.querySelectorAll(".sidebarCategory").forEach(function (item) { + cat.push(item.dataset.nametemplate) + + }); + return cat + } + + + function getCustomNodeOrder(idcat) { + let node = []; + const objMain = document.getElementById(idcat); + objMain.querySelectorAll(".sidebarItem").forEach(function (item) { + node.push(item.dataset.nametemplate) + + }); + return node + } + + function getCustomTemplateStructure() { + let cat = {}; + const custom_templates_main = document.getElementById("custom_templates_main"); + custom_templates_main.querySelectorAll(".sidebarCategory").forEach(function (item) { + + cat[item.dataset.nametemplate] = getCustomNodeOrder(item.dataset.nametemplate + "_ul") + + }); + + return cat + } + + async function readComfyTemplates() { + let comfy_templates; + try { + const response = await fetch('./userdata/comfy.templates.json'); + comfy_templates = await response.json(); + } catch (error) { + console.error('Error fetching data:', error); + } + + + return comfy_templates + + } + async function readComfyTemplatesKV() { + let kvtemplate = {}; + let comfy_templates = await readComfyTemplates(); + for (const template in comfy_templates) { + let templateName = comfy_templates[template] + kvtemplate[templateName.name] = templateName.data + } + + + return kvtemplate + + } + + async function renderList(elementID) { + const data = await readComfyTemplatesKV() //load all templates + + const myPromise = new Promise(async (resolve, reject) => { + const templates = await readTemplates(); + + + //console.log(templates); + const sidebarCustomNodes = document.getElementById(elementID); + //clear elementid + sidebarCustomNodes.innerHTML = ""; + + exclusion_list = [] + for (const template in templates) { + //data[objKey].title.toLowerCase(); + let templateName = templates[template] + const templateItem = document.createElement("li"); + templateItem.classList.add("sidebarCategory"); + templateItem.dataset.nametemplate = templateName; + templateItem.textContent = templateName; + templateItem.draggable = true; + + const currentColor = await getValueFromConfig("sb_ColorCustomTemplates", templateName); + if (currentColor) { + const value_perc = await getConfiguration("sb_opacity") || "1.0"; + + const rgbColor = hexToRgb(currentColor); + + templateItem.style.backgroundColor = `rgba(${rgbColor}, ${value_perc})`; + } + + + + + const displayNamesList = document.createElement("ul"); + displayNamesList.id = templateName + "_ul"; + displayNamesList.classList.add("displayNamesList"); + displayNamesList.style.display = getNodeStatus(templateName); + templateItem.appendChild(displayNamesList); + + let displayName = template; + + + const listNodeForTemplate = await getNodesForTemplate(templateName); + + + + listNodeForTemplate.forEach(displayName => { + try { + const displayNameItem = document.createElement("li"); + displayNameItem.classList.add("sidebarItem"); + displayNameItem.id = "sidebarItem" + displayNameItem.textContent = displayName; + displayNameItem.dataset.nametemplate = displayName; + displayNameItem.dataset.data = data[displayName]; + displayNameItem.draggable = true; + + + + + + exclusion_list.push(displayName); + + + displayNamesList.appendChild(displayNameItem); + + + displayNamesList.addEventListener("drop", function (event) { + event.preventDefault(); + + if (dragItem) { + + if (dragItem.parentElement == event.target.parentElement) { + + displayNamesList.insertBefore(dragItem, event.target); + + reorderNodeInTemplate(templateItem.dataset.nametemplate, getCustomNodeOrder(dragItem.parentElement.id)) + + } + dragItem = null; + + + } + }); + } + catch (err) { + console.log(err); + } + }) + + try { + sidebarCustomNodes.appendChild(templateItem); + } catch (err) { + console.log(err); + } + } + + //adding templates not categorized + let space = document.createElement("br"); + sidebarCustomNodes.appendChild(space); + + let comfy_templates = await readComfyTemplates(); + for (const template in comfy_templates) { + //data[objKey].title.toLowerCase(); + let templateName = comfy_templates[template] + + //if templatename is not in exclusion list + if (!exclusion_list.includes(templateName.name)) { + + + const templateItem = document.createElement("li"); + templateItem.classList.add("sidebarItem"); + templateItem.id = "sidebarItem"; + templateItem.dataset.nametemplate = templateName.name; + templateItem.dataset.data = templateName.data; + templateItem.textContent = templateName.name; + templateItem.draggable = true; + + + + + + let displayName = template; + + + + try { + sidebarCustomNodes.appendChild(templateItem); + } catch (err) { + console.log(err); + } + } + + + + + + + } + + // Preview + const sidebarItems_cat = document.querySelectorAll('#' + elementID + ' .sidebarItem'); + const previewDiv = document.getElementById('previewDiv'); + let sidebad_view_width = document.getElementById("sidebar_views").offsetWidth; + + sidebarItems_cat.forEach(item => { + + item.addEventListener('mouseover', function () { + + if (this.classList.contains('sidebarItem') && this.tagName === 'LI') { + let descriptionDiv = ""; + const itemPosition = getElementPosition(this); + let previewDivTop = 0; + const previewContent = createTemplatePreview(item.dataset.nametemplate, item.dataset.data); + + previewDiv.innerHTML = previewContent + descriptionDiv; + previewDiv.style.display = 'block'; + const correction_offset = 45; + + if (itemPosition.top - this.offsetHeight >= 0 && itemPosition.top + previewDiv.offsetHeight < document.body.offsetHeight) { + previewDivTop = itemPosition.top - this.offsetHeight + + } else if (itemPosition.top - this.offsetHeight - previewDiv.offsetHeight <= 0) { + previewDivTop = 0 + correction_offset; + + + } + else { + + previewDivTop = (itemPosition.top + this.offsetHeight) - previewDiv.offsetHeight; + } + + let sidebar_width = parseInt(getVar("sidebarWidth")) || 500; + + previewDiv.style.top = `${previewDivTop}px`; + + const previewDivLeft = sidebar_width - sidebad_view_width; + + if (sbPosition == "left") { + + previewDiv.style.left = `${previewDivLeft}px`; + } else { + + previewDiv.style.right = `${previewDivLeft}px`; + } + + + + } + }); + + item.addEventListener('mouseout', function () { + previewDiv.style.display = 'none'; + }) + + }); + + + + + + + window.addEventListener('click', function (event) { + + if (!event.target.classList.contains('sidebarItem')) { + previewDiv.style.display = 'none'; + + } + }); + + + + + + + + const templateItems = document.querySelectorAll("#custom_templates_main .sidebarCategory"); + templateItems.forEach(function (templateItem) { + templateItem.addEventListener("click", function (event) { + + if (event.target === event.currentTarget) { + const displayNamesList = event.target.querySelector("ul"); + + displayNamesList.style.display = displayNamesList.style.display === "none" ? "block" : "none"; + + setNodeStatus(displayNamesList.parentElement.dataset.nametemplate, displayNamesList.style.display) + } + }); + + + + }); + resolve(afterRender); + + + + + + }); + + + myPromise.then((callback) => { + + callback(); + }).catch((error) => { + console.error("An error occurred:", error); + }); + } + function searchT(e) { + + if (e.value != "") { + //search in all .sidebarItem + handleSearch(templateSearchToggleCustom, "#custom_templates_main", "searchTemplateInput") + + + } else { + + renderList("custom_templates_main"); + } + + } + + + + async function getNodeMap() { + let templateNodeMap = JSON.parse(await getConfiguration('sb_templateNodeMap')) || {}; + return templateNodeMap; + } + + function getNodesStatus() { + let templateNodeStatus = JSON.parse(localStorage.getItem('sb_templateNodeStatus')) || {}; + return templateNodeStatus; + } + + + async function addTemplate() { + + + const newTemplateName = document.getElementById('sb-templateName').value; + // Check if the template name contains special characters + if (containsSpecialCharacters(newTemplateName)) { + // Alert the user and return if special characters are found + alert("Template name cannot contain special characters."); + return; + } + + + + + let templateNodeMap = await getNodeMap(); + + + if (!templateNodeMap[newTemplateName]) { + + templateNodeMap[newTemplateName] = []; + updateConfiguration('sb_templateNodeMap', JSON.stringify(templateNodeMap)); + createContextualMenu(); + reloadCtxMenu() + renderList("custom_templates_main"); + closeModal() + } else { + alert("Template already exists!"); + return; + } + } + + + // Function to rename a template + + async function renameCategory(oldTemplateName, newTemplateName) { + + + if (containsSpecialCharacters(newTemplateName)) { + // Alert the user and return if special characters are found + alert("Template name cannot contain special characters."); + return; + } + if (oldTemplateName === newTemplateName) { + closeModal() + return; + } + + let templateNodeMap = await getNodeMap(); + + + if (templateNodeMap[newTemplateName]) { + alert("Category already exists!"); + return; + } + + + + let templateNodeStatus = getNodesStatus(); + if (templateNodeMap[oldTemplateName]) { + // Categoty Current Order + let keys = Object.keys(templateNodeMap); + + templateNodeMap[newTemplateName] = templateNodeMap[oldTemplateName]; + delete templateNodeMap[oldTemplateName]; + + // Rebuild Order + let updatedMap = {}; + keys.forEach(key => { + if (key === oldTemplateName) { + updatedMap[newTemplateName] = templateNodeMap[newTemplateName]; + } else { + updatedMap[key] = templateNodeMap[key]; + } + }); + + + + updateConfiguration('sb_templateNodeMap', JSON.stringify(updatedMap)); + //rename status + if (templateNodeStatus[oldTemplateName]) { + + templateNodeStatus[newTemplateName] = templateNodeStatus[oldTemplateName]; + delete templateNodeStatus[oldTemplateName]; + localStorage.setItem('sb_templateNodeStatus', JSON.stringify(templateNodeStatus)); + } + //rename color + renameKeyConfig("sb_ColorCustomTemplates", oldTemplateName, newTemplateName) + + + createContextualMenu(); + reloadCtxMenu() + renderList("custom_templates_main"); + closeModal() + } else { + alert("Template not found!"); + } + } + + async function renameTemplate(oldTemplateName, newTemplateName) { + if (containsSpecialCharacters(newTemplateName, level=1)) { + // Alert the user and return if special characters are found + alert("Template name cannot contain special characters."); + return; + } + const response = await fetch('./userdata/comfy.templates.json'); + const nodeTemplates = await response.json(); + + + // local cache operation + let nodeTemplatesLocal = JSON.parse(localStorage.getItem('Comfy.NodeTemplates')); + for (let node of nodeTemplatesLocal) { + if (node.name === oldTemplateName) { + node.name = newTemplateName; + break; + } + } + localStorage.setItem('Comfy.NodeTemplates', JSON.stringify(nodeTemplatesLocal)); + + //check if name already exists + for (let node of nodeTemplates) { + if (node.name === newTemplateName) { + alert(`The Template name "${newTemplateName}" already exists.`); + return; + } + + } + for (let node of nodeTemplates) { + + if (node.name === oldTemplateName) { + node.name = newTemplateName; + break; + } + } + + await fetch('./userdata/comfy.templates.json', { + method: 'POST', + body: JSON.stringify(nodeTemplates), + headers: { + 'Content-Type': 'application/json' + } + }). then(() => { + renameTemplateNode("sb_templateNodeMap", oldTemplateName, newTemplateName) + renderList("custom_templates_main"); + closeModal() + //refresh page + window.location.reload(); + }); + + + } + + + async function removeTemplate(name) { + + const response = await fetch('./userdata/comfy.templates.json'); + const nodeTemplates = await response.json(); + //let nodeTemplates = JSON.parse(comfy_templates); + + //local cache operation + let nodeTemplatesLocal = JSON.parse(localStorage.getItem('Comfy.NodeTemplates')); + nodeTemplatesLocal = nodeTemplatesLocal.filter(node => node.name !== name); + localStorage.setItem('Comfy.NodeTemplates', JSON.stringify(nodeTemplatesLocal)); + + + + + // Iterate over the nodeTemplates to find and remove the node + for (let i = 0; i < nodeTemplates.length; i++) { + if (nodeTemplates[i].name === name) { + nodeTemplates.splice(i, 1); // Remove the node from the array + break; // Assuming the node names are unique, we can break once we find it + } + } + await fetch('./userdata/comfy.templates.json', { + method: 'POST', + body: JSON.stringify(nodeTemplates), + headers: { + 'Content-Type': 'application/json' + } + }).then(() => { + removeTemplateNode("sb_templateNodeMap", name) + renderList("custom_templates_main"); + //refresh page + window.location.reload(); + }); + + + } + + async function readTemplates() { + let templateNodeMap = await getNodeMap(); + if (templateNodeMap) { + return Object.keys(templateNodeMap); + } + else { + return []; + } + + } + + + // Function to delete a template + async function deleteCategory(e) { + + const name = e.target.dataset.nametemplate; + confirmation = confirm("Are you sure you want to delete the template " + name + "?"); + if (!confirmation) { return; } + + let templateNodeMap = await getNodeMap(); + // If the template name is found, remove it from the array + if (templateNodeMap[name]) { + removeNodeMapTemplate(name); + createContextualMenu(); + removeKeyFromConfig("sb_ColorCustomTemplates", name) + + setTimeout(() => { + + renderList("custom_templates_main"); + }, 200) + reloadCtxMenu() + } else { + alert("Template not found!"); + } + } + + + + ///////////////////////////////////////////////// + + async function getConfig(configName) { + let templateNodeMap = JSON.parse(await getConfiguration(configName)) || {}; + return templateNodeMap; + } + + + + async function getValueFromConfig(configName, key) { + let config = await getConfig(configName); + return config[key] || null; + } + + async function assignValueToConfig(configName, key, value) { + let config = await getConfig(configName); + config[key] = value; + updateConfiguration(configName, JSON.stringify(config)); + } + + async function removeKeyFromConfig(configName, key) { + let config = await getConfig(configName); + delete config[key]; + updateConfiguration(configName, JSON.stringify(config)); + + + } + async function renameKeyConfig(configName, oldKey, newKey) { + let config = await getConfig(configName); + let value = config[oldKey]; + delete config[oldKey]; + config[newKey] = value; + updateConfiguration(configName, JSON.stringify(config)); + } + + + + async function renameTemplateNode(key, oldNodeName, newNodeName) { + let jsonObject = await getConfig(key); + + // Parse the sb_templateNodeMap property to an object + let templateNodeMap = jsonObject + // Iterate over the template node map to find and replace the node name + for (let template in templateNodeMap) { + let nodes = templateNodeMap[template]; + let nodeIndex = nodes.indexOf(oldNodeName); + if (nodeIndex !== -1) { + nodes[nodeIndex] = newNodeName; + break; // Assuming the node names are unique, we can break once we find it + } + } + + // Convert the modified JSON object back to a JSON string + updateConfiguration(key, JSON.stringify(jsonObject)); + } + + async function removeTemplateNode(key, value) { + + let jsonObject = await getConfig(key); + let templateNodeMap = jsonObject + // Iterate over the template node map to find and remove the node name + for (let template in templateNodeMap) { + let nodes = templateNodeMap[template]; + let nodeIndex = nodes.indexOf(value); + if (nodeIndex !== -1) { + nodes.splice(nodeIndex, 1); // Remove the node from the array + break; // Assuming the node names are unique, we can break once we find it + } + } + + updateConfiguration(key, JSON.stringify(jsonObject)); + } + + + + + async function removeKeyConfig(configName) { + removeConfiguration(configName) + renderList("custom_templates_main"); + } + + + + + + + + //////////////////////////////////////////////// + + async function getTemplatesForNode(nodeName) { + let templateNodeMap = await getNodeMap(); + + let templates = []; + for (let template in templateNodeMap) { + if (templateNodeMap.hasOwnProperty(template)) { + if (templateNodeMap[template].includes(nodeName)) { + templates.push(template); + } + } + } + return templates; + } + + async function getNodesForTemplate(templateName) { + let templateNodeMap = await getNodeMap(); + + + return templateNodeMap[templateName] || []; + } + + + async function assignNodeToTemplate(nodeName, templateNames) { + let templateNodeMap = await getNodeMap(); + + let assignedTemplates = await getTemplatesForNode(nodeName); + templateNames = templateNames.filter(template => !assignedTemplates.includes(template)); + + templateNames.forEach(templateName => { + if (!templateNodeMap[templateName]) { + templateNodeMap[templateName] = []; + } + templateNodeMap[templateName].push(nodeName); + }); + updateConfiguration('sb_templateNodeMap', JSON.stringify(templateNodeMap)); + renderList("custom_templates_main"); + } + + + async function removeNodeFromTemplate(nodeName, templateName) { + let templateNodeMap = await getNodeMap(); + + if (templateNodeMap[templateName]) { + templateNodeMap[templateName] = templateNodeMap[templateName].filter(node => node !== nodeName); + updateConfiguration('sb_templateNodeMap', JSON.stringify(templateNodeMap)); + renderList("custom_templates_main"); + + } + } + + + async function removeNodeMapTemplate(templateName) { + let templateNodeMap = await getNodeMap(); + let templateNodeStatus = getNodesStatus(); + delete templateNodeMap[templateName]; + updateConfiguration('sb_templateNodeMap', JSON.stringify(templateNodeMap)); + + if (templateNodeStatus[templateName]) { + delete templateNodeStatus[templateName]; + localStorage.setItem('sb_templateNodeStatus', JSON.stringify(templateNodeStatus)); + + } + } + + + function editNodeTemplate(oldTemplateName, newTemplateName, nodeName) { + + removeNodeFromTemplate(nodeName, oldTemplateName); + + assignNodeToTemplate(nodeName, [newTemplateName]); + } + + + + async function reorderTemplates(orderedKeys) { + let templateNodeMap = await getNodeMap(); + let reorderedTemplateNodeMap = {}; + + + orderedKeys.forEach(templateKey => { + if (templateNodeMap.hasOwnProperty(templateKey)) { + reorderedTemplateNodeMap[templateKey] = templateNodeMap[templateKey]; + } + }); + + updateConfiguration('sb_templateNodeMap', JSON.stringify(reorderedTemplateNodeMap)); + + + } + + + async function reorderNodeInTemplate(templateName, orderedKeys) { + let templateNodeMap = await getNodeMap(); + + if (templateNodeMap[templateName]) { + templateNodeMap[templateName] = orderedKeys; + updateConfiguration('sb_templateNodeMap', JSON.stringify(templateNodeMap)); + } + } + + // Callbacks + function removeNodeFromTemplateCallback(e, trge) { + + removeNodeFromTemplate(e.target.dataset.nametemplate, e.target.parentElement.parentElement.dataset.nametemplate) + } + + function assignNodeToTemplateCallback(e, trge) { + assignNodeToTemplate(e.target.id, [trge]); + } + + function renameCategoryCallback(e, trge) { + const name = e.target.dataset.nametemplate; + openModal(createCustomHtml("rename", name)) + + } + + function colorTemplateCallback(e) { + + const name = e.target.dataset.nametemplate; + openModal(createCustomHtml("color", name, e.target)) + //colorTemplate(e.target.id, e.target.parentElement.parentElement.dataset.nametemplate) + } + + + function removeTemplateCallback(e) { + // Function to delete a template + + const name = e.target.dataset.nametemplate; + + confirmation = confirm("Are you sure you want to delete the template " + name + "?"); + if (!confirmation) { return; } + + removeTemplate(name); + + + + } + + function renameTemplateCallback(e) { + const name = e.target.dataset.nametemplate; + // Recupera l'array dallo storage + openModal(createCustomHtml("renameTemplate", name)) + + + } + + async function colorTemplate(name, value) { + + + const value_perc = await getConfiguration("sb_opacity") || "1.0"; + const rgbColor = hexToRgb(value); + + currentElement.style.backgroundColor = `rgba(${rgbColor}, ${value_perc})`; + + + assignValueToConfig("sb_ColorCustomTemplates", name, value) + } + // Add an event listener to the button to open the modal + function resetColorTemplate(name) { + + removeKeyFromConfig("sb_ColorCustomTemplates", name) + renderList("custom_templates_main"); + + } + + + async function createContextualMenu() { + + //const menuOptions = document.getElementById('menu-options'); + new_menu_options_callback = []; + //for each template + template_menu = await readTemplates(); + callback_menu = []; + for (let i = 0; i < template_menu.length; i++) { + callback_menu.push("assignNodeToTemplateCallback"); + + } + + + console.log("custom template panel loaded") + + } + + + + const searchTemplateIconCustomTemplate = document.querySelector(".searchTemplateIconCustomTemplate"); + + + searchTemplateIconCustomTemplate.addEventListener("click", async function () { + + try { + + templateSearchToggleCustom = !templateSearchToggleCustom; + searchTemplateIconCustomTemplate.style.opacity = templateSearchToggleCustom ? "1" : "0.5"; + + + } catch (error) { + + console.error("Error occurred during search:", error); + } + }); + + return { + openModal, + addSidebarStyles, + createContextualMenu, + renderList, + renameTemplate, + renameCategory, + colorTemplate, + removeKeyConfig, + searchT, + deleteCategory, + addTemplate, + editNodeTemplate, + removeNodeMapTemplate, + reorderTemplates, + reorderNodeInTemplate, + removeNodeFromTemplateCallback, + assignNodeToTemplateCallback, + renameTemplateCallback, + renameCategoryCallback, + colorTemplateCallback, + removeTemplateCallback, + resetColorTemplate + }; + + +})(); +custom_templates.addSidebarStyles("panels/custom_templates/style.css"); +custom_templates.createContextualMenu() +custom_templates.renderList("custom_templates_main"); \ No newline at end of file diff --git a/app/panels/custom_templates/style.css b/app/panels/custom_templates/style.css new file mode 100644 index 0000000..6c7abdd --- /dev/null +++ b/app/panels/custom_templates/style.css @@ -0,0 +1,141 @@ +#custom_templates_main,#custom_templates_header { + + justify-content: center; + align-items: center; + height: 100%; + margin: 0; +} +#custom_templates_main{ + +padding-top: 44px; +} +#custom_templates_header { + margin: 10px 20px; + font-size: 16px; + +} +#custom_templates_header #h_search { + position: absolute; + width: calc(100% - 115px); + margin-top: 0px; + + z-index: 400; + +} + +#custom_templates_header button { +/* padding: 10px 20px; */ +height: 35px; +width: 35px; +font-size: 16px; +background: var(--comfy-input-bg); +border: 2px solid var(--border-color); +color: var(--input-text); +border-radius: 5px; +float: right; +position: absolute; +margin: 2px 2px 2px 8px; +cursor: pointer; +} + +#custom_templates_header button:hover { + border: 2px solid var(--descrip-text); + +} +.sb-template-dialog { +outline: 0; +border: 0; +border-radius: 6px; +background: var(--bg-color); +color: #fff; +box-shadow: inset 1px 1px 0px rgba(255, 255, 255, 0.05), inset -1px -1px 0px rgba(0, 0, 0, 0.5), 2px 2px 20px rgb(0, 0, 0); +max-width: 800px; +min-width: 30rem; +box-sizing: border-box; +font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; +font-size: 1rem; +padding: 20px; +position: fixed; +top: 50%; +left: 50%; +z-index: 1001; +} + +#sb-template-form { +margin-top: 10px; +text-align: center; +} + +.sb-input { +width: 100%; +padding: 8px; +margin-bottom: 10px; +box-sizing: border-box; +border: 2px solid var(--border-color); +border-radius: 4px; +margin-top: 16px; +background: var(--bg-color); +color: var(--input-text); + +} +.sb-input:focus { +outline: none; +border: 2px solid var(--descrip-text); +} + +.sb-button { +background-color: var(--comfy-menu-bg); +color: var(--input-text); +padding: 10px 20px; +border: none; +border-radius: 4px; +cursor: pointer; +margin-top: 10px; +text-transform: uppercase; +font-weight: bold; +} + +.sb-button:hover { +filter: brightness(120%); +} + +#closeModalButton { +background-color: var(--comfy-menu-bg); +position: absolute; +top: 0; +right: 0; +margin: 0; +} + +#closeModalButton:hover { +filter: brightness(120%); + +} +#searchTemplateInput { +box-sizing: border-box; +width: 100%; +border-radius: 5px; +padding: 10px; +border: none; +user-select: none; +background: var(--comfy-input-bg); +color: var(--input-text); +line-height: 1.4; +border: 1px solid var(--border-color); +} + + +#previewDiv .sb_table { + + text-align: center; +} + +.sidebar #previewDiv .sidebarItem, #custom_templates_main .sidebarItem { + + max-width: 100% !important; +} + +#refreshlButton{ + margin-left: 48px !important; +} + diff --git a/app/panels/custom_workflows/custom_workflows.html b/app/panels/custom_workflows/custom_workflows.html new file mode 100644 index 0000000..a2c1d33 --- /dev/null +++ b/app/panels/custom_workflows/custom_workflows.html @@ -0,0 +1,12 @@ +
    + +
    +
    +
    \ No newline at end of file diff --git a/app/panels/custom_workflows/custom_workflows.jsb b/app/panels/custom_workflows/custom_workflows.jsb new file mode 100644 index 0000000..cc9d7e0 --- /dev/null +++ b/app/panels/custom_workflows/custom_workflows.jsb @@ -0,0 +1,1208 @@ +var custom_workflows = (function () { + let currentElement = null; + + async function createWorkflowPreview(nodeID, jsondata) { + try { + const req = JSON.stringify({ action: 'load', path: jsondata }); + + + const response = await fetch('/sidebar/workflow', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: req + }); + + const data = await response.json(); + const jsonData = data; // JSON.parse(jsondata); + + // Elimina i duplicati usando un Set + const nodeTypes = [...new Set(jsonData.nodes.map(node => node.type))]; + + let rows = ""; + nodeTypes.forEach(type => { + rows += `
  • ${type}
  • `; + }); + + return `
    +
    ${nodeID}
    +
    USED NODES
    + +
    `; + } catch (e) { + console.log(e); + return '
    ' + nodeID + '
    USED NODES
    '; + } + } + // MODAL + function createCustomHtml(type, name, elem = null) { + + if (type == "rename") { + return `

    Rename Category

    +
    + + +
    + + + ` + } else if (type == "color") { + currentElement = elem; + return `

    Color ${name}

    +
    + +
    + + + + + ` + }else if(type == "renameWorkflow"){ + elem = elem.replace(/\\/g, '/'); + return `

    Rename Workflow

    +
    + + +
    + + + ` + } + } + + + + function setModalContext(addon = null) { + if (addon == null) { + return ` +

    Add New Workflow

    +
    + + +
    + + `; + } + else { + return addon + } + } + + + + function openModal(addon = null) { + // Create HTML elements for the modal + const modalBackdrop = document.createElement('div'); + modalBackdrop.classList.add('sb-modal-backdrop'); + + const modal = document.createElement('div'); + modal.classList.add('sb-category-dialog'); + modal.innerHTML = setModalContext(addon) + + // Add the modal to the DOM + document.body.appendChild(modalBackdrop); + document.body.appendChild(modal); + + // Add an event listener for the close button + const closeModalButton = modal.querySelector('#closeModalButton'); + closeModalButton.addEventListener('click', closeModal); + try { + // Focus the input field + inputElement = document.getElementById('sb-workflowName'); + inputElement.focus(); + const length = inputElement.value.length; + inputElement.setSelectionRange(length, length); + } catch (err) { + + } + } + + + // Function to open the modal + + //insert button in label with id panel_title_custom_workflows + let workflowSearchToggleCustom = false; + const panel_title_custom_workflows = document.getElementById("panel_title_custom_workflows"); + + const button_custom_workflows = document.createElement("button"); + button_custom_workflows.classList = "expand_node"; + button_custom_workflows.innerHTML = ` + + + + + +`; + + panel_title_custom_workflows.appendChild(button_custom_workflows); + + button_custom_workflows.addEventListener("click", (event) => { + const clickedElement = event.target; + const pinButton = clickedElement.tagName; + if ((pinButton == "rect") && clickedElement.classList.contains("expand_node")) { + + sdExpandAll(); + } + + }) + const clearIconCustomWorkflow = document.querySelector(".clearIconCustomWorkflow"); + const itemSearchInput = document.getElementById("searchWorkflowInput"); + clearIconCustomWorkflow.addEventListener("click", async function () { + try { + + itemSearchInput.value = ""; + renderList("custom_workflows_main"); + + } catch (error) { + console.error("Error occurred during search:", error); + } + }); + + + + function afterRender() { + customMenuWorkflow = ` + { + "menuctx_workflow": "Context Menu", + "menuctx_options": [ + "Rename Category", + "Delete Category", + "Change Color" + ], + "menuctx_subOptions": [ + [ + ] + ], + "menuctx_opt_callback": [ + "custom_workflows.renameCategoryCallback", + "custom_workflows.removeCategory", + "custom_workflows.colorWorkflowCallback" + + ], + "menuctx_sub_opt_callback": [ + [ + ], + [ + ] + ] + }`; + + customMenuWorkflowItem = ` + { + "menuctx_workflow": "Context Menu", + "menuctx_options": [ + "Delete from Category", + "Rename Workflow", + "Delete Workflow" + ], + "menuctx_subOptions": [ + [ + ] + ], + "menuctx_opt_callback": [ + "custom_workflows.removeNodeFromWorkflowCallback", + "custom_workflows.renameWorkflowCallback", + "custom_workflows.removeWorkflowCallback" + + ], + "menuctx_sub_opt_callback": [ + [ + ], + [ + ] + ] + }`; + customMenuWorkflowItemOut = ` + { + "menuctx_workflow": "Context Menu", + "menuctx_options": [ + "Rename Workflow", + "Delete Workflow" + ], + "menuctx_subOptions": [ + [ + ] + ], + "menuctx_opt_callback": [ + "custom_workflows.renameWorkflowCallback", + "custom_workflows.removeWorkflowCallback" + + ], + "menuctx_sub_opt_callback": [ + [ + ], + [ + ] + ] + }`; + + customMenuJSON = JSON.parse(customMenuWorkflow); + customMenuItemJSON = JSON.parse(customMenuWorkflowItem); + customMenuItemJSONOut = JSON.parse(customMenuWorkflowItemOut); + + setContextMenu(customMenuJSON, "#custom_workflows_main .sidebarCategory", 0, "sidebarItem", "sidebarCategory"); + setContextMenu(customMenuItemJSONOut, "#custom_workflows_main .sidebarItem"); + setContextMenu(customMenuItemJSON, "#custom_workflows_main .displayNamesList .sidebarItem"); + + // ORDER CUSTOM CATEGORIES + const custom_workflows_main = document.getElementById("custom_workflows_main"); + + custom_workflows_main.querySelectorAll(".sidebarCategory").forEach(function (item) { + item.addEventListener("dragstart", handleDragStart); + }); + + custom_workflows_main.removeEventListener("dragover", handleDragOver); + custom_workflows_main.addEventListener("dragover", handleDragOver); + + custom_workflows_main.removeEventListener("drop", handleCustomWorkflowsMainDrop); + custom_workflows_main.addEventListener("drop", handleCustomWorkflowsMainDrop); + + //workflow drag and drop + custom_workflows_main.querySelectorAll(".sidebarItem").forEach(function (item) { + item.addEventListener("dragstart", handleDragStart); + }); + + sidebar.removeEventListener("dragstart", handleSidebarDragStart); + sidebar.addEventListener("dragstart", handleSidebarDragStart); + + document.removeEventListener("drop", handleDocumentDropEvent); + document.addEventListener("drop", handleDocumentDropEvent); + + // ORDER CUSTOM NODES + custom_workflows_main.querySelectorAll(".sidebarItem").forEach(function (item) { + item.addEventListener("dragstart", handleDragStart); + }); + } + + + + + // handler + + function handleDragStart(event) { + dragItem = event.target; + } + + function handleDragOver(event) { + event.preventDefault(); + } + + function handleCustomWorkflowsMainDrop(event) { + event.preventDefault(); + if (dragItem) { + if (draggedElement.id !== 'sidebarWorkflowItem') { + if (dragItem.parentElement === event.target.parentElement) { + const custom_workflows_main = document.getElementById("custom_workflows_main"); + custom_workflows_main.insertBefore(dragItem, event.target); + reorderWorkflows(getCustomWorkflowOrder()); + } + } + dragItem = null; + } + } + + function handleSidebarDragStart(event) { + draggedElement = event.target; + } + + async function handleDocumentDropEvent(event) { + //prevent default action + event.preventDefault(); + const sb_workflow_replace = JSON.parse(await getConfiguration('sb_workflow_replace', true)) + if (draggedElement.id === 'sidebarWorkflowItem' && event.target.id === 'graph-canvas') { + + + if (sb_workflow_replace) { + const confirmation = confirm("This will replace the current workflow. Are you sure?"); + if (!confirmation) { return; } + } + //post request to /sidebar/workflow with json with path + const req = JSON.stringify({ action: 'load', path: draggedElement.dataset.data }); + fetch('/sidebar/workflow', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: req + }) + .then(response => response.json()) + .then(data => { + app.loadGraphData(data); + }); + + } else if (draggedElement.id === 'sidebarWorkflowItem' && (event.target.classList.contains('sidebarCategory') || event.target.classList.contains('displayNamesList'))) { + const ulElement = event.target.querySelector('ul'); + if (ulElement) { + ulElement.appendChild(draggedElement); + } else { + event.target.appendChild(draggedElement); + } + + updateConfiguration('sb_workflowNodeMap', JSON.stringify(getCustomWorkflowStructure())); + renderList("custom_workflows_main"); + } else if (draggedElement.id === 'sidebarWorkflowItem' && event.target.parentElement.parentElement.classList.contains('sidebarCategory')) { + event.target.parentElement.insertBefore(draggedElement, event.target); + updateConfiguration('sb_workflowNodeMap', JSON.stringify(getCustomWorkflowStructure())); + renderList("custom_workflows_main"); + } + } + + + + + + + // ORDER CUSTOM CATEGORIES + + function getCustomWorkflowOrder() { + let cat = []; + const custom_workflows_main = document.getElementById("custom_workflows_main"); + custom_workflows_main.querySelectorAll(".sidebarCategory").forEach(function (item) { + cat.push(item.dataset.nameworkflow) + + }); + return cat + } + + + function getCustomNodeOrder(idcat) { + let node = []; + const objMain = document.getElementById(idcat); + objMain.querySelectorAll(".sidebarItem").forEach(function (item) { + node.push(item.dataset.nameworkflow) + + }); + return node + } + + function getCustomWorkflowStructure() { + let cat = {}; + const custom_workflows_main = document.getElementById("custom_workflows_main"); + custom_workflows_main.querySelectorAll(".sidebarCategory").forEach(function (item) { + + cat[item.dataset.nameworkflow] = getCustomNodeOrder(item.dataset.nameworkflow + "_ul") + + }); + + return cat + } + + async function readComfyWorkflows() { + let comfy_workflows; + try { + const response = await fetch('./sidebar/workflows'); + comfy_workflows = await response.json(); + + } catch (error) { + console.error('Error fetching data:', error); + } + return comfy_workflows + + } + + + + async function renderList(elementID) { + const data = await readComfyWorkflows() //load all workflows + + const myPromise = new Promise(async (resolve, reject) => { + const workflows = await readWorkflows(); + + + //console.log(workflows); + const sidebarCustomNodes = document.getElementById(elementID); + //clear elementid + sidebarCustomNodes.innerHTML = ""; + + exclusion_list = [] + for (const workflow in workflows) { + //data[objKey].title.toLowerCase(); + let workflowName = workflows[workflow] + const workflowItem = document.createElement("li"); + workflowItem.classList.add("sidebarCategory"); + workflowItem.dataset.nameworkflow = workflowName; + workflowItem.textContent = workflowName; + workflowItem.draggable = true; + + const currentColor = await getValueFromConfig("sb_ColorCustomWorkflows", workflowName); + + if (currentColor) { + const value_perc = await getConfiguration("sb_opacity") || "1.0"; + + const rgbColor = hexToRgb(currentColor); + + workflowItem.style.backgroundColor = `rgba(${rgbColor}, ${value_perc})`; + } + + + + + const displayNamesList = document.createElement("ul"); + displayNamesList.id = workflowName + "_ul"; + displayNamesList.classList.add("displayNamesList"); + displayNamesList.style.display = getNodeStatus(workflowName); + workflowItem.appendChild(displayNamesList); + + let displayName = workflow; + + + const listNodeForWorkflow = await getNodesForWorkflow(workflowName); + + + + listNodeForWorkflow.forEach(displayName => { + try { + const displayNameItem = document.createElement("li"); + displayNameItem.classList.add("sidebarItem"); + displayNameItem.id = "sidebarWorkflowItem" + displayNameItem.textContent = displayName; + displayNameItem.dataset.nameworkflow = displayName; + displayNameItem.dataset.data = data[displayName]; + displayNameItem.draggable = true; + + + + + + exclusion_list.push(displayName); + + + displayNamesList.appendChild(displayNameItem); + + + displayNamesList.addEventListener("drop", function (event) { + event.preventDefault(); + + if (dragItem) { + + if (dragItem.parentElement == event.target.parentElement) { + + displayNamesList.insertBefore(dragItem, event.target); + + reorderNodeInWorkflow(workflowItem.dataset.nameworkflow, getCustomNodeOrder(dragItem.parentElement.id)) + + } + dragItem = null; + + + } + }); + } + catch (err) { + console.log(err); + } + }) + + try { + sidebarCustomNodes.appendChild(workflowItem); + } catch (err) { + console.log(err); + } + } + + //adding workflows not categorized + let space = document.createElement("br"); + sidebarCustomNodes.appendChild(space); + + let comfy_workflows = await readComfyWorkflows(); + for (const workflow in comfy_workflows) { + + //data[objKey].title.toLowerCase(); + + + //if workflowname is not in exclusion list + if (!exclusion_list.includes(workflow)) { + + + const workflowItem = document.createElement("li"); + workflowItem.classList.add("sidebarItem"); + workflowItem.id = "sidebarWorkflowItem"; + workflowItem.dataset.nameworkflow = workflow; + workflowItem.dataset.data = comfy_workflows[workflow]; + workflowItem.textContent = workflow; + workflowItem.draggable = true; + + + + + + let displayName = workflow; + + + + try { + sidebarCustomNodes.appendChild(workflowItem); + } catch (err) { + console.log(err); + } + } + + + + + } + + // Preview + + const sidebarItems_cat = document.querySelectorAll('#' + elementID + ' .sidebarItem'); + const previewDiv = document.getElementById('previewDiv'); + let sidebad_view_width = document.getElementById("sidebar_views").offsetWidth; + + sidebarItems_cat.forEach(item => { + + item.addEventListener('mouseover', async function () { + const preview = JSON.parse(await getConfiguration('sb_workflow_preview', true)) + + // Preview + if (preview) { + if (this.classList.contains('sidebarItem') && this.tagName === 'LI') { + let descriptionDiv = ""; + const itemPosition = getElementPosition(this); + let previewDivTop = 0; + const previewContent = await createWorkflowPreview(item.dataset.nameworkflow, item.dataset.data); + + previewDiv.innerHTML = previewContent + descriptionDiv; + previewDiv.style.display = 'block'; + const correction_offset = 45; + + if (itemPosition.top - this.offsetHeight >= 0 && itemPosition.top + previewDiv.offsetHeight < document.body.offsetHeight) { + previewDivTop = itemPosition.top - this.offsetHeight + + } else if (itemPosition.top - this.offsetHeight - previewDiv.offsetHeight <= 0) { + previewDivTop = 0 + correction_offset; + + + } + else { + + previewDivTop = (itemPosition.top + this.offsetHeight) - previewDiv.offsetHeight; + } + + let sidebar_width = parseInt(getVar("sidebarWidth")) || 500; + + previewDiv.style.top = `${previewDivTop}px`; + + const previewDivLeft = sidebar_width - sidebad_view_width; + + if (sbPosition == "left") { + + previewDiv.style.left = `${previewDivLeft}px`; + } else { + + previewDiv.style.right = `${previewDivLeft}px`; + } + + + + } + } + }); + + item.addEventListener('mouseout', function () { + previewDiv.style.display = 'none'; + }) + + }); + + + + + + + + + window.addEventListener('click', function (event) { + + if (!event.target.classList.contains('sidebarItem')) { + previewDiv.style.display = 'none'; + + } + }); + + + + + + + + const workflowItems = document.querySelectorAll("#custom_workflows_main .sidebarCategory"); + workflowItems.forEach(function (workflowItem) { + workflowItem.addEventListener("click", function (event) { + + if (event.target === event.currentTarget) { + const displayNamesList = event.target.querySelector("ul"); + + displayNamesList.style.display = displayNamesList.style.display === "none" ? "block" : "none"; + + setNodeStatus(displayNamesList.parentElement.dataset.nameworkflow, displayNamesList.style.display) + } + }); + + + + }); + resolve(afterRender); + + + + + + }); + + + myPromise.then((callback) => { + + callback(); + }).catch((error) => { + console.error("An error occurred:", error); + }); + } + + + + + + function searchT(e) { + + if (e.value != "") { + //search in all .sidebarItem + handleSearch(workflowSearchToggleCustom, "#custom_workflows_main", "searchWorkflowInput") + + + } else { + + renderList("custom_workflows_main"); + } + + } + + + + async function getNodeMap() { + let workflowNodeMap = JSON.parse(await getConfiguration('sb_workflowNodeMap')) || {}; + return workflowNodeMap; + } + + function getNodesStatus() { + let workflowNodeStatus = JSON.parse(localStorage.getItem('sb_workflowNodeStatus')) || {}; + return workflowNodeStatus; + } + + + async function addWorkflow() { + + + const newWorkflowName = document.getElementById('sb-workflowName').value; + // Check if the workflow name contains special characters + if (containsSpecialCharacters(newWorkflowName)) { + // Alert the user and return if special characters are found + alert("Workflow name cannot contain special characters."); + return; + } + + + + + let workflowNodeMap = await getNodeMap(); + + + if (!workflowNodeMap[newWorkflowName]) { + + workflowNodeMap[newWorkflowName] = []; + updateConfiguration('sb_workflowNodeMap', JSON.stringify(workflowNodeMap)); + createContextualMenu(); + reloadCtxMenu() + renderList("custom_workflows_main"); + closeModal() + } else { + alert("Workflow already exists!"); + return; + } + } + + + // Function to rename a workflow + + async function renameCategory(oldWorkflowName, newWorkflowName) { + + + if (containsSpecialCharacters(newWorkflowName)) { + // Alert the user and return if special characters are found + alert("Workflow name cannot contain special characters."); + return; + } + if (oldWorkflowName === newWorkflowName) { + closeModal() + return; + } + + let workflowNodeMap = await getNodeMap(); + + if (workflowNodeMap[newWorkflowName]) { + alert("Category already exists!"); + return; + } + + + + let workflowNodeStatus = getNodesStatus(); + if (workflowNodeMap[oldWorkflowName]) { + // Categoty Current Order + let keys = Object.keys(workflowNodeMap); + + workflowNodeMap[newWorkflowName] = workflowNodeMap[oldWorkflowName]; + delete workflowNodeMap[oldWorkflowName]; + + // Rebuild Order + let updatedMap = {}; + keys.forEach(key => { + if (key === oldWorkflowName) { + updatedMap[newWorkflowName] = workflowNodeMap[newWorkflowName]; + } else { + updatedMap[key] = workflowNodeMap[key]; + } + }); + + + + updateConfiguration('sb_workflowNodeMap', JSON.stringify(updatedMap)); + //rename status + if (workflowNodeStatus[oldWorkflowName]) { + + workflowNodeStatus[newWorkflowName] = workflowNodeStatus[oldWorkflowName]; + delete workflowNodeStatus[oldWorkflowName]; + localStorage.setItem('sb_workflowNodeStatus', JSON.stringify(workflowNodeStatus)); + } + //rename color + renameKeyConfig("sb_ColorCustomWorkflows", oldWorkflowName, newWorkflowName) + + + createContextualMenu(); + reloadCtxMenu() + renderList("custom_workflows_main"); + closeModal() + } else { + alert("Workflow not found!"); + } + } + async function readWorkflows() { + let workflowNodeMap = await getNodeMap(); + if (workflowNodeMap) { + return Object.keys(workflowNodeMap); + } + else { + return []; + } + + } + // Function to delete a workflow + async function removeCategory(e) { + + const name = e.target.dataset.nameworkflow; + confirmation = confirm("Are you sure you want to delete the category " + name + "?"); + if (!confirmation) { return; } + + let workflowNodeMap = await getNodeMap(); + // If the workflow name is found, remove it from the array + if (workflowNodeMap[name]) { + removeNodeMapWorkflow(name); + createContextualMenu(); + removeKeyFromConfig("sb_ColorCustomWorkflows", name) + + setTimeout(() => { + + renderList("custom_workflows_main"); + }, 200) + reloadCtxMenu() + } else { + alert("Workflow not found!"); + } + } + + async function renameWorkflow(path,oldWorkflowName, newWorkflowName) { + + if (containsSpecialCharacters(newWorkflowName, level=1)) { + // Alert the user and return if special characters are found + alert("Workflow name cannot contain special characters."); + return; + } + await fetch('./sidebar/workflow', { + method: 'POST', + body: JSON.stringify({ + action: 'rename', + path: path, + newName: newWorkflowName + }), + headers: { + 'Content-Type': 'application/json' + } + }).then(response => response.json()) + .then(data => { + + if (data.result == 'OK') { + + renameWorkflowNode("sb_workflowNodeMap", oldWorkflowName, newWorkflowName) + + const element = document.getElementById('panel_workflows'); + let position = element.scrollTop; + closeModal() + renderList("custom_workflows_main"); + + //little hack + setTimeout(() => { + element.scrollTop = position; + },200); + } else { + alert(data.result); + } + + + }); + + + } + async function renameWorkflowNode(key, oldNodeName, newNodeName) { + let jsonObject = await getConfig(key); + + // Parse the sb_workflowNodeMap property to an object + let workflowNodeMap = jsonObject + // Iterate over the workflow node map to find and replace the node name + for (let workflow in workflowNodeMap) { + let nodes = workflowNodeMap[workflow]; + let nodeIndex = nodes.indexOf(oldNodeName); + if (nodeIndex !== -1) { + nodes[nodeIndex] = newNodeName; + break; // Assuming the node names are unique, we can break once we find it + } + } + + // Convert the modified JSON object back to a JSON string + updateConfiguration(key, JSON.stringify(jsonObject)); + } + + + async function removeWorkflowNode(key, value) { + + let jsonObject = await getConfig(key); + let workflowNodeMap = jsonObject + // Iterate over the workflow node map to find and remove the node name + for (let workflow in workflowNodeMap) { + let nodes = workflowNodeMap[workflow]; + let nodeIndex = nodes.indexOf(value); + if (nodeIndex !== -1) { + nodes.splice(nodeIndex, 1); // Remove the node from the array + break; // Assuming the node names are unique, we can break once we find it + } + } + + updateConfiguration(key, JSON.stringify(jsonObject)); + } + + + async function removeWorkflow(name,path) { + + confirmation = confirm("Are you sure you want to delete the workflow " + name + "?"); + if (!confirmation) { return; } + await fetch('./sidebar/workflow', { + method: 'POST', + body: JSON.stringify( + { + action: 'delete', + path: path + } + ), + headers: { + 'Content-Type': 'application/json' + } + }).then(() => { + removeWorkflowNode("sb_workflowNodeMap", name) + renderList("custom_workflows_main"); + + }); + + + } + + + + + ///////////////////////////////////////////////// + + async function getConfig(configName) { + let workflowNodeMap = JSON.parse(await getConfiguration(configName)) || {}; + return workflowNodeMap; + } + + + + async function getValueFromConfig(configName, key) { + let config = await getConfig(configName); + return config[key] || null; + } + + async function assignValueToConfig(configName, key, value) { + let config = await getConfig(configName); + config[key] = value; + updateConfiguration(configName, JSON.stringify(config)); + } + + async function removeKeyFromConfig(configName, key) { + let config = await getConfig(configName); + delete config[key]; + updateConfiguration(configName, JSON.stringify(config)); + + + } + async function renameKeyConfig(configName, oldKey, newKey) { + let config = await getConfig(configName); + let value = config[oldKey]; + delete config[oldKey]; + config[newKey] = value; + updateConfiguration(configName, JSON.stringify(config)); + } + + async function removeKeyConfig(configName) { + removeConfiguration(configName) + renderList("custom_workflows_main"); + } + + + + + + + + //////////////////////////////////////////////// + + async function getWorkflowsForNode(nodeName) { + let workflowNodeMap = await getNodeMap(); + + let workflows = []; + for (let workflow in workflowNodeMap) { + if (workflowNodeMap.hasOwnProperty(workflow)) { + if (workflowNodeMap[workflow].includes(nodeName)) { + workflows.push(workflow); + } + } + } + return workflows; + } + + async function getNodesForWorkflow(workflowName) { + let workflowNodeMap = await getNodeMap(); + + + return workflowNodeMap[workflowName] || []; + } + + + async function assignNodeToWorkflow(nodeName, workflowNames) { + let workflowNodeMap = await getNodeMap(); + + let assignedWorkflows = await getWorkflowsForNode(nodeName); + workflowNames = workflowNames.filter(workflow => !assignedWorkflows.includes(workflow)); + + workflowNames.forEach(workflowName => { + if (!workflowNodeMap[workflowName]) { + workflowNodeMap[workflowName] = []; + } + workflowNodeMap[workflowName].push(nodeName); + }); + updateConfiguration('sb_workflowNodeMap', JSON.stringify(workflowNodeMap)); + renderList("custom_workflows_main"); + } + + + async function removeNodeFromWorkflow(nodeName, workflowName) { + let workflowNodeMap = await getNodeMap(); + + if (workflowNodeMap[workflowName]) { + workflowNodeMap[workflowName] = workflowNodeMap[workflowName].filter(node => node !== nodeName); + updateConfiguration('sb_workflowNodeMap', JSON.stringify(workflowNodeMap)); + renderList("custom_workflows_main"); + + } + } + + + async function removeNodeMapWorkflow(workflowName) { + let workflowNodeMap = await getNodeMap(); + let workflowNodeStatus = getNodesStatus(); + delete workflowNodeMap[workflowName]; + updateConfiguration('sb_workflowNodeMap', JSON.stringify(workflowNodeMap)); + + if (workflowNodeStatus[workflowName]) { + delete workflowNodeStatus[workflowName]; + localStorage.setItem('sb_workflowNodeStatus', JSON.stringify(workflowNodeStatus)); + + } + } + + + function editNodeWorkflow(oldWorkflowName, newWorkflowName, nodeName) { + + removeNodeFromWorkflow(nodeName, oldWorkflowName); + + assignNodeToWorkflow(nodeName, [newWorkflowName]); + } + + + + async function reorderWorkflows(orderedKeys) { + let workflowNodeMap = await getNodeMap(); + let reorderedWorkflowNodeMap = {}; + + + orderedKeys.forEach(workflowKey => { + if (workflowNodeMap.hasOwnProperty(workflowKey)) { + reorderedWorkflowNodeMap[workflowKey] = workflowNodeMap[workflowKey]; + } + }); + + updateConfiguration('sb_workflowNodeMap', JSON.stringify(reorderedWorkflowNodeMap)); + + + } + + + async function reorderNodeInWorkflow(workflowName, orderedKeys) { + let workflowNodeMap = await getNodeMap(); + + if (workflowNodeMap[workflowName]) { + workflowNodeMap[workflowName] = orderedKeys; + updateConfiguration('sb_workflowNodeMap', JSON.stringify(workflowNodeMap)); + } + } + + // Callbacks + function removeNodeFromWorkflowCallback(e, trge) { + + removeNodeFromWorkflow(e.target.dataset.nameworkflow, e.target.parentElement.parentElement.dataset.nameworkflow) + } + + function assignNodeToWorkflowCallback(e, trge) { + assignNodeToWorkflow(e.target.id, [trge]); + } + + function renameWorkflowCallback(e, trge) { + const name = e.target.dataset.nameworkflow; + const path = e.target.dataset.data; + openModal(createCustomHtml("renameWorkflow", name, path)) + + } + + function renameCategoryCallback(e, trge) { + const name = e.target.dataset.nameworkflow; + + openModal(createCustomHtml("rename", name)) + + } + + + function removeWorkflowCallback(e, trge) { + const name = e.target.dataset.nameworkflow; + const path = e.target.dataset.data; + + removeWorkflow(name,path) + } + function colorWorkflowCallback(e) { + + const name = e.target.dataset.nameworkflow; + openModal(createCustomHtml("color", name, e.target)) + //colorWorkflow(e.target.id, e.target.parentElement.parentElement.dataset.nameworkflow) + } + + + + async function colorWorkflow(name, value) { + + + const value_perc = await getConfiguration("sb_opacity") || "1.0"; + const rgbColor = hexToRgb(value); + + currentElement.style.backgroundColor = `rgba(${rgbColor}, ${value_perc})`; + + + assignValueToConfig("sb_ColorCustomWorkflows", name, value) + } + // Add an event listener to the button to open the modal + function resetColorWorkflow(name) { + + removeKeyFromConfig("sb_ColorCustomWorkflows", name) + renderList("custom_workflows_main"); + + } + + + async function createContextualMenu() { + + //const menuOptions = document.getElementById('menu-options'); + new_menu_options_callback = []; + + //for each workflow + workflow_menu = await readWorkflows(); + callback_menu = []; + for (let i = 0; i < workflow_menu.length; i++) { + callback_menu.push("assignNodeToWorkflowCallback"); + + } + + + console.log("custom workflow panel loaded") + + } + + + + + const searchWorkflowIconCustomWorkflow = document.querySelector(".searchWorkflowIconCustomWorkflow"); + searchWorkflowIconCustomWorkflow.addEventListener("click", async function () { + + try { + + workflowSearchToggleCustom = !workflowSearchToggleCustom; + searchWorkflowIconCustomWorkflow.style.opacity = workflowSearchToggleCustom ? "1" : "0.5"; + + + } catch (error) { + + console.error("Error occurred during search:", error); + } + }); + + return { + openModal, + addSidebarStyles, + createContextualMenu, + renderList, + renameCategory, + renameWorkflow, + colorWorkflow, + removeKeyConfig, + searchT, + removeCategory, + removeWorkflow, + addWorkflow, + editNodeWorkflow, + removeNodeMapWorkflow, + reorderWorkflows, + reorderNodeInWorkflow, + removeNodeFromWorkflowCallback, + removeWorkflowCallback, + assignNodeToWorkflowCallback, + renameWorkflowCallback, + renameCategoryCallback, + colorWorkflowCallback, + resetColorWorkflow + }; + + +})(); +custom_workflows.addSidebarStyles("panels/custom_workflows/style.css"); +custom_workflows.createContextualMenu() +custom_workflows.renderList("custom_workflows_main"); \ No newline at end of file diff --git a/app/panels/custom_workflows/style.css b/app/panels/custom_workflows/style.css new file mode 100644 index 0000000..0f678dc --- /dev/null +++ b/app/panels/custom_workflows/style.css @@ -0,0 +1,139 @@ +#custom_workflows_main,#custom_workflows_header { + + justify-content: center; + align-items: center; + height: 100%; + margin: 0; +} +#custom_workflows_main{ + +padding-top: 44px; +} +#custom_workflows_header { + margin: 10px 20px; + font-size: 16px; + +} +#custom_workflows_header #h_search { + position: absolute; + width: calc(100% - 115px); + margin-top: 0px; + + z-index: 400; + +} + +#custom_workflows_header button { +/* padding: 10px 20px; */ +height: 35px; +width: 35px; +font-size: 16px; +background: var(--comfy-input-bg); +border: 2px solid var(--border-color); +color: var(--input-text); +border-radius: 5px; +float: right; +position: absolute; +margin: 2px 2px 2px 8px; +cursor: pointer; +} + +#custom_workflows_header button:hover { + border: 2px solid var(--descrip-text); + +} +.sb-workflow-dialog { +outline: 0; +border: 0; +border-radius: 6px; +background: var(--bg-color); +color: #fff; +box-shadow: inset 1px 1px 0px rgba(255, 255, 255, 0.05), inset -1px -1px 0px rgba(0, 0, 0, 0.5), 2px 2px 20px rgb(0, 0, 0); +max-width: 800px; +min-width: 30rem; +box-sizing: border-box; +font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; +font-size: 1rem; +padding: 20px; +position: fixed; +top: 50%; +left: 50%; +z-index: 1001; +} + +#sb-workflow-form { +margin-top: 10px; +text-align: center; +} + +.sb-input { +width: 100%; +padding: 8px; +margin-bottom: 10px; +box-sizing: border-box; +border: 2px solid var(--border-color); +border-radius: 4px; +margin-top: 16px; +background: var(--bg-color); +color: var(--input-text); + +} +.sb-input:focus { +outline: none; +border: 2px solid var(--descrip-text); +} + +.sb-button { +background-color: var(--comfy-menu-bg); +color: var(--input-text); +padding: 10px 20px; +border: none; +border-radius: 4px; +cursor: pointer; +margin-top: 10px; +text-transform: uppercase; +font-weight: bold; +} + +.sb-button:hover { +filter: brightness(120%); +} + +#closeModalButton { +background-color: var(--comfy-menu-bg); +position: absolute; +top: 0; +right: 0; +margin: 0; +} + +#closeModalButton:hover { +filter: brightness(120%); + +} +#searchWorkflowInput { +box-sizing: border-box; +width: 100%; +border-radius: 5px; +padding: 10px; +border: none; +user-select: none; +background: var(--comfy-input-bg); +color: var(--input-text); +line-height: 1.4; +border: 1px solid var(--border-color); +} + + +#previewDiv .sb_table { + + text-align: center; +} + +.sidebar #previewDiv .sidebarItem, #custom_workflows_main .sidebarItem { + + max-width: 100% !important; +} +#refreshlButton{ + margin-left: 48px !important; +} \ No newline at end of file diff --git a/app/views/views.json b/app/views/views.json index ab69457..4ebf518 100644 --- a/app/views/views.json +++ b/app/views/views.json @@ -1,11 +1,31 @@ [ { - "id": "custom_view", + "id": "custom_categories", "icon": "⧉", "description": "Custom Node Categories", "panels": [ "custom_categories" + + ] + }, + + { + "id": "templates", + "icon": "Τ", + "description": "Custom Node templates", + "panels": [ + "custom_templates" + ] + }, + + { + "id": "workflows", + "icon": "W", + "description": "Custom Node workflows", + "panels": [ + "custom_workflows" ] } + ] diff --git a/images/settings.png b/images/settings.png index c3b1c38..a1860b1 100644 Binary files a/images/settings.png and b/images/settings.png differ diff --git a/images/templates.gif b/images/templates.gif new file mode 100644 index 0000000..a990241 Binary files /dev/null and b/images/templates.gif differ diff --git a/images/workflows.gif b/images/workflows.gif new file mode 100644 index 0000000..0ddede5 Binary files /dev/null and b/images/workflows.gif differ diff --git a/pyproject.toml b/pyproject.toml index 66ba26c..edb5028 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-n-sidebar" description = "A simple sidebar for ComfyUI." -version = "1.0.1" +version = "1.1.0" license = "LICENSE" [project.urls]