diff --git a/README.md b/README.md
index 310d135..d5a546b 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,11 @@ to your ComfyUI `custom_nodes` directory
1. Navigate to the cloned repo e.g. `custom_nodes/ComfyUI-N-Sidebar`
2. `git pull`
+# Keyboard Shortcuts
+
+- `Alt+Z` to toggle show/hide sidebar
+- `Alt+X` to focus on the search field
+
# Features
@@ -47,12 +52,17 @@ to your ComfyUI `custom_nodes` directory
### 🔁Reorder Nodes🔁

+
**Note**: The node will be placed **before** the element on which it is dragged!
+### 👁 Preview Node 👁
+
+
### Todo:
-[x] Reordering pinned nodes
-[ ] Node preview (i don't think it will be an image)
-[ ] Color integration with Jovimetrix
+- [x] Reordering pinned nodes
+- [x] Node preview (i don't think it will be an image)
+- [x] Color integration with Jovimetrix
+- [ ] Better search
## Contributing
diff --git a/images/preview.gif b/images/preview.gif
new file mode 100644
index 0000000..ba7b625
Binary files /dev/null and b/images/preview.gif differ
diff --git a/js/fts_fuzzy_match.js b/js/fts_fuzzy_match.js
new file mode 100644
index 0000000..c00af8b
--- /dev/null
+++ b/js/fts_fuzzy_match.js
@@ -0,0 +1,282 @@
+// LICENSE
+//
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// VERSION
+// 0.1.0 (2016-03-28) Initial release
+//
+// AUTHOR
+// Forrest Smith
+//
+// CONTRIBUTORS
+// J�rgen Tjern� - async helper
+// Anurag Awasthi - updated to 0.2.0
+
+const SEQUENTIAL_BONUS = 15; // bonus for adjacent matches
+const SEPARATOR_BONUS = 30; // bonus if match occurs after a separator
+const CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower
+const FIRST_LETTER_BONUS = 15; // bonus if the first letter is matched
+
+const LEADING_LETTER_PENALTY = -5; // penalty applied for every letter in str before the first match
+const MAX_LEADING_LETTER_PENALTY = -15; // maximum penalty for leading letters
+const UNMATCHED_LETTER_PENALTY = -1;
+
+/**
+ * Returns true if each character in pattern is found sequentially within str
+ * @param {*} pattern string
+ * @param {*} str string
+ */
+function fuzzyMatchSimple(pattern, str) {
+ let patternIdx = 0;
+ let strIdx = 0;
+ const patternLength = pattern.length;
+ const strLength = str.length;
+
+ while (patternIdx != patternLength && strIdx != strLength) {
+ const patternChar = pattern.charAt(patternIdx).toLowerCase();
+ const strChar = str.charAt(strIdx).toLowerCase();
+ if (patternChar == strChar) ++patternIdx;
+ ++strIdx;
+ }
+
+ return patternLength != 0 && strLength != 0 && patternIdx == patternLength
+ ? true
+ : false;
+}
+
+/**
+ * Does a fuzzy search to find pattern inside a string.
+ * @param {*} pattern string pattern to search for
+ * @param {*} str string string which is being searched
+ * @returns [boolean, number] a boolean which tells if pattern was
+ * found or not and a search score
+ */
+function fuzzyMatch(pattern, str) {
+ const recursionCount = 0;
+ const recursionLimit = 10;
+ const matches = [];
+ const maxMatches = 256;
+
+ return fuzzyMatchRecursive(
+ pattern,
+ str,
+ 0 /* patternCurIndex */,
+ 0 /* strCurrIndex */,
+ null /* srcMatces */,
+ matches,
+ maxMatches,
+ 0 /* nextMatch */,
+ recursionCount,
+ recursionLimit
+ );
+}
+
+function fuzzyMatchRecursive(
+ pattern,
+ str,
+ patternCurIndex,
+ strCurrIndex,
+ srcMatces,
+ matches,
+ maxMatches,
+ nextMatch,
+ recursionCount,
+ recursionLimit
+) {
+ let outScore = 0;
+
+ // Return if recursion limit is reached.
+ if (++recursionCount >= recursionLimit) {
+ return [false, outScore];
+ }
+
+ // Return if we reached ends of strings.
+ if (patternCurIndex === pattern.length || strCurrIndex === str.length) {
+ return [false, outScore];
+ }
+
+ // Recursion params
+ let recursiveMatch = false;
+ let bestRecursiveMatches = [];
+ let bestRecursiveScore = 0;
+
+ // Loop through pattern and str looking for a match.
+ let firstMatch = true;
+ while (patternCurIndex < pattern.length && strCurrIndex < str.length) {
+ // Match found.
+ if (
+ pattern[patternCurIndex].toLowerCase() === str[strCurrIndex].toLowerCase()
+ ) {
+ if (nextMatch >= maxMatches) {
+ return [false, outScore];
+ }
+
+ if (firstMatch && srcMatces) {
+ matches = [...srcMatces];
+ firstMatch = false;
+ }
+
+ const recursiveMatches = [];
+ const [matched, recursiveScore] = fuzzyMatchRecursive(
+ pattern,
+ str,
+ patternCurIndex,
+ strCurrIndex + 1,
+ matches,
+ recursiveMatches,
+ maxMatches,
+ nextMatch,
+ recursionCount,
+ recursionLimit
+ );
+
+ if (matched) {
+ // Pick best recursive score.
+ if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
+ bestRecursiveMatches = [...recursiveMatches];
+ bestRecursiveScore = recursiveScore;
+ }
+ recursiveMatch = true;
+ }
+
+ matches[nextMatch++] = strCurrIndex;
+ ++patternCurIndex;
+ }
+ ++strCurrIndex;
+ }
+
+ const matched = patternCurIndex === pattern.length;
+
+ if (matched) {
+ outScore = 100;
+
+ // Apply leading letter penalty
+ let penalty = LEADING_LETTER_PENALTY * matches[0];
+ penalty =
+ penalty < MAX_LEADING_LETTER_PENALTY
+ ? MAX_LEADING_LETTER_PENALTY
+ : penalty;
+ outScore += penalty;
+
+ //Apply unmatched penalty
+ const unmatched = str.length - nextMatch;
+ outScore += UNMATCHED_LETTER_PENALTY * unmatched;
+
+ // Apply ordering bonuses
+ for (let i = 0; i < nextMatch; i++) {
+ const currIdx = matches[i];
+
+ if (i > 0) {
+ const prevIdx = matches[i - 1];
+ if (currIdx == prevIdx + 1) {
+ outScore += SEQUENTIAL_BONUS;
+ }
+ }
+
+ // Check for bonuses based on neighbor character value.
+ if (currIdx > 0) {
+ // Camel case
+ const neighbor = str[currIdx - 1];
+ const curr = str[currIdx];
+ if (
+ neighbor !== neighbor.toUpperCase() &&
+ curr !== curr.toLowerCase()
+ ) {
+ outScore += CAMEL_BONUS;
+ }
+ const isNeighbourSeparator = neighbor == "_" || neighbor == " ";
+ if (isNeighbourSeparator) {
+ outScore += SEPARATOR_BONUS;
+ }
+ } else {
+ // First letter
+ outScore += FIRST_LETTER_BONUS;
+ }
+ }
+
+ // Return best result
+ if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
+ // Recursive score is better than "this"
+ matches = [...bestRecursiveMatches];
+ outScore = bestRecursiveScore;
+ return [true, outScore];
+ } else if (matched) {
+ // "this" score is better than recursive
+ return [true, outScore];
+ } else {
+ return [false, outScore];
+ }
+ }
+ return [false, outScore];
+}
+
+/**
+ * Strictly optional utility to help make using fts_fuzzy_match easier for large data sets
+ * Uses setTimeout to process matches before a maximum amount of time before sleeping
+ *
+ * To use:
+ * const asyncMatcher = new ftsFuzzyMatchAsync(fuzzyMatch, "fts", "ForrestTheWoods",
+ * function(results) { console.log(results); });
+ * asyncMatcher.start();
+ *
+ * @param {*} matchFn function Matching function - fuzzyMatchSimple or fuzzyMatch.
+ * @param {*} pattern string Pattern to search for.
+ * @param {*} dataSet array Array of string in which pattern is searched.
+ * @param {*} onComplete function Callback function which is called after search is complete.
+ */
+function ftsFuzzyMatchAsync(matchFn, pattern, dataSet, onComplete) {
+ const ITEMS_PER_CHECK = 1000; // performance.now can be very slow depending on platform
+ const results = [];
+ const max_ms_per_frame = 1000.0 / 30.0; // 30FPS
+ let dataIndex = 0;
+ let resumeTimeout = null;
+
+ // Perform matches for at most max_ms
+ function step() {
+ clearTimeout(resumeTimeout);
+ resumeTimeout = null;
+
+ var stopTime = performance.now() + max_ms_per_frame;
+
+ for (; dataIndex < dataSet.length; ++dataIndex) {
+ if (dataIndex % ITEMS_PER_CHECK == 0) {
+ if (performance.now() > stopTime) {
+ resumeTimeout = setTimeout(step, 1);
+ return;
+ }
+ }
+
+ var str = dataSet[dataIndex];
+ var result = matchFn(pattern, str);
+
+ // A little gross because fuzzy_match_simple and fuzzy_match return different things
+ if (matchFn === fuzzyMatchSimple && result == true) results.push(str);
+ else if (matchFn === fuzzyMatch && result[0] == true) results.push([result[1], str]);
+ }
+
+ onComplete(results);
+ return null;
+ }
+
+ // Abort current process
+ this.cancel = function() {
+ if (resumeTimeout !== null) clearTimeout(resumeTimeout);
+ };
+
+ // Must be called to start matching.
+ // I tried to make asyncMatcher auto-start via "var resumeTimeout = step();"
+ // However setTimout behaving in an unexpected fashion as onComplete insisted on triggering twice.
+ this.start = function() {
+ step();
+ };
+
+ // Process full list. Blocks script execution until complete
+ this.flush = function() {
+ max_ms_per_frame = Infinity;
+ step();
+ };
+}
+
+export { fuzzyMatch, fuzzyMatchSimple, ftsFuzzyMatchAsync };
diff --git a/js/sidebar_node.js b/js/sidebar_node.js
index a95be55..3d7f9ef 100644
--- a/js/sidebar_node.js
+++ b/js/sidebar_node.js
@@ -1,5 +1,21 @@
+
+import { api } from "../../../scripts/api.js"
+import { app } from "../../../scripts/app.js";
import { $el } from "../../../scripts/ui.js";
+
+console.time('execution time');
+async function api_get(url) {
+ var response = await api.fetchApi(url, { cache: "no-store" })
+ return await response.json()
+}
+
+let CUSTOM_COLORS;
+try {
+ const CONFIG_CORE = await api_get("/jovimetrix/config")
+ CUSTOM_COLORS = CONFIG_CORE?.user?.default?.color?.theme;
+} catch {}
+
let categorySearchToggle = false;
function addSidebarStyles() {
const sidebarStyle = `
@@ -10,15 +26,12 @@ function addSidebarStyles() {
.sidebar {
position: absolute;
top: 0;
- left: -250px;
+ left: -250px;
width: fit-content;
height: calc(100% - 19px);
-
color: white;
transition: left 0.3s ease;
z-index: 2;
- overflow: hidden;
-
padding-top: 19px;
left: 0;
user-select: none;
@@ -68,7 +81,7 @@ function addSidebarStyles() {
user-select: none;
background: #222;
color: #fff;
-
+ line-height: 1.4;
}
.sidebar-header {
position: absolute;
@@ -213,6 +226,7 @@ function addSidebarStyles() {
right: 0;
}
+
#sidebarBookmarks .pinButton {
background-color: transparent;
@@ -221,6 +235,14 @@ function addSidebarStyles() {
position: absolute;
right: 0;
}
+
+ .previewButton{
+ background-color: transparent;
+ border: 0;
+ position: absolute;
+ right: 35px;
+ }
+
.pinned{
fill: #999 !important;
opacity: 1 !important;
@@ -284,6 +306,128 @@ function addSidebarStyles() {
fill: white;
cursor: pointer;
}
+
+ .sb_dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: grey;
+ }
+ .node_header {
+ line-height: 1;
+ padding: 8px 13px 7px;
+ background: var(--border-color);
+ margin-bottom: 5px;
+ font-size: 15px;
+ text-wrap: nowrap;
+ overflow: hidden;
+ }
+ .headdot {
+ width: 10px;
+ height: 10px;
+ float: inline-start;
+ margin-right: 8px;
+ }
+ .sb_hidden {
+ display: none;
+ }
+
+ .IMAGE {
+ background-color: #64b5f6;
+ }
+
+ .VAE {
+ background-color: #ff6e6e;
+ }
+
+ .LATENT {
+ background-color: #ff9cf9;
+ }
+
+ .MASK{
+ background-color: #81c784;
+ }
+
+ .CONDITIONING{
+ background-color: #ffa931;
+ }
+
+ .CLIP{
+ background-color: #ffd500;
+ }
+
+ .MODEL{
+ background-color: #b39ddb;
+ }
+
+ .CONTROL_NET{
+ background-color: #a5d6a7;
+ }
+
+
+ #previewDiv {
+ position: absolute;
+ background-color: #353535;
+ font-family: 'Open Sans', sans-serif;
+ font-size: small;
+ color: var(--descrip-text);
+ border: 1px solid var(--descrip-text);
+ display: none;
+ min-width: 300px;
+ width: min-content;
+ height: fit-content;
+ z-index: 9999;
+ border-radius: 12px;
+ overflow: hidden;
+ font-size: 12px;
+ padding-bottom: 10px;
+
+ }
+ .sb_table {
+ display: grid;
+
+ grid-column-gap: 10px; /* Spazio tra le colonne */
+ width: 100%; /* Imposta la larghezza della tabella al 100% del contenitore */
+ }
+
+ .sb_row {
+ display: grid;
+ grid-template-columns: 10px 1fr 1fr 1fr 10px;
+ grid-column-gap: 10px;
+ align-items: baseline;
+ padding-left: 9px;
+ padding-right: 9px;
+ }
+
+ .sb_col {
+ border: 0px solid #000;
+ display: flex;
+ align-items: flex-end;
+ flex-direction: row-reverse;
+ flex-wrap: nowrap;
+ align-content: flex-start;
+ justify-content: flex-end;
+ }
+ .sb_inherit{
+ display: inherit;
+ }
+ .long_field{
+ background: var(--bg-color);
+ border: 2px solid var(--border-color);
+ margin: 5px 5px 0 5px;
+ border-radius: 10px;
+ line-height: 1.7;
+ }
+
+ .sb_arrow{
+ color: var(--fg-color);
+ }
+ .sb_preview_badge{
+ text-align: center;
+ background: black;
+ font-weight: bold;
+ color: var(--error-text);
+ }
`;
const styleElement = $el("style", {
@@ -368,8 +512,6 @@ function sidebarAddNode(name, text, x, y) {
function saveSidebarWidth(sideb) {
const width = sideb.style.width;
-
-
document.cookie = setCookie("sidebarWidth", width, 3000);
}
@@ -378,7 +520,6 @@ function restoreSidebarWidth() {
let width_sidebar = "auto";
let cookieValue = getCookie("sidebarWidth");
if (cookieValue) {
-
width_sidebar = cookieValue;
}
return width_sidebar;
@@ -520,19 +661,188 @@ function sdExpandAll() {
});
}
+function safeObjectKeys(obj) {
+ try {
+ return Object.keys(obj);
+ } catch (error) {
+ //console.error('Error while trying to get object keys:', error);
+ return [];
+ }
+ }
+function createNodePreview(nodeID) {
+
+ const data = LiteGraph.registered_node_types;
+
+ const category = data[nodeID].category;
+ let inputs = data[nodeID].nodeData.input; //divided between optional and required
+ let outputs_name = data[nodeID].nodeData.output_name;
+ let outputs = data[nodeID].nodeData.output;
+ //console.log(category, inputs, outputs);
+ //create array with objectkeys from input and output
+ const inputArray = [];
+ const outputArray = [];
+
+ const inputKeysRequired = safeObjectKeys(inputs.required)
+ const inputKeysOptional = safeObjectKeys(inputs.optional)
+
+
+ for (let i = 0; i < inputKeysRequired.length; i++) {
+ try{
+ let thirdV = null;
+ let secondV = inputs.required[inputKeysRequired[i]][0];
+
+ if (Array.isArray(secondV)){
+ //array
+ secondV = inputs.required[inputKeysRequired[i]][0][0];
+ thirdV = inputs.required[inputKeysRequired[i]][0][0];
+ }
+ if (inputs.required[inputKeysRequired[i]][1]!=undefined){
+
+ //object
+ thirdV = inputs.required[inputKeysRequired[i]][1].default;
+ }
+
+
+ inputArray.push([inputKeysRequired[i],secondV,thirdV]);
+ }catch(err){
+ console.log("error",err)
+ }
+ }
+
+ try{
+ for (let i = 0; i < inputKeysOptional.length; i++) {
+ let thirdV = null;
+ let secondV = inputs.optional[inputKeysOptional[i]][0];
+ if (Array.isArray(secondV)){
+ //array
+ secondV = inputs.optional[inputKeysOptional[i]][0][0];
+ thirdV = inputs.optional[inputKeysOptional[i]][0][0];
+ }
+ if (inputs.optional[inputKeysOptional[i]][1]!=undefined){
+
+ //object
+ thirdV = inputs.optional[inputKeysOptional[i]][1].default;
+ }
+ inputArray.push([inputKeysOptional[i],secondV,thirdV]);
+ }
+
+ }catch(err){
+ console.log(err)
+ }
+
+
+ for (let i = 0; i < outputs.length; i++) {
+ if (outputs_name[i] != undefined) {
+ outputArray.push([outputs[i],outputs_name[i]]);
+ }
+ else{
+ outputArray.push(outputs[i],outputs[i]);
+ }
+
+
+ }
+
+
+
+
+
+ let rows = "";
+ let last_rows = "";
+ let length_loop = outputArray.length;
+ if (inputArray.length> outputArray.length){
+ length_loop = inputArray.length;
+ }
+ for (let i = 0; i < length_loop; i++) {
+
+ const inputList= inputArray[i] ? inputArray[i] : null;
+ const outputList= outputArray[i] ? outputArray[i] : null;
+ let inputName = "";
+ let inputType = "";
+ let inputValue = null;
+ let outputType = "";
+ let outputName = "";
+
+ if (inputList != null){
+ inputName = inputList[0];
+ inputType = inputList[1];
+ inputValue = inputList[2];
+ }else
+ {
+ inputName = "";
+ inputType = "sb_hidden";
+ inputValue = null;
+ }
+ if (outputList != null){
+ outputType = outputList[0];
+ outputName = outputList[1];
+
+ }else
+ {
+ outputType = "sb_hidden";
+ outputName = "";
+ }
+
+
+ if (inputValue != null){
+ last_rows += `