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 = "" - 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():