aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--extensions-builtin/Lora/networks.py4
-rw-r--r--extensions-builtin/Lora/ui_extra_networks_lora.py5
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js8
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py4
-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.js463
-rw-r--r--modules/cmd_args.py2
-rw-r--r--modules/dat_model.py79
-rw-r--r--modules/devices.py11
-rw-r--r--modules/extensions.py5
-rw-r--r--modules/images.py2
-rw-r--r--modules/infotext_utils.py8
-rw-r--r--modules/masking.py43
-rw-r--r--modules/postprocessing.py4
-rw-r--r--modules/processing.py22
-rw-r--r--modules/processing_scripts/seed.py19
-rw-r--r--modules/scripts.py17
-rw-r--r--modules/shared.py3
-rw-r--r--modules/shared_items.py5
-rw-r--r--modules/shared_options.py5
-rw-r--r--modules/styles.py85
-rw-r--r--modules/txt2img.py30
-rw-r--r--modules/ui.py8
-rw-r--r--modules/ui_common.py71
-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--modules/ui_postprocessing.py5
-rw-r--r--modules/ui_prompt_styles.py9
-rw-r--r--modules/ui_toprow.py3
-rw-r--r--scripts/postprocessing_upscale.py2
-rw-r--r--style.css528
39 files changed, 1645 insertions, 489 deletions
diff --git a/extensions-builtin/Lora/networks.py b/extensions-builtin/Lora/networks.py
index 32e10b62..83ea2802 100644
--- a/extensions-builtin/Lora/networks.py
+++ b/extensions-builtin/Lora/networks.py
@@ -260,11 +260,11 @@ def load_networks(names, te_multipliers=None, unet_multipliers=None, dyn_dims=No
loaded_networks.clear()
- networks_on_disk = [available_network_aliases.get(name, None) for name in names]
+ networks_on_disk = [available_networks.get(name, None) if name.lower() in forbidden_network_aliases else available_network_aliases.get(name, None) for name in names]
if any(x is None for x in networks_on_disk):
list_available_networks()
- networks_on_disk = [available_network_aliases.get(name, None) for name in names]
+ networks_on_disk = [available_networks.get(name, None) if name.lower() in forbidden_network_aliases else available_network_aliases.get(name, None) for name in names]
failed_to_load_networks = []
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/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
index 45c7600a..df60c1a1 100644
--- a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
+++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
@@ -218,6 +218,8 @@ onUiLoaded(async() => {
canvas_hotkey_fullscreen: "KeyS",
canvas_hotkey_move: "KeyF",
canvas_hotkey_overlap: "KeyO",
+ canvas_hotkey_shrink_brush: "KeyQ",
+ canvas_hotkey_grow_brush: "KeyW",
canvas_disabled_functions: [],
canvas_show_tooltip: true,
canvas_auto_expand: true,
@@ -227,6 +229,8 @@ onUiLoaded(async() => {
const functionMap = {
"Zoom": "canvas_hotkey_zoom",
"Adjust brush size": "canvas_hotkey_adjust",
+ "Hotkey shrink brush": "canvas_hotkey_shrink_brush",
+ "Hotkey enlarge brush": "canvas_hotkey_grow_brush",
"Moving canvas": "canvas_hotkey_move",
"Fullscreen": "canvas_hotkey_fullscreen",
"Reset Zoom": "canvas_hotkey_reset",
@@ -686,7 +690,9 @@ onUiLoaded(async() => {
const hotkeyActions = {
[hotkeysConfig.canvas_hotkey_reset]: resetZoom,
[hotkeysConfig.canvas_hotkey_overlap]: toggleOverlap,
- [hotkeysConfig.canvas_hotkey_fullscreen]: fitToScreen
+ [hotkeysConfig.canvas_hotkey_fullscreen]: fitToScreen,
+ [hotkeysConfig.canvas_hotkey_shrink_brush]: () => adjustBrushSize(elemId, 10),
+ [hotkeysConfig.canvas_hotkey_grow_brush]: () => adjustBrushSize(elemId, -10)
};
const action = hotkeyActions[event.code];
diff --git a/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
index 2d8d2d1c..89b7c31f 100644
--- a/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
+++ b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
@@ -4,6 +4,8 @@ from modules import shared
shared.options_templates.update(shared.options_section(('canvas_hotkey', "Canvas Hotkeys"), {
"canvas_hotkey_zoom": shared.OptionInfo("Alt", "Zoom canvas", gr.Radio, {"choices": ["Shift","Ctrl", "Alt"]}).info("If you choose 'Shift' you cannot scroll horizontally, 'Alt' can cause a little trouble in firefox"),
"canvas_hotkey_adjust": shared.OptionInfo("Ctrl", "Adjust brush size", gr.Radio, {"choices": ["Shift","Ctrl", "Alt"]}).info("If you choose 'Shift' you cannot scroll horizontally, 'Alt' can cause a little trouble in firefox"),
+ "canvas_hotkey_shrink_brush": shared.OptionInfo("Q", "Shrink the brush size"),
+ "canvas_hotkey_grow_brush": shared.OptionInfo("W", "Enlarge the brush size"),
"canvas_hotkey_move": shared.OptionInfo("F", "Moving the canvas").info("To work correctly in firefox, turn off 'Automatically search the page text when typing' in the browser settings"),
"canvas_hotkey_fullscreen": shared.OptionInfo("S", "Fullscreen Mode, maximizes the picture so that it fits into the screen and stretches it to its full width "),
"canvas_hotkey_reset": shared.OptionInfo("R", "Reset zoom and canvas positon"),
@@ -11,5 +13,5 @@ shared.options_templates.update(shared.options_section(('canvas_hotkey', "Canvas
"canvas_show_tooltip": shared.OptionInfo(True, "Enable tooltip on the canvas"),
"canvas_auto_expand": shared.OptionInfo(True, "Automatically expands an image that does not fit completely in the canvas area, similar to manually pressing the S and R buttons"),
"canvas_blur_prompt": shared.OptionInfo(False, "Take the focus off the prompt when working with a canvas"),
- "canvas_disabled_functions": shared.OptionInfo(["Overlap"], "Disable function that you don't use", gr.CheckboxGroup, {"choices": ["Zoom","Adjust brush size", "Moving canvas","Fullscreen","Reset Zoom","Overlap"]}),
+ "canvas_disabled_functions": shared.OptionInfo(["Overlap"], "Disable function that you don't use", gr.CheckboxGroup, {"choices": ["Zoom","Adjust brush size","Hotkey enlarge brush","Hotkey shrink brush","Moving canvas","Fullscreen","Reset Zoom","Overlap"]}),
}))
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..0c763f71
--- /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" id="{tabname}_{extra_networks_tabname}_controls" style="display:none" >
+ <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..4ef1a96f 100644
--- a/javascript/extraNetworks.js
+++ b/javascript/extraNetworks.js
@@ -16,99 +16,108 @@ 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 tabnav = gradioApp().querySelector('#' + tabname + '_extra_tabs > div.tab-nav');
+ var controlsDiv = document.createElement('DIV');
+ controlsDiv.classList.add('extra-networks-controls-div');
+ tabnav.appendChild(controlsDiv);
+ tabnav.insertBefore(controlsDiv, null);
+
+ var this_tab = gradioApp().querySelector('#' + tabname + '_extra_tabs');
+ 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();
+ applyFilter();
+ extraNetworksApplySort[tabname_full] = applySort;
+ extraNetworksApplyFilter[tabname_full] = applyFilter;
- extraNetworksApplySort[tabname] = applySort;
- extraNetworksApplyFilter[tabname] = applyFilter;
+ var controls = gradioApp().querySelector("#" + tabname_full + "_controls");
+ controlsDiv.insertBefore(controls, null);
+ });
- 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 +146,32 @@ function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePromp
}
-function extraNetworksUrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate)
+function extraNetworksShowControlsForPage(tabname, tabname_full) {
+ gradioApp().querySelectorAll('#' + tabname + '_extra_tabs .extra-networks-controls-div > div').forEach(function(elem) {
+ var targetId = tabname_full + "_controls";
+ elem.style.display = elem.id == targetId ? "" : "none";
+ });
+}
+
+
+function extraNetworksUnrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate)
extraNetworksMovePromptToTab(tabname, '', false, false);
+
+ extraNetworksShowControlsForPage(tabname, null);
}
-function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt) { // called from python when user selects an extra networks tab
+function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt, tabname_full) { // called from python when user selects an extra networks tab
extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt);
+ extraNetworksShowControlsForPage(tabname, tabname_full);
}
-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 +181,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 +192,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 +206,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 +215,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 +228,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 +258,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);
+}
+
+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");
+}
- searchTextarea.value = text;
- updateInput(searchTextarea);
+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 +529,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 +595,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/cmd_args.py b/modules/cmd_args.py
index e58059a1..f1251b6c 100644
--- a/modules/cmd_args.py
+++ b/modules/cmd_args.py
@@ -88,7 +88,7 @@ parser.add_argument("--gradio-img2img-tool", type=str, help='does not do anythin
parser.add_argument("--gradio-inpaint-tool", type=str, help="does not do anything")
parser.add_argument("--gradio-allowed-path", action='append', help="add path to gradio's allowed_paths, make it possible to serve files from it", default=[data_path])
parser.add_argument("--opt-channelslast", action='store_true', help="change memory type for stable diffusion to channels last")
-parser.add_argument("--styles-file", type=str, help="filename to use for styles", default=os.path.join(data_path, 'styles.csv'))
+parser.add_argument("--styles-file", type=str, action='append', help="path or wildcard path of styles files, allow multiple entries.", default=[])
parser.add_argument("--autolaunch", action='store_true', help="open the webui URL in the system's default browser upon launch", default=False)
parser.add_argument("--theme", type=str, help="launches the UI with light or dark theme", default=None)
parser.add_argument("--use-textbox-seed", action='store_true', help="use textbox for seeds in UI (no up/down, but possible to input long seeds)", default=False)
diff --git a/modules/dat_model.py b/modules/dat_model.py
new file mode 100644
index 00000000..495d5f49
--- /dev/null
+++ b/modules/dat_model.py
@@ -0,0 +1,79 @@
+import os
+
+from modules import modelloader, errors
+from modules.shared import cmd_opts, opts
+from modules.upscaler import Upscaler, UpscalerData
+from modules.upscaler_utils import upscale_with_model
+
+
+class UpscalerDAT(Upscaler):
+ def __init__(self, user_path):
+ self.name = "DAT"
+ self.user_path = user_path
+ self.scalers = []
+ super().__init__()
+
+ for file in self.find_models(ext_filter=[".pt", ".pth"]):
+ name = modelloader.friendly_name(file)
+ scaler_data = UpscalerData(name, file, upscaler=self, scale=None)
+ self.scalers.append(scaler_data)
+
+ for model in get_dat_models(self):
+ if model.name in opts.dat_enabled_models:
+ self.scalers.append(model)
+
+ def do_upscale(self, img, path):
+ try:
+ info = self.load_model(path)
+ except Exception:
+ errors.report(f"Unable to load DAT model {path}", exc_info=True)
+ return img
+
+ model_descriptor = modelloader.load_spandrel_model(
+ info.local_data_path,
+ device=self.device,
+ prefer_half=(not cmd_opts.no_half and not cmd_opts.upcast_sampling),
+ expected_architecture="DAT",
+ )
+ return upscale_with_model(
+ model_descriptor,
+ img,
+ tile_size=opts.DAT_tile,
+ tile_overlap=opts.DAT_tile_overlap,
+ )
+
+ def load_model(self, path):
+ for scaler in self.scalers:
+ if scaler.data_path == path:
+ if scaler.local_data_path.startswith("http"):
+ scaler.local_data_path = modelloader.load_file_from_url(
+ scaler.data_path,
+ model_dir=self.model_download_path,
+ )
+ if not os.path.exists(scaler.local_data_path):
+ raise FileNotFoundError(f"DAT data missing: {scaler.local_data_path}")
+ return scaler
+ raise ValueError(f"Unable to find model info: {path}")
+
+
+def get_dat_models(scaler):
+ return [
+ UpscalerData(
+ name="DAT x2",
+ path="https://github.com/n0kovo/dat_upscaler_models/raw/main/DAT/DAT_x2.pth",
+ scale=2,
+ upscaler=scaler,
+ ),
+ UpscalerData(
+ name="DAT x3",
+ path="https://github.com/n0kovo/dat_upscaler_models/raw/main/DAT/DAT_x3.pth",
+ scale=3,
+ upscaler=scaler,
+ ),
+ UpscalerData(
+ name="DAT x4",
+ path="https://github.com/n0kovo/dat_upscaler_models/raw/main/DAT/DAT_x4.pth",
+ scale=4,
+ upscaler=scaler,
+ ),
+ ]
diff --git a/modules/devices.py b/modules/devices.py
index 0321d12c..dfffaf24 100644
--- a/modules/devices.py
+++ b/modules/devices.py
@@ -164,7 +164,11 @@ def manual_cast_forward(target_dtype):
@contextlib.contextmanager
def manual_cast(target_dtype):
+ applied = False
for module_type in patch_module_list:
+ if hasattr(module_type, "org_forward"):
+ continue
+ applied = True
org_forward = module_type.forward
if module_type == torch.nn.MultiheadAttention and has_xpu():
module_type.forward = manual_cast_forward(torch.float32)
@@ -174,8 +178,11 @@ def manual_cast(target_dtype):
try:
yield None
finally:
- for module_type in patch_module_list:
- module_type.forward = module_type.org_forward
+ if applied:
+ for module_type in patch_module_list:
+ if hasattr(module_type, "org_forward"):
+ module_type.forward = module_type.org_forward
+ delattr(module_type, "org_forward")
def autocast(disable=False):
diff --git a/modules/extensions.py b/modules/extensions.py
index 99e7ee60..04bda297 100644
--- a/modules/extensions.py
+++ b/modules/extensions.py
@@ -224,13 +224,16 @@ def list_extensions():
# check for requirements
for extension in extensions:
+ if not extension.enabled:
+ continue
+
for req in extension.metadata.requires:
required_extension = loaded_extensions.get(req)
if required_extension is None:
errors.report(f'Extension "{extension.name}" requires "{req}" which is not installed.', exc_info=False)
continue
- if not extension.enabled:
+ if not required_extension.enabled:
errors.report(f'Extension "{extension.name}" requires "{required_extension.name}" which is disabled.', exc_info=False)
continue
diff --git a/modules/images.py b/modules/images.py
index 87a7bf22..b6f2358c 100644
--- a/modules/images.py
+++ b/modules/images.py
@@ -321,7 +321,7 @@ def resize_image(resize_mode, im, width, height, upscaler_name=None):
return res
-invalid_filename_chars = '<>:"/\\|?*\n\r\t'
+invalid_filename_chars = '#<>:"/\\|?*\n\r\t'
invalid_filename_prefix = ' '
invalid_filename_postfix = ' .'
re_nonletters = re.compile(r'[\s' + string.punctuation + ']+')
diff --git a/modules/infotext_utils.py b/modules/infotext_utils.py
index 9a02cdf2..1049c6c3 100644
--- a/modules/infotext_utils.py
+++ b/modules/infotext_utils.py
@@ -230,7 +230,7 @@ def restore_old_hires_fix_params(res):
res['Hires resize-2'] = height
-def parse_generation_parameters(x: str):
+def parse_generation_parameters(x: str, skip_fields: list[str] | None = None):
"""parses generation parameters string, the one you see in text field under the picture in UI:
```
girl with an artist's beret, determined, blue eyes, desert scene, computer monitors, heavy makeup, by Alphonse Mucha and Charlie Bowater, ((eyeshadow)), (coquettish), detailed, intricate
@@ -240,6 +240,8 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model
returns a dict with field values
"""
+ if skip_fields is None:
+ skip_fields = shared.opts.infotext_skip_pasting
res = {}
@@ -356,8 +358,8 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model
infotext_versions.backcompat(res)
- skip = set(shared.opts.infotext_skip_pasting)
- res = {k: v for k, v in res.items() if k not in skip}
+ for key in skip_fields:
+ res.pop(key, None)
return res
diff --git a/modules/masking.py b/modules/masking.py
index be9f84c7..29a39452 100644
--- a/modules/masking.py
+++ b/modules/masking.py
@@ -3,40 +3,15 @@ from PIL import Image, ImageFilter, ImageOps
def get_crop_region(mask, pad=0):
"""finds a rectangular region that contains all masked ares in an image. Returns (x1, y1, x2, y2) coordinates of the rectangle.
- For example, if a user has painted the top-right part of a 512x512 image", the result may be (256, 0, 512, 256)"""
-
- h, w = mask.shape
-
- crop_left = 0
- for i in range(w):
- if not (mask[:, i] == 0).all():
- break
- crop_left += 1
-
- crop_right = 0
- for i in reversed(range(w)):
- if not (mask[:, i] == 0).all():
- break
- crop_right += 1
-
- crop_top = 0
- for i in range(h):
- if not (mask[i] == 0).all():
- break
- crop_top += 1
-
- crop_bottom = 0
- for i in reversed(range(h)):
- if not (mask[i] == 0).all():
- break
- crop_bottom += 1
-
- return (
- int(max(crop_left-pad, 0)),
- int(max(crop_top-pad, 0)),
- int(min(w - crop_right + pad, w)),
- int(min(h - crop_bottom + pad, h))
- )
+ For example, if a user has painted the top-right part of a 512x512 image, the result may be (256, 0, 512, 256)"""
+ mask_img = mask if isinstance(mask, Image.Image) else Image.fromarray(mask)
+ box = mask_img.getbbox()
+ if box:
+ x1, y1, x2, y2 = box
+ else: # when no box is found
+ x1, y1 = mask_img.size
+ x2 = y2 = 0
+ return max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask_img.size[0]), min(y2 + pad, mask_img.size[1])
def expand_crop_region(crop_region, processing_width, processing_height, image_width, image_height):
diff --git a/modules/postprocessing.py b/modules/postprocessing.py
index 7449b0dc..f1488232 100644
--- a/modules/postprocessing.py
+++ b/modules/postprocessing.py
@@ -62,8 +62,6 @@ def run_postprocessing(extras_mode, image, image_folder, input_dir, output_dir,
else:
image_data = image_placeholder
- shared.state.assign_current_image(image_data)
-
parameters, existing_pnginfo = images.read_info_from_image(image_data)
if parameters:
existing_pnginfo["parameters"] = parameters
@@ -92,6 +90,8 @@ def run_postprocessing(extras_mode, image, image_folder, input_dir, output_dir,
pp.image.info = existing_pnginfo
pp.image.info["postprocessing"] = infotext
+ shared.state.assign_current_image(pp.image)
+
if save_output:
fullfn, _ = images.save_image(pp.image, path=outpath, basename=basename, extension=opts.samples_format, info=infotext, short_filename=True, no_prompt=True, grid=False, pnginfo_section_name="extras", existing_info=existing_pnginfo, forced_filename=forced_filename, suffix=suffix)
diff --git a/modules/processing.py b/modules/processing.py
index dcc807fe..52f00bfb 100644
--- a/modules/processing.py
+++ b/modules/processing.py
@@ -1005,7 +1005,13 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
image = pp.image
mask_for_overlay = getattr(p, "mask_for_overlay", None)
- overlay_image = p.overlay_images[i] if getattr(p, "overlay_images", None) is not None and i < len(p.overlay_images) else None
+
+ if not shared.opts.overlay_inpaint:
+ overlay_image = None
+ elif getattr(p, "overlay_images", None) is not None and i < len(p.overlay_images):
+ overlay_image = p.overlay_images[i]
+ else:
+ overlay_image = None
if p.scripts is not None:
ppmo = scripts.PostProcessMaskOverlayArgs(i, mask_for_overlay, overlay_image)
@@ -1029,6 +1035,11 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
image = apply_overlay(image, p.paste_to, overlay_image)
+ if p.scripts is not None:
+ pp = scripts.PostprocessImageArgs(image)
+ p.scripts.postprocess_image_after_composite(p, pp)
+ image = pp.image
+
if save_samples:
images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], opts.samples_format, info=infotext(i), p=p)
@@ -1227,8 +1238,11 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing):
if not state.processing_has_refined_job_count:
if state.job_count == -1:
state.job_count = self.n_iter
-
- shared.total_tqdm.updateTotal((self.steps + (self.hr_second_pass_steps or self.steps)) * state.job_count)
+ if getattr(self, 'txt2img_upscale', False):
+ total_steps = (self.hr_second_pass_steps or self.steps) * state.job_count
+ else:
+ total_steps = (self.steps + (self.hr_second_pass_steps or self.steps)) * state.job_count
+ shared.total_tqdm.updateTotal(total_steps)
state.job_count = state.job_count * 2
state.processing_has_refined_job_count = True
@@ -1554,7 +1568,7 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing):
if self.inpaint_full_res:
self.mask_for_overlay = image_mask
mask = image_mask.convert('L')
- crop_region = masking.get_crop_region(np.array(mask), self.inpaint_full_res_padding)
+ crop_region = masking.get_crop_region(mask, self.inpaint_full_res_padding)
crop_region = masking.expand_crop_region(crop_region, self.width, self.height, mask.width, mask.height)
x1, y1, x2, y2 = crop_region
diff --git a/modules/processing_scripts/seed.py b/modules/processing_scripts/seed.py
index 2d3cbb97..7a4c0159 100644
--- a/modules/processing_scripts/seed.py
+++ b/modules/processing_scripts/seed.py
@@ -6,6 +6,7 @@ from modules import scripts, ui, errors
from modules.infotext_utils import PasteField
from modules.shared import cmd_opts
from modules.ui_components import ToolButton
+from modules import infotext_utils
class ScriptSeed(scripts.ScriptBuiltinUI):
@@ -77,7 +78,6 @@ class ScriptSeed(scripts.ScriptBuiltinUI):
p.seed_resize_from_h = seed_resize_from_h
-
def connect_reuse_seed(seed: gr.Number, reuse_seed: gr.Button, generation_info: gr.Textbox, is_subseed):
""" Connects a 'reuse (sub)seed' button's click event so that it copies last used
(sub)seed value from generation info the to the seed field. If copying subseed and subseed strength
@@ -85,21 +85,14 @@ def connect_reuse_seed(seed: gr.Number, reuse_seed: gr.Button, generation_info:
def copy_seed(gen_info_string: str, index):
res = -1
-
try:
gen_info = json.loads(gen_info_string)
- index -= gen_info.get('index_of_first_image', 0)
-
- if is_subseed and gen_info.get('subseed_strength', 0) > 0:
- all_subseeds = gen_info.get('all_subseeds', [-1])
- res = all_subseeds[index if 0 <= index < len(all_subseeds) else 0]
- else:
- all_seeds = gen_info.get('all_seeds', [-1])
- res = all_seeds[index if 0 <= index < len(all_seeds) else 0]
-
- except json.decoder.JSONDecodeError:
+ infotext = gen_info.get('infotexts')[index]
+ gen_parameters = infotext_utils.parse_generation_parameters(infotext, [])
+ res = int(gen_parameters.get('Variation seed' if is_subseed else 'Seed', -1))
+ except Exception:
if gen_info_string:
- errors.report(f"Error parsing JSON generation info: {gen_info_string}")
+ errors.report(f"Error retrieving seed from generation info: {gen_info_string}", exc_info=True)
return [res, gr.update()]
diff --git a/modules/scripts.py b/modules/scripts.py
index cf938ebb..060069cf 100644
--- a/modules/scripts.py
+++ b/modules/scripts.py
@@ -262,6 +262,15 @@ class Script:
pass
+ def postprocess_image_after_composite(self, p, pp: PostprocessImageArgs, *args):
+ """
+ Called for every image after it has been generated.
+ Same as postprocess_image but after inpaint_full_res composite
+ So that it operates on the full image instead of the inpaint_full_res crop region.
+ """
+
+ pass
+
def postprocess(self, p, processed, *args):
"""
This function is called after processing ends for AlwaysVisible scripts.
@@ -856,6 +865,14 @@ class ScriptRunner:
except Exception:
errors.report(f"Error running postprocess_image: {script.filename}", exc_info=True)
+ def postprocess_image_after_composite(self, p, pp: PostprocessImageArgs):
+ for script in self.alwayson_scripts:
+ try:
+ script_args = p.script_args[script.args_from:script.args_to]
+ script.postprocess_image_after_composite(p, pp, *script_args)
+ except Exception:
+ errors.report(f"Error running postprocess_image_after_composite: {script.filename}", exc_info=True)
+
def before_component(self, component, **kwargs):
for callback, script in self.on_before_component_elem_id.get(kwargs.get("elem_id"), []):
try:
diff --git a/modules/shared.py b/modules/shared.py
index 63661939..ccdca4e7 100644
--- a/modules/shared.py
+++ b/modules/shared.py
@@ -1,3 +1,4 @@
+import os
import sys
import gradio as gr
@@ -11,7 +12,7 @@ parser = shared_cmd_options.parser
batch_cond_uncond = True # old field, unused now in favor of shared.opts.batch_cond_uncond
parallel_processing_allowed = True
-styles_filename = cmd_opts.styles_file
+styles_filename = cmd_opts.styles_file = cmd_opts.styles_file if len(cmd_opts.styles_file) > 0 else [os.path.join(data_path, 'styles.csv')]
config_filename = cmd_opts.ui_settings_file
hide_dirs = {"visible": not cmd_opts.hide_ui_dir_config}
diff --git a/modules/shared_items.py b/modules/shared_items.py
index 13fb2814..88f63645 100644
--- a/modules/shared_items.py
+++ b/modules/shared_items.py
@@ -8,6 +8,11 @@ def realesrgan_models_names():
return [x.name for x in modules.realesrgan_model.get_realesrgan_models(None)]
+def dat_models_names():
+ import modules.dat_model
+ return [x.name for x in modules.dat_model.get_dat_models(None)]
+
+
def postprocessing_scripts():
import modules.scripts
diff --git a/modules/shared_options.py b/modules/shared_options.py
index 63488f4e..fef1fb83 100644
--- a/modules/shared_options.py
+++ b/modules/shared_options.py
@@ -97,6 +97,9 @@ options_templates.update(options_section(('upscaling', "Upscaling", "postprocess
"ESRGAN_tile": OptionInfo(192, "Tile size for ESRGAN upscalers.", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}).info("0 = no tiling"),
"ESRGAN_tile_overlap": OptionInfo(8, "Tile overlap for ESRGAN upscalers.", gr.Slider, {"minimum": 0, "maximum": 48, "step": 1}).info("Low values = visible seam"),
"realesrgan_enabled_models": OptionInfo(["R-ESRGAN 4x+", "R-ESRGAN 4x+ Anime6B"], "Select which Real-ESRGAN models to show in the web UI.", gr.CheckboxGroup, lambda: {"choices": shared_items.realesrgan_models_names()}),
+ "dat_enabled_models": OptionInfo(["DAT x2", "DAT x3", "DAT x4"], "Select which DAT models to show in the web UI.", gr.CheckboxGroup, lambda: {"choices": shared_items.dat_models_names()}),
+ "DAT_tile": OptionInfo(192, "Tile size for DAT upscalers.", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}).info("0 = no tiling"),
+ "DAT_tile_overlap": OptionInfo(8, "Tile overlap for DAT upscalers.", gr.Slider, {"minimum": 0, "maximum": 48, "step": 1}).info("Low values = visible seam"),
"upscaler_for_img2img": OptionInfo(None, "Upscaler for img2img", gr.Dropdown, lambda: {"choices": [x.name for x in shared.sd_upscalers]}),
}))
@@ -198,6 +201,7 @@ options_templates.update(options_section(('img2img', "img2img", "sd"), {
"return_mask": OptionInfo(False, "For inpainting, include the greyscale mask in results for web"),
"return_mask_composite": OptionInfo(False, "For inpainting, include masked composite in results for web"),
"img2img_batch_show_results_limit": OptionInfo(32, "Show the first N batch img2img results in UI", gr.Slider, {"minimum": -1, "maximum": 1000, "step": 1}).info('0: disable, -1: show all images. Too many images can cause lag'),
+ "overlay_inpaint": OptionInfo(True, "Overlay original for inpaint").info("when inpainting, overlay the original image over the areas that weren't inpainted."),
}))
options_templates.update(options_section(('optimizations', "Optimizations", "sd"), {
@@ -251,6 +255,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/styles.py b/modules/styles.py
index 026c4300..9edcc7e4 100644
--- a/modules/styles.py
+++ b/modules/styles.py
@@ -1,16 +1,15 @@
+from pathlib import Path
import csv
-import fnmatch
import os
-import os.path
import typing
import shutil
class PromptStyle(typing.NamedTuple):
name: str
- prompt: str
- negative_prompt: str
- path: str = None
+ prompt: str | None
+ negative_prompt: str | None
+ path: str | None = None
def merge_prompts(style_prompt: str, prompt: str) -> str:
@@ -79,14 +78,19 @@ def extract_original_prompts(style: PromptStyle, prompt, negative_prompt):
class StyleDatabase:
- def __init__(self, path: str):
+ def __init__(self, paths: list[str | Path]):
self.no_style = PromptStyle("None", "", "", None)
self.styles = {}
- self.path = path
-
- folder, file = os.path.split(self.path)
- filename, _, ext = file.partition('*')
- self.default_path = os.path.join(folder, filename + ext)
+ self.paths = paths
+ self.all_styles_files: list[Path] = []
+
+ folder, file = os.path.split(self.paths[0])
+ if '*' in file or '?' in file:
+ # if the first path is a wildcard pattern, find the first match else use "folder/styles.csv" as the default path
+ self.default_path = next(Path(folder).glob(file), Path(os.path.join(folder, 'styles.csv')))
+ self.paths.insert(0, self.default_path)
+ else:
+ self.default_path = Path(self.paths[0])
self.prompt_fields = [field for field in PromptStyle._fields if field != "path"]
@@ -99,33 +103,31 @@ class StyleDatabase:
"""
self.styles.clear()
- path, filename = os.path.split(self.path)
-
- if "*" in filename:
- fileglob = filename.split("*")[0] + "*.csv"
- filelist = []
- for file in os.listdir(path):
- if fnmatch.fnmatch(file, fileglob):
- filelist.append(file)
- # Add a visible divider to the style list
- half_len = round(len(file) / 2)
- divider = f"{'-' * (20 - half_len)} {file.upper()}"
- divider = f"{divider} {'-' * (40 - len(divider))}"
- self.styles[divider] = PromptStyle(
- f"{divider}", None, None, "do_not_save"
- )
- # Add styles from this CSV file
- self.load_from_csv(os.path.join(path, file))
- if len(filelist) == 0:
- print(f"No styles found in {path} matching {fileglob}")
- return
- elif not os.path.exists(self.path):
- print(f"Style database not found: {self.path}")
- return
- else:
- self.load_from_csv(self.path)
-
- def load_from_csv(self, path: str):
+ # scans for all styles files
+ all_styles_files = []
+ for pattern in self.paths:
+ folder, file = os.path.split(pattern)
+ if '*' in file or '?' in file:
+ found_files = Path(folder).glob(file)
+ [all_styles_files.append(file) for file in found_files]
+ else:
+ # if os.path.exists(pattern):
+ all_styles_files.append(Path(pattern))
+
+ # Remove any duplicate entries
+ seen = set()
+ self.all_styles_files = [s for s in all_styles_files if not (s in seen or seen.add(s))]
+
+ for styles_file in self.all_styles_files:
+ if len(all_styles_files) > 1:
+ # add divider when more than styles file
+ # '---------------- STYLES ----------------'
+ divider = f' {styles_file.stem.upper()} '.center(40, '-')
+ self.styles[divider] = PromptStyle(f"{divider}", None, None, "do_not_save")
+ if styles_file.is_file():
+ self.load_from_csv(styles_file)
+
+ def load_from_csv(self, path: str | Path):
with open(path, "r", encoding="utf-8-sig", newline="") as file:
reader = csv.DictReader(file, skipinitialspace=True)
for row in reader:
@@ -137,7 +139,7 @@ class StyleDatabase:
negative_prompt = row.get("negative_prompt", "")
# Add style to database
self.styles[row["name"]] = PromptStyle(
- row["name"], prompt, negative_prompt, path
+ row["name"], prompt, negative_prompt, str(path)
)
def get_style_paths(self) -> set:
@@ -145,11 +147,11 @@ class StyleDatabase:
# Update any styles without a path to the default path
for style in list(self.styles.values()):
if not style.path:
- self.styles[style.name] = style._replace(path=self.default_path)
+ self.styles[style.name] = style._replace(path=str(self.default_path))
# Create a list of all distinct paths, including the default path
style_paths = set()
- style_paths.add(self.default_path)
+ style_paths.add(str(self.default_path))
for _, style in self.styles.items():
if style.path:
style_paths.add(style.path)
@@ -177,7 +179,6 @@ class StyleDatabase:
def save_styles(self, path: str = None) -> None:
# The path argument is deprecated, but kept for backwards compatibility
- _ = path
style_paths = self.get_style_paths()
diff --git a/modules/txt2img.py b/modules/txt2img.py
index d22a1f31..4efcb4c3 100644
--- a/modules/txt2img.py
+++ b/modules/txt2img.py
@@ -3,7 +3,7 @@ from contextlib import closing
import modules.scripts
from modules import processing, infotext_utils
-from modules.infotext_utils import create_override_settings_dict
+from modules.infotext_utils import create_override_settings_dict, parse_generation_parameters
from modules.shared import opts
import modules.shared as shared
from modules.ui import plaintext_to_html
@@ -64,19 +64,18 @@ def txt2img_upscale(id_task: str, request: gr.Request, gallery, gallery_index, g
p.enable_hr = True
p.batch_size = 1
p.n_iter = 1
+ p.txt2img_upscale = True
geninfo = json.loads(generation_info)
- all_seeds = geninfo["all_seeds"]
- all_subseeds = geninfo["all_subseeds"]
image_info = gallery[gallery_index] if 0 <= gallery_index < len(gallery) else gallery[0]
p.firstpass_image = infotext_utils.image_from_url_text(image_info)
- gallery_index_from_end = len(gallery) - gallery_index
- seed = all_seeds[-gallery_index_from_end if gallery_index_from_end < len(all_seeds) + 1 else 0]
- subseed = all_subseeds[-gallery_index_from_end if gallery_index_from_end < len(all_seeds) + 1 else 0]
- p.seed = seed
- p.subseed = subseed
+ parameters = parse_generation_parameters(geninfo.get('infotexts')[gallery_index], [])
+ p.seed = parameters.get('Seed', -1)
+ p.subseed = parameters.get('Variation seed', -1)
+
+ p.override_settings['save_images_before_highres_fix'] = False
with closing(p):
processed = modules.scripts.scripts_txt2img.run(p, *p.script_args)
@@ -88,18 +87,13 @@ def txt2img_upscale(id_task: str, request: gr.Request, gallery, gallery_index, g
new_gallery = []
for i, image in enumerate(gallery):
- fake_image = Image.new(mode="RGB", size=(1, 1))
-
if i == gallery_index:
- already_saved_as = getattr(processed.images[0], 'already_saved_as', None)
- if already_saved_as is not None:
- fake_image.already_saved_as = already_saved_as
- else:
- fake_image = processed.images[0]
+ geninfo["infotexts"][gallery_index: gallery_index+1] = processed.infotexts
+ new_gallery.extend(processed.images)
else:
- fake_image.already_saved_as = image["name"]
-
- new_gallery.append(fake_image)
+ fake_image = Image.new(mode="RGB", size=(1, 1))
+ fake_image.already_saved_as = image["name"].rsplit('?', 1)[0]
+ new_gallery.append(fake_image)
geninfo["infotexts"][gallery_index] = processed.info
diff --git a/modules/ui.py b/modules/ui.py
index a716a040..177c6872 100644
--- a/modules/ui.py
+++ b/modules/ui.py
@@ -266,7 +266,7 @@ def create_ui():
dummy_component = gr.Label(visible=False)
- extra_tabs = gr.Tabs(elem_id="txt2img_extra_tabs")
+ extra_tabs = gr.Tabs(elem_id="txt2img_extra_tabs", elem_classes=["extra-networks"])
extra_tabs.__enter__()
with gr.Tab("Generation", id="txt2img_generation") as txt2img_generation_tab, ResizeHandleRow(equal_height=False):
@@ -499,7 +499,7 @@ def create_ui():
with gr.Blocks(analytics_enabled=False) as img2img_interface:
toprow = ui_toprow.Toprow(is_img2img=True, is_compact=shared.opts.compact_prompt_box)
- extra_tabs = gr.Tabs(elem_id="img2img_extra_tabs")
+ extra_tabs = gr.Tabs(elem_id="img2img_extra_tabs", elem_classes=["extra-networks"])
extra_tabs.__enter__()
with gr.Tab("Generation", id="img2img_generation") as img2img_generation_tab, ResizeHandleRow(equal_height=False):
@@ -532,7 +532,7 @@ def create_ui():
if category == "image":
with gr.Tabs(elem_id="mode_img2img"):
- img2img_selected_tab = gr.State(0)
+ img2img_selected_tab = gr.Number(value=0, visible=False)
with gr.TabItem('img2img', id='img2img', elem_id="img2img_img2img_tab") as tab_img2img:
init_img = gr.Image(label="Image for img2img", elem_id="img2img_image", show_label=False, source="upload", interactive=True, type="pil", tool="editor", image_mode="RGBA", height=opts.img2img_editor_height)
@@ -613,7 +613,7 @@ def create_ui():
elif category == "dimensions":
with FormRow():
with gr.Column(elem_id="img2img_column_size", scale=4):
- selected_scale_tab = gr.State(value=0)
+ selected_scale_tab = gr.Number(value=0, visible=False)
with gr.Tabs():
with gr.Tab(label="Resize to", elem_id="img2img_tab_resize_to") as tab_scale_to:
diff --git a/modules/ui_common.py b/modules/ui_common.py
index f17259c2..29fe7d0e 100644
--- a/modules/ui_common.py
+++ b/modules/ui_common.py
@@ -1,3 +1,4 @@
+import csv
import dataclasses
import json
import html
@@ -36,12 +37,38 @@ def plaintext_to_html(text, classname=None):
return f"<p class='{classname}'>{content}</p>" if classname else f"<p>{content}</p>"
+def update_logfile(logfile_path, fields):
+ """Update a logfile from old format to new format to maintain CSV integrity."""
+ with open(logfile_path, "r", encoding="utf8", newline="") as file:
+ reader = csv.reader(file)
+ rows = list(reader)
+
+ # blank file: leave it as is
+ if not rows:
+ return
+
+ # file is already synced, do nothing
+ if len(rows[0]) == len(fields):
+ return
+
+ rows[0] = fields
+
+ # append new fields to each row as empty values
+ for row in rows[1:]:
+ while len(row) < len(fields):
+ row.append("")
+
+ with open(logfile_path, "w", encoding="utf8", newline="") as file:
+ writer = csv.writer(file)
+ writer.writerows(rows)
+
+
def save_files(js_data, images, do_make_zip, index):
- import csv
filenames = []
fullfns = []
+ parsed_infotexts = []
- #quick dictionary to class object conversion. Its necessary due apply_filename_pattern requiring it
+ # quick dictionary to class object conversion. Its necessary due apply_filename_pattern requiring it
class MyObject:
def __init__(self, d=None):
if d is not None:
@@ -49,35 +76,55 @@ def save_files(js_data, images, do_make_zip, index):
setattr(self, key, value)
data = json.loads(js_data)
-
p = MyObject(data)
+
path = shared.opts.outdir_save
save_to_dirs = shared.opts.use_save_to_dirs_for_ui
extension: str = shared.opts.samples_format
start_index = 0
- only_one = False
if index > -1 and shared.opts.save_selected_only and (index >= data["index_of_first_image"]): # ensures we are looking at a specific non-grid picture, and we have save_selected_only
- only_one = True
images = [images[index]]
start_index = index
os.makedirs(shared.opts.outdir_save, exist_ok=True)
- with open(os.path.join(shared.opts.outdir_save, "log.csv"), "a", encoding="utf8", newline='') as file:
+ fields = [
+ "prompt",
+ "seed",
+ "width",
+ "height",
+ "sampler",
+ "cfgs",
+ "steps",
+ "filename",
+ "negative_prompt",
+ "sd_model_name",
+ "sd_model_hash",
+ ]
+ logfile_path = os.path.join(shared.opts.outdir_save, "log.csv")
+
+ # NOTE: ensure csv integrity when fields are added by
+ # updating headers and padding with delimeters where needed
+ if os.path.exists(logfile_path):
+ update_logfile(logfile_path, fields)
+
+ with open(logfile_path, "a", encoding="utf8", newline='') as file:
at_start = file.tell() == 0
writer = csv.writer(file)
if at_start:
- writer.writerow(["prompt", "seed", "width", "height", "sampler", "cfgs", "steps", "filename", "negative_prompt"])
+ writer.writerow(fields)
for image_index, filedata in enumerate(images, start_index):
image = image_from_url_text(filedata)
is_grid = image_index < p.index_of_first_image
- i = 0 if is_grid else (image_index - p.index_of_first_image)
p.batch_index = image_index-1
- fullfn, txt_fullfn = modules.images.save_image(image, path, "", seed=p.all_seeds[i], prompt=p.all_prompts[i], extension=extension, info=p.infotexts[image_index], grid=is_grid, p=p, save_to_dirs=save_to_dirs)
+
+ parameters = parameters_copypaste.parse_generation_parameters(data["infotexts"][image_index], [])
+ parsed_infotexts.append(parameters)
+ fullfn, txt_fullfn = modules.images.save_image(image, path, "", seed=parameters['Seed'], prompt=parameters['Prompt'], extension=extension, info=p.infotexts[image_index], grid=is_grid, p=p, save_to_dirs=save_to_dirs)
filename = os.path.relpath(fullfn, path)
filenames.append(filename)
@@ -86,12 +133,12 @@ def save_files(js_data, images, do_make_zip, index):
filenames.append(os.path.basename(txt_fullfn))
fullfns.append(txt_fullfn)
- writer.writerow([data["prompt"], data["seed"], data["width"], data["height"], data["sampler_name"], data["cfg_scale"], data["steps"], filenames[0], data["negative_prompt"]])
+ writer.writerow([parsed_infotexts[0]['Prompt'], parsed_infotexts[0]['Seed'], data["width"], data["height"], data["sampler_name"], data["cfg_scale"], data["steps"], filenames[0], parsed_infotexts[0]['Negative prompt'], data["sd_model_name"], data["sd_model_hash"]])
# Make Zip
if do_make_zip:
- zip_fileseed = p.all_seeds[index-1] if only_one else p.all_seeds[0]
- namegen = modules.images.FilenameGenerator(p, zip_fileseed, p.all_prompts[0], image, True)
+ p.all_seeds = [parameters['Seed'] for parameters in parsed_infotexts]
+ namegen = modules.images.FilenameGenerator(p, parsed_infotexts[0]['Seed'], parsed_infotexts[0]['Prompt'], image, True)
zip_filename = namegen.apply(shared.opts.grid_zip_filename_pattern or "[datetime]_[[model_name]]_[seed]-[seed_last]")
zip_filepath = os.path.join(path, f"{zip_filename}.zip")
diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py
index 62db36f5..325d848e 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()}, '{tabname}_{page.extra_networks_tabname}');"
+ 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/modules/ui_postprocessing.py b/modules/ui_postprocessing.py
index 7a132ac2..7261c2df 100644
--- a/modules/ui_postprocessing.py
+++ b/modules/ui_postprocessing.py
@@ -1,13 +1,14 @@
import gradio as gr
from modules import scripts, shared, ui_common, postprocessing, call_queue, ui_toprow
import modules.infotext_utils as parameters_copypaste
+from modules.ui_components import ResizeHandleRow
def create_ui():
dummy_component = gr.Label(visible=False)
- tab_index = gr.State(value=0)
+ tab_index = gr.Number(value=0, visible=False)
- with gr.Row(equal_height=False, variant='compact'):
+ with ResizeHandleRow(equal_height=False, variant='compact'):
with gr.Column(variant='compact'):
with gr.Tabs(elem_id="mode_extras"):
with gr.TabItem('Single Image', id="single_image", elem_id="extras_single_tab") as tab_single:
diff --git a/modules/ui_prompt_styles.py b/modules/ui_prompt_styles.py
index 0d74c23f..d67e3f17 100644
--- a/modules/ui_prompt_styles.py
+++ b/modules/ui_prompt_styles.py
@@ -22,9 +22,12 @@ def save_style(name, prompt, negative_prompt):
if not name:
return gr.update(visible=False)
- style = styles.PromptStyle(name, prompt, negative_prompt)
+ existing_style = shared.prompt_styles.styles.get(name)
+ path = existing_style.path if existing_style is not None else None
+
+ style = styles.PromptStyle(name, prompt, negative_prompt, path)
shared.prompt_styles.styles[style.name] = style
- shared.prompt_styles.save_styles(shared.styles_filename)
+ shared.prompt_styles.save_styles()
return gr.update(visible=True)
@@ -34,7 +37,7 @@ def delete_style(name):
return
shared.prompt_styles.styles.pop(name, None)
- shared.prompt_styles.save_styles(shared.styles_filename)
+ shared.prompt_styles.save_styles()
return '', '', ''
diff --git a/modules/ui_toprow.py b/modules/ui_toprow.py
index 1abc9117..fbe705be 100644
--- a/modules/ui_toprow.py
+++ b/modules/ui_toprow.py
@@ -107,8 +107,9 @@ class Toprow:
)
def interrupt_function():
- if shared.state.job_count > 1 and shared.opts.interrupt_after_current:
+ if not shared.state.stopping_generation and shared.state.job_count > 1 and shared.opts.interrupt_after_current:
shared.state.stop_generating()
+ gr.Info("Generation will stop after finishing this image, click again to stop immediately.")
else:
shared.state.interrupt()
diff --git a/scripts/postprocessing_upscale.py b/scripts/postprocessing_upscale.py
index a57f9d4a..e269682d 100644
--- a/scripts/postprocessing_upscale.py
+++ b/scripts/postprocessing_upscale.py
@@ -15,7 +15,7 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing):
order = 1000
def ui(self):
- selected_tab = gr.State(value=0)
+ selected_tab = gr.Number(value=0, visible=False)
with gr.Column():
with FormRow():
diff --git a/style.css b/style.css
index 4957c523..4352737e 100644
--- a/style.css
+++ b/style.css
@@ -28,7 +28,7 @@ div.gradio-container{
}
.hidden{
- display: none;
+ display: none !important;
}
.compact{
@@ -879,31 +879,21 @@ 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: 2.7rem;
}
-.extra-networks > div.tab-nav{
- min-height: 3.4rem;
+.extra-networks-controls-div{
+ align-self: center;
+ margin-left: auto;
}
.extra-networks > div > [id *= '_extra_']{
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 +914,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 +985,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 +1002,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 +1021,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 +1102,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 +1176,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-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-control .extra-network-control--search::before {
+ content: "🔎︎";
+ position: absolute;
+ margin: 0.5rem;
+ font-size: 1rem;
+ color: var(--input-placeholder-color);
+}
+
+.extra-network-control .extra-network-control--search {
+ display: inline-flex;
+ position: relative;
+}
+
+.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-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-control .extra-network-control--sort {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.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-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-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-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-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-control .extra-network-control--sort-dir {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.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-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-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-control .extra-network-control--tree-view {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.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-control .extra-network-control--enabled {
+ background-color: var(--neutral-700);
+}
+
+.dark .extra-network-control .extra-network-control--enabled {
+ background-color: var(--neutral-300);
+}
+
+/* ==== REFRESH ICON ACTIONS ==== */
+.extra-network-control .extra-network-control--refresh {
+ padding: 0.25rem;
+ display: inline-flex;
+ cursor: pointer;
+ justify-self: center;
+ align-self: center;
+}
+
+.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-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;
+}