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

-
### ➕Expand/Collapse Categories/Sidebar➖

@@ -92,14 +106,21 @@ This feature is enabled by default, you can disable it in the settings. I've use
### 👁 Preview Node 👁

+### 📄 Templates Support 📄
+
+
+#### 📄 Workflow Support 📄
+
+
### 🎨 ComfyUI Themes Support 🎨

### 🎨 New Settings Panel 🎨

-### 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 += ``
+
+ })
+ return ``;
+
+ }
+ // 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 += ``;
+ });
+
+ return ``;
+ } catch (e) {
+ console.log(e);
+ return '';
+ }
+ }
+ // 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]