From 5e4dfee153daac15679de852902294e6b6ea41d7 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Fri, 12 Apr 2024 12:40:20 -0400 Subject: [PATCH] Finish initial conversion to clusterize using data loader. Need to fix bugs. --- html/extra-networks-btn-dirs-view-item.html | 5 +- html/extra-networks-btn-show-metadata.html | 2 +- html/extra-networks-card.html | 2 +- javascript/clusterize.js | 147 ++-- javascript/extraNetworks.js | 723 ++++++++++---------- javascript/extraNetworksClusterize.js | 207 +++--- javascript/utils.js | 26 +- modules/ui_extra_networks.py | 96 +-- style.css | 1 + 9 files changed, 643 insertions(+), 566 deletions(-) diff --git a/html/extra-networks-btn-dirs-view-item.html b/html/extra-networks-btn-dirs-view-item.html index 218980383..c6847a3cb 100644 --- a/html/extra-networks-btn-dirs-view-item.html +++ b/html/extra-networks-btn-dirs-view-item.html @@ -1,5 +1,4 @@ \ No newline at end of file + onclick='extraNetworksBtnDirsViewItemOnClick(event, "{tabname_full}")' +>{path} \ No newline at end of file diff --git a/html/extra-networks-btn-show-metadata.html b/html/extra-networks-btn-show-metadata.html index ecae4cbdc..f12e7eb3a 100644 --- a/html/extra-networks-btn-show-metadata.html +++ b/html/extra-networks-btn-show-metadata.html @@ -1,4 +1,4 @@
+ onclick="extraNetworksBtnShowMetadataOnClick(event, '{extra_networks_tabname}', '{name}')">
\ No newline at end of file diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html index c2f63d35e..9a0e6fb7a 100644 --- a/html/extra-networks-card.html +++ b/html/extra-networks-card.html @@ -1,4 +1,4 @@ -
+
{background_image} {button_row}
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 = `${data}
`; - 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); }