diff --git a/javascript/clusterize.js b/javascript/clusterize.js
index a2b2a0ef2..daca223a7 100644
--- a/javascript/clusterize.js
+++ b/javascript/clusterize.js
@@ -8,9 +8,11 @@
in an array and load from that; this caused a large memory overhead in the client.
*/
+// Many operations can be lenghty. Try to limit their frequency by debouncing.
const SCROLL_DEBOUNCE_TIME_MS = 50;
-const RESIZE_OBSERVER_DEBOUNCE_TIME_MS = 100;
+const RESIZE_OBSERVER_DEBOUNCE_TIME_MS = 50; // should be less than refresh debounce time
const ELEMENT_OBSERVER_DEBOUNCE_TIME_MS = 100;
+const REFRESH_DEBOUNCE_TIME_MS = 50;
class Clusterize {
scroll_elem = null;
@@ -22,6 +24,7 @@ class Clusterize {
cols_in_block: 1,
blocks_in_cluster: 5,
tag: null,
+ id_attr: "data-div-id",
show_no_data_row: true,
no_data_class: "clusterize-no-data",
no_data_text: "No data",
@@ -29,6 +32,7 @@ class Clusterize {
callbacks: {},
};
setup_has_run = false;
+ enabled = false;
#is_mac = null;
#ie = null;
#max_items = null;
@@ -37,6 +41,7 @@ class Clusterize {
#scroll_top = 0;
#last_cluster = false;
#scroll_debounce = 0;
+ #refresh_debounce_timer = null;
#resize_observer = null;
#resize_observer_timer = null;
#element_observer = null;
@@ -94,11 +99,18 @@ class Clusterize {
}
// ==== PUBLIC FUNCTIONS ====
+ enable(state) {
+ // if no state is passed, we enable by default.
+ this.enabled = state !== false;
+ }
+
async setup() {
- if (this.setup_has_run) {
+ if (this.setup_has_run || !this.enabled) {
return;
}
+ this.#fixElementReferences();
+
await this.#insertToDOM();
this.scroll_elem.scrollTop = this.#scroll_top;
@@ -110,7 +122,7 @@ class Clusterize {
}
clear() {
- if (!this.setup_has_run) {
+ if (!this.setup_has_run || !this.enabled) {
return;
}
@@ -127,17 +139,27 @@ class Clusterize {
}
async refresh(force) {
- if (!this.setup_has_run) {
+ // Refresh can be a longer operation so we want to debounce it to
+ // avoid refreshing too often.
+ if (!this.setup_has_run || !this.enabled) {
return;
}
- if (this.#getRowsHeight() || force) {
- await this.update()
- }
+ clearTimeout(this.#refresh_debounce_timer);
+ this.#refresh_debounce_timer = setTimeout(
+ async () => {
+ this.#fixElementReferences();
+
+ if (this.#recalculateDims() || force) {
+ await this.update()
+ }
+ },
+ REFRESH_DEBOUNCE_TIME_MS,
+ )
}
async update() {
- if (!this.setup_has_run) {
+ if (!this.setup_has_run || !this.enabled) {
return;
}
@@ -161,7 +183,7 @@ class Clusterize {
}
async setMaxItems(max_items) {
- if (!this.setup_has_run) {
+ if (!this.setup_has_run || !this.enabled) {
this.#max_items = max_items;
return;
}
@@ -173,7 +195,7 @@ class Clusterize {
// If the number of items changed, we need to update the cluster.
this.#max_items = max_items;
- await this.refresh(true);
+ await this.refresh();
// Apply sort to the updated data.
await this.sortData();
@@ -185,6 +207,9 @@ class Clusterize {
}
async initData() {
+ if (!this.enabled) {
+ return;
+ }
return await this.options.callbacks.initData.call(this);
}
@@ -193,6 +218,9 @@ class Clusterize {
}
async fetchData(idx_start, idx_end) {
+ if (!this.enabled) {
+ return;
+ }
return await this.options.callbacks.fetchData.call(this, idx_start, idx_end);
}
@@ -201,12 +229,15 @@ class Clusterize {
}
async sortData() {
- if (!this.setup_has_run) {
+ if (!this.setup_has_run || !this.enabled) {
return;
}
+ this.#fixElementReferences();
+
// Sort is applied to the filtered data.
await this.options.callbacks.sortData.call(this);
+ this.#recalculateDims();
await this.#insertToDOM();
}
@@ -215,7 +246,7 @@ class Clusterize {
}
async filterData() {
- if (!this.setup_has_run) {
+ if (!this.setup_has_run || !this.enabled) {
return;
}
@@ -238,54 +269,60 @@ class Clusterize {
if (!this.options.tag) {
this.options.tag = this.content_elem.children[0].tagName.toLowerCase();
}
- this.#getRowsHeight();
+ this.#recalculateDims();
}
- #getRowsHeight() {
+ #recalculateDims() {
const prev_item_height = this.options.item_height;
const prev_item_width = this.options.item_width;
const prev_rows_in_block = this.options.rows_in_block;
const prev_cols_in_block = this.options.cols_in_block;
+ const prev_options = JSON.stringify(this.options);
this.options.cluster_height = 0;
this.options.cluster_width = 0;
+
if (!this.#max_items) {
return;
}
- const rows = this.content_elem.children;
- if (!rows.length) {
- return;
- }
- const nodes = rows[0].children;
- if (!nodes.length) {
+ // Get the first element that isn't one of our placeholder rows.
+ const node = this.content_elem.querySelector(
+ `${this.options.tag}:not(clusterize-extra-row):not(clusterize-no-data)`
+ );
+ if (!isElement(node)) {
return;
}
- const node = nodes[Math.floor(nodes.length / 2)];
const node_dims = getComputedDims(node);
this.options.item_height = node_dims.height;
this.options.item_width = node_dims.width;
+
// consider table's browser spacing
- if (this.options.tag === "tr" && getStyle("borderCollapse", this.content_elem) !== "collapse") {
- const spacing = parseInt(getStyle("borderSpacing", this.content_elem), 10) || 0;
+ if (this.options.tag === "tr" && getComputedProperty(this.content_elem, "borderCollapse") !== "collapse") {
+ const spacing = parseInt(getComputedProperty(this.content_elem, "borderSpacing"), 10) || 0;
this.options.item_height += spacing;
this.options.item_width += spacing;
}
- // consider margins and margins collapsing
- if (this.options.tag !== "tr") {
- const margin_top = parseInt(getStyle("marginTop", node), 10) || 0;
- const margin_right = parseInt(getStyle("marginRight", node), 10) || 0;
- const margin_bottom = parseInt(getStyle("marginBottom", node), 10) || 0;
- const margin_left = parseInt(getStyle("marginLeft", node), 10) || 0;
- this.options.item_height += Math.max(margin_top, margin_bottom);
- this.options.item_width += Math.max(margin_left, margin_right);
+
+ // Update rows in block to match the number of elements that can fit in the view.
+ const content_padding = getComputedPaddingDims(this.content_elem);
+ let content_gap = parseFloat(getComputedProperty(this.content_elem, "gap"));
+ if (isNumber(content_gap)) {
+ this.options.item_width += content_gap;
+ this.options.item_height += content_gap;
}
- // Update rows in block to match the number of elements that can fit in the scroll element view.
- this.options.rows_in_block = calcRowsPerCol(this.scroll_elem, node);
- this.options.cols_in_block = calcColsPerRow(this.content_elem, node);
- console.log("HERE:", this.scroll_elem, this.content_elem, node, this.options.rows_in_block, this.options.cols_in_block);
+ const inner_width = this.scroll_elem.clientWidth - content_padding.width;
+ const inner_height = this.scroll_elem.clientHeight - content_padding.height;
+ // Since we don't allow horizontal scrolling, we want to round down for columns.
+ const cols_in_block = Math.floor(inner_width / this.options.item_width);
+ // Round up for rows so that we don't cut rows off from the view.
+ const rows_in_block = Math.ceil(inner_height / this.options.item_height);
+
+ // Always need at least 1 row/col in block
+ this.options.cols_in_block = Math.max(1, cols_in_block);
+ this.options.rows_in_block = Math.max(1, rows_in_block);
this.options.block_height = this.options.item_height * this.options.rows_in_block;
this.options.block_width = this.options.item_width * this.options.cols_in_block;
@@ -293,8 +330,9 @@ class Clusterize {
this.options.cluster_height = this.options.blocks_in_cluster * this.options.block_height;
this.options.cluster_width = this.options.block_width;
- this.#max_rows = parseInt(this.#max_items / this.options.cols_in_block, 10);
+ this.#max_rows = Math.ceil(this.#max_items / this.options.cols_in_block, 10);
+ return prev_options === JSON.stringify(this.options);
return (
prev_item_height !== this.options.item_height ||
prev_item_width !== this.options.item_width ||
@@ -339,19 +377,29 @@ class Clusterize {
const rows_above = top_offset < 1 ? rows_start + 1 : rows_start;
const idx_start = Math.max(0, rows_start * this.options.cols_in_block);
- const idx_end = rows_end * this.options.cols_in_block;
+ const idx_end = Math.min(this.#max_items, rows_end * this.options.cols_in_block);
+
const this_cluster_rows = await this.fetchData(idx_start, idx_end);
+
+ if (this_cluster_rows.length < this.options.rows_in_block) {
+ return {
+ top_offset: 0,
+ bottom_offset: 0,
+ rows_above: 0,
+ rows: this_cluster_rows.length ? this_cluster_rows : this.#generateEmptyRow(),
+ };
+ }
+
return {
top_offset: top_offset,
bottom_offset: bottom_offset,
rows_above: rows_above,
- rows: Array.isArray(this_cluster_rows) ? this_cluster_rows : [],
+ rows: this_cluster_rows,
};
}
async #insertToDOM() {
if (!this.options.cluster_height || !this.options.cluster_width) {
- console.log("HERE");
const rows = await this.fetchData(0, 1);
this.#exploreEnvironment(rows, this.#cache);
}
@@ -360,7 +408,7 @@ class Clusterize {
let this_cluster_rows = [];
for (let i = 0; i < data.rows.length; i += this.options.cols_in_block) {
const new_row = data.rows.slice(i, i + this.options.cols_in_block).join("");
- this_cluster_rows.push(`
${new_row}
`);
+ this_cluster_rows.push(new_row);
}
this_cluster_rows = this_cluster_rows.join("");
const this_cluster_content_changed = this.#checkChanges("data", this_cluster_rows, this.#cache);
@@ -373,6 +421,7 @@ class Clusterize {
this.options.keep_parity && layout.push(this.#renderExtraTag("keep-parity"));
layout.push(this.#renderExtraTag("top-space", data.top_offset));
}
+
layout.push(this_cluster_rows);
data.bottom_offset && layout.push(this.#renderExtraTag("bottom-space", data.bottom_offset));
this.options.callbacks.clusterWillChange && this.options.callbacks.clusterWillChange();
@@ -381,7 +430,7 @@ class Clusterize {
this.content_elem.style["counter-increment"] = `clusterize-counter ${data.rows_above - 1}`;
this.options.callbacks.clusterChanged && this.options.callbacks.clusterChanged();
} else if (only_bottom_offset_changed) {
- this.content_elem.lastChild.style.height = `${data.bottom_offset}px`;
+ this.content_elem.lastElementChild.style.height = `${data.bottom_offset}px`;
}
}
@@ -391,10 +440,10 @@ class Clusterize {
const div = document.createElement("div");
let last;
div.innerHTML = `
`;
- while ((last = content_elem.lastChild)) {
+ while ((last = content_elem.lastElementChild)) {
content_elem.removeChild(last);
}
- const rows_nodes = this.#getChildNodes(div.firstChild.firstChild);
+ const rows_nodes = this.#getChildNodes(div.firstElementChild.firstElementChild);
while (rows_nodes.length) {
content_elem.appendChild(rows_nodes.shift());
}
@@ -452,7 +501,7 @@ class Clusterize {
}
async #onResize() {
- await this.refresh();
+ await this.refresh(true);
}
#fixElementReferences() {
@@ -460,12 +509,12 @@ class Clusterize {
return;
}
- // If association for elements is broken, replace them with instance version.
- if (!this.scroll_elem.isConnected || !this.content_elem.isConnected) {
- document.getElementByid(this.scroll_id).replaceWith(this.scroll_elem);
- // refresh since sizes may have changed.
- this.refresh(true);
+ if (!isNullOrUndefined(this.content_elem.offsetParent)) {
+ return;
}
+
+ // If association for elements is broken, replace them with instance version.
+ document.getElementById(this.scroll_id).replaceWith(this.scroll_elem);
}
#setupElementObservers() {
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js
index 8d27f70a2..3c2e38f54 100644
--- a/javascript/extraNetworks.js
+++ b/javascript/extraNetworks.js
@@ -11,15 +11,300 @@ const re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/;
const re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g;
const re_extranet_neg = /\(([^:^>]+:[\d.]+)\)/;
const re_extranet_g_neg = /\(([^:^>]+:[\d.]+)\)/g;
-const activePromptTextarea = {};
-const clusterizers = {};
var globalPopup = null;
var globalPopupInner = null;
const storedPopupIds = {};
const extraPageUserMetadataEditors = {};
+const extra_networks_tabs = {};
// A flag used by the `waitForBool` promise to determine when we first load Ui Options.
const initialUiOptionsLoaded = {state: false};
+class ExtraNetworksTab {
+ tree_list;
+ cards_list;
+ container_elem;
+ controls_elem;
+ txt_search_elem;
+ prompt_container_elem;
+ prompts_elem;
+ prompt_row_elem;
+ neg_prompt_row_elem;
+ txt_prompt_elem;
+ txt_neg_prompt_elem;
+ active_prompt_elem;
+ show_prompt = true;
+ show_neg_prompt = true;
+ compact_prompt_en = false;
+ constructor({tabname, extra_networks_tabname}) {
+ this.tabname = tabname;
+ this.extra_networks_tabname = extra_networks_tabname;
+ this.tabname_full = `${tabname}_${extra_networks_tabname}`;
+ }
+
+ async setup(pane, controls_div) {
+ this.container_elem = pane;
+
+ // get page elements
+ await Promise.all([
+ waitForElement(`#${this.tabname_full}_pane .extra-network-controls`).then(elem => this.controls_elem = elem),
+ waitForElement(`#${this.tabname}_prompt_container`).then(elem => this.prompt_container_elem = elem),
+ waitForElement(`#${this.tabname_full}_prompts`).then(elem => this.prompts_elem = elem),
+ waitForElement(`#${this.tabname}_prompt_row`).then(elem => this.prompt_row_elem = elem),
+ waitForElement(`#${this.tabname}_neg_prompt_row`).then(elem => this.neg_prompt_row_elem = elem),
+ waitForElement(`#${this.tabname_full}_tree_list_scroll_area`),
+ waitForElement(`#${this.tabname_full}_tree_list_content_area`),
+ waitForElement(`#${this.tabname_full}_cards_list_scroll_area`),
+ waitForElement(`#${this.tabname_full}_cards_list_content_area`),
+ ]);
+
+ this.txt_search_elem = this.controls_elem.querySelector(".extra-network-control--search-text");
+
+ // determine whether compact prompt mode is enabled.
+ // cannot await this since it may not exist on page depending on user setting.
+ this.compact_prompt_en = isElement(gradioApp().querySelector(".toprow-compact-tools"));
+
+ // setup this tab's controls
+ this.controls_elem.id = `${this.tabname_full}_controls`;
+ controls_div.insertBefore(this.controls_elem, null);
+
+ await this.setupTreeList();
+ await this.setupCardsList();
+
+ this.registerPrompt();
+
+ if (this.container_elem.style.display === "none") {
+ this.hideControls();
+ } else {
+ this.showControls();
+ }
+ }
+
+ async registerPrompt() {
+ await Promise.all([
+ waitForElement(`#${this.tabname}_prompt > label > textarea`).then(elem => this.txt_prompt_elem = elem),
+ waitForElement(`#${this.tabname}_neg_prompt > label > textarea`).then(elem => this.txt_neg_prompt_elem = elem),
+ ]);
+ this.active_prompt_elem = this.txt_prompt_elem;
+ this.txt_prompt_elem.addEventListener("focus", () => this.active_prompt_elem = this.txt_prompt_elem);
+ this.txt_neg_prompt_elem.addEventListener("focus", () => this.active_prompt_elem = this.txt_neg_prompt_elem);
+ }
+
+ async setupTreeList() {
+ if (this.tree_list instanceof ExtraNetworksClusterizeTreeList) {
+ this.tree_list.destroy();
+ }
+ this.tree_list = new ExtraNetworksClusterizeTreeList({
+ tabname: this.tabname,
+ extra_networks_tabname: this.extra_networks_tabname,
+ scrollId: `${this.tabname_full}_tree_list_scroll_area`,
+ contentId: `${this.tabname_full}_tree_list_content_area`,
+ tag: "button",
+ callbacks: {
+ initData: this.onInitTreeData,
+ fetchData: this.onFetchTreeData,
+ },
+ });
+ await this.tree_list.setup();
+ }
+
+ async setupCardsList() {
+ if (this.cards_list instanceof ExtraNetworksClusterizeCardsList) {
+ this.cards_list.destroy();
+ }
+ this.cards_list = new ExtraNetworksClusterizeCardsList({
+ tabname: this.tabname,
+ extra_networks_tabname: this.extra_networks_tabname,
+ scrollId: `${this.tabname_full}_cards_list_scroll_area`,
+ contentId: `${this.tabname_full}_cards_list_content_area`,
+ tag: "div",
+ callbacks: {
+ initData: this.onInitCardsData,
+ fetchData: this.onFetchCardsData,
+ },
+ });
+ await this.cards_list.setup();
+ }
+
+ movePrompt(show_prompt=true, show_neg_prompt=true) {
+ // This function only applies when compact prompt mode is enabled.
+ if (!this.compact_prompt_en) {
+ return;
+ }
+
+ if (show_neg_prompt) {
+ this.prompts_elem.insertBefore(this.neg_prompt_row_elem, this.prompts_elem.firstChild);
+ }
+
+ if (show_prompt) {
+ this.prompts_elem.insertBefore(this.prompt_row_elem, this.prompts_elem.firstChild);
+ }
+
+ this.prompts_elem.classList.toggle("extra-page-prompts-active", show_neg_prompt || show_prompt);
+ }
+
+ async refreshSingleCard(name) {
+ await requestGetPromise(
+ "./sd_extra_networks/get-single-card",
+ {
+ tabname: this.tabname,
+ extra_networks_tabname: this.extra_networks_tabname,
+ name: name,
+ },
+ (data) => {
+ if (data && data.html) {
+ const card = this.cards_list.content_elem.querySelector(`.card[data-name="${name}"]`);
+ const new_div = document.createElement("div");
+ new_div.innerHTML = data.html;
+ const new_card = new_div.firstElementChild;
+ new_card.style.display = "";
+ card.parentElement.insertBefore(new_card, card);
+ card.parentElement.removeChild(card);
+ }
+ },
+ );
+ }
+
+ showControls() {
+ this.controls_elem.classList.remove("hidden");
+ }
+
+ hideControls() {
+ this.controls_elem.classList.add("hidden");
+ }
+
+ async refresh() {
+ const btn_dirs_view = this.controls_elem.querySelector(".extra-network-control--dirs-view");
+ const btn_tree_view = this.controls_elem.querySelector(".extra-network-control--tree-view");
+ const div_dirs = this.container_elem.querySelector(".extra-network-content--dirs-view");
+ const div_tree = this.container_elem.querySelector(
+ `.extra-network-content.resize-handle-col:has(> #${this.tabname_full}_tree_list_scroll_area)`
+ );
+
+ // Remove "hidden" class if button is enabled, otherwise add it.
+ div_dirs.classList.toggle("hidden", !("selected" in btn_dirs_view.dataset));
+ div_tree.classList.toggle("hidden", !("selected" in btn_tree_view.dataset));
+
+ await Promise.all([this.setupTreeList(), this.setupCardsList()]);
+ this.tree_list.enable();
+ this.cards_list.enable();
+ await Promise.all([this.tree_list.load(true), this.cards_list.load(true)]);
+ }
+
+ async load(show_prompt, show_neg_prompt) {
+ this.movePrompt(show_prompt=show_prompt, show_neg_prompt=show_neg_prompt);
+ this.showControls();
+ this.tree_list.enable(true);
+ this.cards_list.enable(true);
+ await Promise.all([this.tree_list.load(), this.cards_list.load()]);
+ }
+
+ unload() {
+ this.movePrompt(false, false);
+ this.hideControls();
+ this.tree_list.enable(false);
+ this.cards_list.enable(false);
+ }
+
+ applyFilter() {
+ // We only want to filter/sort the cards list.
+ this.cards_list.setFilterStr(this.txt_search_elem.value.toLowerCase());
+
+ // If the search input has changed since selecting a button to populate it
+ // then we want to disable the button that previously populated the search input.
+
+ // tree view buttons
+ let btn = this.container_elem.querySelector(".tree-list-item[data-selected='']");
+ if (isElement(btn) && btn.dataset.path !== this.txt_search_elem.value && "selected" in btn.dataset) {
+ this.tree_list.onRowSelected(btn.dataset.divId, btn, false);
+ }
+ // dirs view buttons
+ btn = this.container_elem.querySelector(".extra-network-dirs-view-button[data-selected='']");
+ if (isElement(btn) && btn.textContent.trim() !== this.txt_search_elem.value) {
+ delete btn.dataset.selected;
+ }
+ }
+
+ async onInitCardsData() {
+ const res = await requestGetPromise(
+ "./sd_extra_networks/init-cards-data",
+ {
+ tabname: this.tabname,
+ extra_networks_tabname: this.extra_networks_tabname,
+ },
+ );
+ return JSON.parse(res);
+ }
+
+ async onInitTreeData() {
+ const res = await requestGetPromise(
+ "./sd_extra_networks/init-tree-data",
+ {
+ tabname: this.tabname,
+ extra_networks_tabname: this.extra_networks_tabname,
+ },
+ );
+ return JSON.parse(res);
+ }
+
+ async onFetchCardsData(div_ids) {
+ const res = await requestGetPromise(
+ "./sd_extra_networks/fetch-cards-data",
+ {
+ extra_networks_tabname: this.extra_networks_tabname,
+ div_ids: div_ids,
+ },
+ );
+ return JSON.parse(res);
+ }
+
+ async onFetchTreeData(div_ids) {
+ const res = await requestGetPromise(
+ "./sd_extra_networks/fetch-tree-data",
+ {
+ extra_networks_tabname: this.extra_networks_tabname,
+ div_ids: div_ids,
+ },
+ );
+ return JSON.parse(res);
+ }
+
+ updateSearch(text) {
+ this.txt_search_elem.value = text;
+ updateInput(this.txt_search_elem);
+ this.applyFilter();
+ }
+
+ autoSetTreeWidth() {
+ const row = this.container_elem.querySelector(".resize-handle-row");
+ if (!isElementLogError(row)) {
+ return;
+ }
+
+ const left_col = row.firstElementChild;
+ if (!isElementLogError(left_col)) {
+ return;
+ }
+
+ // If the left column is hidden then we don't want to do anything.
+ if (left_col.classList.contains("hidden")) {
+ return;
+ }
+
+ const pad = parseFloat(row.style.gridTemplateColumns.split(" ")[1]);
+ const min_left_col_width = parseFloat(left_col.style.flexBasis.slice(0, -2));
+ // We know that the tree list is the left column. That is the only one we want to resize.
+ let max_width = this.tree_list.getMaxRowWidth();
+ if (!isNumber(max_width)) {
+ return;
+ }
+ // Add the resize handle's padding to the result and default to minLeftColWidth if necessary.
+ max_width = Math.max(max_width + pad, min_left_col_width);
+
+ // Mimicks resizeHandle.js::setLeftColGridTemplate().
+ row.style.gridTemplateColumns = `${max_width}px ${pad}px 1fr`;
+ }
+}
+
//
function popup(contents) {
@@ -62,77 +347,6 @@ function closePopup() {
// ==== GENERAL EXTRA NETWORKS FUNCTIONS ====
-function extraNetworksClusterizersEnable(tabname_full) {
- for (const [_tabname_full, tab_clusterizers] of Object.entries(clusterizers)) {
- for (const v of Object.values(tab_clusterizers)) {
- v.enable(_tabname_full === tabname_full);
- }
- }
-}
-
-async function extraNetworksClusterizersLoadTab({
- tabname_full,
- selected = false,
- fetch_data = false,
-}) {
- if (!keyExistsLogError(clusterizers, tabname_full)) {
- return;
- }
-
- if (selected) {
- extraNetworksClusterizersEnable(tabname_full);
- }
-
- for (const v of Object.values(clusterizers[tabname_full])) {
- await v.load(fetch_data);
- await v.refresh(true);
- }
-}
-
-function extraNetworksRegisterPromptForTab(tabname, id) {
- var textarea = gradioApp().querySelector(`#${id} > label > textarea`);
-
- if (!activePromptTextarea[tabname]) {
- activePromptTextarea[tabname] = textarea;
- }
-
- textarea.addEventListener("focus", function() {
- activePromptTextarea[tabname] = textarea;
- });
-}
-
-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 elem = id ? gradioApp().getElementById(id) : null;
-
- if (showNegativePrompt && elem) {
- elem.insertBefore(negPrompt, elem.firstChild);
- } else {
- promptContainer.insertBefore(negPrompt, promptContainer.firstChild);
- }
-
- if (showPrompt && elem) {
- elem.insertBefore(prompt, elem.firstChild);
- } else {
- promptContainer.insertBefore(prompt, promptContainer.firstChild);
- }
-
- if (elem) {
- elem.classList.toggle('extra-page-prompts-active', showNegativePrompt || showPrompt);
- }
-}
-
-function extraNetworksShowControlsForPage(tabname, tabname_full) {
- gradioApp().querySelectorAll(`#${tabname}_extra_tabs .extra-network-controls-div > div`).forEach((elem) => {
- let show = `${tabname_full}_controls` === elem.id;
- elem.classList.toggle("hidden", !show);
- });
-}
-
function extraNetworksRemoveFromPrompt(textarea, text, is_neg) {
let match = text.match(is_neg ? re_extranet_neg : re_extranet);
let replaced = false;
@@ -164,7 +378,7 @@ function extraNetworksRemoveFromPrompt(textarea, text, is_neg) {
}
}
} else {
- res = textarea.value.replaceAll(new RegExp(`((?:${extraTextBeforeNet})?${text})`, "g"), "");
+ res = textarea.value.replaceAll(new RegExp(`((?:${prefix})?${text})`, "g"), "");
replaced = (res !== textarea.value);
}
@@ -290,162 +504,11 @@ function extraNetworksRefreshSingleCard(tabname, extra_networks_tabname, name) {
async function extraNetworksRefreshTab(tabname_full) {
/** called from python when user clicks the extra networks refresh tab button */
- // Reapply controls since they don't change on refresh.
- const controls = gradioApp().getElementById(`${tabname_full}_controls`);
- let btn_dirs_view = controls.querySelector(".extra-network-control--dirs-view");
- let btn_tree_view = controls.querySelector(".extra-network-control--tree-view");
-
- const pane = gradioApp().getElementById(`${tabname_full}_pane`);
- let div_dirs = pane.querySelector(".extra-network-content--dirs-view");
- let div_tree = pane.querySelector(`.extra-network-content.resize-handle-col:has(> #${tabname_full}_tree_list_scroll_area)`);
-
- // Remove "hidden" class if button is enabled, otherwise add it.
- div_dirs.classList.toggle("hidden", !("selected" in btn_dirs_view.dataset));
- div_tree.classList.toggle("hidden", !("selected" in btn_tree_view.dataset));
-
- await waitForKeyInObject({k: tabname_full, obj: clusterizers});
- for (const _tabname_full of Object.keys(clusterizers)) {
- let selected = _tabname_full == tabname_full;
- await extraNetworksClusterizersLoadTab({
- tabname_full: _tabname_full,
- selected: selected,
- fetch_data: true,
- });
- }
-}
-
-function extraNetworksAutoSetTreeWidth(pane) {
- if (!isElementLogError(pane)) {
- return;
- }
-
- const tabname_full = pane.dataset.tabnameFull;
-
- // This event is only applied to the currently selected tab if has clusterize lists.
- if (!keyExists(clusterizers, tabname_full)) {
- return;
- }
-
- const row = pane.querySelector(".resize-handle-row");
- if (!isElementLogError(row)) {
- return;
- }
-
- const left_col = row.firstElementChild;
- if (!isElementLogError(left_col)) {
- return;
- }
-
- // If the left column is hidden then we don't want to do anything.
- if (left_col.classList.contains("hidden")) {
- return;
- }
-
- const pad = parseFloat(row.style.gridTemplateColumns.split(" ")[1]);
- const min_left_col_width = parseFloat(left_col.style.flexBasis.slice(0, -2));
- // We know that the tree list is the left column. That is the only one we want to resize.
- let max_width = clusterizers[tabname_full].tree_list.getMaxRowWidth();
- // Add the resize handle's padding to the result and default to minLeftColWidth if necessary.
- max_width = Math.max(max_width + pad, min_left_col_width);
-
- // Mimicks resizeHandle.js::setLeftColGridTemplate().
- row.style.gridTemplateColumns = `${max_width}px ${pad}px 1fr`;
-}
-
-function extraNetworksApplyFilter(tabname_full) {
- if (!keyExistsLogError(clusterizers, tabname_full)) {
- return;
- }
-
- const pane = gradioApp().getElementById(`${tabname_full}_pane`);
- if (!isElementLogError(pane)) {
- return;
- }
-
- const txt_search = gradioApp().querySelector(`#${tabname_full}_controls .extra-network-control--search-text`);
- if (!isElementLogError(txt_search)) {
- return;
- }
-
- // We only want to filter/sort the cards list.
- clusterizers[tabname_full].cards_list.setFilterStr(txt_search.value.toLowerCase());
-
- // If the search input has changed since selecting a button to populate it
- // then we want to disable the button that previously populated the search input.
- // tree view buttons
- let btn = pane.querySelector(".tree-list-item[data-selected='']");
- if (isElement(btn) && btn.dataset.path !== txt_search.value && "selected" in btn.dataset) {
- clusterizers[tabname_full].tree_list.onRowSelected(btn.dataset.divId, btn, false);
- }
- // dirs view buttons
- btn = pane.querySelector(".extra-network-dirs-view-button[data-selected='']");
- if (isElement(btn) && btn.textContent.trim() !== txt_search.value) {
- delete btn.dataset.selected;
- }
+ extra_networks_tabs[tabname_full].refresh();
}
// ==== EVENT HANDLING ====
-async function extraNetworksInitCardsData(tabname, extra_networks_tabname) {
- const res = await requestGetPromise(
- "./sd_extra_networks/init-cards-data",
- {
- tabname: tabname,
- extra_networks_tabname: extra_networks_tabname,
- },
- );
- return JSON.parse(res);
-}
-
-async function extraNetworksInitTreeData(tabname, extra_networks_tabname) {
- const res = await requestGetPromise(
- "./sd_extra_networks/init-tree-data",
- {
- tabname: tabname,
- extra_networks_tabname: extra_networks_tabname,
- },
- );
- return JSON.parse(res);
-}
-
-async function extraNetworksOnInitData(tabname, extra_networks_tabname, class_name) {
- if (class_name === "ExtraNetworksClusterizeTreeList") {
- return await extraNetworksInitTreeData(tabname, extra_networks_tabname);
- } else if (class_name === "ExtraNetworksClusterizeCardsList") {
- return await extraNetworksInitCardsData(tabname, extra_networks_tabname);
- }
-}
-
-async function extraNetworksFetchCardsData(extra_networks_tabname, div_ids) {
- const res = await requestGetPromise(
- "./sd_extra_networks/fetch-cards-data",
- {
- extra_networks_tabname: extra_networks_tabname,
- div_ids: div_ids,
- },
- );
- return JSON.parse(res);
-}
-
-async function extraNetworksFetchTreeData(extra_networks_tabname, div_ids) {
- const res = await requestGetPromise(
- "./sd_extra_networks/fetch-tree-data",
- {
- extra_networks_tabname: extra_networks_tabname,
- div_ids: div_ids,
- },
- );
- return JSON.parse(res);
-}
-
-async function extraNetworksOnFetchData(class_name, extra_networks_tabname, div_ids) {
- if (class_name === "ExtraNetworksClusterizeTreeList") {
- return await extraNetworksFetchTreeData(extra_networks_tabname, div_ids);
- } else if (class_name === "ExtraNetworksClusterizeCardsList") {
- return await extraNetworksFetchCardsData(extra_networks_tabname, div_ids);
- }
-}
-
function extraNetworksFetchMetadata(extra_networks_tabname, card_name) {
const _showError = () => { extraNetworksShowMetadata("there was an error getting metadata"); };
@@ -463,51 +526,48 @@ function extraNetworksFetchMetadata(extra_networks_tabname, card_name) {
);
}
-function extraNetworksUnrelatedTabSelected(tabname) {
+function extraNetworksUnrelatedTabSelected() {
/** called from python when user selects an unrelated tab (generate) */
- extraNetworksMovePromptToTab(tabname, '', false, false);
- extraNetworksShowControlsForPage(tabname, null);
+ for (const [k, v] of Object.entries(extra_networks_tabs)) {
+ v.unload();
+ }
}
-async function extraNetworksTabSelected(
- tabname,
- id,
- showPrompt,
- showNegativePrompt,
- tabname_full,
-) {
+async function extraNetworksTabSelected(tabname_full, show_prompt, show_neg_prompt) {
/** called from python when user selects an extra networks tab */
- extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt);
- extraNetworksShowControlsForPage(tabname, tabname_full);
-
- await waitForKeyInObject({k: tabname_full, obj: clusterizers});
- await extraNetworksClusterizersLoadTab({
- tabname_full: tabname_full,
- selected: true,
- fetch_data: false,
- });
+ for (const [k, v] of Object.entries(extra_networks_tabs)) {
+ if (k === tabname_full) {
+ v.load(show_prompt=show_prompt, show_neg_prompt=show_neg_prompt);
+ } else {
+ v.unload();
+ }
+ }
}
function extraNetworksBtnDirsViewItemOnClick(event, tabname_full) {
/** Handles `onclick` events for buttons in the directory view. */
- const txt_search = gradioApp().querySelector(`#${tabname_full}_controls .extra-network-control--search-text`);
-
+ const tab = extra_networks_tabs[tabname_full];
+ const container_elem = tab.container_elem;
+ const txt_search_elem = tab.txt_search_elem;
+
const _deselect_all_buttons = () => {
- gradioApp().querySelectorAll(".extra-network-dirs-view-button").forEach((elem) => {
+ container_elem.querySelectorAll(
+ ".extra-network-dirs-view-button[data-selected='']"
+ ).forEach(elem => {
delete elem.dataset.selected;
});
};
const _select_button = (elem) => {
_deselect_all_buttons();
- // Update search input with select button's path.
+ // update search input with selected button's path.
elem.dataset.selected = "";
- txt_search.value = elem.textContent.trim();
+ txt_search_elem.value = elem.textContent.trim();
};
const _deselect_button = (elem) => {
delete elem.dataset.selected;
- txt_search.value = "";
+ txt_search_elem.value = "";
};
if ("selected" in event.target.dataset) {
@@ -516,16 +576,15 @@ function extraNetworksBtnDirsViewItemOnClick(event, tabname_full) {
_select_button(event.target);
}
- updateInput(txt_search);
- extraNetworksApplyFilter(tabname_full);
+ updateInput(txt_search_elem);
+ tab.applyFilter();
}
function extraNetworksControlSearchClearOnClick(event, tabname_full) {
/** Dispatches custom event when the `clear` button in a search input is clicked. */
- let clear_btn = event.target.closest(".extra-network-control--search-clear");
- let txt_search = clear_btn.previousElementSibling;
- txt_search.value = "";
- txt_search.dispatchEvent(
+ const txt_search_elem = extra_networks_tabs[tabname_full].txt_search_elem;
+ txt_search_elem.value = "";
+ txt_search_elem.dispatchEvent(
new CustomEvent(
"extra-network-control--search-clear",
{bubbles: true, detail: {tabname_full: tabname_full}},
@@ -535,18 +594,16 @@ function extraNetworksControlSearchClearOnClick(event, tabname_full) {
function extraNetworksControlSortModeOnClick(event, tabname_full) {
/** Handles `onclick` events for Sort Mode buttons. */
- event.currentTarget.parentElement.querySelectorAll('.extra-network-control--sort-mode').forEach(elem => {
+ const tab = extra_networks_tabs[tabname_full];
+ tab.controls_elem.querySelectorAll(".extra-network-control--sort-mode").forEach(elem => {
delete elem.dataset.selected;
});
event.currentTarget.dataset.selected = "";
- if (!keyExists(clusterizers, tabname_full)) {
- return;
- }
-
const sort_mode_str = event.currentTarget.dataset.sortMode.toLowerCase();
- clusterizers[tabname_full].cards_list.setSortMode(sort_mode_str);
+
+ tab.cards_list.setSortMode(sort_mode_str);
}
function extraNetworksControlSortDirOnClick(event, tabname_full) {
@@ -555,6 +612,8 @@ function extraNetworksControlSortDirOnClick(event, tabname_full) {
* Modifies the data attributes of the Sort Direction button to cycle between
* ascending and descending sort directions.
*/
+ const tab = extra_networks_tabs[tabname_full];
+
const curr_sort_dir_str = event.currentTarget.dataset.sortDir.toLowerCase();
if (!["ascending", "descending"].includes(curr_sort_dir_str)) {
console.error(`Invalid sort_dir_str: ${curr_sort_dir_str}`);
@@ -565,11 +624,7 @@ function extraNetworksControlSortDirOnClick(event, tabname_full) {
event.currentTarget.dataset.sortDir = sort_dir_str;
event.currentTarget.setAttribute("title", `Sort ${sort_dir_str}`);
- if (!keyExists(clusterizers, tabname_full)) {
- return;
- }
-
- clusterizers[tabname_full].cards_list.setSortDir(sort_dir_str);
+ tab.cards_list.setSortDir(sort_dir_str);
}
function extraNetworksControlTreeViewOnClick(event, tabname_full) {
@@ -586,11 +641,9 @@ function extraNetworksControlTreeViewOnClick(event, tabname_full) {
show = true;
}
- if (!keyExists(clusterizers, tabname_full)) {
- return;
- }
- clusterizers[tabname_full].tree_list.scroll_elem.parentElement.classList.toggle("hidden", !show);
- clusterizers[tabname_full].tree_list.enable(show);
+ const tab = extra_networks_tabs[tabname_full];
+ tab.tree_list.scroll_elem.parentElement.classList.toggle("hidden", !show);
+ tab.tree_list.enable(show);
}
function extraNetworksControlDirsViewOnClick(event, tabname_full) {
@@ -605,8 +658,10 @@ function extraNetworksControlDirsViewOnClick(event, tabname_full) {
delete event.currentTarget.dataset.selected;
}
- const pane = gradioApp().getElementById(`${tabname_full}_pane`);
- pane.querySelector(".extra-network-content--dirs-view").classList.toggle("hidden", !show);
+ const tab = extra_networks_tabs[tabname_full];
+ tab.container_elem.querySelector(
+ ".extra-network-content--dirs-view"
+ ).classList.toggle("hidden", !show);
}
function extraNetworksControlRefreshOnClick(event, tabname_full) {
@@ -620,29 +675,27 @@ function extraNetworksControlRefreshOnClick(event, tabname_full) {
// reset states
initialUiOptionsLoaded.state = false;
- // We want to reset all clusterizers on refresh click so that the viewing area
+ // We want to reset all tabs lists on refresh click so that the viewing area
// shows that it is loading new data.
- for (const _tabname_full of Object.keys(clusterizers)) {
- for (const v of Object.values(clusterizers[_tabname_full])) {
- v.clear();
- }
+ for (tab of Object.values(extra_networks_tabs)) {
+ tab.tree_list.destroy();
+ tab.cards_list.destroy();
}
// Fire an event for this button click.
gradioApp().getElementById(`${tabname_full}_extra_refresh_internal`).dispatchEvent(new Event("click"));
}
-function extraNetworksCardOnClick(event, tabname) {
+function extraNetworksCardOnClick(event, tabname_full) {
const elem = event.currentTarget;
- const prompt_elem = gradioApp().querySelector(`#${tabname}_prompt > label > textarea`);
- const neg_prompt_elem = gradioApp().querySelector(`#${tabname}_neg_prompt > label > textarea`);
+ const tab = extra_networks_tabs[tabname_full];
if ("negPrompt" in elem.dataset) {
- extraNetworksUpdatePrompt(prompt_elem, elem.dataset.prompt);
- extraNetworksUpdatePrompt(neg_prompt_elem, elem.dataset.negPrompt);
+ extraNetworksUpdatePrompt(tab.txt_prompt_elem, elem.dataset.prompt);
+ extraNetworksUpdatePrompt(tab.txt_neg_prompt_elem, elem.dataset.negPrompt);
} else if ("allowNeg" in elem.dataset) {
- extraNetworksUpdatePrompt(activePromptTextarea[tabname], elem.dataset.prompt);
+ extraNetworksUpdatePrompt(tab.active_prompt_elem, elem.dataset.prompt);
} else {
- extraNetworksUpdatePrompt(prompt_elem, elem.dataset.prompt);
+ extraNetworksUpdatePrompt(tab.txt_prompt_elem, elem.dataset.prompt);
}
}
@@ -651,7 +704,25 @@ function extraNetworksTreeFileOnClick(event, btn, tabname_full) {
}
function extraNetworksTreeDirectoryOnClick(event, btn, tabname_full) {
- return;
+ const true_targ = event.target;
+ const div_id = btn.dataset.divId;
+
+ const tab = extra_networks_tabs[tabname_full];
+
+ if (true_targ.matches(".tree-list-item-action--leading, .tree-list-item-action-chevron")) {
+ // If user clicks on the chevron, then we do not select the folder.
+ const prev_selected_elem = gradioApp().querySelector(".tree-list-item[data-selected='']");
+ tab.tree_list.onRowExpandClick(div_id, btn);
+ const selected_elem = gradioApp().querySelector(".tree-list-item[data-selected='']");
+ if (isElement(prev_selected_elem) && !isElement(selected_elem)) {
+ // is a selected element was removed, clear filter.
+ tab.updateSearch("");
+ }
+ } else {
+ // user clicked anywhere else on the row
+ tab.tree_list.onRowSelected(div_id, btn);
+ tab.updateSearch("selected" in btn.dataset ? btn.dataset.path : "");
+ }
}
function extraNetworksTreeOnClick(event, tabname_full) {
@@ -712,18 +783,19 @@ function extraNetworksSetupEventDelegators() {
window.addEventListener("resizeHandleDblClick", event => {
// See resizeHandle.js::onDoubleClick() for event detail.
event.stopPropagation();
- extraNetworksAutoSetTreeWidth(event.target.closest(".extra-network-pane"));
+ const pane = event.target.closest(".extra-network-pane");
+ extra_networks_tabs[pane.dataset.tabnameFull].autoSetTreeWidth();
});
// Update search filter whenever the search input's clear button is pressed.
window.addEventListener("extra-network-control--search-clear", event => {
event.stopPropagation();
- extraNetworksApplyFilter(event.detail.tabname_full);
+ extra_networks_tabs[event.detail.tabname_full].applyFilter();
});
// Debounce search text input. This way we only search after user is done typing.
- const search_input_debounce = debounce((tabname_full) => {
- extraNetworksApplyFilter(tabname_full);
+ const search_input_debounce = debounce(tabname_full => {
+ extra_networks_tabs[tabname_full].applyFilter();
}, SEARCH_INPUT_DEBOUNCE_TIME_MS);
window.addEventListener("keyup", event => {
@@ -744,72 +816,35 @@ function extraNetworksSetupEventDelegators() {
});
}
-async function extraNetworksSetupTabContent(tabname, pane, controls_div) {
- const tabname_full = pane.id;
- const extra_networks_tabname = tabname_full.replace(`${tabname}_`, "");
-
- const controls = await waitForElement(`#${tabname_full}_pane .extra-network-controls`);
- await waitForElement(`#${tabname_full}_pane .extra-network-content--dirs-view`);
-
- controls.id = `${tabname_full}_controls`;
- controls_div.insertBefore(controls, null);
-
- clusterizers[tabname_full] = {
- tree_list: new ExtraNetworksClusterizeTreeList({
- tabname: tabname,
- extra_networks_tabname: extra_networks_tabname,
- scrollId: `${tabname_full}_tree_list_scroll_area`,
- contentId: `${tabname_full}_tree_list_content_area`,
- tag: "div",
- callbacks: {
- initData: extraNetworksOnInitData,
- fetchData: extraNetworksOnFetchData,
- },
- }),
- cards_list: new ExtraNetworksClusterizeCardsList({
- tabname: tabname,
- extra_networks_tabname: extra_networks_tabname,
- scrollId: `${tabname_full}_cards_list_scroll_area`,
- contentId: `${tabname_full}_cards_list_content_area`,
- tag: "div",
- callbacks: {
- initData: extraNetworksOnInitData,
- fetchData: extraNetworksOnFetchData,
- },
- }),
- };
-
- await clusterizers[tabname_full].tree_list.setup();
- await clusterizers[tabname_full].cards_list.setup();
-
- if (pane.style.display !== "none") {
- extraNetworksShowControlsForPage(tabname, tabname_full);
- }
-}
-
async function extraNetworksSetupTab(tabname) {
- let controls_div;
-
const this_tab = await waitForElement(`#${tabname}_extra_tabs`);
const tab_nav = await waitForElement(`#${tabname}_extra_tabs > div.tab-nav`);
-
- controls_div = document.createElement("div");
+ const controls_div = document.createElement("div");
+
+ controls_div.id = `${tabname}_extra_network_controls_div`;
controls_div.classList.add("extra-network-controls-div");
tab_nav.appendChild(controls_div);
tab_nav.insertBefore(controls_div, null);
+
const panes = this_tab.querySelectorAll(`:scope > .tabitem[id^="${tabname}_"]`);
for (const pane of panes) {
- await extraNetworksSetupTabContent(tabname, pane, controls_div);
+ const tabname_full = pane.id;
+ const extra_networks_tabname = tabname_full.replace(`${tabname}_`, "");
+ extra_networks_tabs[tabname_full] = new ExtraNetworksTab({
+ tabname: tabname,
+ extra_networks_tabname: extra_networks_tabname,
+ });
+ await extra_networks_tabs[tabname_full].setup(pane, controls_div);
}
- extraNetworksRegisterPromptForTab(tabname, `${tabname}_prompt`);
- extraNetworksRegisterPromptForTab(tabname, `${tabname}_neg_prompt`);
}
async function extraNetworksSetup() {
await waitForBool(initialUiOptionsLoaded);
- extraNetworksSetupTab('txt2img');
- extraNetworksSetupTab('img2img');
+ await Promise.all([
+ extraNetworksSetupTab('txt2img'),
+ extraNetworksSetupTab('img2img'),
+ ]);
extraNetworksSetupEventDelegators();
}
diff --git a/javascript/extraNetworksClusterize.js b/javascript/extraNetworksClusterize.js
index 79c9d766a..4f5919c95 100644
--- a/javascript/extraNetworksClusterize.js
+++ b/javascript/extraNetworksClusterize.js
@@ -10,15 +10,56 @@ class NotImplementedError extends Error {
}
}
+const LRU_MAX_ITEMS = 250;
+class LRU {
+ constructor(max = LRU_MAX_ITEMS) {
+ this.max = max;
+ this.cache = new Map();
+ }
+
+ clear() {
+ this.cache.clear();
+ }
+
+ get(key) {
+ key = String(key);
+ let item = this.cache.get(key);
+ if (!isNullOrUndefined(item)) {
+ this.cache.delete(key);
+ this.cache.set(key, item);
+ }
+ return item;
+ }
+
+ set(key, val) {
+ key = String(key);
+ if (this.cache.has(key)) {
+ this.cache.delete(key);
+ } else if (this.cache.size === this.max) {
+ this.cache.delete(this.first());
+ }
+ this.cache.set(key, val);
+ }
+
+ has(key) {
+ key = String(key);
+ return this.cache.has(key);
+ }
+
+ first() {
+ return this.cache.keys().next().value;
+ }
+}
+
class ExtraNetworksClusterize extends Clusterize {
data_obj = {};
data_obj_keys_sorted = [];
+ lru = null;
sort_reverse = false;
default_sort_fn = this.sortByDivId;
sort_fn = this.default_sort_fn;
tabname = "";
extra_networks_tabname = "";
- enabled = false;
// Override base class defaults
default_sort_mode_str = "divId";
@@ -32,6 +73,7 @@ class ExtraNetworksClusterize extends Clusterize {
super(args);
this.tabname = getValueThrowError(args, "tabname");
this.extra_networks_tabname = getValueThrowError(args, "extra_networks_tabname");
+ this.lru = new LRU();
}
sortByDivId(data) {
@@ -43,12 +85,12 @@ class ExtraNetworksClusterize extends Clusterize {
await this.initData();
// can't use super class' sort since it relies on setup being run first.
// but we do need to make sure to sort the new data before continuing.
- await this.options.callbacks.sortData.call(this);
await this.setMaxItems(Object.keys(this.data_obj).length);
+ await this.options.callbacks.sortData.call(this);
}
async setup() {
- if (this.setup_has_run) {
+ if (this.setup_has_run || !this.enabled) {
return;
}
@@ -69,18 +111,14 @@ class ExtraNetworksClusterize extends Clusterize {
} else if (force_init_data) {
await this.reinitData();
} else {
- await this.refresh(true);
+ await this.refresh();
}
}
- enable(state) {
- // if no state is passed, we enable by default.
- this.enabled = state !== false;
- }
-
clear() {
this.data_obj = {};
this.data_obj_keys_sorted = [];
+ this.lru.clear();
super.clear();
}
@@ -108,7 +146,7 @@ class ExtraNetworksClusterize extends Clusterize {
if (isString(filter_str) && this.filter_str !== filter_str.toLowerCase()) {
this.filter_str = filter_str.toLowerCase();
} else if (isNullOrUndefined(this.filter_str)) {
- this.filter_str = this.default_filter_str;
+ this.filter_str = this.default_filter_str.toLowerCase();
} else {
return;
}
@@ -120,6 +158,44 @@ class ExtraNetworksClusterize extends Clusterize {
throw new NotImplementedError();
}
+ idxRangeToDivIds(idx_start, idx_end) {
+ const n_items = idx_end - idx_start;
+ const div_ids = [];
+ for (const div_id of this.data_obj_keys_sorted.slice(idx_start)) {
+ if (this.data_obj[div_id].visible) {
+ div_ids.push(div_id);
+ }
+ if (div_ids.length >= n_items) {
+ break;
+ }
+ }
+ return div_ids;
+ }
+
+ async fetchDivIds(div_ids) {
+ const lru_keys = Array.from(this.lru.cache.keys());
+ const cached_div_ids = div_ids.filter(x => lru_keys.includes(x));
+ const missing_div_ids = div_ids.filter(x => !lru_keys.includes(x));
+
+ const data = {};
+ // Fetch any div IDs not in the LRU Cache using our callback.
+ if (missing_div_ids.length !== 0) {
+ Object.assign(
+ data,
+ await this.options.callbacks.fetchData.call(this, missing_div_ids),
+ );
+ }
+
+ // Now load any cached IDs from the LRU Cache
+ for (const div_id of cached_div_ids) {
+ if (this.data_obj[div_id].visible) {
+ data[div_id] = this.lru.get(div_id);
+ }
+ }
+
+ return data;
+ }
+
async fetchDataDefaultCallback() {
throw new NotImplementedError();
}
@@ -171,18 +247,13 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
#setVisibility(div_id, visible) {
/** Recursively sets the visibility of a div_id and its children. */
- for (const child_id of this.data_obj[div_id].children) {
- this.data_obj[child_id].visible = visible;
- if (visible) {
- if (this.data_obj[div_id].expanded) {
- this.#setVisibility(child_id, visible);
- }
- } else {
- if (this.selected_div_id === child_id) {
- this.selected_div_id = null;
- }
- this.#setVisibility(child_id, visible);
- }
+ if (!visible && this.selected_div_id === div_id) {
+ this.selected_div_id = null;
+ }
+ const this_obj = this.data_obj[div_id];
+ this_obj.visible = visible;
+ for (const child_id of this_obj.children) {
+ this.#setVisibility(child_id, visible && this_obj.expanded);
}
}
@@ -214,17 +285,17 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
/** Calculates the width of the widest row in the list. */
if (!this.enabled) {
// Inactive list is not displayed on screen. Can't calculate size.
- return false;
+ return;
}
if (this.content_elem.children.length === 0) {
// If there is no data then just skip.
- return false;
+ return;
}
let max_width = 0;
- for (let i = 0; i < this.content_elem.children.length; i += this.n_cols) {
+ for (let i = 0; i < this.content_elem.children.length; i += this.options.cols_in_block) {
let row_width = 0;
- for (let j = 0; j < this.n_cols; j++) {
+ for (let j = 0; j < this.options.cols_in_block; j++) {
const child = this.content_elem.children[i + j];
const child_style = window.getComputedStyle(child, null);
const prev_style = child.style.cssText;
@@ -257,11 +328,11 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
const visible = this.data_obj[div_id].expanded;
for (const child_id of this.data_obj[div_id].children) {
- this.#setVisibility(child_id, visible)
+ this.#setVisibility(child_id, visible);
}
- this.#setVisibility()
- await this.setMaxItems(Object.values(this.data_obj).filter(v => v.visible).length);
+ const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
+ await this.setMaxItems(new_len);
}
async initData() {
@@ -273,35 +344,18 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
expanded: bool,
}
*/
- this.data_obj = await this.options.callbacks.initData.call(
- this,
- this.tabname,
- this.extra_networks_tabname,
- this.constructor.name,
- );
+ this.data_obj = await this.options.callbacks.initData.call(this);
}
async fetchData(idx_start, idx_end) {
if (!this.enabled) {
return [];
}
- const n_items = idx_end - idx_start;
- const div_ids = [];
- for (const div_id of this.data_obj_keys_sorted.slice(idx_start)) {
- if (this.data_obj[div_id].visible) {
- div_ids.push(div_id);
- }
- if (div_ids.length >= n_items) {
- break;
- }
- }
- const data = await this.options.callbacks.fetchData.call(
- this,
- this.constructor.name,
- this.extra_networks_tabname,
- div_ids,
- );
+ const data = await this.fetchDivIds(this.idxRangeToDivIds(idx_start, idx_end));
+ const data_ids_sorted = Object.keys(data).sort((a, b) => {
+ return this.data_obj_keys_sorted.indexOf(a) - this.data_obj_keys_sorted.indexOf(b);
+ });
// we have to calculate the box shadows here since the element is on the page
// at this point and we can get its computed styles.
@@ -309,11 +363,14 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
const text_size = style.getPropertyValue("--button-large-text-size");
const res = [];
- for (const [div_id, html_str] of Object.entries(data)) {
- const parsed_html = htmlStringToElement(html_str);
+ for (const div_id of data_ids_sorted) {
+ const html_str = data[div_id];
+ const parsed_html = isElement(html_str) ? html_str : htmlStringToElement(html_str);
const depth = Number(parsed_html.dataset.depth);
parsed_html.style.paddingLeft = `calc(${depth} * ${text_size})`;
parsed_html.style.boxShadow = this.getBoxShadow(depth);
+ // Roots come expanded by default. Need to delete if it exists.
+ delete parsed_html.dataset.expanded;
if (this.data_obj[div_id].expanded) {
parsed_html.dataset.expanded = "";
}
@@ -321,8 +378,9 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
parsed_html.dataset.selected = "";
}
res.push(parsed_html.outerHTML);
+ this.lru.set(String(div_id), parsed_html);
}
-
+
return res;
}
@@ -371,37 +429,26 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize {
sort_
: string, (for various sort modes)
}
*/
- this.data_obj = await this.options.callbacks.initData.call(
- this,
- this.tabname,
- this.extra_networks_tabname,
- this.constructor.name,
- );
+ this.data_obj = await this.options.callbacks.initData.call(this);
}
async fetchData(idx_start, idx_end) {
if (!this.enabled) {
- return;
+ return [];
}
- const n_items = idx_end - idx_start;
- const div_ids = [];
- for (const div_id of this.data_obj_keys_sorted.slice(idx_start)) {
- if (this.data_obj[div_id].visible) {
- div_ids.push(div_id);
- }
- if (div_ids.length >= n_items) {
- break;
- }
- }
-
- const data = await this.options.callbacks.fetchData.call(
- this,
- this.constructor.name,
- this.extra_networks_tabname,
- div_ids,
- );
- return Object.values(data);
+ const data = await this.fetchDivIds(this.idxRangeToDivIds(idx_start, idx_end));
+ const data_ids_sorted = Object.keys(data).sort((a, b) => {
+ return this.data_obj_keys_sorted.indexOf(a) - this.data_obj_keys_sorted.indexOf(b);
+ });
+
+ const res = [];
+ for (const div_id of data_ids_sorted) {
+ res.push(data[div_id]);
+ this.lru.set(div_id, data[div_id]);
+ }
+
+ return res;
}
async sortData() {
@@ -429,7 +476,7 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize {
/** Filters data by a string and returns number of items after filter. */
let n_visible = 0;
for (const [div_id, v] of Object.entries(this.data_obj)) {
- let visible = v.search_terms.indexOf(this.filter_str) != -1;
+ let visible = v.search_terms.toLowerCase().indexOf(this.filter_str) != -1;
if (v.search_only && this.filter_str.length < 4) {
visible = false;
}
diff --git a/javascript/utils.js b/javascript/utils.js
index 99427fc6b..94b9e1343 100644
--- a/javascript/utils.js
+++ b/javascript/utils.js
@@ -154,10 +154,17 @@ function querySelectorThrowError(selector) {
}
/** Functions for getting dimensions of elements. */
+function getStyle(elem) {
+ return window.getComputedStyle ? window.getComputedStyle(elem) : elem.currentStyle;
+}
+
+function getComputedProperty(elem, prop) {
+ return getStyle(elem)[prop];
+}
function getComputedPropertyDims(elem, prop) {
/** Returns the top/left/bottom/right float dimensions of an element for the specified property. */
- const style = window.getComputedStyle(elem, null);
+ const style = getStyle(elem);
return {
top: parseFloat(style.getPropertyValue(`${prop}-top`)),
left: parseFloat(style.getPropertyValue(`${prop}-left`)),
@@ -209,19 +216,6 @@ function getComputedDims(elem) {
};
}
-function calcColsPerRow(parent, child) {
- /** Calculates the number of columns of children that can fit in a parent's visible width. */
- const parent_inner_width = parent.offsetWidth - getComputedPaddingDims(parent).width;
- return parseInt(parent_inner_width / getComputedDims(child).width, 10);
-
-}
-
-function calcRowsPerCol(parent, child) {
- /** Calculates the number of rows of children that can fit in a parent's visible height. */
- const parent_inner_height = parent.offsetHeight - getComputedPaddingDims(parent).height;
- return parseInt(parent_inner_height / getComputedDims(child).height, 10);
-}
-
/** Functions for asynchronous operations. */
function debounce(handler, timeout_ms) {
@@ -394,10 +388,6 @@ function clamp(x, min, max) {
return Math.max(min, Math.min(x, max));
}
-function getStyle(prop, elem) {
- return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
-}
-
function htmlStringToElement(s) {
/** Converts an HTML string into an Element type. */
let parser = new DOMParser();
diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py
index a48226fdb..d55cf58b0 100644
--- a/modules/ui_extra_networks.py
+++ b/modules/ui_extra_networks.py
@@ -89,8 +89,8 @@ class DirectoryTreeNode:
abspath: str,
parent: Optional["DirectoryTreeNode"] = None,
) -> None:
- self.abspath = abspath
self.root_dir = root_dir
+ self.abspath = abspath
self.parent = parent
self.depth = 0
@@ -118,9 +118,12 @@ class DirectoryTreeNode:
if self.is_dir:
for x in os.listdir(self.abspath):
child_path = os.path.join(self.abspath, x)
- DirectoryTreeNode(self.root_dir, child_path, self).build(items)
+ # Add all directories but only add files if they are in the items dict.
+ if os.path.isdir(child_path) or child_path in items:
+ DirectoryTreeNode(self.root_dir, child_path, self).build(items)
else:
self.item = items.get(self.abspath, None)
+
def flatten(self, res: dict, dirs_only: bool = False) -> None:
"""Flattens the keys/values of the tree nodes into a dictionary.
@@ -135,7 +138,7 @@ class DirectoryTreeNode:
"""
if self.abspath in res:
raise KeyError(f"duplicate key: {self.abspath}")
-
+
if not dirs_only or (dirs_only and self.is_dir):
res[self.abspath] = self
@@ -148,58 +151,6 @@ class DirectoryTreeNode:
for child in self.children:
child.apply(fn)
-
-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
@@ -254,9 +205,7 @@ def fetch_cover_images(extra_networks_tabname: str = "", item: str = "", index:
def init_tree_data(tabname: str = "", extra_networks_tabname: str = "") -> JSONResponse:
page = get_page_by_name(extra_networks_tabname)
-
data = page.generate_tree_view_data(tabname)
-
return JSONResponse(data, status_code=200)
def fetch_tree_data(
@@ -305,7 +254,8 @@ def get_metadata(extra_networks_tabname: str = "", item: str = "") -> JSONRespon
return JSONResponse({})
# those are cover images, and they are too big to display in UI as text
- metadata = {i: metadata[i] for i in metadata if i != 'ssmd_cover_images'}
+ # FIXME: WHY WAS THIS HERE?
+ #metadata = {i: metadata[i] for i in metadata if i != 'ssmd_cover_images'}
return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)})
@@ -376,7 +326,10 @@ class ExtraNetworksPage:
self.keys_by_modified = []
def refresh(self):
- pass
+ self.items = {}
+ self.cards = {}
+ self.tree = {}
+ self.tree_roots = {}
def read_user_metadata(self, item, use_cache=True):
filename = item.get("filename", None)
@@ -515,14 +468,14 @@ class ExtraNetworksPage:
if shared.opts.extra_networks_card_width:
style += f"width: {shared.opts.extra_networks_card_width}px;"
- background_image = None
+ background_image = ""
preview = html.escape(item.get("preview", "") or "")
if preview:
background_image = f'
'
onclick = item.get("onclick", None)
if onclick is None:
- onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');")
+ onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}_{self.extra_networks_tabname}');")
button_row = self.get_button_row(tabname, item)
@@ -582,7 +535,6 @@ class ExtraNetworksPage:
style=style,
onclick=onclick,
data_attributes=data_attributes_str,
- sort_keys=sort_keys,
background_image=background_image,
button_row=button_row,
search_terms=search_terms_html,
@@ -647,8 +599,11 @@ class ExtraNetworksPage:
if node.parent is not None:
parent_id = path_to_div_id.get(node.parent.abspath, None)
- if node.item is None: # directory
- dir_is_empty = show_files and any(x.item is not None for x in node.children)
+ if node.is_dir: # directory
+ if show_files:
+ dir_is_empty = node.children == []
+ else:
+ dir_is_empty = not any(x.item.is_dir for x in node.children)
self.tree[div_id].html = self.build_tree_html_dict_row(
tabname=tabname,
label=os.path.basename(node.abspath),
@@ -660,7 +615,7 @@ class ExtraNetworksPage:
"data-parent-id": parent_id,
"data-tree-entry-type": "dir",
"data-depth": node.depth,
- "data-path": node.abspath,
+ "data-path": node.relpath,
"data-expanded": node.parent is None, # Expand root directories
},
)
@@ -671,7 +626,7 @@ class ExtraNetworksPage:
onclick = node.item.get("onclick", None)
if onclick is None:
- onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');")
+ onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}_{self.extra_networks_tabname}');")
item_name = node.item.get("name", "").strip()
self.tree[div_id].html = self.build_tree_html_dict_row(
@@ -746,6 +701,7 @@ class ExtraNetworksPage:
"path": html.escape(node.relpath),
}) for node in dir_nodes
])
+
return dirs_html
def create_html(self, tabname, *, empty=False):
@@ -781,6 +737,8 @@ class ExtraNetworksPage:
# a common path.
for path in self.allowed_directories_for_previews():
abspath = os.path.abspath(path)
+ if not os.path.exists(abspath):
+ continue
self.tree_roots[abspath] = DirectoryTreeNode(os.path.dirname(abspath), abspath, None)
self.tree_roots[abspath].build(tree_items)
@@ -959,11 +917,9 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
fn=None,
_js=(
"function(){extraNetworksTabSelected("
- f"'{tabname}', "
- f"'{tabname}_{page.extra_networks_tabname}_prompts', "
+ f"'{tabname}_{page.extra_networks_tabname}', "
f"{str(page.allow_prompt).lower()}, "
- f"{str(page.allow_negative_prompt).lower()}, "
- f"'{tabname}_{page.extra_networks_tabname}'"
+ f"{str(page.allow_negative_prompt).lower()}"
");}"
),
inputs=[],
diff --git a/style.css b/style.css
index d0ae6b41d..f5b0998d8 100644
--- a/style.css
+++ b/style.css
@@ -1192,6 +1192,7 @@ body.resizing .resize-handle {
.clusterize-content {
flex: 1;
outline: 0;
+ gap: var(--spacing-sm);
counter-reset: clusterize-counter;
padding: var(--spacing-md);
}