aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAUTOMATIC1111 <16777216c@gmail.com>2024-01-22 22:24:06 +0300
committerGitHub <noreply@github.com>2024-01-22 22:24:06 +0300
commit569dc1919c23329ceaec2162f86ca0fafdee39f5 (patch)
treed8b6b6591fe83eb8eaa5d0f5e46d7a434b58f8de
parent8a6a4ad894c2e28bcb6924490396997bfed7f07f (diff)
parent26e1cd7ec47c8d234d2ea3f189b1147329c9059c (diff)
Merge pull request #14588 from Sj-Si/feature/extra-networks-tree-view
Feature: Extra Networks Tree View
-rw-r--r--extensions-builtin/Lora/ui_extra_networks_lora.py5
-rw-r--r--html/extra-networks-card.html17
-rw-r--r--html/extra-networks-copy-path-button.html5
-rw-r--r--html/extra-networks-edit-item-button.html4
-rw-r--r--html/extra-networks-metadata-button.html4
-rw-r--r--html/extra-networks-pane.html55
-rw-r--r--html/extra-networks-tree-button.html23
-rw-r--r--javascript/extraNetworks.js443
-rw-r--r--modules/shared_options.py1
-rw-r--r--modules/ui_extra_networks.py555
-rw-r--r--modules/ui_extra_networks_checkpoints.py8
-rw-r--r--modules/ui_extra_networks_hypernets.py6
-rw-r--r--modules/ui_extra_networks_textual_inversion.py5
-rw-r--r--modules/ui_extra_networks_user_metadata.py2
-rw-r--r--style.css523
15 files changed, 1316 insertions, 340 deletions
diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py
index e714fac4..66d15dd0 100644
--- a/extensions-builtin/Lora/ui_extra_networks_lora.py
+++ b/extensions-builtin/Lora/ui_extra_networks_lora.py
@@ -24,13 +24,16 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
alias = lora_on_disk.get_alias()
+ search_terms = [self.search_terms_from_path(lora_on_disk.filename)]
+ if lora_on_disk.hash:
+ search_terms.append(lora_on_disk.hash)
item = {
"name": name,
"filename": lora_on_disk.filename,
"shorthash": lora_on_disk.shorthash,
"preview": self.find_preview(path),
"description": self.find_description(path),
- "search_term": self.search_terms_from_path(lora_on_disk.filename) + " " + (lora_on_disk.hash or ""),
+ "search_terms": search_terms,
"local_preview": f"{path}.{shared.opts.samples_format}",
"metadata": lora_on_disk.metadata,
"sort_keys": {'default': index, **self.get_sort_keys(lora_on_disk.filename)},
diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html
index 39674666..f1d959a6 100644
--- a/html/extra-networks-card.html
+++ b/html/extra-networks-card.html
@@ -1,14 +1,9 @@
-<div class='card' style={style} onclick={card_clicked} data-name="{name}" {sort_keys}>
+<div class="card" style="{style}" onclick="{card_clicked}" data-name="{name}" {sort_keys}>
{background_image}
- <div class="button-row">
- {metadata_button}
- {edit_button}
- </div>
- <div class='actions'>
- <div class='additional'>
- <span style="display:none" class='search_term{search_only}'>{search_term}</span>
- </div>
- <span class='name'>{name}</span>
- <span class='description'>{description}</span>
+ <div class="button-row">{copy_path_button}{metadata_button}{edit_button}</div>
+ <div class="actions">
+ <div class="additional">{search_terms}</div>
+ <span class="name">{name}</span>
+ <span class="description">{description}</span>
</div>
</div>
diff --git a/html/extra-networks-copy-path-button.html b/html/extra-networks-copy-path-button.html
new file mode 100644
index 00000000..8083bb03
--- /dev/null
+++ b/html/extra-networks-copy-path-button.html
@@ -0,0 +1,5 @@
+<div class="copy-path-button card-button"
+ title="Copy path to clipboard"
+ onclick="extraNetworksCopyCardPath(event, '{filename}')"
+ data-clipboard-text="{filename}">
+</div> \ No newline at end of file
diff --git a/html/extra-networks-edit-item-button.html b/html/extra-networks-edit-item-button.html
new file mode 100644
index 00000000..0fe43082
--- /dev/null
+++ b/html/extra-networks-edit-item-button.html
@@ -0,0 +1,4 @@
+<div class="edit-button card-button"
+ title="Edit metadata"
+ onclick="extraNetworksEditUserMetadata(event, '{tabname}', '{extra_networks_tabname}', '{name}')">
+</div> \ No newline at end of file
diff --git a/html/extra-networks-metadata-button.html b/html/extra-networks-metadata-button.html
new file mode 100644
index 00000000..285b5b3b
--- /dev/null
+++ b/html/extra-networks-metadata-button.html
@@ -0,0 +1,4 @@
+<div class="metadata-button card-button"
+ title="Show internal metadata"
+ onclick="extraNetworksRequestMetadata(event, '{extra_networks_tabname}', '{name}')">
+</div> \ No newline at end of file
diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html
new file mode 100644
index 00000000..9f5b3ece
--- /dev/null
+++ b/html/extra-networks-pane.html
@@ -0,0 +1,55 @@
+<div id='{tabname}_{extra_networks_tabname}_pane' class='extra-network-pane'>
+ <div class="extra-network-control">
+ <div class="extra-network-control--search">
+ <input
+ id="{tabname}_{extra_networks_tabname}_extra_search"
+ class="extra-network-control--search-text"
+ type="search"
+ placeholder="Filter files"
+ >
+ </div>
+ <div
+ id="{tabname}_{extra_networks_tabname}_extra_sort"
+ class="extra-network-control--sort"
+ data-sortmode="{data_sortmode}"
+ data-sortkey="{data_sortkey}"
+ title="Sort by path"
+ onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
+ >
+ <i class="extra-network-control--sort-icon"></i>
+ </div>
+ <div
+ id="{tabname}_{extra_networks_tabname}_extra_sort_dir"
+ class="extra-network-control--sort-dir"
+ data-sortdir="{data_sortdir}"
+ title="Sort ascending"
+ onclick="extraNetworksControlSortDirOnClick(event, '{tabname}', '{extra_networks_tabname}');"
+ >
+ <i class="extra-network-control--sort-dir-icon"></i>
+ </div>
+ <div
+ id="{tabname}_{extra_networks_tabname}_extra_tree_view"
+ class="extra-network-control--tree-view {tree_view_btn_extra_class}"
+ title="Enable Tree View"
+ onclick="extraNetworksControlTreeViewOnClick(event, '{tabname}', '{extra_networks_tabname}');"
+ >
+ <i class="extra-network-control--tree-view-icon"></i>
+ </div>
+ <div
+ id="{tabname}_{extra_networks_tabname}_extra_refresh"
+ class="extra-network-control--refresh"
+ title="Refresh page"
+ onclick="extraNetworksControlRefreshOnClick(event, '{tabname}', '{extra_networks_tabname}');"
+ >
+ <i class="extra-network-control--refresh-icon"></i>
+ </div>
+ </div>
+ <div class="extra-network-pane-content">
+ <div id='{tabname}_{extra_networks_tabname}_tree' class='extra-network-tree {tree_view_div_extra_class}'>
+ {tree_html}
+ </div>
+ <div id='{tabname}_{extra_networks_tabname}_cards' class='extra-network-cards'>
+ {items_html}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/html/extra-networks-tree-button.html b/html/extra-networks-tree-button.html
new file mode 100644
index 00000000..9dc2e2a4
--- /dev/null
+++ b/html/extra-networks-tree-button.html
@@ -0,0 +1,23 @@
+<span data-filterable-item-text hidden>{search_terms}</span>
+<div class="tree-list-content {subclass}"
+ type="button"
+ onclick="extraNetworksTreeOnClick(event, '{tabname}', '{extra_networks_tabname}');{onclick_extra}"
+ data-path="{data_path}"
+ data-hash="{data_hash}"
+>
+ <span class='tree-list-item-action tree-list-item-action--leading'>
+ {action_list_item_action_leading}
+ </span>
+ <span class="tree-list-item-visual tree-list-item-visual--leading">
+ {action_list_item_visual_leading}
+ </span>
+ <span class="tree-list-item-label tree-list-item-label--truncate">
+ {action_list_item_label}
+ </span>
+ <span class="tree-list-item-visual tree-list-item-visual--trailing">
+ {action_list_item_visual_trailing}
+ </span>
+ <span class="tree-list-item-action tree-list-item-action--trailing">
+ {action_list_item_action_trailing}
+ </span>
+</div> \ No newline at end of file
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js
index f1ad19a6..ce788328 100644
--- a/javascript/extraNetworks.js
+++ b/javascript/extraNetworks.js
@@ -16,99 +16,100 @@ function toggleCss(key, css, enable) {
}
function setupExtraNetworksForTab(tabname) {
- gradioApp().querySelector('#' + tabname + '_extra_tabs').classList.add('extra-networks');
-
- var tabs = gradioApp().querySelector('#' + tabname + '_extra_tabs > div');
- var searchDiv = gradioApp().getElementById(tabname + '_extra_search');
- var search = searchDiv.querySelector('textarea');
- var sort = gradioApp().getElementById(tabname + '_extra_sort');
- var sortOrder = gradioApp().getElementById(tabname + '_extra_sortorder');
- var refresh = gradioApp().getElementById(tabname + '_extra_refresh');
- var showDirsDiv = gradioApp().getElementById(tabname + '_extra_show_dirs');
- var showDirs = gradioApp().querySelector('#' + tabname + '_extra_show_dirs input');
- var promptContainer = gradioApp().querySelector('.prompt-container-compact#' + tabname + '_prompt_container');
- var negativePrompt = gradioApp().querySelector('#' + tabname + '_neg_prompt');
-
- tabs.appendChild(searchDiv);
- tabs.appendChild(sort);
- tabs.appendChild(sortOrder);
- tabs.appendChild(refresh);
- tabs.appendChild(showDirsDiv);
-
- var applyFilter = function() {
- var searchTerm = search.value.toLowerCase();
-
- gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) {
- var searchOnly = elem.querySelector('.search_only');
- var text = elem.querySelector('.name').textContent.toLowerCase() + " " + elem.querySelector('.search_term').textContent.toLowerCase();
-
- var visible = text.indexOf(searchTerm) != -1;
-
- if (searchOnly && searchTerm.length < 4) {
- visible = false;
- }
+ function registerPrompt(tabname, id) {
+ var textarea = gradioApp().querySelector("#" + id + " > label > textarea");
+
+ if (!activePromptTextarea[tabname]) {
+ activePromptTextarea[tabname] = textarea;
+ }
- elem.style.display = visible ? "" : "none";
+ textarea.addEventListener("focus", function() {
+ activePromptTextarea[tabname] = textarea;
});
+ }
- applySort();
- };
+ var this_tab = gradioApp().querySelector('#' + tabname + '_extra_tabs');
+ this_tab.classList.add('extra-networks');
+ this_tab.querySelectorAll(":scope > [id^='" + tabname + "_']").forEach(function(elem) {
+ // tabname_full = {tabname}_{extra_networks_tabname}
+ var tabname_full = elem.id;
+ var search = gradioApp().querySelector("#" + tabname_full + "_extra_search");
+ var sort_mode = gradioApp().querySelector("#" + tabname_full + "_extra_sort");
+ var sort_dir = gradioApp().querySelector("#" + tabname_full + "_extra_sort_dir");
+ var refresh = gradioApp().querySelector("#" + tabname_full + "_extra_refresh");
+
+ // If any of the buttons above don't exist, we want to skip this iteration of the loop.
+ if (!search || !sort_mode || !sort_dir || !refresh) {
+ return; // `return` is equivalent of `continue` but for forEach loops.
+ }
- var applySort = function() {
- var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card');
+ var applyFilter = function() {
+ var searchTerm = search.value.toLowerCase();
+ gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) {
+ var searchOnly = elem.querySelector('.search_only');
+ var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms'), function(t) {
+ return t.textContent.toLowerCase();
+ }).join(" ");
+
+ var visible = text.indexOf(searchTerm) != -1;
+ if (searchOnly && searchTerm.length < 4) {
+ visible = false;
+ }
+ if (visible) {
+ elem.classList.remove("hidden");
+ } else {
+ elem.classList.add("hidden");
+ }
+ });
- var reverse = sortOrder.classList.contains("sortReverse");
- var sortKey = sort.querySelector("input").value.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name";
- sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1);
- var sortKeyStore = sortKey + "-" + (reverse ? "Descending" : "Ascending") + "-" + cards.length;
+ applySort();
+ };
- if (sortKeyStore == sort.dataset.sortkey) {
- return;
- }
- sort.dataset.sortkey = sortKeyStore;
+ var applySort = function() {
+ var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card');
+ var reverse = sort_dir.dataset.sortdir == "Descending";
+ var sortKey = sort_mode.dataset.sortmode.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name";
+ sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1);
+ var sortKeyStore = sortKey + "-" + (reverse ? "Descending" : "Ascending") + "-" + cards.length;
- cards.forEach(function(card) {
- card.originalParentElement = card.parentElement;
- });
- var sortedCards = Array.from(cards);
- sortedCards.sort(function(cardA, cardB) {
- var a = cardA.dataset[sortKey];
- var b = cardB.dataset[sortKey];
- if (!isNaN(a) && !isNaN(b)) {
- return parseInt(a) - parseInt(b);
+ if (sortKeyStore == sort_mode.dataset.sortkey) {
+ return;
}
+ sort_mode.dataset.sortkey = sortKeyStore;
+
+ cards.forEach(function(card) {
+ card.originalParentElement = card.parentElement;
+ });
+ var sortedCards = Array.from(cards);
+ sortedCards.sort(function(cardA, cardB) {
+ var a = cardA.dataset[sortKey];
+ var b = cardB.dataset[sortKey];
+ if (!isNaN(a) && !isNaN(b)) {
+ return parseInt(a) - parseInt(b);
+ }
- return (a < b ? -1 : (a > b ? 1 : 0));
- });
- if (reverse) {
- sortedCards.reverse();
- }
- cards.forEach(function(card) {
- card.remove();
- });
- sortedCards.forEach(function(card) {
- card.originalParentElement.appendChild(card);
- });
- };
-
- search.addEventListener("input", applyFilter);
- sortOrder.addEventListener("click", function() {
- sortOrder.classList.toggle("sortReverse");
+ return (a < b ? -1 : (a > b ? 1 : 0));
+ });
+ if (reverse) {
+ sortedCards.reverse();
+ }
+ cards.forEach(function(card) {
+ card.remove();
+ });
+ sortedCards.forEach(function(card) {
+ card.originalParentElement.appendChild(card);
+ });
+ };
+
+ search.addEventListener("input", applyFilter);
applySort();
+ applyFilter();
+ extraNetworksApplySort[tabname_full] = applySort;
+ extraNetworksApplyFilter[tabname_full] = applyFilter;
});
- applyFilter();
-
- extraNetworksApplySort[tabname] = applySort;
- extraNetworksApplyFilter[tabname] = applyFilter;
- var showDirsUpdate = function() {
- var css = '#' + tabname + '_extra_tabs .extra-network-subdirs { display: none; }';
- toggleCss(tabname + '_extra_show_dirs_style', css, !showDirs.checked);
- localSet('extra-networks-show-dirs', showDirs.checked ? 1 : 0);
- };
- showDirs.checked = localGet('extra-networks-show-dirs', 1) == 1;
- showDirs.addEventListener("change", showDirsUpdate);
- showDirsUpdate();
+ registerPrompt(tabname, tabname + "_prompt");
+ registerPrompt(tabname, tabname + "_neg_prompt");
}
function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt) {
@@ -137,21 +138,20 @@ function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePromp
}
-function extraNetworksUrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate)
+function extraNetworksUnrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate)
extraNetworksMovePromptToTab(tabname, '', false, false);
}
function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt) { // called from python when user selects an extra networks tab
extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt);
-
}
-function applyExtraNetworkFilter(tabname) {
- setTimeout(extraNetworksApplyFilter[tabname], 1);
+function applyExtraNetworkFilter(tabname_full) {
+ setTimeout(extraNetworksApplyFilter[tabname_full], 1);
}
-function applyExtraNetworkSort(tabname) {
- setTimeout(extraNetworksApplySort[tabname], 1);
+function applyExtraNetworkSort(tabname_full) {
+ setTimeout(extraNetworksApplySort[tabname_full], 1);
}
var extraNetworksApplyFilter = {};
@@ -161,27 +161,8 @@ var activePromptTextarea = {};
function setupExtraNetworks() {
setupExtraNetworksForTab('txt2img');
setupExtraNetworksForTab('img2img');
-
- function registerPrompt(tabname, id) {
- var textarea = gradioApp().querySelector("#" + id + " > label > textarea");
-
- if (!activePromptTextarea[tabname]) {
- activePromptTextarea[tabname] = textarea;
- }
-
- textarea.addEventListener("focus", function() {
- activePromptTextarea[tabname] = textarea;
- });
- }
-
- registerPrompt('txt2img', 'txt2img_prompt');
- registerPrompt('txt2img', 'txt2img_neg_prompt');
- registerPrompt('img2img', 'img2img_prompt');
- registerPrompt('img2img', 'img2img_neg_prompt');
}
-onUiLoaded(setupExtraNetworks);
-
var re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/;
var re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g;
@@ -191,8 +172,8 @@ function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) {
var m = text.match(isNeg ? re_extranet_neg : re_extranet);
var replaced = false;
var newTextareaText;
+ var extraTextBeforeNet = opts.extra_networks_add_text_separator;
if (m) {
- var extraTextBeforeNet = opts.extra_networks_add_text_separator;
var extraTextAfterNet = m[2];
var partToSearch = m[1];
var foundAtPosition = -1;
@@ -205,7 +186,6 @@ function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) {
}
return found;
});
-
if (foundAtPosition >= 0) {
if (extraTextAfterNet && newTextareaText.substr(foundAtPosition, extraTextAfterNet.length) == extraTextAfterNet) {
newTextareaText = newTextareaText.substr(0, foundAtPosition) + newTextareaText.substr(foundAtPosition + extraTextAfterNet.length);
@@ -215,13 +195,8 @@ function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) {
}
}
} else {
- newTextareaText = textarea.value.replaceAll(new RegExp(text, "g"), function(found) {
- if (found == text) {
- replaced = true;
- return "";
- }
- return found;
- });
+ newTextareaText = textarea.value.replaceAll(new RegExp(`((?:${extraTextBeforeNet})?${text})`, "g"), "");
+ replaced = (newTextareaText != textarea.value);
}
if (replaced) {
@@ -233,7 +208,6 @@ function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) {
}
function updatePromptArea(text, textArea, isNeg) {
-
if (!tryToRemoveExtraNetworkFromPrompt(textArea, text, isNeg)) {
textArea.value = textArea.value + opts.extra_networks_add_text_separator + text;
}
@@ -264,13 +238,200 @@ function saveCardPreview(event, tabname, filename) {
event.preventDefault();
}
-function extraNetworksSearchButton(tabs_id, event) {
- var searchTextarea = gradioApp().querySelector("#" + tabs_id + ' > label > textarea');
- var button = event.target;
- var text = button.classList.contains("search-all") ? "" : button.textContent.trim();
+function extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname) {
+ /**
+ * Processes `onclick` events when user clicks on files in tree.
+ *
+ * @param event The generated event.
+ * @param btn The clicked `tree-list-item` button.
+ * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.
+ * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.
+ */
+ // NOTE: Currently unused.
+ return;
+}
+
+function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_networks_tabname) {
+ /**
+ * Processes `onclick` events when user clicks on directories in tree.
+ *
+ * Here is how the tree reacts to clicks for various states:
+ * unselected unopened directory: Diretory is selected and expanded.
+ * unselected opened directory: Directory is selected.
+ * selected opened directory: Directory is collapsed and deselected.
+ * chevron is clicked: Directory is expanded or collapsed. Selected state unchanged.
+ *
+ * @param event The generated event.
+ * @param btn The clicked `tree-list-item` button.
+ * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.
+ * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.
+ */
+ var ul = btn.nextElementSibling;
+ // This is the actual target that the user clicked on within the target button.
+ // We use this to detect if the chevron was clicked.
+ var true_targ = event.target;
+
+ function _expand_or_collapse(_ul, _btn) {
+ // Expands <ul> if it is collapsed, collapses otherwise. Updates button attributes.
+ if (_ul.hasAttribute("hidden")) {
+ _ul.removeAttribute("hidden");
+ _btn.dataset.expanded = "";
+ } else {
+ _ul.setAttribute("hidden", "");
+ delete _btn.dataset.expanded;
+ }
+ }
+
+ function _remove_selected_from_all() {
+ // Removes the `selected` attribute from all buttons.
+ var sels = document.querySelectorAll("div.tree-list-content");
+ [...sels].forEach(el => {
+ delete el.dataset.selected;
+ });
+ }
+
+ function _select_button(_btn) {
+ // Removes `data-selected` attribute from all buttons then adds to passed button.
+ _remove_selected_from_all();
+ _btn.dataset.selected = "";
+ }
+
+ function _update_search(_tabname, _extra_networks_tabname, _search_text) {
+ // Update search input with select button's path.
+ var search_input_elem = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_search");
+ search_input_elem.value = _search_text;
+ updateInput(search_input_elem);
+ }
+
+
+ // If user clicks on the chevron, then we do not select the folder.
+ if (true_targ.matches(".tree-list-item-action--leading, .tree-list-item-action-chevron")) {
+ _expand_or_collapse(ul, btn);
+ } else {
+ // User clicked anywhere else on the button.
+ if ("selected" in btn.dataset && !(ul.hasAttribute("hidden"))) {
+ // If folder is select and open, collapse and deselect button.
+ _expand_or_collapse(ul, btn);
+ delete btn.dataset.selected;
+ _update_search(tabname, extra_networks_tabname, "");
+ } else if (!(!("selected" in btn.dataset) && !(ul.hasAttribute("hidden")))) {
+ // If folder is open and not selected, then we don't collapse; just select.
+ // NOTE: Double inversion sucks but it is the clearest way to show the branching here.
+ _expand_or_collapse(ul, btn);
+ _select_button(btn, tabname, extra_networks_tabname);
+ _update_search(tabname, extra_networks_tabname, btn.dataset.path);
+ } else {
+ // All other cases, just select the button.
+ _select_button(btn, tabname, extra_networks_tabname);
+ _update_search(tabname, extra_networks_tabname, btn.dataset.path);
+ }
+ }
+}
+
+function extraNetworksTreeOnClick(event, tabname, extra_networks_tabname) {
+ /**
+ * Handles `onclick` events for buttons within an `extra-network-tree .tree-list--tree`.
+ *
+ * Determines whether the clicked button in the tree is for a file entry or a directory
+ * then calls the appropriate function.
+ *
+ * @param event The generated event.
+ * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.
+ * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.
+ */
+ var btn = event.currentTarget;
+ var par = btn.parentElement;
+ if (par.dataset.treeEntryType === "file") {
+ extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname);
+ } else {
+ extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_networks_tabname);
+ }
+}
+
+function extraNetworksControlSortOnClick(event, tabname, extra_networks_tabname) {
+ /**
+ * Handles `onclick` events for the Sort Mode button.
+ *
+ * Modifies the data attributes of the Sort Mode button to cycle between
+ * various sorting modes.
+ *
+ * @param event The generated event.
+ * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.
+ * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.
+ */
+ var curr_mode = event.currentTarget.dataset.sortmode;
+ var el_sort_dir = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_sort_dir");
+ var sort_dir = el_sort_dir.dataset.sortdir;
+ if (curr_mode == "path") {
+ event.currentTarget.dataset.sortmode = "name";
+ event.currentTarget.dataset.sortkey = "sortName-" + sort_dir + "-640";
+ event.currentTarget.setAttribute("title", "Sort by filename");
+ } else if (curr_mode == "name") {
+ event.currentTarget.dataset.sortmode = "date_created";
+ event.currentTarget.dataset.sortkey = "sortDate_created-" + sort_dir + "-640";
+ event.currentTarget.setAttribute("title", "Sort by date created");
+ } else if (curr_mode == "date_created") {
+ event.currentTarget.dataset.sortmode = "date_modified";
+ event.currentTarget.dataset.sortkey = "sortDate_modified-" + sort_dir + "-640";
+ event.currentTarget.setAttribute("title", "Sort by date modified");
+ } else {
+ event.currentTarget.dataset.sortmode = "path";
+ event.currentTarget.dataset.sortkey = "sortPath-" + sort_dir + "-640";
+ event.currentTarget.setAttribute("title", "Sort by path");
+ }
+ applyExtraNetworkSort(tabname + "_" + extra_networks_tabname);
+}
+
+function extraNetworksControlSortDirOnClick(event, tabname, extra_networks_tabname) {
+ /**
+ * Handles `onclick` events for the Sort Direction button.
+ *
+ * Modifies the data attributes of the Sort Direction button to cycle between
+ * ascending and descending sort directions.
+ *
+ * @param event The generated event.
+ * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.
+ * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.
+ */
+ if (event.currentTarget.dataset.sortdir == "Ascending") {
+ event.currentTarget.dataset.sortdir = "Descending";
+ event.currentTarget.setAttribute("title", "Sort descending");
+ } else {
+ event.currentTarget.dataset.sortdir = "Ascending";
+ event.currentTarget.setAttribute("title", "Sort ascending");
+ }
+ applyExtraNetworkSort(tabname + "_" + extra_networks_tabname);
+}
- searchTextarea.value = text;
- updateInput(searchTextarea);
+function extraNetworksControlTreeViewOnClick(event, tabname, extra_networks_tabname) {
+ /**
+ * Handles `onclick` events for the Tree View button.
+ *
+ * Toggles the tree view in the extra networks pane.
+ *
+ * @param event The generated event.
+ * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.
+ * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.
+ */
+ gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_tree").classList.toggle("hidden");
+ event.currentTarget.classList.toggle("extra-network-control--enabled");
+}
+
+function extraNetworksControlRefreshOnClick(event, tabname, extra_networks_tabname) {
+ /**
+ * Handles `onclick` events for the Refresh Page button.
+ *
+ * In order to actually call the python functions in `ui_extra_networks.py`
+ * to refresh the page, we created an empty gradio button in that file with an
+ * event handler that refreshes the page. So what this function here does
+ * is it manually raises a `click` event on that button.
+ *
+ * @param event The generated event.
+ * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.
+ * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.
+ */
+ var btn_refresh_internal = gradioApp().getElementById(tabname + "_extra_refresh_internal");
+ btn_refresh_internal.dispatchEvent(new Event("click"));
}
var globalPopup = null;
@@ -348,6 +509,11 @@ function requestGet(url, data, handler, errorHandler) {
xhr.send(js);
}
+function extraNetworksCopyCardPath(event, path) {
+ navigator.clipboard.writeText(path);
+ event.stopPropagation();
+}
+
function extraNetworksRequestMetadata(event, extraPage, cardName) {
var showError = function() {
extraNetworksShowMetadata("there was an error getting metadata");
@@ -409,3 +575,36 @@ window.addEventListener("keydown", function(event) {
closePopup();
}
});
+
+/**
+ * Setup custom loading for this script.
+ * We need to wait for all of our HTML to be generated in the extra networks tabs
+ * before we can actually run the `setupExtraNetworks` function.
+ * The `onUiLoaded` function actually runs before all of our extra network tabs are
+ * finished generating. Thus we needed this new method.
+ *
+ */
+
+var uiAfterScriptsCallbacks = [];
+var uiAfterScriptsTimeout = null;
+var executedAfterScripts = false;
+
+function scheduleAfterScriptsCallbacks() {
+ clearTimeout(uiAfterScriptsTimeout);
+ uiAfterScriptsTimeout = setTimeout(function() {
+ executeCallbacks(uiAfterScriptsCallbacks);
+ }, 200);
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+ var mutationObserver = new MutationObserver(function(m) {
+ if (!executedAfterScripts &&
+ gradioApp().querySelectorAll("[id$='_extra_search']").length == 8) {
+ executedAfterScripts = true;
+ scheduleAfterScriptsCallbacks();
+ }
+ });
+ mutationObserver.observe(gradioApp(), {childList: true, subtree: true});
+});
+
+uiAfterScriptsCallbacks.push(setupExtraNetworks);
diff --git a/modules/shared_options.py b/modules/shared_options.py
index 74a2a67f..ec5cb026 100644
--- a/modules/shared_options.py
+++ b/modules/shared_options.py
@@ -254,6 +254,7 @@ options_templates.update(options_section(('extra_networks', "Extra Networks", "s
"extra_networks_card_show_desc": OptionInfo(True, "Show description on card"),
"extra_networks_card_order_field": OptionInfo("Path", "Default order field for Extra Networks cards", gr.Dropdown, {"choices": ['Path', 'Name', 'Date Created', 'Date Modified']}).needs_reload_ui(),
"extra_networks_card_order": OptionInfo("Ascending", "Default order for Extra Networks cards", gr.Dropdown, {"choices": ['Ascending', 'Descending']}).needs_reload_ui(),
+ "extra_networks_tree_view_default_enabled": OptionInfo(False, "Enables the Extra Networks directory tree view by default").needs_reload_ui(),
"extra_networks_add_text_separator": OptionInfo(" ", "Extra networks separator").info("extra text to add before <...> when adding extra network to prompt"),
"ui_extra_networks_tab_reorder": OptionInfo("", "Extra networks tab order").needs_reload_ui(),
"textual_inversion_print_at_load": OptionInfo(False, "Print a list of Textual Inversion embeddings when loading model"),
diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py
index 62db36f5..157b3a6d 100644
--- a/modules/ui_extra_networks.py
+++ b/modules/ui_extra_networks.py
@@ -2,6 +2,8 @@ import functools
import os.path
import urllib.parse
from pathlib import Path
+from typing import Optional, Union
+from dataclasses import dataclass
from modules import shared, ui_extra_networks_user_metadata, errors, extra_networks, util
from modules.images import read_info_from_image, save_image_with_geninfo
@@ -11,14 +13,11 @@ import html
from fastapi.exceptions import HTTPException
from modules.infotext_utils import image_from_url_text
-from modules.ui_components import ToolButton
extra_pages = []
allowed_dirs = set()
-
default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"]
-
@functools.cache
def allowed_preview_extensions_with_extra(extra_extensions=None):
return set(default_allowed_preview_extensions) | set(extra_extensions or [])
@@ -28,6 +27,62 @@ def allowed_preview_extensions():
return allowed_preview_extensions_with_extra((shared.opts.samples_format, ))
+@dataclass
+class ExtraNetworksItem:
+ """Wrapper for dictionaries representing ExtraNetworks items."""
+ item: dict
+
+
+def get_tree(paths: Union[str, list[str]], items: dict[str, ExtraNetworksItem]) -> dict:
+ """Recursively builds a directory tree.
+
+ Args:
+ paths: Path or list of paths to directories. These paths are treated as roots from which
+ the tree will be built.
+ items: A dictionary associating filepaths to an ExtraNetworksItem instance.
+
+ Returns:
+ The result directory tree.
+ """
+ if isinstance(paths, (str,)):
+ paths = [paths]
+
+ def _get_tree(_paths: list[str], _root: str):
+ _res = {}
+ for path in _paths:
+ relpath = os.path.relpath(path, _root)
+ if os.path.isdir(path):
+ dir_items = os.listdir(path)
+ # Ignore empty directories.
+ if not dir_items:
+ continue
+ dir_tree = _get_tree([os.path.join(path, x) for x in dir_items], _root)
+ # We only want to store non-empty folders in the tree.
+ if dir_tree:
+ _res[relpath] = dir_tree
+ else:
+ if path not in items:
+ continue
+ # Add the ExtraNetworksItem to the result.
+ _res[relpath] = items[path]
+ return _res
+
+ res = {}
+ # Handle each root directory separately.
+ # Each root WILL have a key/value at the root of the result dict though
+ # the value can be an empty dict if the directory is empty. We want these
+ # placeholders for empty dirs so we can inform the user later.
+ for path in paths:
+ root = os.path.dirname(path)
+ relpath = os.path.relpath(path, root)
+ # Wrap the path in a list since that is what the `_get_tree` expects.
+ res[relpath] = _get_tree([path], root)
+ if res[relpath]:
+ # We need to pull the inner path out one for these root dirs.
+ res[relpath] = res[relpath][relpath]
+
+ return res
+
def register_page(page):
"""registers extra networks page for the UI; recommend doing it in on_before_ui() callback for extensions"""
@@ -80,7 +135,7 @@ def get_single_card(page: str = "", tabname: str = "", name: str = ""):
item = page.items.get(name)
page.read_user_metadata(item)
- item_html = page.create_html_for_item(item, tabname)
+ item_html = page.create_item_html(tabname, item)
return JSONResponse({"html": item_html})
@@ -96,18 +151,24 @@ def quote_js(s):
s = s.replace('"', '\\"')
return f'"{s}"'
-
class ExtraNetworksPage:
def __init__(self, title):
self.title = title
self.name = title.lower()
- self.id_page = self.name.replace(" ", "_")
- self.card_page = shared.html("extra-networks-card.html")
+ # This is the actual name of the extra networks tab (not txt2img/img2img).
+ self.extra_networks_tabname = self.name.replace(" ", "_")
self.allow_prompt = True
self.allow_negative_prompt = False
self.metadata = {}
self.items = {}
self.lister = util.MassFileLister()
+ # HTML Templates
+ self.pane_tpl = shared.html("extra-networks-pane.html")
+ self.card_tpl = shared.html("extra-networks-card.html")
+ self.btn_tree_tpl = shared.html("extra-networks-tree-button.html")
+ self.btn_copy_path_tpl = shared.html("extra-networks-copy-path-button.html")
+ self.btn_metadata_tpl = shared.html("extra-networks-metadata-button.html")
+ self.btn_edit_item_tpl = shared.html("extra-networks-edit-item-button.html")
def refresh(self):
pass
@@ -129,117 +190,69 @@ class ExtraNetworksPage:
def search_terms_from_path(self, filename, possible_directories=None):
abspath = os.path.abspath(filename)
-
for parentdir in (possible_directories if possible_directories is not None else self.allowed_directories_for_previews()):
- parentdir = os.path.abspath(parentdir)
+ parentdir = os.path.dirname(os.path.abspath(parentdir))
if abspath.startswith(parentdir):
- return abspath[len(parentdir):].replace('\\', '/')
+ return os.path.relpath(abspath, parentdir)
return ""
- def create_html(self, tabname):
- self.lister.reset()
-
- items_html = ''
-
- self.metadata = {}
-
- subdirs = {}
- for parentdir in [os.path.abspath(x) for x in self.allowed_directories_for_previews()]:
- for root, dirs, _ in sorted(os.walk(parentdir, followlinks=True), key=lambda x: shared.natural_sort_key(x[0])):
- for dirname in sorted(dirs, key=shared.natural_sort_key):
- x = os.path.join(root, dirname)
-
- if not os.path.isdir(x):
- continue
-
- subdir = os.path.abspath(x)[len(parentdir):].replace("\\", "/")
-
- if shared.opts.extra_networks_dir_button_function:
- if not subdir.startswith("/"):
- subdir = "/" + subdir
- else:
- while subdir.startswith("/"):
- subdir = subdir[1:]
-
- is_empty = len(os.listdir(x)) == 0
- if not is_empty and not subdir.endswith("/"):
- subdir = subdir + "/"
-
- if ("/." in subdir or subdir.startswith(".")) and not shared.opts.extra_networks_show_hidden_directories:
- continue
-
- subdirs[subdir] = 1
-
- if subdirs:
- subdirs = {"": 1, **subdirs}
-
- subdirs_html = "".join([f"""
-<button class='lg secondary gradio-button custom-button{" search-all" if subdir=="" else ""}' onclick='extraNetworksSearchButton("{tabname}_extra_search", event)'>
-{html.escape(subdir if subdir!="" else "all")}
-</button>
-""" for subdir in subdirs])
-
- self.items = {x["name"]: x for x in self.list_items()}
- for item in self.items.values():
- metadata = item.get("metadata")
- if metadata:
- self.metadata[item["name"]] = metadata
-
- if "user_metadata" not in item:
- self.read_user_metadata(item)
-
- items_html += self.create_html_for_item(item, tabname)
-
- if items_html == '':
- dirs = "".join([f"<li>{x}</li>" for x in self.allowed_directories_for_previews()])
- items_html = shared.html("extra-networks-no-cards.html").format(dirs=dirs)
-
- self_name_id = self.name.replace(" ", "_")
-
- res = f"""
-<div id='{tabname}_{self_name_id}_subdirs' class='extra-network-subdirs extra-network-subdirs-cards'>
-{subdirs_html}
-</div>
-<div id='{tabname}_{self_name_id}_cards' class='extra-network-cards'>
-{items_html}
-</div>
-"""
-
- return res
-
- def create_item(self, name, index=None):
- raise NotImplementedError()
-
- def list_items(self):
- raise NotImplementedError()
-
- def allowed_directories_for_previews(self):
- return []
-
- def create_html_for_item(self, item, tabname):
+ def create_item_html(
+ self,
+ tabname: str,
+ item: dict,
+ template: Optional[str] = None,
+ ) -> Union[str, dict]:
+ """Generates HTML for a single ExtraNetworks Item.
+
+ Args:
+ tabname: The name of the active tab.
+ item: Dictionary containing item information.
+ template: Optional template string to use.
+
+ Returns:
+ If a template is passed: HTML string generated for this item.
+ Can be empty if the item is not meant to be shown.
+ If no template is passed: A dictionary containing the generated item's attributes.
"""
- Create HTML for card item in tab tabname; can return empty string if the item is not meant to be shown.
- """
-
preview = item.get("preview", None)
+ style_height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else ''
+ style_width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else ''
+ style_font_size = f"font-size: {shared.opts.extra_networks_card_text_scale*100}%;"
+ card_style = style_height + style_width + style_font_size
+ background_image = f'<img src="{html.escape(preview)}" class="preview" loading="lazy">' if preview else ''
onclick = item.get("onclick", None)
if onclick is None:
- if "negative_prompt" in item:
- onclick = '"' + html.escape(f"""return cardClicked({quote_js(tabname)}, {item["prompt"]}, {item["negative_prompt"]}, {"true" if self.allow_negative_prompt else "false"})""") + '"'
- else:
- onclick = '"' + html.escape(f"""return cardClicked({quote_js(tabname)}, {item["prompt"]}, {'""'}, {"true" if self.allow_negative_prompt else "false"})""") + '"'
-
- height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else ''
- width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else ''
- background_image = f'<img src="{html.escape(preview)}" class="preview" loading="lazy">' if preview else ''
- metadata_button = ""
+ # Don't quote prompt/neg_prompt since they are stored as js strings already.
+ onclick_js_tpl = "cardClicked('{tabname}', {prompt}, {neg_prompt}, {allow_neg});"
+ onclick = onclick_js_tpl.format(
+ **{
+ "tabname": tabname,
+ "prompt": item["prompt"],
+ "neg_prompt": item.get("negative_prompt", "''"),
+ "allow_neg": str(self.allow_negative_prompt).lower(),
+ }
+ )
+ onclick = html.escape(onclick)
+
+ btn_copy_path = self.btn_copy_path_tpl.format(**{"filename": item["filename"]})
+ btn_metadata = ""
metadata = item.get("metadata")
if metadata:
- metadata_button = f"<div class='metadata-button card-button' title='Show internal metadata' onclick='extraNetworksRequestMetadata(event, {quote_js(self.name)}, {quote_js(html.escape(item['name']))})'></div>"
-
- edit_button = f"<div class='edit-button card-button' title='Edit metadata' onclick='extraNetworksEditUserMetadata(event, {quote_js(tabname)}, {quote_js(self.id_page)}, {quote_js(html.escape(item['name']))})'></div>"
+ btn_metadata = self.btn_metadata_tpl.format(
+ **{
+ "extra_networks_tabname": self.extra_networks_tabname,
+ "name": html.escape(item["name"]),
+ }
+ )
+ btn_edit_item = self.btn_edit_item_tpl.format(
+ **{
+ "tabname": tabname,
+ "extra_networks_tabname": self.extra_networks_tabname,
+ "name": html.escape(item["name"]),
+ }
+ )
local_path = ""
filename = item.get("filename", "")
@@ -259,26 +272,282 @@ class ExtraNetworksPage:
if search_only and shared.opts.extra_networks_hidden_models == "Never":
return ""
- sort_keys = " ".join([f'data-sort-{k}="{html.escape(str(v))}"' for k, v in item.get("sort_keys", {}).items()]).strip()
-
+ sort_keys = " ".join(
+ [
+ f'data-sort-{k}="{html.escape(str(v))}"'
+ for k, v in item.get("sort_keys", {}).items()
+ ]
+ ).strip()
+
+ search_terms_html = ""
+ search_term_template = "<span class='hidden {class}'>{search_term}</span>"
+ for search_term in item.get("search_terms", []):
+ search_terms_html += search_term_template.format(
+ **{
+ "class": f"search_terms{' search_only' if search_only else ''}",
+ "search_term": search_term,
+ }
+ )
+
+ # Some items here might not be used depending on HTML template used.
args = {
"background_image": background_image,
- "style": f"'display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%'",
- "prompt": item.get("prompt", None),
- "tabname": quote_js(tabname),
+ "card_clicked": onclick,
+ "copy_path_button": btn_copy_path,
+ "description": (item.get("description", "") or "" if shared.opts.extra_networks_card_show_desc else ""),
+ "edit_button": btn_edit_item,
"local_preview": quote_js(item["local_preview"]),
+ "metadata_button": btn_metadata,
"name": html.escape(item["name"]),
- "description": (item.get("description") or "" if shared.opts.extra_networks_card_show_desc else ""),
- "card_clicked": onclick,
- "save_card_preview": '"' + html.escape(f"""return saveCardPreview(event, {quote_js(tabname)}, {quote_js(item["local_preview"])})""") + '"',
- "search_term": item.get("search_term", ""),
- "metadata_button": metadata_button,
- "edit_button": edit_button,
+ "prompt": item.get("prompt", None),
+ "save_card_preview": html.escape(f"return saveCardPreview(event, '{tabname}', '{item['local_preview']}');"),
"search_only": " search_only" if search_only else "",
+ "search_terms": search_terms_html,
"sort_keys": sort_keys,
+ "style": card_style,
+ "tabname": tabname,
+ "extra_networks_tabname": self.extra_networks_tabname,
}
- return self.card_page.format(**args)
+ if template:
+ return template.format(**args)
+ else:
+ return args
+
+ def create_tree_dir_item_html(
+ self,
+ tabname: str,
+ dir_path: str,
+ content: Optional[str] = None,
+ ) -> Optional[str]:
+ """Generates HTML for a directory item in the tree.
+
+ The generated HTML is of the format:
+ ```html
+ <li class="tree-list-item tree-list-item--has-subitem">
+ <div class="tree-list-content tree-list-content-dir"></div>
+ <ul class="tree-list tree-list--subgroup">
+ {content}
+ </ul>
+ </li>
+ ```
+
+ Args:
+ tabname: The name of the active tab.
+ dir_path: Path to the directory for this item.
+ content: Optional HTML string that will be wrapped by this <ul>.
+
+ Returns:
+ HTML formatted string.
+ """
+ if not content:
+ return None
+
+ btn = self.btn_tree_tpl.format(
+ **{
+ "search_terms": "",
+ "subclass": "tree-list-content-dir",
+ "tabname": tabname,
+ "extra_networks_tabname": self.extra_networks_tabname,
+ "onclick_extra": "",
+ "data_path": dir_path,
+ "data_hash": "",
+ "action_list_item_action_leading": "<i class='tree-list-item-action-chevron'></i>",
+ "action_list_item_visual_leading": "🗀",
+ "action_list_item_label": os.path.basename(dir_path),
+ "action_list_item_visual_trailing": "",
+ "action_list_item_action_trailing": "",
+ }
+ )
+ ul = f"<ul class='tree-list tree-list--subgroup' hidden>{content}</ul>"
+ return (
+ "<li class='tree-list-item tree-list-item--has-subitem' data-tree-entry-type='dir'>"
+ f"{btn}{ul}"
+ "</li>"
+ )
+
+ def create_tree_file_item_html(self, tabname: str, file_path: str, item: dict) -> str:
+ """Generates HTML for a file item in the tree.
+
+ The generated HTML is of the format:
+ ```html
+ <li class="tree-list-item tree-list-item--subitem">
+ <span data-filterable-item-text hidden></span>
+ <div class="tree-list-content tree-list-content-file"></div>
+ </li>
+ ```
+
+ Args:
+ tabname: The name of the active tab.
+ file_path: The path to the file for this item.
+ item: Dictionary containing the item information.
+
+ Returns:
+ HTML formatted string.
+ """
+ item_html_args = self.create_item_html(tabname, item)
+ action_buttons = "".join(
+ [
+ item_html_args["copy_path_button"],
+ item_html_args["metadata_button"],
+ item_html_args["edit_button"],
+ ]
+ )
+ action_buttons = f"<div class=\"button-row\">{action_buttons}</div>"
+ btn = self.btn_tree_tpl.format(
+ **{
+ "search_terms": "",
+ "subclass": "tree-list-content-file",
+ "tabname": tabname,
+ "extra_networks_tabname": self.extra_networks_tabname,
+ "onclick_extra": item_html_args["card_clicked"],
+ "data_path": file_path,
+ "data_hash": item["shorthash"],
+ "action_list_item_action_leading": "<i class='tree-list-item-action-chevron'></i>",
+ "action_list_item_visual_leading": "🗎",
+ "action_list_item_label": item["name"],
+ "action_list_item_visual_trailing": "",
+ "action_list_item_action_trailing": action_buttons,
+ }
+ )
+ return (
+ "<li class='tree-list-item tree-list-item--subitem' data-tree-entry-type='file'>"
+ f"{btn}"
+ "</li>"
+ )
+
+ def create_tree_view_html(self, tabname: str) -> str:
+ """Generates HTML for displaying folders in a tree view.
+
+ Args:
+ tabname: The name of the active tab.
+
+ Returns:
+ HTML string generated for this tree view.
+ """
+ res = ""
+
+ # Setup the tree dictionary.
+ roots = self.allowed_directories_for_previews()
+ tree_items = {v["filename"]: ExtraNetworksItem(v) for v in self.items.values()}
+ tree = get_tree([os.path.abspath(x) for x in roots], items=tree_items)
+
+ if not tree:
+ return res
+
+ def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> Optional[str]:
+ """Recursively builds HTML for a tree.
+
+ Args:
+ data: Dictionary representing a directory tree. Can be NoneType.
+ Data keys should be absolute paths from the root and values
+ should be subdirectory trees or an ExtraNetworksItem.
+
+ Returns:
+ If data is not None: HTML string
+ Else: None
+ """
+ if not data:
+ return None
+
+ # Lists for storing <li> items html for directories and files separately.
+ _dir_li = []
+ _file_li = []
+
+ for k, v in sorted(data.items(), key=lambda x: shared.natural_sort_key(x[0])):
+ if isinstance(v, (ExtraNetworksItem,)):
+ _file_li.append(self.create_tree_file_item_html(tabname, k, v.item))
+ else:
+ _dir_li.append(self.create_tree_dir_item_html(tabname, k, _build_tree(v)))
+
+ # Directories should always be displayed before files so we order them here.
+ return "".join(_dir_li) + "".join(_file_li)
+
+ # Add each root directory to the tree.
+ for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])):
+ item_html = self.create_tree_dir_item_html(tabname, k, _build_tree(v))
+ # Only add non-empty entries to the tree.
+ if item_html is not None:
+ res += item_html
+
+ return f"<ul class='tree-list tree-list--tree'>{res}</ul>"
+
+ def create_card_view_html(self, tabname: str) -> str:
+ """Generates HTML for the network Card View section for a tab.
+
+ This HTML goes into the `extra-networks-pane.html` <div> with
+ `id='{tabname}_{extra_networks_tabname}_cards`.
+
+ Args:
+ tabname: The name of the active tab.
+
+ Returns:
+ HTML formatted string.
+ """
+ res = ""
+ for item in self.items.values():
+ res += self.create_item_html(tabname, item, self.card_tpl)
+
+ if res == "":
+ dirs = "".join([f"<li>{x}</li>" for x in self.allowed_directories_for_previews()])
+ res = shared.html("extra-networks-no-cards.html").format(dirs=dirs)
+
+ return res
+
+ def create_html(self, tabname):
+ """Generates an HTML string for the current pane.
+
+ The generated HTML uses `extra-networks-pane.html` as a template.
+
+ Args:
+ tabname: The name of the active tab.
+
+ Returns:
+ HTML formatted string.
+ """
+ self.lister.reset()
+ self.metadata = {}
+ self.items = {x["name"]: x for x in self.list_items()}
+ # Populate the instance metadata for each item.
+ for item in self.items.values():
+ metadata = item.get("metadata")
+ if metadata:
+ self.metadata[item["name"]] = metadata
+
+ if "user_metadata" not in item:
+ self.read_user_metadata(item)
+
+ data_sortdir = shared.opts.extra_networks_card_order
+ data_sortmode = shared.opts.extra_networks_card_order_field.lower().replace("sort", "").replace(" ", "_").rstrip("_").strip()
+ data_sortkey = f"{data_sortmode}-{data_sortdir}-{len(self.items)}"
+ tree_view_btn_extra_class = ""
+ tree_view_div_extra_class = "hidden"
+ if shared.opts.extra_networks_tree_view_default_enabled:
+ tree_view_btn_extra_class = "extra-network-control--enabled"
+ tree_view_div_extra_class = ""
+
+ return self.pane_tpl.format(
+ **{
+ "tabname": tabname,
+ "extra_networks_tabname": self.extra_networks_tabname,
+ "data_sortmode": data_sortmode,
+ "data_sortkey": data_sortkey,
+ "data_sortdir": data_sortdir,
+ "tree_view_btn_extra_class": tree_view_btn_extra_class,
+ "tree_view_div_extra_class": tree_view_div_extra_class,
+ "tree_html": self.create_tree_view_html(tabname),
+ "items_html": self.create_card_view_html(tabname),
+ }
+ )
+
+ def create_item(self, name, index=None):
+ raise NotImplementedError()
+
+ def list_items(self):
+ raise NotImplementedError()
+
+ def allowed_directories_for_previews(self):
+ return []
def get_sort_keys(self, path):
"""
@@ -298,7 +567,7 @@ class ExtraNetworksPage:
Find a preview PNG for a given path (without extension) and call link_preview on it.
"""
- potential_files = sum([[path + "." + ext, path + ".preview." + ext] for ext in allowed_preview_extensions()], [])
+ potential_files = sum([[f"{path}.{ext}", f"{path}.preview.{ext}"] for ext in allowed_preview_extensions()], [])
for file in potential_files:
if self.lister.exists(file):
@@ -369,10 +638,7 @@ def pages_in_preferred_order(pages):
return sorted(pages, key=lambda x: tab_scores[x.name])
-
def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
- from modules.ui import switch_values_symbol
-
ui = ExtraNetworksUi()
ui.pages = []
ui.pages_contents = []
@@ -382,46 +648,35 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
related_tabs = []
+ button_refresh = gr.Button("Refresh", elem_id=f"{tabname}_extra_refresh_internal", visible=False)
+
for page in ui.stored_extra_pages:
- with gr.Tab(page.title, elem_id=f"{tabname}_{page.id_page}", elem_classes=["extra-page"]) as tab:
- with gr.Column(elem_id=f"{tabname}_{page.id_page}_prompts", elem_classes=["extra-page-prompts"]):
+ with gr.Tab(page.title, elem_id=f"{tabname}_{page.extra_networks_tabname}", elem_classes=["extra-page"]) as tab:
+ with gr.Column(elem_id=f"{tabname}_{page.extra_networks_tabname}_prompts", elem_classes=["extra-page-prompts"]):
pass
- elem_id = f"{tabname}_{page.id_page}_cards_html"
+ elem_id = f"{tabname}_{page.extra_networks_tabname}_cards_html"
page_elem = gr.HTML('Loading...', elem_id=elem_id)
ui.pages.append(page_elem)
-
- page_elem.change(fn=lambda: None, _js='function(){applyExtraNetworkFilter(' + quote_js(tabname) + '); return []}', inputs=[], outputs=[])
-
editor = page.create_user_metadata_editor(ui, tabname)
editor.create_ui()
ui.user_metadata_editors.append(editor)
-
related_tabs.append(tab)
- edit_search = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", elem_classes="search", placeholder="Search...", visible=False, interactive=True)
- dropdown_sort = gr.Dropdown(choices=['Path', 'Name', 'Date Created', 'Date Modified', ], value=shared.opts.extra_networks_card_order_field, elem_id=tabname+"_extra_sort", elem_classes="sort", multiselect=False, visible=False, show_label=False, interactive=True, label=tabname+"_extra_sort_order")
- button_sortorder = ToolButton(switch_values_symbol, elem_id=tabname+"_extra_sortorder", elem_classes=["sortorder"] + ([] if shared.opts.extra_networks_card_order == "Ascending" else ["sortReverse"]), visible=False, tooltip="Invert sort order")
- button_refresh = gr.Button('Refresh', elem_id=tabname+"_extra_refresh", visible=False)
- checkbox_show_dirs = gr.Checkbox(True, label='Show dirs', elem_id=tabname+"_extra_show_dirs", elem_classes="show-dirs", visible=False)
-
- ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False)
- ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=tabname+"_preview_filename", visible=False)
-
- tab_controls = [edit_search, dropdown_sort, button_sortorder, button_refresh, checkbox_show_dirs]
+ ui.button_save_preview = gr.Button('Save preview', elem_id=f"{tabname}_save_preview", visible=False)
+ ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=f"{tabname}_preview_filename", visible=False)
for tab in unrelated_tabs:
- tab.select(fn=lambda: [gr.update(visible=False) for _ in tab_controls], _js='function(){ extraNetworksUrelatedTabSelected("' + tabname + '"); }', inputs=[], outputs=tab_controls, show_progress=False)
+ tab.select(fn=None, _js=f"function(){{extraNetworksUnrelatedTabSelected('{tabname}');}}", inputs=[], outputs=[], show_progress=False)
for page, tab in zip(ui.stored_extra_pages, related_tabs):
- allow_prompt = "true" if page.allow_prompt else "false"
- allow_negative_prompt = "true" if page.allow_negative_prompt else "false"
-
- jscode = 'extraNetworksTabSelected("' + tabname + '", "' + f"{tabname}_{page.id_page}_prompts" + '", ' + allow_prompt + ', ' + allow_negative_prompt + ');'
-
- tab.select(fn=lambda: [gr.update(visible=True) for _ in tab_controls], _js='function(){ ' + jscode + ' }', inputs=[], outputs=tab_controls, show_progress=False)
-
- dropdown_sort.change(fn=lambda: None, _js="function(){ applyExtraNetworkSort('" + tabname + "'); }")
+ jscode = (
+ "function(){{"
+ f"extraNetworksTabSelected('{tabname}', '{tabname}_{page.extra_networks_tabname}_prompts', {str(page.allow_prompt).lower()}, {str(page.allow_negative_prompt).lower()});"
+ f"applyExtraNetworkFilter('{tabname}_{page.extra_networks_tabname}');"
+ "}}"
+ )
+ tab.select(fn=None, _js=jscode, inputs=[], outputs=[], show_progress=False)
def create_html():
ui.pages_contents = [pg.create_html(ui.tabname) for pg in ui.stored_extra_pages]
@@ -438,6 +693,8 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
return ui.pages_contents
interface.load(fn=pages_html, inputs=[], outputs=ui.pages)
+ # NOTE: Event is manually fired in extraNetworks.js:extraNetworksTreeRefreshOnClick()
+ # button is unused and hidden at all times. Only used in order to fire this event.
button_refresh.click(fn=refresh, inputs=[], outputs=ui.pages)
return ui
@@ -487,5 +744,3 @@ def setup_ui(ui, gallery):
for editor in ui.user_metadata_editors:
editor.setup_ui(gallery)
-
-
diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py
index 1693e71f..a8c33671 100644
--- a/modules/ui_extra_networks_checkpoints.py
+++ b/modules/ui_extra_networks_checkpoints.py
@@ -2,7 +2,6 @@ import html
import os
from modules import shared, ui_extra_networks, sd_models
-from modules.ui_extra_networks import quote_js
from modules.ui_extra_networks_checkpoints_user_metadata import CheckpointUserMetadataEditor
@@ -21,14 +20,17 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage):
return
path, ext = os.path.splitext(checkpoint.filename)
+ search_terms = [self.search_terms_from_path(checkpoint.filename)]
+ if checkpoint.sha256:
+ search_terms.append(checkpoint.sha256)
return {
"name": checkpoint.name_for_extra,
"filename": checkpoint.filename,
"shorthash": checkpoint.shorthash,
"preview": self.find_preview(path),
"description": self.find_description(path),
- "search_term": self.search_terms_from_path(checkpoint.filename) + " " + (checkpoint.sha256 or ""),
- "onclick": '"' + html.escape(f"""return selectCheckpoint({quote_js(name)})""") + '"',
+ "search_terms": search_terms,
+ "onclick": html.escape(f"return selectCheckpoint('{name}');"),
"local_preview": f"{path}.{shared.opts.samples_format}",
"metadata": checkpoint.metadata,
"sort_keys": {'default': index, **self.get_sort_keys(checkpoint.filename)},
diff --git a/modules/ui_extra_networks_hypernets.py b/modules/ui_extra_networks_hypernets.py
index c96c4fa3..2fb4bd19 100644
--- a/modules/ui_extra_networks_hypernets.py
+++ b/modules/ui_extra_networks_hypernets.py
@@ -20,14 +20,16 @@ class ExtraNetworksPageHypernetworks(ui_extra_networks.ExtraNetworksPage):
path, ext = os.path.splitext(full_path)
sha256 = sha256_from_cache(full_path, f'hypernet/{name}')
shorthash = sha256[0:10] if sha256 else None
-
+ search_terms = [self.search_terms_from_path(path)]
+ if sha256:
+ search_terms.append(sha256)
return {
"name": name,
"filename": full_path,
"shorthash": shorthash,
"preview": self.find_preview(path),
"description": self.find_description(path),
- "search_term": self.search_terms_from_path(path) + " " + (sha256 or ""),
+ "search_terms": search_terms,
"prompt": quote_js(f"<hypernet:{name}:") + " + opts.extra_networks_default_multiplier + " + quote_js(">"),
"local_preview": f"{path}.preview.{shared.opts.samples_format}",
"sort_keys": {'default': index, **self.get_sort_keys(path + ext)},
diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py
index 1b334fda..deb7cb87 100644
--- a/modules/ui_extra_networks_textual_inversion.py
+++ b/modules/ui_extra_networks_textual_inversion.py
@@ -18,13 +18,16 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage):
return
path, ext = os.path.splitext(embedding.filename)
+ search_terms = [self.search_terms_from_path(embedding.filename)]
+ if embedding.hash:
+ search_terms.append(embedding.hash)
return {
"name": name,
"filename": embedding.filename,
"shorthash": embedding.shorthash,
"preview": self.find_preview(path),
"description": self.find_description(path),
- "search_term": self.search_terms_from_path(embedding.filename) + " " + (embedding.hash or ""),
+ "search_terms": search_terms,
"prompt": quote_js(embedding.name),
"local_preview": f"{path}.preview.{shared.opts.samples_format}",
"sort_keys": {'default': index, **self.get_sort_keys(embedding.filename)},
diff --git a/modules/ui_extra_networks_user_metadata.py b/modules/ui_extra_networks_user_metadata.py
index 989a649b..2ca937fd 100644
--- a/modules/ui_extra_networks_user_metadata.py
+++ b/modules/ui_extra_networks_user_metadata.py
@@ -14,7 +14,7 @@ class UserMetadataEditor:
self.ui = ui
self.tabname = tabname
self.page = page
- self.id_part = f"{self.tabname}_{self.page.id_page}_edit_user_metadata"
+ self.id_part = f"{self.tabname}_{self.page.extra_networks_tabname}_edit_user_metadata"
self.box = None
diff --git a/style.css b/style.css
index 4957c523..ff1d9072 100644
--- a/style.css
+++ b/style.css
@@ -28,7 +28,7 @@ div.gradio-container{
}
.hidden{
- display: none;
+ display: none !important;
}
.compact{
@@ -879,13 +879,6 @@ footer {
margin-bottom: 1em;
}
-.extra-network-cards{
- height: calc(100vh - 24rem);
- overflow: clip scroll;
- resize: vertical;
- min-height: 52rem;
-}
-
.extra-networks > div.tab-nav{
min-height: 3.4rem;
}
@@ -894,16 +887,8 @@ footer {
margin: 0.3em;
}
-.extra-network-subdirs{
- padding: 0.2em 0.35em;
-}
-
-.extra-network-subdirs button{
- margin: 0 0.15em;
-}
.extra-networks .tab-nav .search,
-.extra-networks .tab-nav .sort,
-.extra-networks .tab-nav .show-dirs
+.extra-networks .tab-nav .sort
{
margin: 0.3em;
align-self: center;
@@ -924,53 +909,69 @@ footer {
width: auto;
}
-.extra-network-cards .nocards{
+.extra-network-pane .nocards{
margin: 1.25em 0.5em 0.5em 0.5em;
}
-.extra-network-cards .nocards h1{
+.extra-network-pane .nocards h1{
font-size: 1.5em;
margin-bottom: 1em;
}
-.extra-network-cards .nocards li{
+.extra-network-pane .nocards li{
margin-left: 0.5em;
}
+.extra-network-pane .card .button-row{
+ display: inline-flex;
+ visibility: hidden;
+ color: white;
+}
-.extra-network-cards .card .button-row{
- display: none;
+.extra-network-pane .card .button-row {
position: absolute;
- color: white;
right: 0;
- z-index: 1
+ z-index: 1;
}
-.extra-network-cards .card:hover .button-row{
- display: flex;
+
+.extra-network-pane .card:hover .button-row{
+ visibility: visible;
}
-.extra-network-cards .card .card-button{
+.extra-network-pane .card-button{
color: white;
}
-.extra-network-cards .card .metadata-button:before{
+.extra-network-pane .copy-path-button::before {
+ content: "⎘";
+}
+
+.extra-network-pane .metadata-button::before{
content: "🛈";
}
-.extra-network-cards .card .edit-button:before{
+.extra-network-pane .edit-button::before{
content: "🛠";
}
-.extra-network-cards .card .card-button {
+.extra-network-pane .card-button {
+ width: 1.5em;
text-shadow: 2px 2px 3px black;
+ color: white;
padding: 0.25em 0.1em;
- font-size: 200%;
- width: 1.5em;
}
-.extra-network-cards .card .card-button:hover{
+
+.extra-network-pane .card-button:hover{
color: red;
}
+.extra-network-pane .card .card-button {
+ font-size: 2rem;
+}
+
+.extra-network-pane .card-minimal .card-button {
+ font-size: 1rem;
+}
.standalone-card-preview.card .preview{
position: absolute;
@@ -979,7 +980,7 @@ footer {
height:100%;
}
-.extra-network-cards .card, .standalone-card-preview.card{
+.extra-network-pane .card, .standalone-card-preview.card{
display: inline-block;
margin: 0.5rem;
width: 16rem;
@@ -996,15 +997,15 @@ footer {
background-image: url('./file=html/card-no-preview.png')
}
-.extra-network-cards .card:hover{
+.extra-network-pane .card:hover{
box-shadow: 0 0 2px 0.3em rgba(0, 128, 255, 0.35);
}
-.extra-network-cards .card .actions .additional{
+.extra-network-pane .card .actions .additional{
display: none;
}
-.extra-network-cards .card .actions{
+.extra-network-pane .card .actions{
position: absolute;
bottom: 0;
left: 0;
@@ -1015,45 +1016,45 @@ footer {
text-shadow: 0 0 0.2em black;
}
-.extra-network-cards .card .actions *{
+.extra-network-pane .card .actions *{
color: white;
}
-.extra-network-cards .card .actions .name{
+.extra-network-pane .card .actions .name{
font-size: 1.7em;
font-weight: bold;
line-break: anywhere;
}
-.extra-network-cards .card .actions .description {
+.extra-network-pane .card .actions .description {
display: block;
max-height: 3em;
white-space: pre-wrap;
line-height: 1.1;
}
-.extra-network-cards .card .actions .description:hover {
+.extra-network-pane .card .actions .description:hover {
max-height: none;
}
-.extra-network-cards .card .actions:hover .additional{
+.extra-network-pane .card .actions:hover .additional{
display: block;
}
-.extra-network-cards .card ul{
+.extra-network-pane .card ul{
margin: 0.25em 0 0.75em 0.25em;
cursor: unset;
}
-.extra-network-cards .card ul a{
+.extra-network-pane .card ul a{
cursor: pointer;
}
-.extra-network-cards .card ul a:hover{
+.extra-network-pane .card ul a:hover{
color: red;
}
-.extra-network-cards .card .preview{
+.extra-network-pane .card .preview{
position: absolute;
object-fit: cover;
width: 100%;
@@ -1096,9 +1097,6 @@ div.block.gradio-box.edit-user-metadata {
margin-top: 1.5em;
}
-
-
-
div.block.gradio-box.popup-dialog, .popup-dialog {
width: 56em;
background: var(--body-background-fill);
@@ -1173,3 +1171,430 @@ body.resizing .resize-handle {
left: 7.5px;
border-left: 1px dashed var(--border-color-primary);
}
+
+/* ========================= */
+.extra-network-pane {
+ display: flex;
+ height: calc(100vh - 24rem);
+ resize: vertical;
+ min-height: 52rem;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.extra-network-pane .extra-network-pane-content {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+.extra-network-pane .extra-network-tree {
+ flex: 1;
+ font-size: 1rem;
+ border: 1px solid var(--block-border-color);
+ overflow: clip auto !important;
+}
+
+.extra-network-pane .extra-network-cards {
+ flex: 3;
+ overflow: clip auto !important;
+ border: 1px solid var(--block-border-color);
+}
+
+.extra-network-pane .extra-network-tree .tree-list {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ width: 100%;
+ overflow: hidden;
+}
+
+
+.extra-network-pane .extra-network-cards::-webkit-scrollbar,
+.extra-network-pane .extra-network-tree::-webkit-scrollbar {
+ background-color: transparent;
+ width: 16px;
+}
+
+.extra-network-pane .extra-network-cards::-webkit-scrollbar-track,
+.extra-network-pane .extra-network-tree::-webkit-scrollbar-track {
+ background-color: transparent;
+ background-clip: content-box;
+}
+
+.extra-network-pane .extra-network-cards::-webkit-scrollbar-thumb,
+.extra-network-pane .extra-network-tree::-webkit-scrollbar-thumb {
+ background-color: var(--border-color-primary);
+ border-radius: 16px;
+ border: 4px solid var(--background-fill-primary);
+}
+
+.extra-network-pane .extra-network-cards::-webkit-scrollbar-button,
+.extra-network-pane .extra-network-tree::-webkit-scrollbar-button {
+ display: none;
+}
+
+.extra-network-pane .extra-network-control {
+ position: relative;
+ display: grid;
+ width: 100%;
+ padding: 0 !important;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ font-size: 1rem;
+ text-align: left;
+ user-select: none;
+ background-color: transparent;
+ border: none;
+ transition: background 33.333ms linear;
+ grid-template-rows: min-content;
+ grid-template-columns: minmax(0, auto) repeat(4, min-content);
+ grid-gap: 0.1rem;
+ align-items: start;
+}
+
+.extra-network-tree .tree-list--tree {}
+
+/* Remove auto indentation from tree. Will be overridden later. */
+.extra-network-tree .tree-list--subgroup {
+ margin: 0 !important;
+ padding: 0 !important;
+ box-shadow: 0.5rem 0 0 var(--body-background-fill) inset,
+ 0.7rem 0 0 var(--neutral-800) inset;
+}
+
+/* Set indentation for each depth of tree. */
+.extra-network-tree .tree-list--subgroup > .tree-list-item {
+ margin-left: 0.4rem !important;
+ padding-left: 0.4rem !important;
+}
+
+/* Styles for tree <li> elements. */
+.extra-network-tree .tree-list-item {
+ list-style: none;
+ position: relative;
+ background-color: transparent;
+}
+
+/* Directory <ul> visibility based on data-expanded attribute. */
+.extra-network-tree .tree-list-content+.tree-list--subgroup {
+ height: 0;
+ visibility: hidden;
+ opacity: 0;
+}
+
+.extra-network-tree .tree-list-content[data-expanded]+.tree-list--subgroup {
+ height: auto;
+ visibility: visible;
+ opacity: 1;
+}
+
+/* File <li> */
+.extra-network-tree .tree-list-item--subitem {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+
+/* <li> containing <ul> */
+.extra-network-tree .tree-list-item--has-subitem {}
+
+/* BUTTON ELEMENTS */
+/* <button> */
+.extra-network-tree .tree-list-content {
+ position: relative;
+ display: grid;
+ width: 100%;
+ padding: 0 !important;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ font-size: 1rem;
+ text-align: left;
+ user-select: none;
+ background-color: transparent;
+ border: none;
+ transition: background 33.333ms linear;
+ grid-template-rows: min-content;
+ grid-template-areas: "leading-action leading-visual label trailing-visual trailing-action";
+ grid-template-columns: min-content min-content minmax(0, auto) min-content min-content;
+ grid-gap: 0.1rem;
+ align-items: start;
+ flex-grow: 1;
+ flex-basis: 100%;
+}
+/* Buttons for directories. */
+.extra-network-tree .tree-list-content-dir {}
+
+/* Buttons for files. */
+.extra-network-tree .tree-list-item--has-subitem .tree-list--subgroup > li:first-child {
+ padding-top: 0.5rem !important;
+}
+
+.dark .extra-network-tree div.tree-list-content:hover {
+ -webkit-transition: all 0.05s ease-in-out;
+ transition: all 0.05s ease-in-out;
+ background-color: var(--neutral-800);
+}
+
+.dark .extra-network-tree div.tree-list-content[data-selected] {
+ background-color: var(--neutral-700);
+}
+
+.extra-network-tree div.tree-list-content[data-selected] {
+ background-color: var(--neutral-300);
+}
+
+.extra-network-tree div.tree-list-content:hover {
+ -webkit-transition: all 0.05s ease-in-out;
+ transition: all 0.05s ease-in-out;
+ background-color: var(--neutral-200);
+}
+
+/* ==== CHEVRON ICON ACTIONS ==== */
+/* Define the animation for the arrow when it is clicked. */
+.extra-network-tree .tree-list-content-dir .tree-list-item-action-chevron {
+ -ms-transform: rotate(135deg);
+ -webkit-transform: rotate(135deg);
+ transform: rotate(135deg);
+ transition: transform 0.2s;
+}
+
+.extra-network-tree .tree-list-content-dir[data-expanded] .tree-list-item-action-chevron {
+ -ms-transform: rotate(225deg);
+ -webkit-transform: rotate(225deg);
+ transform: rotate(225deg);
+ transition: transform 0.2s;
+}
+
+.tree-list-item-action-chevron {
+ display: inline-flex;
+ /* Uses box shadow to generate a pseudo chevron `>` icon. */
+ padding: 0.3rem;
+ box-shadow: 0.1rem 0.1rem 0 0 var(--neutral-200) inset;
+ transform: rotate(135deg);
+}
+
+/* ==== SEARCH INPUT ACTIONS ==== */
+/* Add icon to left side of <input> */
+.extra-network-pane .extra-network-control .extra-network-control--search::before {
+ content: "🔎︎";
+ position: absolute;
+ margin: 0.5rem;
+ font-size: 1rem;
+ color: var(--input-placeholder-color);
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--search {
+ display: inline-flex;
+ position: relative;
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--search .extra-network-control--search-text {
+ border: 1px solid var(--button-secondary-border-color);
+ border-radius: 0.5rem;
+ color: var(--button-secondary-text-color);
+ background-color: transparent;
+ width: 100%;
+ padding-left: 2rem;
+ line-height: 1rem;
+}
+
+/* <input> clear button (x on right side) styling */
+.extra-network-pane .extra-network-control .extra-network-control--search .extra-network-control--search-text::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ height: 1rem;
+ width: 1rem;
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>');
+ mask-repeat: no-repeat;
+ mask-position: center center;
+ mask-size: 100%;
+ background-color: var(--input-placeholder-color);
+}
+
+/* ==== SORT ICON ACTIONS ==== */
+.extra-network-pane .extra-network-control .extra-network-control--sort {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort .extra-network-control--sort-icon {
+ height: 1.5rem;
+ width: 1.5rem;
+ mask-repeat: no-repeat;
+ mask-position: center center;
+ mask-size: 100%;
+ background-color: var(--input-placeholder-color);
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="path"] .extra-network-control--sort-icon {
+ mask-image: url('data:image/svg+xml,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M1 5C1 3.34315 2.34315 2 4 2H8.43845C9.81505 2 11.015 2.93689 11.3489 4.27239L11.7808 6H13.5H20C21.6569 6 23 7.34315 23 9V11C23 11.5523 22.5523 12 22 12C21.4477 12 21 11.5523 21 11V9C21 8.44772 20.5523 8 20 8H13.5H11.7808H4C3.44772 8 3 8.44772 3 9V10V19C3 19.5523 3.44772 20 4 20H9C9.55228 20 10 20.4477 10 21C10 21.5523 9.55228 22 9 22H4C2.34315 22 1 20.6569 1 19V10V9V5ZM3 6.17071C3.31278 6.06015 3.64936 6 4 6H9.71922L9.40859 4.75746C9.2973 4.3123 8.89732 4 8.43845 4H4C3.44772 4 3 4.44772 3 5V6.17071ZM20.1716 18.7574C20.6951 17.967 21 17.0191 21 16C21 13.2386 18.7614 11 16 11C13.2386 11 11 13.2386 11 16C11 18.7614 13.2386 21 16 21C17.0191 21 17.967 20.6951 18.7574 20.1716L21.2929 22.7071C21.6834 23.0976 22.3166 23.0976 22.7071 22.7071C23.0976 22.3166 23.0976 21.6834 22.7071 21.2929L20.1716 18.7574ZM13 16C13 14.3431 14.3431 13 16 13C17.6569 13 19 14.3431 19 16C19 17.6569 17.6569 19 16 19C14.3431 19 13 17.6569 13 16Z" fill="%23000000"></path></g></svg>');
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="name"] .extra-network-control--sort-icon {
+ mask-image: url('data:image/svg+xml,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M17.1841 6.69223C17.063 6.42309 16.7953 6.25 16.5002 6.25C16.2051 6.25 15.9374 6.42309 15.8162 6.69223L11.3162 16.6922C11.1463 17.07 11.3147 17.514 11.6924 17.6839C12.0701 17.8539 12.5141 17.6855 12.6841 17.3078L14.1215 14.1136H18.8789L20.3162 17.3078C20.4862 17.6855 20.9302 17.8539 21.308 17.6839C21.6857 17.514 21.8541 17.07 21.6841 16.6922L17.1841 6.69223ZM16.5002 8.82764L14.7965 12.6136H18.2039L16.5002 8.82764Z" fill="%231C274C"></path><path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M2.25 7C2.25 6.58579 2.58579 6.25 3 6.25H13C13.4142 6.25 13.75 6.58579 13.75 7C13.75 7.41421 13.4142 7.75 13 7.75H3C2.58579 7.75 2.25 7.41421 2.25 7Z" fill="%231C274C"></path><path opacity="0.5" d="M2.25 12C2.25 11.5858 2.58579 11.25 3 11.25H10C10.4142 11.25 10.75 11.5858 10.75 12C10.75 12.4142 10.4142 12.75 10 12.75H3C2.58579 12.75 2.25 12.4142 2.25 12Z" fill="%231C274C"></path><path opacity="0.5" d="M2.25 17C2.25 16.5858 2.58579 16.25 3 16.25H8C8.41421 16.25 8.75 16.5858 8.75 17C8.75 17.4142 8.41421 17.75 8 17.75H3C2.58579 17.75 2.25 17.4142 2.25 17Z" fill="%231C274C"></path></g></svg>');
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="date_created"] .extra-network-control--sort-icon {
+ mask-image: url('data:image/svg+xml,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M17 11C14.2386 11 12 13.2386 12 16C12 18.7614 14.2386 21 17 21C19.7614 21 22 18.7614 22 16C22 13.2386 19.7614 11 17 11ZM17 11V9M2 9V15.8C2 16.9201 2 17.4802 2.21799 17.908C2.40973 18.2843 2.71569 18.5903 3.09202 18.782C3.51984 19 4.0799 19 5.2 19H13M2 9V8.2C2 7.0799 2 6.51984 2.21799 6.09202C2.40973 5.71569 2.71569 5.40973 3.09202 5.21799C3.51984 5 4.0799 5 5.2 5H13.8C14.9201 5 15.4802 5 15.908 5.21799C16.2843 5.40973 16.5903 5.71569 16.782 6.09202C17 6.51984 17 7.0799 17 8.2V9M2 9H17M5 3V5M14 3V5M15 16H17M17 16H19M17 16V14M17 16V18" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>');
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="date_modified"] .extra-network-control--sort-icon {
+ mask-image: url('data:image/svg+xml,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M10 21H6.2C5.0799 21 4.51984 21 4.09202 20.782C3.71569 20.5903 3.40973 20.2843 3.21799 19.908C3 19.4802 3 18.9201 3 17.8V8.2C3 7.0799 3 6.51984 3.21799 6.09202C3.40973 5.71569 3.71569 5.40973 4.09202 5.21799C4.51984 5 5.0799 5 6.2 5H17.8C18.9201 5 19.4802 5 19.908 5.21799C20.2843 5.40973 20.5903 5.71569 20.782 6.09202C21 6.51984 21 7.0799 21 8.2V10M7 3V5M17 3V5M3 9H21M13.5 13.0001L7 13M10 17.0001L7 17M14 21L16.025 20.595C16.2015 20.5597 16.2898 20.542 16.3721 20.5097C16.4452 20.4811 16.5147 20.4439 16.579 20.399C16.6516 20.3484 16.7152 20.2848 16.8426 20.1574L21 16C21.5523 15.4477 21.5523 14.5523 21 14C20.4477 13.4477 19.5523 13.4477 19 14L14.8426 18.1574C14.7152 18.2848 14.6516 18.3484 14.601 18.421C14.5561 18.4853 14.5189 18.5548 14.4903 18.6279C14.458 18.7102 14.4403 18.7985 14.405 18.975L14 21Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>');
+}
+
+/* ==== SORT DIRECTION ICON ACTIONS ==== */
+.extra-network-pane .extra-network-control .extra-network-control--sort-dir {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort-dir .extra-network-control--sort-dir-icon {
+ height: 1.5rem;
+ width: 1.5rem;
+ mask-repeat: no-repeat;
+ mask-position: center center;
+ mask-size: 100%;
+ background-color: var(--input-placeholder-color);
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort-dir[data-sortdir="Ascending"] .extra-network-control--sort-dir-icon {
+ mask-image: url('data:image/svg+xml,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M13 12H21M13 8H21M13 16H21M6 7V17M6 7L3 10M6 7L9 10" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>');
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--sort-dir[data-sortdir="Descending"] .extra-network-control--sort-dir-icon {
+ mask-image: url('data:image/svg+xml,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M13 12H21M13 8H21M13 16H21M6 7V17M6 17L3 14M6 17L9 14" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>');
+}
+
+/* ==== TREE VIEW ICON ACTIONS ==== */
+.extra-network-pane .extra-network-control .extra-network-control--tree-view {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--tree-view .extra-network-control--tree-view-icon {
+ height: 1.5rem;
+ width: 1.5rem;
+ mask-image: url('data:image/svg+xml,<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="black"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill="black" d="M16 10v-4h-11v1h-2v-3h9v-4h-12v4h2v10h3v2h11v-4h-11v1h-2v-5h2v2z"></path></g></svg>');
+ mask-repeat: no-repeat;
+ mask-position: center center;
+ mask-size: 100%;
+ background-color: var(--input-placeholder-color);
+}
+
+.dark .extra-network-pane .extra-network-control .extra-network-control--enabled {
+ background-color: var(--neutral-700);
+}
+
+.dark .extra-network-pane .extra-network-control .extra-network-control--enabled {
+ background-color: var(--neutral-300);
+}
+
+/* ==== REFRESH ICON ACTIONS ==== */
+.extra-network-pane .extra-network-control .extra-network-control--refresh {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--refresh .extra-network-control--refresh-icon {
+ height: 1.5rem;
+ width: 1.5rem;
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="bevel"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38"/></svg>');
+ mask-repeat: no-repeat;
+ mask-position: center center;
+ mask-size: 100%;
+ background-color: var(--input-placeholder-color);
+}
+
+.extra-network-pane .extra-network-control .extra-network-control--refresh-icon:active {
+ -ms-transform: rotate(180deg);
+ -webkit-transform: rotate(180deg);
+ transform: rotate(180deg);
+ transition: transform 0.2s;
+}
+
+/* ==== TREE GRID CONFIG ==== */
+
+/* Text for button. */
+.extra-network-tree .tree-list-item-label {
+ position: relative;
+ line-height: 1.25rem;
+ color: var(--button-secondary-text-color);
+ grid-area: label;
+ padding-left: 0.5rem;
+}
+
+/* Text for button truncated. */
+.extra-network-tree .tree-list-item-label--truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Icon for button. */
+.extra-network-tree .tree-list-item-visual {
+ min-height: 1rem;
+ color: var(--button-secondary-text-color);
+ pointer-events: none;
+ align-items: right;
+}
+
+
+/* Icon for button when it is before label. */
+.extra-network-tree .tree-list-item-visual--leading {
+ grid-area: leading-visual;
+ width: 1rem;
+ text-align: right;
+}
+
+/* Icon for button when it is after label. */
+.extra-network-tree .tree-list-item-visual--trailing {
+ grid-area: trailing-visual;
+ width: 1rem;
+ text-align: right;
+}
+
+/* Dropdown arrow for button. */
+.extra-network-tree .tree-list-item-action--leading {
+ margin-right: 0.5rem;
+ margin-left: 0.2rem;
+}
+
+.extra-network-tree .tree-list-content-file .tree-list-item-action--leading {
+ visibility: hidden;
+}
+
+.extra-network-tree .tree-list-item-action--leading {
+ grid-area: leading-action;
+}
+
+.extra-network-tree .tree-list-item-action--trailing {
+ grid-area: trailing-action;
+ display: inline-flex;
+}
+
+.extra-network-tree .tree-list-content .button-row {
+ display: inline-flex;
+ visibility: hidden;
+ color: var(--button-secondary-text-color);
+
+}
+
+.extra-network-tree .tree-list-content:hover .button-row {
+ visibility: visible;
+}