add clusterize lists. need to debug.

This commit is contained in:
Sj-Si 2024-03-13 17:11:44 -04:00
parent 3e0146f9bd
commit 83e85ad0e9
14 changed files with 1263 additions and 596 deletions

View file

@ -43,8 +43,8 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
self.read_user_metadata(item)
activation_text = item["user_metadata"].get("activation text")
preferred_weight = item["user_metadata"].get("preferred weight", 0.0)
item["prompt"] = quote_js(f"<lora:{alias}:") + " + " + (str(preferred_weight) if preferred_weight else "opts.extra_networks_default_multiplier") + " + " + quote_js(">")
item["prompt"] = quote_js(f"<lora:{alias}:{str(preferred_weight) if preferred_weight else shared.opts.extra_networks_default_multiplier}>")
if activation_text:
item["prompt"] += " + " + quote_js(" " + activation_text)

View file

@ -1,4 +1,4 @@
<div class="card" style="{style}" onclick="{card_clicked}" data-name="{name}" {sort_keys}>
<div class="card" style="{style}" onclick="{card_clicked}" data-name="{name}" data-div-id={div_id} {sort_keys}>
{background_image}
<div class="button-row">{copy_path_button}{metadata_button}{edit_button}</div>
<div class="actions">

View file

@ -1,5 +1,5 @@
<div class="copy-path-button card-button"
title="Copy path to clipboard"
onclick="extraNetworksCopyCardPath(event, '{filename}')"
onclick="extraNetworksCopyPathToClipboard(event, '{filename}')"
data-clipboard-text="{filename}">
</div>

View file

@ -0,0 +1,5 @@
<button
class='lg secondary gradio-button custom-button {extra_class}'
onclick='extraNetworksSearchButton(event, "{tabname_full}")'>
{path}
</button>

View file

@ -1,8 +1,11 @@
<div class="extra-network-pane-content-tree resize-handle-row">
<div id='{tabname}_{extra_networks_tabname}_tree' class='extra-network-tree' style='flex-basis: {extra_networks_tree_view_default_width}px'>
{tree_html}
<div
id='{tabname}_{extra_networks_tabname}_tree_scroll_area'
class='extra-network-tree clusterize-scroll {tree_scroll_area_div_extra_class}'
style='flex-basis: {extra_networks_tree_view_default_width}px'>
<div id='{tabname}_{extra_networks_tabname}_tree_content_area' class='clusterize-content'></div>
</div>
<div id='{tabname}_{extra_networks_tabname}_cards' class='extra-network-cards' style='flex-grow: 1;'>
{items_html}
<div id='{tabname}_{extra_networks_tabname}_cards_scroll_area' class='extra-network-cards clusterize-scroll' style='flex-grow: 1;'>
<div id='{tabname}_{extra_networks_tabname}_cards_content_area' class='clusterize-content'></div>
</div>
</div>

View file

@ -1,48 +1,57 @@
<div id='{tabname}_{extra_networks_tabname}_pane' class='extra-network-pane {tree_view_div_default_display_class}'>
<div class="extra-network-control" id="{tabname}_{extra_networks_tabname}_controls" style="display:none" >
<div id="{tabname}_{extra_networks_tabname}_controls" class="extra-network-control" style="display:none">
<div class="extra-network-control--search">
<span class="extra-network-control--search-icon">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16">
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"></path>
</svg>
</span>
<input
id="{tabname}_{extra_networks_tabname}_extra_search"
class="extra-network-control--search-text"
type="search"
type="text"
placeholder="Search"
>
<button role="button" class="extra-network-control--search-clear" onclick="extraNetworksControlSearchClearOnClick(event, '{tabname}_{extra_networks_tabname}');">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16">
<path d="M2.343 13.657A8 8 0 1 1 13.658 2.343 8 8 0 0 1 2.343 13.657ZM6.03 4.97a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042L6.94 8 4.97 9.97a.749.749 0 0 0 .326 1.275.749.749 0 0 0 .734-.215L8 9.06l1.97 1.97a.749.749 0 0 0 1.275-.326.749.749 0 0 0-.215-.734L9.06 8l1.97-1.97a.749.749 0 0 0-.326-1.275.749.749 0 0 0-.734.215L8 6.94Z"></path>
</svg>
</button>
</div>
<small>Sort: </small>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_path"
class="extra-network-control--sort{sort_path_active}"
data-sortkey="default"
data-sort-mode="default"
title="Sort by path"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
onclick="extraNetworksControlSortModeOnClick(event, '{tabname}_{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
</div>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_name"
class="extra-network-control--sort{sort_name_active}"
data-sortkey="name"
data-sort-mode="name"
title="Sort by name"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
onclick="extraNetworksControlSortModeOnClick(event, '{tabname}_{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
</div>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_date_created"
class="extra-network-control--sort{sort_date_created_active}"
data-sortkey="date_created"
data-sort-mode="date_created"
title="Sort by date created"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
onclick="extraNetworksControlSortModeOnClick(event, '{tabname}_{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
</div>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_date_modified"
class="extra-network-control--sort{sort_date_modified_active}"
data-sortkey="date_modified"
data-sort-mode="date_modified"
title="Sort by date modified"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
onclick="extraNetworksControlSortModeOnClick(event, '{tabname}_{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
</div>
@ -51,9 +60,9 @@
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_dir"
class="extra-network-control--sort-dir"
data-sortdir="{data_sortdir}"
data-sort-dir="{data_sortdir}"
title="Sort ascending"
onclick="extraNetworksControlSortDirOnClick(event, '{tabname}', '{extra_networks_tabname}');"
onclick="extraNetworksControlSortDirOnClick(event, '{tabname}_{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-dir-icon"></i>
</div>
@ -64,7 +73,7 @@
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}');"
onclick="extraNetworksControlTreeViewOnClick(event, '{tabname}_{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--tree-view-icon"></i>
</div>
@ -72,7 +81,7 @@
id="{tabname}_{extra_networks_tabname}_extra_refresh"
class="extra-network-control--refresh"
title="Refresh page"
onclick="extraNetworksControlRefreshOnClick(event, '{tabname}', '{extra_networks_tabname}');"
onclick="extraNetworksControlRefreshOnClick(event, '{tabname}_{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--refresh-icon"></i>
</div>

View file

@ -1,23 +0,0 @@
<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>

View file

@ -0,0 +1,20 @@
<div class="tree-list-item tree-list-item--{subitem}" {data_attributes}>
<span data-filterable-item-text hidden>{search_terms}</span>
<div class="tree-list-content tree-list-content-{btn_type}" type="button" onclick="extraNetworksTreeOnClick(event, '{tabname}_{extra_networks_tabname}');{onclick_extra}">
<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>
</div>

View file

@ -1,3 +1,72 @@
const re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/;
const re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g;
const re_extranet_neg = /\(([^:^>]+:[\d.]+)\)/;
const re_extranet_g_neg = /\(([^:^>]+:[\d.]+)\)/g;
const extraNetworksApplyFilter = {};
const extraNetworksApplySort = {};
const activePromptTextarea = {};
const clusterizers = {};
const extra_networks_json_proxy = {};
const extra_networks_proxy_listener = setupProxyListener(
extra_networks_json_proxy,
function() {},
proxyJsonUpdated,
);
var globalPopup = null;
var globalPopupInner = null;
const storedPopupIds = {};
const extraPageUserMetadataEditors = {};
const uiAfterScriptsCallbacks = [];
var uiAfterScriptsTimeout = null;
var executedAfterScripts = false;
function waitForElement(selector) {
/** Promise that waits for an element to exist in DOM. */
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
});
}
function setupProxyListener(target, pre_handler, post_handler) {
/** Sets up a listener for variable changes. */
var proxy = new Proxy(target, {
set: function (t, k, v) {
pre_handler.call(t, k, v);
t[k] = v;
post_handler.call(t, k, v);
return true;
}
});
return proxy
}
function proxyJsonUpdated(k, v) {
/** Callback triggered when JSON data is updated by the proxy listener. */
// use `this` for current object
// We don't do error handling here because we want to fail gracefully if data is
// not yet present.
if (!(v.dataset.tabnameFull in clusterizers)) {
// Clusterizers not yet initialized.
return;
}
clusterizers[v.dataset.tabnameFull][v.dataset.proxyName].parseJson(v.dataset.json);
}
function toggleCss(key, css, enable) {
var style = document.getElementById(key);
if (enable && !style) {
@ -15,9 +84,35 @@ function toggleCss(key, css, enable) {
}
}
function extraNetworksRefreshTab(tabname_full) {
if (!(tabname_full in clusterizers)) {
return;
}
for (_tabname_full of Object.keys(clusterizers)) {
if (_tabname_full === tabname_full) {
// Set the selected tab as active since it is now visible on page.
clusterizers[_tabname_full].tree_list.enable();
clusterizers[_tabname_full].cards_list.enable();
} else {
// Deactivate all other tabs since they are no longer visible.
clusterizers[_tabname_full].tree_list.disable();
clusterizers[_tabname_full].cards_list.disable();
}
}
clusterizers[tabname_full].tree_list.rebuild();
clusterizers[tabname_full].cards_list.rebuild();
for (var elem of gradioApp().querySelectorAll('.extra-networks-script-data')) {
extra_networks_proxy_listener[`${elem.dataset.tabnameFull}_${elem.dataset.proxyName}`] = elem;
}
}
function setupExtraNetworksForTab(tabname) {
function registerPrompt(tabname, id) {
var textarea = gradioApp().querySelector("#" + id + " > label > textarea");
var textarea = gradioApp().querySelector(`#${id} > label > textarea`);
if (!activePromptTextarea[tabname]) {
activePromptTextarea[tabname] = textarea;
@ -28,112 +123,124 @@ function setupExtraNetworksForTab(tabname) {
});
}
var tabnav = gradioApp().querySelector('#' + tabname + '_extra_tabs > div.tab-nav');
var controlsDiv = document.createElement('DIV');
controlsDiv.classList.add('extra-networks-controls-div');
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_dir = gradioApp().querySelector("#" + tabname_full + "_extra_sort_dir");
var refresh = gradioApp().querySelector("#" + tabname_full + "_extra_refresh");
var currentSort = '';
var this_tab = gradioApp().querySelector(`#${tabname}_extra_tabs`);
this_tab.querySelectorAll(`:scope > [id^="${tabname}_"]`).forEach(function(elem) {
let tabname_full = elem.id;
let txt_search;
let btn_sort_mode;
let btn_sort_dir;
let btn_refresh;
// If any of the buttons above don't exist, we want to skip this iteration of the loop.
if (!search || !sort_dir || !refresh) {
return; // `return` is equivalent of `continue` but for forEach loops.
}
var applyFilter = function(force) {
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, .description'), 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");
}
});
applySort(force);
};
var applySort = function(force) {
var cards = gradioApp().querySelectorAll('#' + tabname_full + ' div.card');
var parent = gradioApp().querySelector('#' + tabname_full + "_cards");
var reverse = sort_dir.dataset.sortdir == "Descending";
var activeSearchElem = gradioApp().querySelector('#' + tabname_full + "_controls .extra-network-control--sort.extra-network-control--enabled");
var sortKey = activeSearchElem ? activeSearchElem.dataset.sortkey : "default";
var sortKeyDataField = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1);
var sortKeyStore = sortKey + "-" + sort_dir.dataset.sortdir + "-" + cards.length;
if (sortKeyStore == currentSort && !force) {
var applyFilter = function() {
if (!(tabname_full in clusterizers)) {
console.error(`applyFilter: ${tabname_full} not in clusterizers:`);
return;
}
currentSort = sortKeyStore;
var sortedCards = Array.from(cards);
sortedCards.sort(function(cardA, cardB) {
var a = cardA.dataset[sortKeyDataField];
var b = cardB.dataset[sortKeyDataField];
if (!isNaN(a) && !isNaN(b)) {
return parseInt(a) - parseInt(b);
}
return (a < b ? -1 : (a > b ? 1 : 0));
});
if (reverse) {
sortedCards.reverse();
}
parent.innerHTML = '';
var frag = document.createDocumentFragment();
sortedCards.forEach(function(card) {
frag.appendChild(card);
});
parent.appendChild(frag);
// Only touch cards_list. tree_list remains static.
clusterizers[tabname_full].cards_list.setSortMode(btn_sort_mode);
clusterizers[tabname_full].cards_list.setSortDir(btn_sort_dir);
clusterizers[tabname_full].cards_list.applyFilter(txt_search.value);
};
search.addEventListener("input", function() {
applyFilter();
});
applySort();
applyFilter();
extraNetworksApplySort[tabname_full] = applySort;
extraNetworksApplyFilter[tabname_full] = applyFilter;
var controls = gradioApp().querySelector("#" + tabname_full + "_controls");
controlsDiv.insertBefore(controls, null);
var applySort = function() {
if (!(tabname_full in clusterizers)) {
console.error(`applySort: ${tabname_full} not in clusterizers:`);
return;
}
// Only touch cards_list. tree_list remains static.
clusterizers[tabname_full].cards_list.setSortMode(btn_sort_mode);
clusterizers[tabname_full].cards_list.setSortDir(btn_sort_dir);
clusterizers[tabname_full].cards_list.applyFilter(txt_search.value); // filter also sorts
};
extraNetworksApplySort[tabname_full] = applySort;
/** #TODO
* Figure out if we can use the following in the clusterize setup:
* var frag = document.createDocumentFragment();
* sortedCards.forEach(function(card) {
* frag.appendChild(card);
* });
* parent.appendChild(frag);
*/
if (elem.style.display != "none") {
extraNetworksShowControlsForPage(tabname, tabname_full);
}
// Wait for all required elements before setting up the tab.
waitForElement(`#${tabname_full}_extra_search`)
.then((el) => {
txt_search = el;
})
.then(() => {
waitForElement(`#${tabname_full}_extra_sort_mode`)
.then((el) => { btn_sort_mode = el; });
})
.then(() => {
waitForElement(`#${tabname_full}_extra_sort_dir`)
.then((el) => { btn_sort_dir = el; });
})
.then(() => {
waitForElement(`#${tabname_full}_extra_refresh`)
.then((el) => { btn_refresh = el; });
})
.then(() => {
waitForElement(`#${tabname_full}_tree_list_scroll_area > #${tabname_full}_tree_list_content_area`)
.then(() => { return; });
})
.then(() => {
waitForElement(`#${tabname_full}_cards_list_scroll_area > #${tabname_full}_cards_list_content_area`)
.then(() => { return; });
})
.then(() => {
console.log("LOADING TAB:", tabname_full, clusterizers[tabname_full]);
// Now that we have our elements in DOM, we create the clusterize lists.
clusterizers[tabname_full] = {
tree_list: new ExtraNetworksClusterizeTreeList({
scroll_id: `${tabname_full}_tree_list_scroll_area`,
content_id: `${tabname_full}_tree_list_content_area`,
}),
cards_list: new ExtraNetworksClusterizeCardsList({
scroll_id: `${tabname_full}_cards_list_scroll_area`,
content_id: `${tabname_full}_cards_list_content_area`,
}),
};
applyFilter();
// Debounce search text input. This way we only search after user is done typing.
let typing_timer;
let done_typing_interval_ms = 250;
txt_search.addEventListener("keyup", () => {
clearTimeout(typing_timer);
if (txt_search.value) {
typing_timer = setTimeout(applyFilter, done_typing_interval_ms);
}
});
// Triggered on "enter" key or when "x" is clicked to clear search.
txt_search.addEventListener("extra-network-control--search-clear", applyFilter);
// Insert the controls into the page.
var controls = gradioApp().querySelector(`#${tabname_full}_controls`);
controlsDiv.insertBefore(controls, null);
if (elem.style.display != "none") {
extraNetworksShowControlsForPage(tabname, tabname_full);
}
});
});
registerPrompt(tabname, tabname + "_prompt");
registerPrompt(tabname, tabname + "_neg_prompt");
registerPrompt(tabname, `${tabname}_prompt`);
registerPrompt(tabname, `${tabname}_neg_prompt`);
}
function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt) {
if (!gradioApp().querySelector('.toprow-compact-tools')) return; // only applicable for compact prompt layout
var promptContainer = gradioApp().getElementById(tabname + '_prompt_container');
var prompt = gradioApp().getElementById(tabname + '_prompt_row');
var negPrompt = gradioApp().getElementById(tabname + '_neg_prompt_row');
var promptContainer = gradioApp().getElementById(`${tabname}_prompt_container`);
var prompt = gradioApp().getElementById(`${tabname}_prompt_row`);
var negPrompt = gradioApp().getElementById(`${tabname}_neg_prompt_row`);
var elem = id ? gradioApp().getElementById(id) : null;
if (showNegativePrompt && elem) {
@ -155,8 +262,8 @@ function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePromp
function extraNetworksShowControlsForPage(tabname, tabname_full) {
gradioApp().querySelectorAll('#' + tabname + '_extra_tabs .extra-networks-controls-div > div').forEach(function(elem) {
var targetId = tabname_full + "_controls";
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";
});
}
@ -172,40 +279,70 @@ function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt, t
extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt);
extraNetworksShowControlsForPage(tabname, tabname_full);
for (_tabname_full of Object.keys(clusterizers)) {
if (_tabname_full === tabname_full) {
// Set the selected tab as active since it is now visible on page.
clusterizers[_tabname_full].tree_list.enable();
clusterizers[_tabname_full].cards_list.enable();
} else {
// Deactivate all other tabs since they are no longer visible.
clusterizers[_tabname_full].tree_list.disable();
clusterizers[_tabname_full].cards_list.disable();
}
}
if (!document.body.contains(clusterizers[tabname_full].tree_list.scroll_elem)) {
clusterizers[tabname_full].tree_list.rebuild();
}
if (!document.body.contains(clusterizers[tabname_full].cards_list.scroll_elem)) {
clusterizers[tabname_full].cards_list.rebuild();
}
}
function applyExtraNetworkFilter(tabname_full) {
var doFilter = function() {
var applyFunction = extraNetworksApplyFilter[tabname_full];
if (applyFunction) {
applyFunction(true);
}
};
setTimeout(doFilter, 1);
setTimeout(extraNetworksApplyFilter[tabname_full], 1);
}
function applyExtraNetworkSort(tabname_full) {
var doSort = function() {
extraNetworksApplySort[tabname_full](true);
};
setTimeout(doSort, 1);
setTimeout(extraNetworksApplySort[tabname_full], 1);
}
var extraNetworksApplyFilter = {};
var extraNetworksApplySort = {};
var activePromptTextarea = {};
function setupExtraNetworksData() {
// Manually force read the json data.
for (var elem of gradioApp().querySelectorAll('.extra-networks-script-data')) {
extra_networks_proxy_listener[`${elem.dataset.tabnameFull}_${elem.dataset.proxyName}`] = elem;
}
}
function setupExtraNetworks() {
setupExtraNetworksForTab('txt2img');
setupExtraNetworksForTab('img2img');
// Handle window resizes. Delay of 500ms after resize before firing an event
// as a way of "debouncing" resizes.
var resize_timer;
window.addEventListener("resize", () => {
clearTimeout(resize_timer);
resize_timer = setTimeout(function() {
// Update rows for each list.
for (_tabname_full of Object.keys(clusterizers)) {
clusterizers[_tabname_full].tree_list.updateRows();
clusterizers[_tabname_full].cards_list.updateRows();
}
}, 500); // ms
});
// Handle resizeHandle resizes. Only fires on mouseup after resizing.
window.addEventListener("resizeHandleResized", (e) => {
for (_tabname_full of Object.keys(clusterizers)) {
// Update rows for each list.
clusterizers[_tabname_full].tree_list.updateRows();
clusterizers[_tabname_full].cards_list.updateRows();
}
});
}
var re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/;
var re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g;
var re_extranet_neg = /\(([^:^>]+:[\d.]+)\)/;
var re_extranet_g_neg = /\(([^:^>]+:[\d.]+)\)/g;
function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) {
var m = text.match(isNeg ? re_extranet_neg : re_extranet);
var replaced = false;
@ -255,17 +392,17 @@ function updatePromptArea(text, textArea, isNeg) {
function cardClicked(tabname, textToAdd, textToAddNegative, allowNegativePrompt) {
if (textToAddNegative.length > 0) {
updatePromptArea(textToAdd, gradioApp().querySelector("#" + tabname + "_prompt > label > textarea"));
updatePromptArea(textToAddNegative, gradioApp().querySelector("#" + tabname + "_neg_prompt > label > textarea"), true);
updatePromptArea(textToAdd, gradioApp().querySelector(`#${tabname}_prompt > label > textarea`));
updatePromptArea(textToAddNegative, gradioApp().querySelector(`#${tabname}_neg_prompt > label > textarea`), true);
} else {
var textarea = allowNegativePrompt ? activePromptTextarea[tabname] : gradioApp().querySelector("#" + tabname + "_prompt > label > textarea");
var textarea = allowNegativePrompt ? activePromptTextarea[tabname] : gradioApp().querySelector(`#${tabname}_prompt > label > textarea`);
updatePromptArea(textToAdd, textarea);
}
}
function saveCardPreview(event, tabname, filename) {
var textarea = gradioApp().querySelector("#" + tabname + '_preview_filename > label > textarea');
var button = gradioApp().getElementById(tabname + '_save_preview');
var textarea = gradioApp().querySelector(`#${tabname}_preview_filename > label > textarea`);
var button = gradioApp().getElementById(`${tabname}_save_preview`);
textarea.value = filename;
updateInput(textarea);
@ -276,8 +413,8 @@ function saveCardPreview(event, tabname, filename) {
event.preventDefault();
}
function extraNetworksSearchButton(tabname, extra_networks_tabname, event) {
var searchTextarea = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_search");
function extraNetworksSearchButton(event, tabname_full) {
var searchTextarea = gradioApp().querySelector("#" + tabname_full + "_extra_search");
var button = event.target;
var text = button.classList.contains("search-all") ? "" : button.textContent.trim();
@ -285,20 +422,20 @@ function extraNetworksSearchButton(tabname, extra_networks_tabname, event) {
updateInput(searchTextarea);
}
function extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname) {
function extraNetworksTreeProcessFileClick(event, btn, tabname_full) {
/**
* 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.
* @param event The generated event.
* @param btn The clicked `tree-list-item` button.
* @param tabname_full The full active tabname.
* i.e. txt2img_lora, img2img_checkpoints, etc.
*/
// NOTE: Currently unused.
return;
}
function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_networks_tabname) {
function extraNetworksTreeProcessDirectoryClick(event, btn, tabname_full) {
/**
* Processes `onclick` events when user clicks on directories in tree.
*
@ -308,94 +445,100 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_netwo
* 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.
* @param event The generated event.
* @param btn The clicked `tree-list-item` button.
* @param tabname_full The full active tabname.
* i.e. txt2img_lora, img2img_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;
const div_id = btn.dataset.divId;
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", "");
function _expandOrCollapse(_btn) {
// Expands/Collapses all children of the button.
if ("expanded" in _btn.dataset) {
delete _btn.dataset.expanded;
clusterizers[tabname_full].tree_list.removeChildRows(div_id);
} else {
_btn.dataset.expanded = "";
clusterizers[tabname_full].tree_list.addChildRows(div_id);
}
// update html after changing attr.
clusterizers[tabname_full].tree_list.updateDivContent(div_id, _btn.outerHTML);
clusterizers[tabname_full].tree_list.updateRows();
}
function _remove_selected_from_all() {
function _removeSelectedFromAll() {
// Removes the `selected` attribute from all buttons.
var sels = document.querySelectorAll("div.tree-list-content");
var sels = document.querySelectorAll(".tree-list-item");
[...sels].forEach(el => {
delete el.dataset.selected;
});
}
function _select_button(_btn) {
function _selectButton(_btn) {
// Removes `data-selected` attribute from all buttons then adds to passed button.
_remove_selected_from_all();
_removeSelectedFromAll();
_btn.dataset.selected = "";
}
function _update_search(_tabname, _extra_networks_tabname, _search_text) {
function _updateSearch(_search_text) {
// Update search input with select button's path.
var search_input_elem = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_search");
var search_input_elem = gradioApp().querySelector("#" + tabname_full + "_extra_search");
search_input_elem.value = _search_text;
updateInput(search_input_elem);
applyExtraNetworksFilter(tabname_full);
}
// 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);
_expandOrCollapse(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);
if ("selected" in btn.dataset) {
// If folder is selected, deselect button.
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);
_expandOrCollapse(ul, btn);
_updateSearch("");
} else {
// All other cases, just select the button.
_select_button(btn, tabname, extra_networks_tabname);
_update_search(tabname, extra_networks_tabname, btn.dataset.path);
// If folder is not selected, select it.
_selectButton(btn);
_updateSearch(btn.dataset.path);
}
}
}
function extraNetworksTreeOnClick(event, tabname, extra_networks_tabname) {
function extraNetworksTreeOnClick(event, tabname_full) {
/**
* 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.
* @param event The generated event.
* @param tabname_full The full active tabname.
* i.e. txt2img_lora, img2img_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);
}
let btn = event.target.closest(".tree-list-item");
if (btn.dataset.treeEntryType === "file") {
extraNetworksTreeProcessFileClick(event, btn, tabname_full);
} else {
extraNetworksTreeProcessDirectoryClick(event, btn, tabname_full);
}
event.stopPropagation();
}
function extraNetworksControlSortOnClick(event, tabname, extra_networks_tabname) {
function extraNetworksControlSearchClearOnClick(event, tabname_full) {
/** Clears the search <input> text. */
let clear_btn = event.target.closest(".extra-network-control--search-clear");
let txt_search = clear_btn.previousElementSibling;
txt_search.value = "";
txt_search.dispatchEvent(new CustomEvent("extra-network-control--search-clear", {}));
}
function extraNetworksControlSortModeOnClick(event, tabname_full) {
/** Handles `onclick` events for Sort Mode buttons. */
var self = event.currentTarget;
@ -407,49 +550,49 @@ function extraNetworksControlSortOnClick(event, tabname, extra_networks_tabname)
self.classList.add('extra-network-control--enabled');
applyExtraNetworkSort(tabname + "_" + extra_networks_tabname);
applyExtraNetworkSort(tabname_full);
}
function extraNetworksControlSortDirOnClick(event, tabname, extra_networks_tabname) {
function extraNetworksControlSortDirOnClick(event, tabname_full) {
/**
* 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.
* @param event The generated event.
* @param tabname_full The full active tabname.
* i.e. txt2img_lora, img2img_checkpoints, etc.
*/
if (event.currentTarget.dataset.sortdir == "Ascending") {
event.currentTarget.dataset.sortdir = "Descending";
if (event.currentTarget.dataset.sortDir.toLowerCase() == "ascending") {
event.currentTarget.dataset.sortDir = "descending";
event.currentTarget.setAttribute("title", "Sort descending");
} else {
event.currentTarget.dataset.sortdir = "Ascending";
event.currentTarget.dataset.sortDir = "ascending";
event.currentTarget.setAttribute("title", "Sort ascending");
}
applyExtraNetworkSort(tabname + "_" + extra_networks_tabname);
applyExtraNetworkSort(tabname_full);
}
function extraNetworksControlTreeViewOnClick(event, tabname, extra_networks_tabname) {
function extraNetworksControlTreeViewOnClick(event, tabname_full) {
/**
* 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.
* @param event The generated event.
* @param tabname_full The full active tabname.
* i.e. txt2img_lora, img2img_checkpoints, etc.
*/
var button = event.currentTarget;
button.classList.toggle("extra-network-control--enabled");
var show = !button.classList.contains("extra-network-control--enabled");
var pane = gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_pane");
var pane = gradioApp().getElementById(`${tabname_full}_pane`);
pane.classList.toggle("extra-network-dirs-hidden", show);
}
function extraNetworksControlRefreshOnClick(event, tabname, extra_networks_tabname) {
function extraNetworksControlRefreshOnClick(event, tabname_full) {
/**
* Handles `onclick` events for the Refresh Page button.
*
@ -458,17 +601,14 @@ function extraNetworksControlRefreshOnClick(event, tabname, extra_networks_tabna
* 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.
* @param event The generated event.
* @param tabname_full The full active tabname.
* i.e. txt2img_lora, img2img_checkpoints, etc.
*/
var btn_refresh_internal = gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_extra_refresh_internal");
var btn_refresh_internal = gradioApp().getElementById(`${tabname_full}_extra_refresh_internal`);
btn_refresh_internal.dispatchEvent(new Event("click"));
}
var globalPopup = null;
var globalPopupInner = null;
function closePopup() {
if (!globalPopup) return;
globalPopup.style.display = "none";
@ -498,7 +638,6 @@ function popup(contents) {
globalPopup.style.display = "flex";
}
var storedPopupIds = {};
function popupId(id) {
if (!storedPopupIds[id]) {
storedPopupIds[id] = gradioApp().getElementById(id);
@ -605,7 +744,7 @@ function requestGet(url, data, handler, errorHandler) {
xhr.send(js);
}
function extraNetworksCopyCardPath(event, path) {
function extraNetworksCopyCardPathToClipboard(event, path) {
navigator.clipboard.writeText(path);
event.stopPropagation();
}
@ -626,8 +765,6 @@ function extraNetworksRequestMetadata(event, extraPage, cardName) {
event.stopPropagation();
}
var extraPageUserMetadataEditors = {};
function extraNetworksEditUserMetadata(event, tabname, extraPage, cardName) {
var id = tabname + '_' + extraPage + '_edit_user_metadata';
@ -672,38 +809,4 @@ window.addEventListener("keydown", function(event) {
}
});
/**
* 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);
}
onUiLoaded(function() {
var mutationObserver = new MutationObserver(function(m) {
let existingSearchfields = gradioApp().querySelectorAll("[id$='_extra_search']").length;
let neededSearchfields = gradioApp().querySelectorAll("[id$='_extra_tabs'] > .tab-nav > button").length - 2;
if (!executedAfterScripts && existingSearchfields >= neededSearchfields) {
mutationObserver.disconnect();
executedAfterScripts = true;
scheduleAfterScriptsCallbacks();
}
});
mutationObserver.observe(gradioApp(), {childList: true, subtree: true});
});
uiAfterScriptsCallbacks.push(setupExtraNetworks);
onUiLoaded(setupExtraNetworks);

View file

@ -0,0 +1,479 @@
// Collators used for sorting.
const INT_COLLATOR = new Intl.Collator([], {numeric: true});
const STR_COLLATOR = new Intl.Collator("en", {numeric: true, sensitivity: "base"});
function compress(string) {
/** Compresses a string into a base64 encoded GZipped string. */
const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
const blobToBase64 = blob => new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(',')[1]);
reader.readAsDataURL(blob);
});
const byteArray = new TextEncoder().encode(string);
writer.write(byteArray);
writer.close();
return new Response(cs.readable).blob().then(blobToBase64);
};
function decompress(base64string) {
/** Decompresses a base64 encoded GZipped string. */
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const bytes = Uint8Array.from(atob(base64string), c => c.charCodeAt(0));
writer.write(bytes);
writer.close();
return new Response(ds.readable).arrayBuffer().then(function (arrayBuffer) {
return new TextDecoder().decode(arrayBuffer);
});
}
const parseHtml = function(str) {
const tmp = document.implementation.createHTMLDocument('');
tmp.body.innerHTML = str;
return [...tmp.body.childNodes];
}
const getComputedValue = function(container, css_property) {
return parseInt(
window.getComputedStyle(container, null)
.getPropertyValue(css_property)
.split("px")[0]
);
};
const calcColsPerRow = function(parent) {
// Returns the number of columns in a row of a flexbox.
//const parent = document.querySelector(selector);
const parent_width = getComputedValue(parent, "width");
const parent_padding_left = getComputedValue(parent,"padding-left");
const parent_padding_right = getComputedValue(parent,"padding-right");
const child = parent.firstElementChild;
const child_width = getComputedValue(child,"width");
const child_margin_left = getComputedValue(child,"margin-left");
const child_margin_right = getComputedValue(child,"margin-right");
var parent_width_no_padding = parent_width - parent_padding_left - parent_padding_right;
const child_width_with_margin = child_width + child_margin_left + child_margin_right;
parent_width_no_padding += child_margin_left + child_margin_right;
return parseInt(parent_width_no_padding / child_width_with_margin);
}
const calcRowsPerCol = function(container, parent) {
// Returns the number of columns in a row of a flexbox.
//const parent = document.querySelector(selector);
const parent_height = getComputedValue(container, "height");
const parent_padding_top = getComputedValue(container,"padding-top");
const parent_padding_bottom = getComputedValue(container,"padding-bottom");
const child = parent.firstElementChild;
const child_height = getComputedValue(child,"height");
const child_margin_top = getComputedValue(child,"margin-top");
const child_margin_bottom = getComputedValue(child,"margin-bottom");
var parent_height_no_padding = parent_height - parent_padding_top - parent_padding_bottom;
const child_height_with_margin = child_height + child_margin_top + child_margin_bottom;
parent_height_no_padding += child_margin_top + child_margin_bottom;
return parseInt(parent_height_no_padding / child_height_with_margin);
}
class ExtraNetworksClusterize {
constructor(
{
scroll_id,
content_id,
rows_in_block = 10,
blocks_in_cluster = 4,
show_no_data_row = true,
callbacks = {},
} = {
rows_in_block: 10,
blocks_in_cluster: 4,
show_no_data_row: true,
callbacks: {},
}
) {
if (scroll_id === undefined) {
console.error("scroll_id is undefined!");
}
if (content_id === undefined) {
console.error("content_id is undefined!");
}
this.scroll_id = scroll_id;
this.content_id = content_id;
this.rows_in_block = rows_in_block;
this.blocks_in_cluster = blocks_in_cluster;
this.show_no_data_row = show_no_data_row;
this.callbacks = callbacks;
this.enabled = false;
this.encoded_str = "";
this.no_data_text = "Directory is empty.";
this.no_data_class = "nocards";
this.scroll_elem = document.getElementById(this.scroll_id);
this.content_elem = document.getElementById(this.content_id);
this.n_rows = 1;
this.n_cols = 1;
this.sort_fn = this.sortByDivId;
this.sort_reverse = false;
this.data_obj = {};
this.data_obj_keys_sorted = [];
this.clusterize = new Clusterize(
{
rows: [],
scrollId: this.scroll_id,
contentId: this.content_id,
rows_in_block: this.rows_in_block,
blocks_in_cluster: this.blocks_in_cluster,
show_no_data_row: this.show_no_data_row,
callbacks: this.callbacks,
}
);
}
enable() {
this.enabled = true;
}
disable() {
this.enabled = false;
}
parseJson(encoded_str) {
if (this.encoded_str === encoded_str) {
return;
}
Promise.resolve(encoded_str)
.then(v => decompress(v))
.then(v => JSON.parse(v))
.then(v => this.updateJson(v))
.then(() => this.encoded_str = encoded_str);
}
sortByDivId() {
// Sort data_obj keys (div_id) as numbers.
this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => INT_COLLATOR.compare(a, b));
}
applySort() {
this.sort_fn()
if (this.sort_reverse) {
this.data_obj_keys_sorted = this.data_obj_keys_sorted.reverse();
}
this.updateRows();
}
applyFilter() {
// the base class filter just sorts the values and updates the rows.
this.applySort();
this.updateRows();
}
filterRows(obj) {
var results = [];
for (const div_id of this.data_obj_keys_sorted) {
if (obj[div_id].active) {
results.push(obj[div_id].element.outerHTML);
}
}
return results;
}
updateDivContent(div_id, content) {
/** Updates an element in the dataset. Does not call update_rows(). */
if (!(div_id in this.data_obj)) {
console.error("div_id not in data_obj:", div_id);
} else if (typeof content === "object") {
this.data_obj[div_id].element = parseHtml(content.outerHTML)[0];
return true;
} else if (typeof content === "string") {
this.data_obj[div_id].element = parseHtml(content)[0];
return true;
} else {
console.error("Invalid content:", div_id, content);
}
return false;
}
updateRows() {
this.clusterize.update(this.filterRows(this.data_obj));
this.clusterize.refresh();
}
nrows() {
return this.clusterize.getRowsAmount();
}
updateItemDims() {
if (!this.enabled) {
// Inactive list is not displayed on screen. Would error if trying to resize.
return;
}
if (this.nrows() <= 0) {
// If there is no data then just skip.
return;
}
// Calculate the visible rows and colums for the clusterize-content area.
let n_cols = calcColsPerRow(this.content_elem);
let n_rows = calcRowsPerCol(this.content_elem.parentElement, this.content_elem);
n_cols = isNaN(n_cols) || n_cols <= 0 ? 1 : n_cols;
n_rows = isNaN(n_rows) || n_rows <= 0 ? 1 : n_rows;
if (n_cols != this.n_cols || n_rows != this.n_rows) {
// Sizes have changed. Update the instance values.
this.n_cols = n_cols;
this.n_rows = n_rows;
this.rows_in_block = this.n_rows;
}
}
rebuild() {
this.clusterize.destroy();
// Get new references to elements since they may have changed.
this.scroll_elem = document.getElementById(this.scroll_id);
this.content_elem = document.getElementById(this.content_id);
this.clusterize = new Clusterize(
{
rows: this.filterRows(this.data_obj),
scrollId: this.scroll_id,
contentId: this.content_id,
rows_in_block: this.rows_in_block,
blocks_in_cluster: this.blocks_in_cluster,
show_no_data_row: this.show_no_data_row,
no_data_text: this.no_data_text,
no_data_class: this.no_data_class,
callbacks: this.callbacks,
}
);
// Apply existing sort mode.
this.applyFilter();
this.updateItemDims();
this.updateRows();
}
}
class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
constructor(...args) {
super(...args);
}
getBoxShadow(depth) {
// Generates style for a multi-level box shadow for vertical indentation lines.
let res = "";
var style = getComputedStyle(document.body);
let bg = style.getPropertyValue("--body-background-fill");
let fg = style.getPropertyValue("--neutral-800");
for (let i = 1; i <= depth; i++) {
res += `${i - 0.6}rem 0 0 ${bg} inset,`;
res += `${i - 0.4}rem 0 0 ${fg} inset`;
res += (i+1 > depth) ? "" : ", ";
}
return res;
}
updateJson(json) {
for (const [k, v] of Object.entries(json)) {
let div_id = k;
let parsed_html = parseHtml(v)[0];
// parent_id = -1 if item is at root level
let parent_id = "parentId" in parsed_html.dataset ? parsed_html.dataset.parentId : -1;
let expanded = "expanded" in parsed_html.dataset;
let depth = Number(parsed_html.dataset.depth);
parsed_html.style.paddingLeft = `${depth}em`;
parsed_html.style.boxShadow = this.getBoxShadow(depth);
// Add the updated html to the data object.
this.data_obj[div_id] = {
element: parsed_html,
active: parent_id === -1, // always show root
expanded: expanded || (parent_id === -1), // always expand root
parent: parent_id,
children: [], // populated later
};
}
// Build list of children for each element in dataset.
for (const [k, v] of Object.entries(this.data_obj)) {
if (v.parent === -1) {
continue;
} else if (!(v.parent in this.data_obj)) {
console.error("parent not in data:", v.parent);
} else {
this.data_obj[v.parent].children.push(k);
}
}
// Handle expanding of rows on initial load
for (const [k, v] of Object.entries(this.data_obj)) {
if (v.parent === -1) {
// Always show root level.
this.data_obj[k].active = true;
} else if (this.data_obj[v.parent].expanded && this.data_obj[v.parent].active) {
// Parent is both active and expanded. show child
this.data_obj[k].active = true;
} else {
this.data_obj[k].active = false;
}
}
this.applyFilter();
}
removeChildRows(div_id) {
for (const child_id of this.data_obj[div_id].children) {
this.data_obj[child_id].active = false;
this.data_obj[child_id].expanded = false;
delete this.data_obj[child_id].element.dataset.expanded;
this.removeChildRows(child_id);
}
}
addChildRows(div_id) {
for (const child_id of this.data_obj[div_id].children) {
this.data_obj[child_id].active = true;
if (this.data_obj[child_id].expanded) {
this.addChildRows(child_id);
}
}
}
}
class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize {
constructor(...args) {
super(...args);
this.sort_mode_str = "default";
this.sort_dir_str = "ascending";
this.filter_str = "";
}
updateJson(json) {
for (const [k, v] of Object.entries(json)) {
let div_id = k;
let parsed_html = parseHtml(v)[0];
// Add the updated html to the data object.
this.data_obj[div_id] = {
element: parsed_html,
active: true,
};
}
this.applyFilter();
}
filterRows(obj) {
let filtered_rows = super.filterRows(obj);
let res = [];
for (let i = 0; i < filtered_rows.length; i += this.n_cols) {
res.push(filtered_rows.slice(i, i + this.n_cols).join(""));
}
return res;
}
sortByName() {
this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => {
return STR_COLLATOR.compare(
this.data_obj[a].element.dataset.sortName,
this.data_obj[b].element.dataset.sortName,
);
});
}
sortByPath() {
this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => {
return STR_COLLATOR.compare(
this.data_obj[a].element.dataset.sortPath,
this.data_obj[b].element.dataset.sortPath,
);
});
}
sortByCreated() {
this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => {
return INT_COLLATOR.compare(
this.data_obj[a].element.dataset.sortCreated,
this.data_obj[b].element.dataset.sortCreated,
);
});
}
sortByModified() {
this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => {
return INT_COLLATOR.compare(
this.data_obj[a].element.dataset.sortModified,
this.data_obj[b].element.dataset.sortModified,
);
});
}
setSortMode(btn_sort_mode) {
this.sort_mode_str = btn_sort_mode.dataset.sortMode.toLowerCase();
}
setSortDir(btn_sort_dir) {
this.sort_dir_str = btn_sort_dir.dataset.sortDir.toLowerCase();
}
applySort() {
this.sort_reverse = this.sort_dir_str === "descending";
switch(this.sort_mode_str) {
case "name":
this.sort_fn = this.sortByName;
break;
case "path":
this.sort_fn = this.sortByPath;
break;
case "created":
this.sort_fn = this.sortByCreated;
break;
case "modified":
this.sort_fn = this.sortByModified;
break;
default:
this.sort_fn = this.sortByDivId;
break;
}
super.applySort();
}
applyFilter(filter_str) {
if (filter_str !== undefined) {
this.filter_str = filter_str.toLowerCase();
}
for (const [k, v] of Object.entries(this.data_obj)) {
let search_only = v.element.querySelector(".search_only");
let text = Array.prototype.map.call(v.element.querySelectorAll(".search_terms"), function(t) {
return t.textContent.toLowerCase();
}).join(" ");
let visible = text.indexOf(this.filter_str) != -1;
if (search_only && this.filter_str.length < 4) {
visible = false;
}
this.data_obj[k].active = visible;
}
this.applySort();
this.updateRows();
}
}

View file

@ -173,6 +173,13 @@
R.tracking = false;
document.body.classList.remove('resizing');
// Fire a custom event at end of resizing.
R.parent.dispatchEvent(
new CustomEvent("resizeHandleResized", {
bubbles: true,
}),
);
}
});
});

View file

@ -4,6 +4,9 @@ import urllib.parse
from pathlib import Path
from typing import Optional, Union
from dataclasses import dataclass
import gzip
import base64
import re
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
@ -32,6 +35,182 @@ class ExtraNetworksItem:
"""Wrapper for dictionaries representing ExtraNetworks items."""
item: dict
def build_row(
div_id: int,
tabname: str,
extra_networks_tabname: str,
label: str,
btn_type: str,
btn_copy_path_tpl: str,
btn_edit_item_tpl: str,
btn_metadata_tpl: str,
tree_row_tpl: str,
parent_id: Optional[int] = None,
data_depth: Optional[int] = None,
data_path: Optional[str] = None,
data_hash: Optional[str] = None,
data_prompt: Optional[str] = None,
data_neg_prompt: Optional[str] = None,
data_allow_neg: Optional[str] = None,
onclick_extra: Optional[str] = None,
) -> str:
if btn_type not in ["file", "dir"]:
raise ValueError("Invalid button type:", btn_type)
subitem = "has-subitem"
action_list_item_action_leading = "<i class='tree-list-item-action-chevron'></i>"
action_list_item_visual_leading = "🗀"
action_list_item_action_trailing = ""
action_list_item_visual_trailing = ""
if btn_type == "file":
subitem = "subitem"
action_list_item_visual_leading = "🗎"
# Action buttons
action_list_item_visual_trailing += '<div class="button-row">'
action_list_item_visual_trailing += btn_copy_path_tpl.format(
**{"filename": data_path}
)
action_list_item_visual_trailing += btn_edit_item_tpl.format(
**{
"tabname": tabname,
"extra_networks_tabname": extra_networks_tabname,
"name": label,
}
)
action_list_item_visual_trailing += btn_metadata_tpl.format(
**{"extra_networks_tabname": extra_networks_tabname, "name": label}
)
action_list_item_visual_trailing += "</div>"
data_attributes = ""
data_attributes += f"data-path={data_path} " if data_path is not None else ""
data_attributes += f"data-hash={data_hash} " if data_hash is not None else ""
data_attributes += f"data-prompt={data_prompt} " if data_prompt else ""
data_attributes += f"data-neg-prompt={data_neg_prompt} " if data_neg_prompt else ""
data_attributes += f"data-allow-neg={data_allow_neg} " if data_allow_neg else ""
data_attributes += (
f"data-tree-entry-type={btn_type} " if btn_type is not None else ""
)
data_attributes += f"data-div-id={div_id} " if div_id is not None else ""
data_attributes += f"data-parent-id={parent_id} " if parent_id is not None else ""
data_attributes += f"data-depth={data_depth} " if data_depth is not None else ""
data_attributes += (
"data-expanded " if parent_id is None else ""
) # inverted to expand root
res = tree_row_tpl.format(
**{
"data_attributes": data_attributes,
"subitem": subitem,
"search_terms": "",
"btn_type": btn_type,
"tabname": tabname,
"onclick_extra": onclick_extra if onclick_extra else "",
"extra_networks_tabname": extra_networks_tabname,
"action_list_item_action_leading": action_list_item_action_leading,
"action_list_item_visual_leading": action_list_item_visual_leading,
"action_list_item_label": label,
"action_list_item_visual_trailing": action_list_item_visual_trailing,
"action_list_item_action_trailing": action_list_item_action_trailing,
}
)
res = res.strip()
res = re.sub(" +", " ", res.replace("\n", ""))
return res
def build_tree(
tree: dict,
res: dict,
tabname: str,
extra_networks_tabname: str,
div_id: int,
depth: int,
btn_copy_path_tpl: str,
btn_edit_item_tpl: str,
btn_metadata_tpl: str,
tree_row_tpl: str,
allow_negative_prompt: Optional[bool] = None,
parent_id: Optional[int] = None,
) -> int:
for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])):
if not isinstance(v, (ExtraNetworksItem,)):
# dir
if div_id in res:
raise KeyError("div_id already in res:", div_id)
res[div_id] = build_row(
div_id=div_id,
parent_id=parent_id,
tabname=tabname,
extra_networks_tabname=extra_networks_tabname,
label=k,
data_depth=depth,
data_path=k,
btn_type="dir",
btn_copy_path_tpl=btn_copy_path_tpl,
btn_edit_item_tpl=btn_edit_item_tpl,
btn_metadata_tpl=btn_metadata_tpl,
tree_row_tpl=tree_row_tpl,
)
last_div_id = build_tree(
tree=v,
res=res,
depth=depth + 1,
div_id=div_id + 1,
parent_id=div_id,
tabname=tabname,
extra_networks_tabname=extra_networks_tabname,
allow_negative_prompt=allow_negative_prompt,
btn_copy_path_tpl=btn_copy_path_tpl,
btn_edit_item_tpl=btn_edit_item_tpl,
btn_metadata_tpl=btn_metadata_tpl,
tree_row_tpl=tree_row_tpl,
)
div_id = last_div_id
else:
# file
if div_id in res:
raise KeyError("div_id already in res:", div_id)
onclick = v.item.get("onclick", None)
if onclick is None:
# 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": v.item["prompt"],
"neg_prompt": v.item.get("negative_prompt", "''"),
"allow_neg": str(allow_negative_prompt).lower(),
}
)
onclick = html.escape(onclick)
res[div_id] = build_row(
div_id=div_id,
parent_id=parent_id,
tabname=tabname,
extra_networks_tabname=extra_networks_tabname,
label=v.item["name"],
data_depth=depth,
data_path=v.item["filename"],
data_hash=v.item["shorthash"],
data_prompt=html.escape(v.item.get("prompt", "''")),
data_neg_prompt=html.escape(v.item.get("negative_prompt", "''")),
data_allow_neg=str(allow_negative_prompt).lower(),
onclick_extra=onclick,
btn_type="file",
btn_copy_path_tpl=btn_copy_path_tpl,
btn_edit_item_tpl=btn_edit_item_tpl,
btn_metadata_tpl=btn_metadata_tpl,
tree_row_tpl=tree_row_tpl,
)
div_id += 1
return div_id
def get_tree(paths: Union[str, list[str]], items: dict[str, ExtraNetworksItem]) -> dict:
"""Recursively builds a directory tree.
@ -167,10 +346,11 @@ class ExtraNetworksPage:
self.pane_content_tree_tpl = shared.html("extra-networks-pane-tree.html")
self.pane_content_dirs_tpl = shared.html("extra-networks-pane-dirs.html")
self.card_tpl = shared.html("extra-networks-card.html")
self.btn_tree_tpl = shared.html("extra-networks-tree-button.html")
self.tree_row_tpl = shared.html("extra-networks-tree-row.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")
self.btn_dirs_view_tpl = shared.html("extra-networks-dirs-view-button.html")
def refresh(self):
pass
@ -204,6 +384,7 @@ class ExtraNetworksPage:
tabname: str,
item: dict,
template: Optional[str] = None,
div_id: Optional[int] = None,
) -> Union[str, dict]:
"""Generates HTML for a single ExtraNetworks Item.
@ -297,6 +478,7 @@ class ExtraNetworksPage:
# Some items here might not be used depending on HTML template used.
args = {
"div_id": "" if div_id is None else div_id,
"background_image": background_image,
"card_clicked": onclick,
"copy_path_button": btn_copy_path,
@ -305,7 +487,6 @@ class ExtraNetworksPage:
"local_preview": quote_js(item["local_preview"]),
"metadata_button": btn_metadata,
"name": html.escape(item["name"]),
"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,
@ -313,6 +494,9 @@ class ExtraNetworksPage:
"style": card_style,
"tabname": tabname,
"extra_networks_tabname": self.extra_networks_tabname,
"data_prompt": item.get("prompt", "''"),
"data_neg_prompt": item.get("negative_prompt", "''"),
"data_allow_neg": str(self.allow_negative_prompt).lower(),
}
if template:
@ -320,108 +504,6 @@ class ExtraNetworksPage:
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.
@ -431,7 +513,7 @@ class ExtraNetworksPage:
Returns:
HTML string generated for this tree view.
"""
res = ""
res = {}
# Setup the tree dictionary.
roots = self.allowed_directories_for_previews()
@ -441,43 +523,24 @@ class ExtraNetworksPage:
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>"
build_tree(
tree=tree,
res=res,
depth=0,
div_id=0,
parent_id=None,
tabname=tabname,
extra_networks_tabname=self.extra_networks_tabname,
allow_negative_prompt=self.allow_negative_prompt,
btn_copy_path_tpl=self.btn_copy_path_tpl,
btn_edit_item_tpl=self.btn_edit_item_tpl,
btn_metadata_tpl=self.btn_metadata_tpl,
tree_row_tpl=self.tree_row_tpl,
)
res = base64.b64encode(gzip.compress(json.dumps(res).encode("utf-8"))).decode("utf-8")
return f'<div class="extra-networks-script-data" data-tabname-full={tabname}_{self.extra_networks_tabname} data-proxy-name=tree_list data-json={res} hidden></div>'
# FIXME
def create_dirs_view_html(self, tabname: str) -> str:
"""Generates HTML for displaying folders."""
@ -511,12 +574,12 @@ class ExtraNetworksPage:
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}", "{self.extra_networks_tabname}", event)'>
{html.escape(subdir if subdir != "" else "all")}
</button>
""" for subdir in subdirs])
subdirs_html = "".join([
self.btn_dirs_view_tpl.format(**{
"extra_class": "search-all" if subdir == "" else "",
"tabname_full": f"{tabname}_{self.extra_networks_tabname}",
}) for subdir in subdirs
])
return subdirs_html
def create_card_view_html(self, tabname: str, *, none_message) -> str:
@ -532,15 +595,12 @@ class ExtraNetworksPage:
Returns:
HTML formatted string.
"""
res = []
for item in self.items.values():
res.append(self.create_item_html(tabname, item, self.card_tpl))
res = {}
for i, item in self.items.values():
res[i] = self.create_item_html(tabname, item, self.card_tpl, div_id=i)
if not res:
dirs = "".join([f"<li>{x}</li>" for x in self.allowed_directories_for_previews()])
res = [none_message or shared.html("extra-networks-no-cards.html").format(dirs=dirs)]
return "".join(res)
res = base64.b64encode(gzip.compress(json.dumps(res).encode("utf-8"))).decode("utf-8")
return f'<div class="extra-networks-script-data" data-tabname-full={tabname}_{self.extra_networks_tabname} data-proxy-name=cards_list data-json={res} hidden></div>'
def create_html(self, tabname, *, empty=False):
"""Generates an HTML string for the current pane.
@ -574,13 +634,14 @@ class ExtraNetworksPage:
page_params = {
"tabname": tabname,
"extra_networks_tabname": self.extra_networks_tabname,
"data_sortdir": shared.opts.extra_networks_card_order,
"data_sort_dir": shared.opts.extra_networks_card_order.lower().strip(),
"data_sort_mode": shared.opts.extra_networks_card_order_field.lower().strip(),
"sort_path_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Path' else '',
"sort_name_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Name' else '',
"sort_date_created_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Date Created' else '',
"sort_date_modified_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Date Modified' else '',
"tree_view_btn_extra_class": "extra-network-control--enabled" if show_tree else "",
"items_html": self.create_card_view_html(tabname, none_message="Loading..." if empty else None),
"cards_html": self.create_card_view_html(tabname, none_message="Loading..." if empty else None),
"extra_networks_tree_view_default_width": shared.opts.extra_networks_tree_view_default_width,
"tree_view_div_default_display_class": "" if show_tree else "extra-network-dirs-hidden",
}
@ -722,10 +783,10 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
for page, tab in zip(ui.stored_extra_pages, related_tabs):
jscode = (
"function(){{"
"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)
@ -736,7 +797,17 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
return ui.pages_contents
button_refresh = gr.Button("Refresh", elem_id=f"{tabname}_{page.extra_networks_tabname}_extra_refresh_internal", visible=False)
button_refresh.click(fn=refresh, inputs=[], outputs=ui.pages).then(fn=lambda: None, _js="function(){ " + f"applyExtraNetworkFilter('{tabname}_{page.extra_networks_tabname}');" + " }").then(fn=lambda: None, _js='setupAllResizeHandles')
button_refresh.click(
fn=refresh,
inputs=[],
outputs=ui.pages,
).then(
fn=lambda: None,
_js='setupAllResizeHandles'
).then(
fn=lambda: None,
_js=f"function(){{ extraNetworksRefreshTab('{tabname}_{page.extra_networks_tabname}'); }}",
)
def create_html():
ui.pages_contents = [pg.create_html(ui.tabname) for pg in ui.stored_extra_pages]
@ -746,7 +817,17 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
create_html()
return ui.pages_contents
interface.load(fn=pages_html, inputs=[], outputs=ui.pages).then(fn=lambda: None, _js='setupAllResizeHandles')
interface.load(
fn=pages_html,
inputs=[],
outputs=ui.pages,
).then(
fn=lambda: None,
_js="setupAllResizeHandles",
).then(
fn=lambda: None,
_js="setupExtraNetworksData",
)
return ui

View file

@ -25,6 +25,9 @@ def javascript_html():
if shared.cmd_opts.theme:
head += f'<script type="text/javascript">set_theme(\"{shared.cmd_opts.theme}\");</script>\n'
# Allows for very large scrollable regions without bloating DOM.
head += '<script src="https://cdn.jsdelivr.net/npm/clusterize.js@1.0.0/clusterize.min.js"></script>\n'
return head
@ -37,6 +40,9 @@ def css_html():
for cssfile in scripts.list_files_with_name("style.css"):
head += stylesheet(cssfile)
# Used by clusterize.js
head += '<link href="https://cdn.jsdelivr.net/npm/clusterize.js@1.0.0/clusterize.min.css" rel="stylesheet">'
user_css = os.path.join(data_path, "user.css")
if os.path.exists(user_css):
head += stylesheet(user_css)

295
style.css
View file

@ -949,9 +949,6 @@ footer {
display: inline-flex;
visibility: hidden;
color: white;
}
.extra-network-pane .card .button-row {
position: absolute;
right: 0;
z-index: 1;
@ -963,6 +960,11 @@ footer {
.extra-network-pane .card-button{
color: white;
width: 1.5em;
text-shadow: 2px 2px 3px black;
color: white;
padding: 0.25em 0.1em;
font-size: 2rem;
}
.extra-network-pane .copy-path-button::before {
@ -977,25 +979,10 @@ footer {
content: "🛠";
}
.extra-network-pane .card-button {
width: 1.5em;
text-shadow: 2px 2px 3px black;
color: white;
padding: 0.25em 0.1em;
}
.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;
object-fit: cover;
@ -1196,6 +1183,26 @@ body.resizing .resize-handle {
}
/* ========================= */
.clusterize-scroll::-webkit-scrollbar {
background-color: transparent;
width: 16px;
}
.clusterize-scroll::-webkit-scrollbar-track {
background-color: transparent;
background-clip: content-box;
}
.clusterize-scroll::-webkit-scrollbar-thumb {
background-color: var(--border-color-primary);
border-radius: 16px;
border: 4px solid var(--background-fill-primary);
}
.clusterize-scroll::-webkit-scrollbar-button {
display: none;
}
.extra-network-pane {
display: flex;
height: calc(100vh - 24rem);
@ -1295,53 +1302,6 @@ body.resizing .resize-handle {
margin: 0 0.5rem 0 0.75rem;
}
.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 {
@ -1351,8 +1311,7 @@ body.resizing .resize-handle {
padding: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
font-size: 1rem;
text-align: left;
font-size: 1em;
user-select: none;
background-color: transparent;
border: none;
@ -1360,25 +1319,26 @@ body.resizing .resize-handle {
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;
grid-gap: 0.1em;
place-items: center stretch;
text-align: center;
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);
}
.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);
}
.dark .extra-network-tree div.tree-list-content[data-selected] {
background-color: var(--neutral-700);
}
@ -1387,83 +1347,104 @@ body.resizing .resize-handle {
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);
.tree-list-item {
position: relative;
background: transparent;
margin: 0;
padding: 0;
}
/* ==== 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;
.tree-list-item-action-chevron {
display: inline-flex;
height: 1em;
width: 1em;
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="M7 7H17V17" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>');
mask-repeat: no-repeat;
mask-position: center center;
mask-size: 100%;
background-color: var(--input-placeholder-color);
transform: rotate(45deg);
}
.extra-network-tree .tree-list-content-dir[data-expanded] .tree-list-item-action-chevron {
.tree-list-item[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);
transition: transform 0.2s; /* currently broken since we remove/add elements to clusterize.js list */
}
/* ==== 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;
.extra-network-control--search {
display: flex;
flex-direction: row;
position: relative;
outline: var(--input-border-width) solid var(--input-border-color);
border-radius: var(--input-radius);
align-items: center;
place-content: center;
gap: var(--spacing-sm);
box-sizing: border-box;
background: var(--input-background-fill);
}
.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;
/* Add outline to the contianer when the inner search-text <input> has focus. */
.extra-network-control--search:focus-within {
outline: calc(var(--input-border-width) * 2) solid var(--input-border-color-focus);
}
.extra-network-control .extra-network-control--search .extra-network-control--search-text::placeholder {
color: var(--input-placeholder-color);
/* Search icon on left of container. */
.extra-network-control--search .extra-network-control--search-icon {
fill: var(--input-placeholder-color);
background: transparent;
user-select: none;
padding: var(--input-padding) var(--spacing-sm) var(--input-padding) var(--input-padding);
margin: 0;
}
/* <input> text in container. */
.extra-network-control--search .extra-network-control--search-text {
display: flex;
color: var(--body-text-color);
background: transparent;
font-weight: var(--input-text-weight);
font-size: var(--input-text-size);
line-height: var(--line-sm);
padding: var(--input-padding) var(--spacing-sm) var(--input-padding) var(--spacing-sm);
margin: 0;
border: none;
outline: none;
}
/* <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;
/* Make "clear" button visible when user has entered text in search-text <input>. */
.extra-network-control--search .extra-network-control--search-text:not(:placeholder-shown)+.extra-network-control--search-clear {
visibility: visible;
}
/* "Clear" button on right of container. Invisible until user enters text in <input>. */
.extra-network-control--search .extra-network-control--search-clear {
display: flex;
visibility: hidden;
fill: var(--input-placeholder-color);
background: transparent;
border: var(--input-border-width) solid transparent;
border-radius: 0 var(--input-radius) var(--input-radius) 0;
transition: background var(--button-transition);
user-select: 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);
align-self: stretch; /* forces button to fill vertical */
align-items: center;
padding: var(--input-padding);
}
.extra-network-control--search .extra-network-control--search-clear:hover {
background: var(--button-secondary-background-fill-hover);
}
/* ==== SORT ICON ACTIONS ==== */
.extra-network-control .extra-network-control--sort {
.extra-network-control--sort-mode {
padding: 0.25rem;
display: inline-flex;
cursor: pointer;
@ -1471,7 +1452,7 @@ body.resizing .resize-handle {
align-self: center;
}
.extra-network-control .extra-network-control--sort .extra-network-control--sort-icon {
.extra-network-control--sort-mode-icon {
height: 1.5rem;
width: 1.5rem;
mask-repeat: no-repeat;
@ -1480,24 +1461,24 @@ body.resizing .resize-handle {
background-color: var(--input-placeholder-color);
}
.extra-network-control .extra-network-control--sort[data-sortkey="default"] .extra-network-control--sort-icon {
.extra-network-control--sort-mode[data-sort-mode="path"] .extra-network-control--sort-mode-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-sortkey="name"] .extra-network-control--sort-icon {
.extra-network-control--sort-mode[data-sort-mode="name"] .extra-network-control--sort-mode-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-sortkey="date_created"] .extra-network-control--sort-icon {
.extra-network-control--sort-mode[data-sort-mode="date_created"] .extra-network-control--sort-mode-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-sortkey="date_modified"] .extra-network-control--sort-icon {
.extra-network-control--sort-mode[data-sort-mode="date_modified"] .extra-network-control--sort-mode-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 {
.extra-network-control--sort-dir {
padding: 0.25rem;
display: inline-flex;
cursor: pointer;
@ -1505,7 +1486,7 @@ body.resizing .resize-handle {
align-self: center;
}
.extra-network-control .extra-network-control--sort-dir .extra-network-control--sort-dir-icon {
.extra-network-control--sort-dir-icon {
height: 1.5rem;
width: 1.5rem;
mask-repeat: no-repeat;
@ -1514,11 +1495,11 @@ body.resizing .resize-handle {
background-color: var(--input-placeholder-color);
}
.extra-network-control .extra-network-control--sort-dir[data-sortdir="Ascending"] .extra-network-control--sort-dir-icon {
.extra-network-control--sort-dir[data-sort-dir="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 {
.extra-network-control--sort-dir[data-sort-dir="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>');
}
@ -1583,24 +1564,25 @@ body.resizing .resize-handle {
/* ==== TREE GRID CONFIG ==== */
/* Text for button. */
.extra-network-tree .tree-list-item-label {
.tree-list-item-label {
position: relative;
line-height: 1.25rem;
line-height: 1.25em;
color: var(--button-secondary-text-color);
grid-area: label;
padding-left: 0.5rem;
padding-left: 0.25em;
text-align: start;
}
/* Text for button truncated. */
.extra-network-tree .tree-list-item-label--truncate {
.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;
.tree-list-item-visual {
min-height: 1em;
color: var(--button-secondary-text-color);
pointer-events: none;
align-items: right;
@ -1608,46 +1590,41 @@ body.resizing .resize-handle {
/* Icon for button when it is before label. */
.extra-network-tree .tree-list-item-visual--leading {
.tree-list-item-visual--leading {
grid-area: leading-visual;
width: 1rem;
width: 1em;
text-align: right;
}
/* Icon for button when it is after label. */
.extra-network-tree .tree-list-item-visual--trailing {
.tree-list-item-visual--trailing {
grid-area: trailing-visual;
width: 1rem;
width: 1em;
text-align: right;
}
/* Dropdown arrow for button. */
.extra-network-tree .tree-list-item-action--leading {
margin-right: 0.5rem;
margin-left: 0.2rem;
.tree-list-item-action--leading {
grid-area: leading-action;
margin-right: 0.5em;
margin-left: 0.2em;
}
.extra-network-tree .tree-list-content-file .tree-list-item-action--leading {
.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 {
.tree-list-item-action--trailing {
grid-area: trailing-action;
display: inline-flex;
}
.extra-network-tree .tree-list-content .button-row {
.tree-list-content .button-row {
display: inline-flex;
visibility: hidden;
color: var(--button-secondary-text-color);
width: 0;
}
.extra-network-tree .tree-list-content:hover .button-row {
.tree-list-content:hover .button-row {
visibility: visible;
width: auto;
}