diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html
index 9a0e6fb7a..b306316df 100644
--- a/html/extra-networks-card.html
+++ b/html/extra-networks-card.html
@@ -2,7 +2,6 @@
{background_image}
{button_row}
-
{search_terms}
{name}
{description}
diff --git a/javascript/clusterize.js b/javascript/clusterize.js
index e0fe8a883..245fd9d45 100644
--- a/javascript/clusterize.js
+++ b/javascript/clusterize.js
@@ -198,16 +198,8 @@ class Clusterize {
this.#max_items = max_items;
return this.#max_items !== max_items;
}
- if (this.#max_items === max_items) {
- // No change. do nothing.
- return false;
- }
- // If the number of items changed, we need to update the cluster.
+
this.#max_items = max_items;
- await this.refresh();
- // Apply sort to the updated data.
- await this.sortData();
- return true;
}
// ==== PRIVATE FUNCTIONS ====
@@ -266,6 +258,8 @@ class Clusterize {
// Filter is applied to entire dataset.
const max_items = await this.options.callbacks.filterData();
await this.setMaxItems(max_items);
+ await this.refresh(true);
+ await this.sortData();
}
#exploreEnvironment(rows, cache) {
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js
index d039c392a..606a89934 100644
--- a/javascript/extraNetworks.js
+++ b/javascript/extraNetworks.js
@@ -128,7 +128,7 @@ class ExtraNetworksTab {
this.setSortMode(sort_mode_elem.dataset.sortMode);
this.setSortDir(sort_dir_elem.dataset.sortDir);
- this.setFilterStr(this.txt_search_elem.value.toLowerCase());
+ this.setFilterStr(this.txt_search_elem.value);
this.registerPrompt();
@@ -214,9 +214,9 @@ class ExtraNetworksTab {
this.cards_list.setSortDir(this.sort_dir_str);
}
- setFilterStr(filter_str) {
+ setFilterStr(filter_str, is_dir) {
this.filter_str = filter_str;
- this.cards_list.setFilterStr(this.filter_str);
+ this.cards_list.setFilterStr(this.filter_str, is_dir === true);
}
movePrompt(show_prompt = true, show_neg_prompt = true) {
@@ -310,9 +310,9 @@ class ExtraNetworksTab {
this.cards_list.enable(false);
}
- applyFilter() {
+ applyFilter({is_dir = false} = {is_dir: false}) {
// We only want to filter/sort the cards list.
- this.setFilterStr(this.txt_search_elem.value.toLowerCase());
+ this.setFilterStr(this.txt_search_elem.value, is_dir);
// If the search input has changed since selecting a button to populate it
// then we want to disable the button that previously populated the search input.
@@ -460,10 +460,10 @@ class ExtraNetworksTab {
}
}
- updateSearch(text) {
+ updateSearch(text, ...args) {
this.txt_search_elem.value = text;
updateInput(this.txt_search_elem);
- this.applyFilter();
+ this.applyFilter(...args);
}
autoSetTreeWidth() {
@@ -761,7 +761,7 @@ function extraNetworksBtnDirsViewItemOnClick(event, tabname_full) {
_deselect_all_buttons();
// update search input with selected button's path.
elem.dataset.selected = "";
- txt_search_elem.value = elem.textContent.trim();
+ tab.updateSearch(elem.textContent.trim(), {is_dir: true});
// Select the corresponding tree view button.
if ("selected" in elem.dataset) {
@@ -774,7 +774,7 @@ function extraNetworksBtnDirsViewItemOnClick(event, tabname_full) {
const _deselect_button = (elem) => {
delete elem.dataset.selected;
- txt_search_elem.value = "";
+ tab.updateSearch("");
// deselect tree view rows
tab.tree_list.onRowSelected(); // empty params deselects all rows.
};
@@ -785,9 +785,6 @@ function extraNetworksBtnDirsViewItemOnClick(event, tabname_full) {
_select_button(event.target);
}
- updateInput(txt_search_elem);
- tab.applyFilter();
-
event.stopPropagation();
}
@@ -960,7 +957,8 @@ function extraNetworksTreeDirectoryOnClick(event, btn, tabname_full) {
});
}
- tab.updateSearch("selected" in btn.dataset ? btn.dataset.path : "");
+ const search_txt = "selected" in btn.dataset ? btn.dataset.path : "";
+ tab.updateSearch(search_txt, {is_dir: btn.dataset.treeEntryType === "dir" && search_txt !== ""});
}
const selected_elem = gradioApp().querySelector(".tree-list-item[data-selected='']");
if (isElement(prev_selected_elem) && !isElement(selected_elem)) {
diff --git a/javascript/extraNetworksClusterize.js b/javascript/extraNetworksClusterize.js
index 5b1a44fc5..3326014d3 100644
--- a/javascript/extraNetworksClusterize.js
+++ b/javascript/extraNetworksClusterize.js
@@ -40,6 +40,8 @@ class ExtraNetworksClusterize extends Clusterize {
tabname = "";
extra_networks_tabname = "";
+ filter_as_dir = false;
+
// Override base class defaults
default_sort_mode_str = "divId";
default_sort_dir_str = "ascending";
@@ -64,6 +66,7 @@ class ExtraNetworksClusterize extends Clusterize {
// can't use super class' sort since it relies on setup being run first.
// but we do need to make sure to sort the new data before continuing.
await this.setMaxItems(Object.keys(this.data_obj).length);
+ await this.refresh(true);
await this.options.callbacks.sortData();
}
@@ -138,15 +141,14 @@ class ExtraNetworksClusterize extends Clusterize {
this.sortData();
}
- setFilterStr(filter_str) {
- if (isString(filter_str) && this.filter_str !== filter_str.toLowerCase()) {
- this.filter_str = filter_str.toLowerCase();
- } else if (isNullOrUndefined(this.filter_str)) {
- this.filter_str = this.default_filter_str.toLowerCase();
- } else {
- return;
+ setFilterStr(filter_str, is_dir) {
+ if (isString(filter_str) && this.filter_str !== filter_str) {
+ this.filter_str = filter_str;
+ } else if (isNullOrUndefined(filter_str)) {
+ this.filter_str = this.default_filter_str;
}
+ this.filter_as_dir = is_dir === true;
this.filterData();
}
@@ -358,10 +360,9 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
}
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
- const max_items_changed = await this.setMaxItems(new_len);
- if (!max_items_changed) {
- await this.refresh(true);
- }
+ await this.setMaxItems(new_len);
+ await this.refresh(true);
+ await this.sortData();
}
async onCollapseAllClick(div_id) {
@@ -384,10 +385,9 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
}
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
- const max_items_changed = await this.setMaxItems(new_len);
- if (!max_items_changed) {
- await this.refresh(true);
- }
+ await this.setMaxItems(new_len);
+ await this.refresh(true);
+ await this.sortData();
}
async onRowExpandClick(div_id, elem) {
@@ -405,10 +405,9 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
}
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
- const max_items_changed = await this.setMaxItems(new_len);
- if (!max_items_changed) {
- await this.refresh(true);
- }
+ await this.setMaxItems(new_len);
+ await this.refresh(true);
+ await this.sortData();
}
async initData() {
@@ -493,7 +492,10 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize {
sortByPath(data) {
return Object.keys(data).sort((a, b) => {
- return STR_COLLATOR.compare(data[a].sort_path, data[b].sort_path);
+ // Wrap the paths in File objects to allow for proper sorting of filepaths.
+ const a_file = new File([""], data[a].sort_path);
+ const b_file = new File([""], data[b].sort_path);
+ return a_file - b_file;
});
}
@@ -586,7 +588,24 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize {
/** Filters data by a string and returns number of items after filter. */
let n_visible = 0;
for (const [div_id, v] of Object.entries(this.data_obj)) {
- let visible = v.search_terms.toLowerCase().indexOf(this.filter_str) != -1;
+ let visible;
+ if (this.filter_str && this.filter_as_dir) {
+ // Filtering as directory only shows direct children. Case sensitive
+ // comparison against the relative directory of each object.
+ visible = this.filter_str === v.rel_parent_dir;
+ } else if (v.search_only && this.filter_str.length >= 4) {
+ // Custom filter for items marked search_only=true.
+ // TODO: Not ideal. This disregards any search_terms set on the model.
+ // However the search terms are currently set up in a way that would
+ // reveal hidden models if the user searches for any visible parent
+ // directories. For example, searching for "Lora" would reveal a hidden
+ // model in "Lora/.hidden/model.safetensors" since that full path is
+ // included in the search terms.
+ visible = v.rel_parent_dir.toLowerCase().indexOf(this.filter_str.toLowerCase()) !== -1;
+ } else {
+ // All other filters treated case insensitive.
+ visible = v.search_terms.toLowerCase().indexOf(this.filter_str.toLowerCase()) !== -1;
+ }
if (v.search_only && this.filter_str.length < 4) {
visible = false;
}
@@ -595,7 +614,6 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize {
n_visible++;
}
}
-
return n_visible;
}
}
diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py
index 43932f91f..4e616b95d 100644
--- a/modules/ui_extra_networks.py
+++ b/modules/ui_extra_networks.py
@@ -49,6 +49,8 @@ class CardListItem(ListItem):
super().__init__(*args, **kwargs)
self.visible: bool = False
+ self.abspath = ""
+ self.relpath = ""
self.sort_keys = {}
self.search_terms = ""
self.search_only = False
@@ -244,7 +246,7 @@ class ExtraNetworksPage:
self.tree_roots = {}
def read_user_metadata(self, item, use_cache=True):
- filename = item.get("filename", None)
+ filename = os.path.normpath(item.get("filename", None))
metadata = extra_networks.get_user_metadata(filename, lister=self.lister if use_cache else None)
desc = metadata.get("description", None)
@@ -363,7 +365,7 @@ class ExtraNetworksPage:
"""Generates a row of buttons for use in Tree/Cards View items."""
metadata = item.get("metadata", None)
name = item.get("name", "")
- filename = item.get("filename", "")
+ filename = os.path.normpath(item.get("filename", ""))
button_row_tpl = '{btn_copy_path}{btn_edit_item}{btn_metadata}
'
@@ -420,7 +422,7 @@ class ExtraNetworksPage:
button_row = self.get_button_row(tabname, item)
- filename = item.get("filename", "")
+ filename = os.path.normpath(item.get("filename", ""))
# if this is true, the item must not be shown in the default view,
# and must instead only be shown when searching for it
show_hidden_models = str(shared.opts.extra_networks_hidden_models).strip().lower()
@@ -428,7 +430,7 @@ class ExtraNetworksPage:
search_only = False
else:
# If any parent dirs are hidden, the model is also hidden.
- search_only = any(os.path.basename(x).startswith(".") for x in Path(filename).parents)
+ search_only = any(x.startswith(".") for x in filename.split(os.sep))
if search_only and show_hidden_models == "never":
return ""
@@ -437,16 +439,6 @@ class ExtraNetworksPage:
for sort_mode, sort_key in item.get("sort_keys", {}).items():
sort_keys[sort_mode.strip().lower()] = html.escape(str(sort_key))
- search_terms_html = ""
- search_terms_tpl = "{search_term}"
- for search_term in item.get("search_terms", []):
- search_terms_html += search_terms_tpl.format(
- **{
- "class": f"search_terms{' search_only' if search_only else ''}",
- "search_term": search_term,
- }
- )
-
description = ""
if shared.opts.extra_networks_card_show_desc:
description = item.get("description", "") or ""
@@ -455,7 +447,7 @@ class ExtraNetworksPage:
description = html.escape(description)
data_name = item.get("name", "").strip()
- data_path = item.get("filename", "").strip()
+ data_path = os.path.normpath(item.get("filename", "").strip())
data_attributes = {
"data-div-id": f'"{div_id}"' if div_id else '""',
"data-name": f'"{data_name}"',
@@ -464,7 +456,6 @@ class ExtraNetworksPage:
"data-prompt": item.get("prompt", "").strip(),
"data-neg-prompt": item.get("negative_prompt", "").strip(),
"data-allow-neg": self.allow_negative_prompt,
- **{f"data-sort-{sort_mode}": f'"{sort_key}"' for sort_mode, sort_key in sort_keys.items()},
}
data_attributes_str = ""
@@ -482,7 +473,6 @@ class ExtraNetworksPage:
data_attributes=data_attributes_str,
background_image=background_image,
button_row=button_row,
- search_terms=search_terms_html,
name=html.escape(item["name"].strip()),
description=description,
)
@@ -509,9 +499,15 @@ class ExtraNetworksPage:
search_only = False
else:
# If any parent dirs are hidden, the model is also hidden.
- filename = item.get("filename", "")
- search_only = any(os.path.basename(x).startswith(".") for x in Path(filename).parents)
+ filename = os.path.normpath(item.get("filename", ""))
+ search_only = any(x.startswith(".") for x in filename.split(os.sep))
self.cards[div_id] = CardListItem(div_id, card_html)
+ self.cards[div_id].abspath = os.path.normpath(item.get("filename", ""))
+ for parent_dir in self.allowed_directories_for_previews():
+ parent_dir = os.path.dirname(os.path.abspath(parent_dir))
+ if self.cards[div_id].abspath.startswith(parent_dir):
+ self.cards[div_id].relpath = os.path.relpath(self.cards[div_id].abspath, parent_dir)
+ break
self.cards[div_id].sort_keys = sort_keys
self.cards[div_id].search_terms = " ".join(search_terms)
self.cards[div_id].search_only = search_only
@@ -526,8 +522,18 @@ class ExtraNetworksPage:
res = {}
for div_id, card_item in self.cards.items():
+ rel_parent_dir = os.path.dirname(card_item.relpath)
+ if (card_item.search_only):
+ parents = card_item.relpath.split(os.sep)
+ idxs = [i for i, x in enumerate(parents) if x.startswith(".")]
+ if len(idxs) > 0:
+ rel_parent_dir = os.sep.join(parents[idxs[0]:])
+ else:
+ print(f"search_only is enabled but no hidden dir found: {card_item.abspath}")
+
res[div_id] = {
**{f"sort_{mode}": key for mode, key in card_item.sort_keys.items()},
+ "rel_parent_dir": rel_parent_dir,
"search_terms": card_item.search_terms,
"search_only": card_item.search_only,
"visible": not card_item.search_only,
@@ -606,7 +612,7 @@ class ExtraNetworksPage:
onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}_{self.extra_networks_tabname}');")
item_name = node.item.get("name", "").strip()
- data_path = node.item.get("filename", "").strip()
+ data_path = os.path.normpath(node.item.get("filename", "").strip())
tree_item.html = self.build_tree_html_row(
tabname=tabname,
label=html.escape(item_name),
@@ -715,7 +721,7 @@ class ExtraNetworksPage:
self.read_user_metadata(item)
# Setup the tree dictionary.
- tree_items = {v["filename"]: v for v in self.items.values()}
+ tree_items = {os.path.normpath(v["filename"]): v for v in self.items.values()}
# Create a DirectoryTreeNode for each root directory since they might not share
# a common path.
for path in self.allowed_directories_for_previews():