From 587799373986e0957d3c750118dcf19c423f1194 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Fri, 29 Mar 2024 12:21:02 -0400 Subject: [PATCH] Switch to using api calls to retrieve data. --- html/extra-networks-card.html | 4 +- html/extra-networks-edit-item-button.html | 2 +- html/extra-networks-metadata-button.html | 2 +- html/extra-networks-pane.html | 2 - javascript/extraNetworks.js | 167 +++++- javascript/extraNetworksClusterizeList.js | 605 +++++++++++----------- modules/ui_extra_networks.py | 356 +++++++------ style.css | 14 +- 8 files changed, 635 insertions(+), 517 deletions(-) diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html index f50a69501..c2f63d35e 100644 --- a/html/extra-networks-card.html +++ b/html/extra-networks-card.html @@ -1,6 +1,6 @@ -
+
{background_image} -
{copy_path_button}{metadata_button}{edit_button}
+ {button_row}
{search_terms}
{name} diff --git a/html/extra-networks-edit-item-button.html b/html/extra-networks-edit-item-button.html index fd728600f..80be264e7 100644 --- a/html/extra-networks-edit-item-button.html +++ b/html/extra-networks-edit-item-button.html @@ -1,4 +1,4 @@
+ onclick="extraNetworksEditItemOnClick(event, '{tabname}_{extra_networks_tabname}', '{name}')">
\ No newline at end of file diff --git a/html/extra-networks-metadata-button.html b/html/extra-networks-metadata-button.html index 4ef013bc0..856444fa1 100644 --- a/html/extra-networks-metadata-button.html +++ b/html/extra-networks-metadata-button.html @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index 17bc7e608..c69281964 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -163,7 +163,5 @@
- {tree_data_div} - {cards_data_div} \ No newline at end of file diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 5ab263796..27a456e64 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -51,6 +51,15 @@ const isElementLogError = x => { return false; }; +const isFunction = x => typeof x === "function"; +const isFunctionLogError = x => { + if (isFunction(x)) { + return true; + } + console.error("expected function type, got:", typeof x); + return false; +} + const getElementByIdLogError = selector => { let elem = gradioApp().getElementById(selector); isElementLogError(elem); @@ -207,7 +216,18 @@ function extraNetworksRefreshTab(tabname_full) { div_tree.classList.toggle("hidden", !("selected" in btn_tree_view.dataset)); waitForKeyInObject({k: tabname_full, obj: clusterizers}) - .then(() => extraNetworkClusterizersOnTabLoad(tabname_full)); + .then(() => { + // We want to reload all tabs when refresh is clicked, but we only want to + // enable the tab on which the refresh button was clicked. + for (const _tabname_full of Object.keys(clusterizers)) { + let selected = _tabname_full === tabname_full; + extraNetworkClusterizersLoadTab({ + tabname_full:_tabname_full, + selected: selected, + fetch_data: true + }); + } + }); } function extraNetworksRegisterPromptForTab(tabname, id) { @@ -224,6 +244,7 @@ function extraNetworksRegisterPromptForTab(tabname, id) { function extraNetworksSetupTabContent(tabname, pane, controls_div) { const tabname_full = pane.id; + const extra_networks_tabname = tabname_full.replace(`${tabname}_`, ""); let controls; Promise.all([ @@ -240,20 +261,31 @@ function extraNetworksSetupTabContent(tabname, pane, controls_div) { // Now that we have our elements in DOM, we create the clusterize lists. clusterizers[tabname_full] = { tree_list: new ExtraNetworksClusterizeTreeList({ - data_id: `${tabname_full}_tree_list_data`, + tabname: tabname, + extra_networks_tabname: extra_networks_tabname, scroll_id: `${tabname_full}_tree_list_scroll_area`, content_id: `${tabname_full}_tree_list_content_area`, + data_request_callback: extraNetworksRequestListData, }), cards_list: new ExtraNetworksClusterizeCardsList({ - data_id: `${tabname_full}_cards_list_data`, + tabname: tabname, + extra_networks_tabname: extra_networks_tabname, scroll_id: `${tabname_full}_cards_list_scroll_area`, content_id: `${tabname_full}_cards_list_content_area`, + data_request_callback: extraNetworksRequestListData, }), }; if (pane.style.display != "none") { extraNetworksShowControlsForPage(tabname, tabname_full); } + (async() => { + await extraNetworkClusterizersLoadTab({ + tabname_full: tabname_full, + selected: false, + fetch_data: true + }); + })(); }); } @@ -321,7 +353,13 @@ function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt, t extraNetworksShowControlsForPage(tabname, tabname_full); waitForKeyInObject({k: tabname_full, obj: clusterizers}) - .then(() => extraNetworkClusterizersOnTabLoad(tabname_full)); + .then(() => { + extraNetworkClusterizersLoadTab({ + tabname_full: tabname_full, + selected: true, + fetch_data: false, + }); + }); } function extraNetworksApplyFilter(tabname_full) { @@ -338,6 +376,7 @@ function extraNetworksApplyFilter(tabname_full) { // We only want to filter/sort the cards list. clusterizers[tabname_full].cards_list.applyFilter(txt_search.value.toLowerCase()); + clusterizers[tabname_full].cards_list.update(); // 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. @@ -364,21 +403,42 @@ function extraNetworksClusterizersEnable(tabname_full) { } } -function extraNetworkClusterizersOnTabLoad(tabname_full) { /** promise */ - return new Promise(resolve => { - // Enables a tab's clusterizer, updates its data, and rebuilds it. +function extraNetworkClusterizersLoadTab({ + tabname_full = "", + selected = false, + fetch_data = false, +}={}) { + /** Loads clusterize data for a tab. + * + * Args: + * tabname_full [str]: The clusterize tab to load. Does not need to be the active + * tab however if it isn't the active tab then `selected` should be set to + * `false` to prevent oddities caused by the tab not being visible in the page. + * selected [bool]: Whether the tab is selected. This controls whether the + * clusterize list will be enabled which affects its operations. + * fetch_data [bool]: Whether to fetch new data for the clusterize list. + */ + return new Promise((resolve, reject) => { if (!(tabname_full in clusterizers)) { return resolve(); } (async() => { - // Enable then load the selected tab's clusterize lists. - extraNetworksClusterizersEnable(tabname_full); + if (selected) { + extraNetworksClusterizersEnable(tabname_full); + } for (const v of Object.values(clusterizers[tabname_full])) { - await v.load(); + if (fetch_data) { + await v.setup(); + } else { + await v.load(); + } } })().then(() => { return resolve(); + }).catch(error => { + console.error("Error loading tab:", error); + return reject(error); }); }); } @@ -514,13 +574,17 @@ function updatePromptArea(text, textArea, isNeg) { updateInput(textArea); } -function cardClicked(tabname, textToAdd, textToAddNegative, allowNegativePrompt) { - if (textToAddNegative.length > 0) { - updatePromptArea(textToAdd, gradioApp().querySelector(`#${tabname}_prompt > label > textarea`)); - updatePromptArea(textToAddNegative, gradioApp().querySelector(`#${tabname}_neg_prompt > label > textarea`), true); +function extraNetworksCardOnClick(event, tabname) { + const elem = event.currentTarget; + const prompt_elem = gradioApp().querySelector(`#${tabname}_prompt > label > textarea`); + const neg_prompt_elem = gradioApp().querySelector(`#${tabname}_neg_prompt > label > textarea`); + if ("negPrompt" in elem.dataset){ + updatePromptArea(elem.dataset.prompt, prompt_elem); + updatePromptArea(elem.dataset.negPrompt, neg_prompt_elem); + } else if ("allowNeg" in elem.dataset) { + updatePromptArea(elem.dataset.prompt, activePromptTextarea[tabname]); } else { - var textarea = allowNegativePrompt ? activePromptTextarea[tabname] : gradioApp().querySelector(`#${tabname}_prompt > label > textarea`); - updatePromptArea(textToAdd, textarea); + updatePromptArea(elem.dataset.prompt, prompt_elem); } } @@ -745,6 +809,15 @@ function extraNetworksControlRefreshOnClick(event, tabname_full) { */ // reset states initialUiOptionsLoaded.state = false; + + // We want to reset all clusterizers 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.reset(); + } + } + // Fire an event for this button click. gradioApp().getElementById(`${tabname_full}_extra_refresh_internal`).dispatchEvent(new Event("click")); } @@ -858,6 +931,31 @@ function extraNetworksShowMetadata(text) { return; } +function requestGetPromise(url, data) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + let args = Object.keys(data).map(k => { + return encodeURIComponent(k) + "=" + encodeURIComponent(data[k]); + }).join("&"); + xhr.open("GET", url + "?" + args, true); + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + try { + resolve(xhr.responseText); + } catch (error) { + reject(error); + } + } else { + reject({status: this.status, statusText: xhr.statusText}); + } + } + }; + xhr.send(JSON.stringify(data)); + }); +} + function requestGet(url, data, handler, errorHandler) { var xhr = new XMLHttpRequest(); var args = Object.keys(data).map(function(k) { @@ -889,44 +987,57 @@ function extraNetworksCopyPathToClipboard(event, path) { event.stopPropagation(); } -function extraNetworksRequestMetadata(event, extraPage) { +async function extraNetworksRequestListData(tabname, extra_networks_tabname, class_name) { + return await requestGetPromise( + "./sd_extra_networks/get-list-data", + { + tabname: tabname, + extra_networks_tabname: extra_networks_tabname, + list_type: class_name, + }, + ); +} + +function extraNetworksRequestMetadata(extra_networks_tabname, card_name) { var showError = function() { extraNetworksShowMetadata("there was an error getting metadata"); }; - var cardName = event.target.parentElement.parentElement.getAttribute("data-name"); - - requestGet("./sd_extra_networks/metadata", {page: extraPage, item: cardName}, function(data) { + requestGet("./sd_extra_networks/metadata", {page: extra_networks_tabname, item: card_name}, function(data) { if (data && data.metadata) { extraNetworksShowMetadata(data.metadata); } else { showError(); } }, showError); +} +function extraNetworksMetadataButtonOnClick(event, extra_networks_tabname, card_name) { + extraNetworksRequestMetadata(extra_networks_tabname, card_name); event.stopPropagation(); } -function extraNetworksEditUserMetadata(event, tabname, extraPage, cardName) { - var id = tabname + '_' + extraPage + '_edit_user_metadata'; - - var editor = extraPageUserMetadataEditors[id]; +function extraNetworksEditUserMetadata(tabname_full, card_name) { + const id = `${tabname_full}_edit_user_metadata`; + let editor = extraPageUserMetadataEditors[id]; if (!editor) { editor = {}; editor.page = gradioApp().getElementById(id); - editor.nameTextarea = gradioApp().querySelector("#" + id + "_name" + ' textarea'); - editor.button = gradioApp().querySelector("#" + id + "_button"); + editor.nameTextarea = gradioApp().querySelector(`#${id}_name textarea`); + editor.button = gradioApp().querySelector(`#${id}_button`); extraPageUserMetadataEditors[id] = editor; } - var cardName = event.target.parentElement.parentElement.getAttribute("data-name"); - editor.nameTextarea.value = cardName; + editor.nameTextarea.value = card_name; updateInput(editor.nameTextarea); editor.button.click(); popup(editor.page); +} +function extraNetworksEditItemOnClick(event, tabname_full, card_name) { + extraNetworksEditUserMetadata(tabname_full, card_name); event.stopPropagation(); } diff --git a/javascript/extraNetworksClusterizeList.js b/javascript/extraNetworksClusterizeList.js index a6136f2ed..7ae3a2aaf 100644 --- a/javascript/extraNetworksClusterizeList.js +++ b/javascript/extraNetworksClusterizeList.js @@ -93,7 +93,7 @@ async function decompress(base64string) { writer.write(bytes); writer.close(); const arrayBuffer = await new Response(ds.readable).arrayBuffer(); - return new TextDecoder().decode(arrayBuffer); + return await JSON.parse(new TextDecoder().decode(arrayBuffer)); } catch (error) { throw new InvalidCompressedJsonDataError(error); } @@ -123,9 +123,11 @@ class ExtraNetworksClusterize { /** Base class for a clusterize list. Cannot be used directly. */ constructor( { - data_id, + tabname, + extra_networks_tabname, scroll_id, content_id, + data_request_callback, rows_in_block = 10, blocks_in_cluster = 4, show_no_data_row = true, @@ -138,7 +140,10 @@ class ExtraNetworksClusterize { } ) { // Do not continue if any of the required parameters are invalid. - if (!isStringLogError(data_id)) { + if (!isStringLogError(tabname)) { + return; + } + if (!isStringLogError(extra_networks_tabname)) { return; } if (!isStringLogError(scroll_id)) { @@ -147,10 +152,15 @@ class ExtraNetworksClusterize { if (!isStringLogError(content_id)) { return; } + if (!isFunctionLogError(data_request_callback)) { + return; + } - this.data_id = data_id; + this.tabname = tabname; + this.extra_networks_tabname = extra_networks_tabname; this.scroll_id = scroll_id; this.content_id = content_id; + this.data_request_callback = data_request_callback; this.rows_in_block = rows_in_block; this.blocks_in_cluster = blocks_in_cluster; this.show_no_data_row = show_no_data_row; @@ -158,14 +168,13 @@ class ExtraNetworksClusterize { this.clusterize = null; - this.data_elem = null; this.scroll_elem = null; this.content_elem = null; + this.element_observer = null; + this.resize_observer = null; this.resize_observer_timer = null; - this.element_observer = null; - this.data_update_timer = null; // Used to control logic. Many functions immediately return when disabled. this.enabled = false; @@ -176,7 +185,7 @@ class ExtraNetworksClusterize { this.no_data_text = "No results."; this.no_data_class = "clusterize-no-data"; - this.n_rows = 1; + this.n_rows = this.rows_in_block; this.n_cols = 1; this.data_obj = {}; @@ -184,72 +193,214 @@ class ExtraNetworksClusterize { this.sort_fn = this.sortByDivId; this.sort_reverse = false; - - // Setup our event handlers only after our elements exist in DOM. - Promise.all([ - waitForElement(`#${this.data_id}`).then((elem) => this.data_elem = elem), - waitForElement(`#${this.scroll_id}`).then((elem) => this.scroll_elem = elem), - waitForElement(`#${this.content_id}`).then((elem) => this.content_elem = elem), - ]).then(() => { - this.setupElementObservers(); - this.setupResizeHandlers(); - }); } - enable(enabled) { - /** Enables or disabled this instance. */ - // All values other than `true` for `enabled` result in this.enabled=false. - this.enabled = !(enabled !== true); + reset() { + /** Destroy clusterize instance and set all instance variables to defaults. */ + this.destroy(); + + this.teardownElementObservers(); + this.teardownResizeObservers(); + + this.clusterize = null; + this.scroll_elem = null; + this.content_elem = null; + this.enabled = false; + this.encoded_str = ""; + this.n_rows = this.rows_in_block; + this.n_cols = 1; + this.data_obj = {}; + this.data_obj_keys_sorted = []; + this.sort_fn = this.sortByDivId; + this.sort_reverse = false; } - load() { /** promise */ - /** Loads this instance into the view. - * - * Calling this function should be all that is needed in order to fully update - * and display the clusterize list. - */ + async setup() { return new Promise(resolve => { - waitForElement(`#${this.data_id}`) - .then((elem) => this.data_elem = elem) - .then(() => this.parseJson(this.data_elem.dataset.json)) - .then(() => { + // Setup our event handlers only after our elements exist in DOM. + Promise.all([ + waitForElement(`#${this.scroll_id}`).then((elem) => this.scroll_elem = elem), + waitForElement(`#${this.content_id}`).then((elem) => this.content_elem = elem), + ]).then(() => { + this.setupElementObservers(); + this.setupResizeObservers(); + return this.fetchData(); + }).then(encoded_str => { + if (isNullOrUndefined(encoded_str)) { + // no new data to load. break from chain. return resolve(); - }); + } + return this.parseJson(encoded_str); + }).then(json => { + if (isNullOrUndefined(json)) { + return resolve(); + } + this.clear(); + this.updateJson(json); + }).then(() => { + this.sortData(); + }).then(() => { + // since calculateDims manually adds an element from our data_obj, + // we don't need clusterize initialzied to calculate dims. + this.calculateDims(); + this.applyFilter(); + this.rebuild(this.getFilteredRows()); + return resolve(); + }).catch(error => { + console.error("setup:: error in promise:", error); + return resolve(); + }); }); } - parseJson(encoded_str) { /** promise */ + async load() { + return new Promise(resolve => { + if (isNullOrUndefined(this.clusterize)) { + // This occurs whenever we click on a tab before initialization and setup + // have fully completed for this instance. + return resolve(this.setup()); + } + if (this.calculateDims()) { + // Since dimensions updated, we need to apply the filter and rebuild. + this.applyFilter(); + this.rebuild(this.getFilteredRows()); + } else { + this.refresh(true); + } + return resolve(); + }); + } + + async fetchData() { + let encoded_str = await this.data_request_callback( + this.tabname, + this.extra_networks_tabname, + this.constructor.name, + ); + if (this.encoded_str === encoded_str) { + // no change to the data since last call. ignore. + return null; + } + this.encoded_str = encoded_str; + return this.encoded_str; + } + + async parseJson(encoded_str) { /** promise */ /** Parses a base64 encoded and gzipped JSON string and sets up a clusterize instance. */ return new Promise((resolve, reject) => { - // Skip parsing if the string hasnt actually updated. - if (this.encoded_str === encoded_str) { - return resolve(); - } Promise.resolve(encoded_str) .then(v => decompress(v)) - .then(v => JSON.parse(v)) - .then(v => { - if (!isNullOrUndefined(this.clusterize)) { - this.data_obj = {}; - this.data_obj_keys_sorted = []; - this.clear(); - this.content_elem.innerHTML = "
Loading...
"; - } - return v; - }) - .then(v => this.updateJson(v)) - .then(() => { - this.encoded_str = encoded_str; - this.rebuild(); - this.applyFilter(); - return resolve(); - }) + .then(v => resolve(v)) .catch(error => { return reject(error); }); }); } + calculateDims() { + let res = false; + // Cannot calculate dims if not enabled since our elements won't be visible. + if (!this.enabled) { + return res; + } + + // Cannot do anything if we have no data. + if (this.data_obj_keys_sorted.length <= 0) { + return res; + } + + // Repair before anything else so we can actually get dimensions. + this.repair(); + + // Add an element to the container manually so we can calculate dims. + const child = htmlStringToElement(this.data_obj[this.data_obj_keys_sorted[0]].html); + this.content_elem.prepend(child); + + let n_cols = calcColsPerRow(this.content_elem, child); + let n_rows = calcRowsPerCol(this.scroll_elem, child); + n_cols = (isNaN(n_cols) || n_cols <= 0) ? 1 : n_cols; + n_rows = (isNaN(n_rows) || n_rows <= 0) ? 1 : n_rows; + n_rows += 2; + if (n_cols != this.n_cols || n_rows != this.n_rows) { + // Sizes have changed. Update the instance values. + this.n_cols = n_cols; + this.n_rows = n_rows; + this.rows_in_block = this.n_rows; + res = true; + } + + // Remove the temporary element from DOM. + child.remove(); + + return res; + } + + sortData() { + /** Sorts the rows using the instance's `sort_fn`. + * + * It is expected that a subclass will override this function to update the + * instance's `sort_fn` then call `super.sortData()` to apply the sorting. + */ + this.sort_fn(); + if (this.sort_reverse) { + this.data_obj_keys_sorted = this.data_obj_keys_sorted.reverse(); + } + } + + applyFilter() { + /** Should be overridden by child class. */ + this.sortData(); + if (!isNullOrUndefined(this.clusterize)) { + this.update(this.getFilteredRows()); + } + //this.rebuild(this.getFilteredRows()); + } + + getFilteredRows() { + let rows = []; + let active_keys = this.data_obj_keys_sorted.filter(k => this.data_obj[k].active); + for (let i = 0; i < active_keys.length; i += this.n_cols) { + rows.push( + active_keys.slice(i, i + this.n_cols) + .map(k => this.data_obj[k].html) + .join("") + ); + } + return rows; + } + + rebuild(rows) { + if (!isNullOrUndefined(this.clusterize)) { + this.clusterize.destroy(true); + this.clusterize = null; + } + + if (isNullOrUndefined(rows) || !Array.isArray(rows)) { + rows = []; + } + + this.clusterize = new Clusterize( + { + rows: rows, + scrollId: this.scroll_id, + contentId: this.content_id, + rows_in_block: this.rows_in_block, + tag: "div", + blocks_in_cluster: this.blocks_in_cluster, + show_no_data_row: this.show_no_data_row, + no_data_text: this.no_data_text, + no_data_class: this.no_data_class, + callbacks: this.callbacks, + } + ); + } + + enable(enabled) { + /** Enables or disables this instance. */ + // All values other than `true` for `enabled` result in this.enabled=false. + this.enabled = !(enabled !== true); + } + updateJson(json) { /** promise */ console.error("Base class method called. Must be overridden by subclass."); return new Promise(resolve => { @@ -262,43 +413,11 @@ class ExtraNetworksClusterize { this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => INT_COLLATOR.compare(a, b)); } - applySort() { - /** Sorts the rows using the instance's `sort_fn`. - * - * It is expected that a subclass will override this function to update the - * instance's `sort_fn` then call `super.applySort()` to apply the sorting. - */ - this.sort_fn(); - if (this.sort_reverse) { - this.data_obj_keys_sorted = this.data_obj_keys_sorted.reverse(); - } - } - - applyFilter() { - /** Sorts then updates the rows. - * - * Should be overridden by subclass. Base class doesn't apply any filters. - */ - this.applySort(); - this.updateRows(); - } - - getRows(obj) { - /** Returns an array of html strings of all active rows. */ - var results = []; - for (const div_id of this.data_obj_keys_sorted) { - if (obj[div_id].active) { - results.push(obj[div_id].html); - } - } - return results; - } - updateDivContent(div_id, content) { /** Updates an element's html in the dataset. * * NOTE: This function only updates the dataset. Calling function must call - * updateRows() to apply these changes to the view. Adding this call to this + * rebuild() to apply these changes to the view. Adding this call to this * function would be very slow in the case where many divs need their content * updated at the same time. */ @@ -317,88 +436,19 @@ class ExtraNetworksClusterize { return false; } - updateRows() { - /** Updates the instance using the stored rows in our data object. - * - * Should be called whenever we change order or number of rows. - */ - // If we don't have any entries in the dataset, then just return. - if (this.data_obj_keys_sorted.length === 0 || Object.keys(this.data_obj).length === 0) { - return; - } - - this.refresh(true); - - // Rebuild with `force=false` so we only rebuild if dimensions change. - this.rebuild(false); - } - getMaxRowWidth() { console.error("getMaxRowWidth:: Not implemented in base class. Must be overridden."); return; } - recalculateDims() { - /** Recalculates the number of rows and columns that can fit within the scroll view. - * - * Returns whether the rows/columns have changed indicating that we need to rebuild. - */ - let rebuild_required = false; - let clear_before_return = false; - - if (!this.enabled) { - // Inactive list is not displayed on screen. Would error if trying to resize. - return false; - } - if (Object.keys(this.data_obj).length === 0 || this.data_obj_keys_sorted.length === 0) { - // If there is no data then just skip. - return false; - } - - // If no rows exist, we need to add one so we can calculate rows/cols. - // We remove this row before returning. - if (this.rowCount() === 0) { // || this.content_elem.innerHTML === "") { - this.clear(); - this.update([this.data_obj[this.data_obj_keys_sorted[0]].html]); - clear_before_return = true; - } - - const child = this.content_elem.querySelector(":not(.clusterize-extra-row)"); - if (isNullOrUndefined(child)) { - if (clear_before_return) { - this.clear(); - return rebuild_required; - } - } - - // Calculate the visible rows and colums for the clusterize-content area. - let n_cols = calcColsPerRow(this.content_elem, child); - let n_rows = calcRowsPerCol(this.scroll_elem, child); - n_cols = (isNaN(n_cols) || n_cols <= 0) ? 1 : n_cols; - n_rows = (isNaN(n_rows) || n_rows <= 0) ? 1 : n_rows; - - // Add two extra rows to account for partial row visibility on top and bottom - // of the content element view region. - n_rows += 2; - - if (n_cols != this.n_cols || n_rows != this.n_rows) { - // Sizes have changed. Update the instance values. - this.n_cols = n_cols; - this.n_rows = n_rows; - this.rows_in_block = this.n_rows; - rebuild_required = true; - } - - // If we added a temporary row earlier, remove before returning. - if (clear_before_return) { - this.clear(); - } - - return rebuild_required; - } - repair() { /** Fixes element association in DOM. Returns whether a fix was performed. */ + if (!this.enabled) { + return false; + } + if (!isElement(this.scroll_elem) || !isElement(this.content_elem)) { + return false; + } // If association for elements is broken, replace them with instance version. if (!this.scroll_elem.isConnected || !this.content_elem.isConnected) { gradioApp().getElementById(this.scroll_id).replaceWith(this.scroll_elem); @@ -415,83 +465,20 @@ class ExtraNetworksClusterize { return false; } - rebuild(force) { - /** Rebuilds, updates, or initializes a clusterize instance. - * - * TODO: Possibly rename this function to make its purpose more clear. - * - * Performs one of the following: - * 1. Initializes a new instance if we haven't already. - * 2. Destroys and reinitializes an instance if we pass `force=true` or if - * the size of the elements has changed causing the number of items - * that we can show on screen to be updated. - * 3. Simply updates the clusterize instance's rows with our current data - * if none of the other conditions are met. - * - */ - // Only accept boolean values for `force` parameter. Default to false. - if (force !== true) { - force = false; - } - - if (isNullOrUndefined(this.clusterize)) { - this.init(); - } else if (this.recalculateDims() || force) { - this.destroy(); - this.clusterize = null; - this.init(); - } else { - this.update(); - } - } - - init(rows) { - /** Initializes a Clusterize.js instance. */ - if (!isNullOrUndefined(this.clusterize)) { - // If we have already initialized, don't do it again. - return; - } - - if (isNullOrUndefined(rows) && isNullOrUndefined(this.data_obj)) { - // data hasnt been loaded yet and we arent provided any. skip. - return; - } - - if (isNullOrUndefined(rows)) { - // if we aren't passed any rows, use the instance's data object. - rows = this.data_obj; - } else if (Array.isArray(rows) && !(rows.every(row => isString(row)))) { - console.error("Invalid data type for rows. Expected array[string]."); - return; - } - - this.clusterize = new Clusterize( - { - rows: this.getRows(rows), - scrollId: this.scroll_id, - contentId: this.content_id, - rows_in_block: this.rows_in_block, - tag: "div", - blocks_in_cluster: this.blocks_in_cluster, - show_no_data_row: this.show_no_data_row, - no_data_text: this.no_data_text, - no_data_class: this.no_data_class, - callbacks: this.callbacks, - } - ); - } - onResize(elem_id) { /** Callback whenever one of our visible elements is resized. */ - this.updateRows(); + if (!this.enabled) { + return; + } + this.refresh(true); + if (this.calculateDims()) { + this.rebuild(this.getFilteredRows()); + } } onElementDetached(elem_id) { /** Callback whenever one of our elements has become detached from the DOM. */ switch (elem_id) { - case this.data_id: - waitForElement(`#${this.data_id}`).then((elem) => this.data_elem = elem); - break; case this.scroll_id: this.repair(); break; @@ -503,28 +490,6 @@ class ExtraNetworksClusterize { } } - onDataChanged(elem) { - /** Callback whenever the data element is modified. */ - return new Promise((resolve) => { - this.parseJson(elem.dataset.json) - .then(() => { - return resolve(); - }) - .catch(error => { - if (error instanceof InvalidCompressedJsonDataError) { - // on error, roll back to the previous data string. - // this prevents an infinite loop whenever the data string - // is invalid. - console.error("rolling back json data due to invalid data:", error); - elem.dataset.json = this.encoded_str; - } else { - console.error("unhandled error caused by updated json data field:", error); - } - return resolve(); - }); - }); - } - setupElementObservers() { /** Listens for changes to the data, scroll, and content elements. * @@ -543,18 +508,6 @@ class ExtraNetworksClusterize { return; } - let data_elem = gradioApp().getElementById(this.data_id); - if (data_elem && data_elem !== this.data_elem) { - this.onElementDetached(data_elem.id); - } else if (data_elem && data_elem.dataset.json !== this.encoded_str) { - // we don't want to get blasted with data updates so just wait for - // the data to settle down before updating. - clearTimeout(this.data_update_timer); - this.data_update_timer = setTimeout(() => { - this.onDataChanged(data_elem); - }, JSON_UPDATE_DEBOUNCE_TIME_MS); - } - let scroll_elem = gradioApp().getElementById(this.scroll_id); if (scroll_elem && scroll_elem !== this.scroll_elem) { this.onElementDetached(scroll_elem.id); @@ -568,7 +521,15 @@ class ExtraNetworksClusterize { this.element_observer.observe(gradioApp(), {subtree: true, childList: true, attributes: true}); } - setupResizeHandlers() { + teardownElementObservers() { + if (!isNullOrUndefined(this.element_observer)) { + this.element_observer.takeRecords(); + this.element_observer.disconnect(); + } + this.element_observer = null; + } + + setupResizeObservers() { /** Handles any updates to the size of both the Scroll and Content elements. */ this.resize_observer = new ResizeObserver((entries) => { for (const entry of entries) { @@ -584,7 +545,39 @@ class ExtraNetworksClusterize { this.resize_observer.observe(this.content_elem); } + teardownResizeObservers() { + if (!isNullOrUndefined(this.resize_observer)) { + this.resize_observer.disconnect(); + } + if (!isNullOrUndefined(this.resize_observer_timer)) { + clearTimeout(this.resize_observer_timer); + this.resize_observer_timer = null; + } + this.resize_observer = null; + this.resize_observer_timer = null; + } + /* ==== Clusterize.Js FUNCTION WRAPPERS ==== */ + update(rows) { + /** Updates the clusterize rows. */ + if (isNullOrUndefined(rows) || !Array.isArray(rows)) { + rows = this.getFilteredRows(); + } + this.clusterize.update(rows); + } + + clear() { + /** Clears the clusterize list and this instance's data. */ + if (!isNullOrUndefined(this.clusterize)) { + this.clusterize.clear(); + this.data_obj = {}; + this.data_obj_keys_sorted = []; + if (isElement(this.content_elem)) { + this.content_elem.innerHTML = "
Loading...
"; + } + } + } + refresh(force) { /** Refreshes the clusterize instance so that it can recalculate its dims. * `force` [boolean]: If true, tells clusterize to refresh regardless of whether @@ -606,28 +599,19 @@ class ExtraNetworksClusterize { return this.clusterize.getRowsAmount(); } - clear() { - /** Removes all rows from the clusterize dataset. */ - this.clusterize.clear(); - } - - update(rows) { - /** Adds rows from a list of element strings. */ - if (rows === undefined || rows === null) { - // If not passed, use the default method of getting rows. - rows = this.getRows(this.data_obj); - } else if (!Array.isArray(rows) || !(rows.every(row => typeof row === "string"))) { - console.error("Invalid data type for rows. Expected array[string]."); - return; - } - this.clusterize.update(rows); - } - destroy() { /** Destroys a clusterize instance and removes its rows from the page. */ // Passing `true` prevents clusterize from dumping every row in its dataset // to the DOM. This kills performance so we never want to do this. - this.clusterize.destroy(true); + if (!isNullOrUndefined(this.clusterize)) { + this.clusterize.destroy(true); + this.clusterize = null; + } + this.data_obj = {}; + this.data_obj_keys_sorted = []; + if (isElement(this.content_elem)) { + this.content_elem.innerHTML = "
Loading...
"; + } } } @@ -640,6 +624,11 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { this.selected_div_id = null; } + reset() { + this.selected_div_id = null; + super.reset(); + } + getBoxShadow(depth) { /** Generates style for a multi-level box shadow for vertical indentation lines. * This is used to indicate the depth of a directory/file within a directory tree. @@ -757,7 +746,11 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { this.addChildRows(div_id); } this.updateDivContent(div_id, elem); - this.updateRows(); + if (this.calculateDims()) { + this.rebuild(this.getFilteredRows()); + } else { + this.update(this.getFilteredRows()); + } } _setRowSelectedState(div_id, elem, new_state) { @@ -802,7 +795,8 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { this.selected_div_id = div_id; } } - this.updateRows(); + + this.update(this.getFilteredRows()); } getMaxRowWidth() { @@ -848,12 +842,22 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { super(...args); this.no_data_text = "No files matching filter."; - this.sort_mode_str = "path"; - this.sort_dir_str = "ascending"; + this.default_sort_mode_str = "path"; + this.default_sort_dir_str = "ascending"; this.default_filter_str = ""; + + this.sort_mode_str = this.default_sort_mode_str; + this.sort_dir_str = this.default_sort_dir_str; this.filter_str = this.default_filter_str; } + reset() { + this.sort_mode_str = this.default_sort_mode_str; + this.sort_dir_str = this.default_sort_dir_str; + this.filter_str = this.default_filter_str; + super.reset(); + } + updateJson(json) { /** Processes JSON object and adds each entry to our data object. */ return new Promise(resolve => { @@ -892,20 +896,6 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { }); } - getRows(obj) { - /** Returns array of rows as html strings after combining into pseudo-columns. - * Since Clusterize.js doesn't support columns, we need to manually calculate - * the number of columns that can fit in our view space then combine those - * elements into a single entry as a "row" string to pass to Clusterize.js. - */ - let rows = super.getRows(obj); - let res = []; - for (let i = 0; i < rows.length; i += this.n_cols) { - res.push(rows.slice(i, i + this.n_cols).join("")); - } - return res; - } - sortByName() { this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => { return STR_COLLATOR.compare( @@ -950,7 +940,7 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { this.sort_dir_str = sort_dir_str; } - applySort() { + sortData() { this.sort_reverse = this.sort_dir_str === "descending"; switch (this.sort_mode_str) { @@ -970,7 +960,7 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { this.sort_fn = this.sortByDivId; break; } - super.applySort(); + super.sortData(); } applyFilter(filter_str) { @@ -989,8 +979,7 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { this.data_obj[k].active = visible; } - this.applySort(); - this.updateRows(); + super.applyFilter() } getMaxRowWidth() { diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index d0221c072..f87e620fc 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -9,6 +9,7 @@ from dataclasses import dataclass import zlib import base64 import re +from starlette.responses import JSONResponse, PlainTextResponse from modules import shared, ui_extra_networks_user_metadata, errors, extra_networks, util from modules.images import read_info_from_image, save_image_with_geninfo @@ -142,9 +143,47 @@ def fetch_cover_images(page: str = "", item: str = "", index: int = 0): raise ValueError(f"File cannot be fetched: {item}. Failed to load cover image.") from err -def get_metadata(page: str = "", item: str = ""): - from starlette.responses import JSONResponse +def get_list_data( + tabname: str = "", + extra_networks_tabname: str = "", + list_type: Optional[str] = None, +) -> PlainTextResponse: + """Responds to API GET requests on /sd_extra_networks/get-list-data with list data. + Args: + tabname: The primary tab name containing the data. + (i.e. txt2img, img2img) + extra_networks_tabname: The selected extra networks tabname. + (i.e. lora, hypernetworks, etc.) + list_type: The type of list data to retrieve. This reflects the + class name used in `extraNetworksClusterizeList.js`. + + Returns: + The string data result along with a status code. + A status_code of 501 is returned on error, 200 on success. + """ + res = "" + status_code = 200 + + page = next(iter([ + x for x in extra_pages + if x.extra_networks_tabname == extra_networks_tabname + ]), None) + + if page is None: + return PlainTextResponse(res, status_code=501) + + if list_type == "ExtraNetworksClusterizeTreeList": + res = page.generate_tree_view_data(tabname) + elif list_type == "ExtraNetworksClusterizeCardsList": + res = page.generate_cards_view_data(tabname) + else: + status_code = 501 # HTTP_501_NOT_IMPLEMENTED + + return PlainTextResponse(res, status_code=status_code) + + +def get_metadata(page: str = "", item: str = "") -> JSONResponse: page = next(iter([x for x in extra_pages if x.name == page]), None) if page is None: return JSONResponse({}) @@ -158,9 +197,7 @@ def get_metadata(page: str = "", item: str = ""): return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)}) -def get_single_card(page: str = "", tabname: str = "", name: str = ""): - from starlette.responses import JSONResponse - +def get_single_card(page: str = "", tabname: str = "", name: str = "") -> JSONResponse: page = next(iter([x for x in extra_pages if x.name == page]), None) try: @@ -171,16 +208,16 @@ def get_single_card(page: str = "", tabname: str = "", name: str = ""): item = page.items.get(name) page.read_user_metadata(item, use_cache=False) - item_html = page.create_item_html(tabname, item, shared.html("extra-networks-card.html")) + item_html = page.create_card_html(tabname=tabname, item=item) return JSONResponse({"html": item_html}) - def add_pages_to_demo(app): app.add_api_route("/sd_extra_networks/thumb", fetch_file, methods=["GET"]) app.add_api_route("/sd_extra_networks/cover-images", fetch_cover_images, methods=["GET"]) app.add_api_route("/sd_extra_networks/metadata", get_metadata, methods=["GET"]) app.add_api_route("/sd_extra_networks/get-single-card", get_single_card, methods=["GET"]) + app.add_api_route("/sd_extra_networks/get-list-data", get_list_data, methods=["GET"]) def quote_js(s): @@ -241,27 +278,21 @@ class ExtraNetworksPage: def build_tree_html_dict_row( self, - div_id: int, tabname: str, label: str, btn_type: str, - btn_title: str, + data_attributes: dict = {}, + btn_title: Optional[str] = None, dir_is_empty: bool = False, - metadata: Optional[str] = None, - parent_id: Optional[int] = None, - data_depth: Optional[int] = None, - data_path: Optional[str] = None, - data_hash: Optional[str] = None, - data_prompt: Optional[str] = None, - data_neg_prompt: Optional[str] = None, - data_allow_neg: Optional[str] = None, + item: Optional[dict] = None, onclick_extra: Optional[str] = None, ) -> str: if btn_type not in ["file", "dir"]: raise ValueError("Invalid button type:", btn_type) label = label.strip() - btn_title = btn_title.strip() + # If not specified, title will just reflect the label + btn_title = btn_title.strip() if btn_title else label action_list_item_action_leading = "" action_list_item_visual_leading = "🗀" @@ -274,42 +305,21 @@ class ExtraNetworksPage: if btn_type == "file": action_list_item_visual_leading = "🗎" # Action buttons - action_list_item_action_trailing += '
' - action_list_item_action_trailing += self.btn_copy_path_tpl.format( - **{"filename": data_path} - ) - action_list_item_action_trailing += self.btn_edit_item_tpl.format( - **{ - "tabname": tabname, - "extra_networks_tabname": self.extra_networks_tabname, - "name": label, - } - ) - if metadata: - action_list_item_action_trailing += self.btn_metadata_tpl.format( - **{"extra_networks_tabname": self.extra_networks_tabname, "name": label} - ) - action_list_item_action_trailing += "
" + if item is not None: + action_list_item_action_trailing += self.get_button_row(tabname, item) - data_attributes = "" - data_attributes += f"data-path={data_path} " if data_path is not None else "" - data_attributes += f"data-hash={data_hash} " if data_hash is not None else "" - data_attributes += f"data-prompt={data_prompt} " if data_prompt else "" - data_attributes += f"data-neg-prompt={data_neg_prompt} " if data_neg_prompt else "" - data_attributes += f"data-allow-neg={data_allow_neg} " if data_allow_neg else "" - data_attributes += ( - f"data-tree-entry-type={btn_type} " if btn_type is not None else "" - ) - data_attributes += f"data-div-id={div_id} " if div_id is not None else "" - data_attributes += f"data-parent-id={parent_id} " if parent_id is not None else "" - data_attributes += f"data-depth={data_depth} " if data_depth is not None else "" - data_attributes += ( - "data-expanded " if parent_id is None else "" - ) # inverted to expand root + data_attributes_str = "" + for k, v in data_attributes.items(): + if isinstance(v, (bool,)): + # Boolean data attributes only need a key when true. + if v: + data_attributes_str += f"{k} " + elif v not in [None, "", "\'\'", "\"\""]: + data_attributes_str += f"{k}={v} " res = self.tree_row_tpl.format( **{ - "data_attributes": data_attributes, + "data_attributes": data_attributes_str, "search_terms": "", "btn_type": btn_type, "btn_title": btn_title, @@ -327,7 +337,6 @@ class ExtraNetworksPage: res = re.sub(" +", " ", res.replace("\n", "")) return res - def build_tree_html_dict( self, tree: dict, @@ -355,15 +364,19 @@ class ExtraNetworksPage: dir_is_empty = True res[div_id] = self.build_tree_html_dict_row( - div_id=div_id, - parent_id=parent_id, tabname=tabname, label=os.path.basename(k), - data_depth=depth, - data_path=k, btn_type="dir", btn_title=k, dir_is_empty=dir_is_empty, + data_attributes={ + "data-div-id": div_id, + "data-parent-id": parent_id, + "data-tree-entry-type": "dir", + "data-depth": depth, + "data-path": k, + "data-expanded": parent_id is None, # Expand root directories + }, ) last_div_id = self.build_tree_html_dict( tree=v, @@ -386,46 +399,63 @@ class ExtraNetworksPage: onclick = v.item.get("onclick", None) if onclick is None: # Don't quote prompt/neg_prompt since they are stored as js strings already. - onclick_js_tpl = ( - "cardClicked('{tabname}', {prompt}, {neg_prompt}, {allow_neg});" - ) - onclick = onclick_js_tpl.format( - **{ - "tabname": tabname, - "extra_networks_tabname": self.extra_networks_tabname, - "prompt": v.item["prompt"], - "neg_prompt": v.item.get("negative_prompt", "''"), - "allow_neg": str(self.allow_negative_prompt).lower(), - } - ) - onclick = html.escape(onclick) + onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');") res[div_id] = self.build_tree_html_dict_row( - div_id=div_id, - parent_id=parent_id, tabname=tabname, - label=v.item["name"], - metadata=v.item.get("metadata", None), - data_depth=depth, - data_path=v.item["filename"], - data_hash=v.item["shorthash"], - data_prompt=html.escape(v.item.get("prompt", "''")), - data_neg_prompt=html.escape(v.item.get("negative_prompt", "''")), - data_allow_neg=str(self.allow_negative_prompt).lower(), - onclick_extra=onclick, + label=html.escape(v.item.get("name", "").strip()), btn_type="file", - btn_title=v.item["name"], + data_attributes={ + "data-div-id": div_id, + "data-parent-id": parent_id, + "data-tree-entry-type": "file", + "data-name": v.item.get("name", "").strip(), + "data-depth": depth, + "data-path": v.item.get("filename", "").strip(), + "data-hash": v.item.get("shorthash", None), + "data-prompt": v.item.get("prompt", "").strip(), + "data-neg-prompt": v.item.get("negative_prompt", "").strip(), + "data-allow-neg": self.allow_negative_prompt, + }, + item=v.item, + onclick_extra=onclick, ) div_id += 1 return div_id - def create_item_html( + def get_button_row(self, tabname: str, item: dict) -> str: + metadata = item.get("metadata", None) + name = item.get("name", "") + filename = item.get("filename", "") + + button_row_tpl = '
{btn_copy_path}{btn_edit_item}{btn_metadata}
' + + btn_copy_path = self.btn_copy_path_tpl.format(filename=filename) + btn_edit_item = self.btn_edit_item_tpl.format( + tabname=tabname, + extra_networks_tabname=self.extra_networks_tabname, + name=name, + ) + btn_metadata = "" + if metadata: + btn_metadata = self.btn_metadata_tpl.format( + extra_networks_tabname=self.extra_networks_tabname, + name=name, + ) + + return button_row_tpl.format( + btn_copy_path=btn_copy_path, + btn_edit_item=btn_edit_item, + btn_metadata=btn_metadata, + ) + + + def create_card_html( self, tabname: str, item: dict, - template: Optional[str] = None, div_id: Optional[int] = None, - ) -> Union[str, dict]: + ) -> str: """Generates HTML for a single ExtraNetworks Item. Args: @@ -434,47 +464,21 @@ class ExtraNetworksPage: template: Optional template string to use. Returns: - If a template is passed: HTML string generated for this item. - Can be empty if the item is not meant to be shown. - If no template is passed: A dictionary containing the generated item's attributes. + HTML string generated for this item. Can be empty if the item is not meant to be shown. """ preview = item.get("preview", None) style_height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else '' style_width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else '' style_font_size = f"font-size: {shared.opts.extra_networks_card_text_scale*100}%;" - card_style = style_height + style_width + style_font_size + style = style_height + style_width + style_font_size background_image = f'' if preview else '' onclick = item.get("onclick", None) if onclick is None: # Don't quote prompt/neg_prompt since they are stored as js strings already. - onclick_js_tpl = "cardClicked('{tabname}', {prompt}, {neg_prompt}, {allow_neg});" - onclick = onclick_js_tpl.format( - **{ - "tabname": tabname, - "extra_networks_tabname": self.extra_networks_tabname, - "prompt": item["prompt"], - "neg_prompt": item.get("negative_prompt", "''"), - "allow_neg": str(self.allow_negative_prompt).lower(), - } - ) - onclick = html.escape(onclick) + onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');") - btn_copy_path = self.btn_copy_path_tpl.format(**{"filename": item["filename"]}) - btn_metadata = "" - metadata = item.get("metadata") - if metadata: - btn_metadata = self.btn_metadata_tpl.format( - **{ - "extra_networks_tabname": self.extra_networks_tabname, - } - ) - btn_edit_item = self.btn_edit_item_tpl.format( - **{ - "tabname": tabname, - "extra_networks_tabname": self.extra_networks_tabname, - } - ) + button_row = self.get_button_row(tabname, item) local_path = "" filename = item.get("filename", "") @@ -502,9 +506,9 @@ class ExtraNetworksPage: ).strip() search_terms_html = "" - search_term_template = "" + search_terms_tpl = "" for search_term in item.get("search_terms", []): - search_terms_html += search_term_template.format( + search_terms_html += search_terms_tpl.format( **{ "class": f"search_terms{' search_only' if search_only else ''}", "search_term": search_term, @@ -515,33 +519,56 @@ class ExtraNetworksPage: if not shared.opts.extra_networks_card_description_is_html: description = html.escape(description) - # Some items here might not be used depending on HTML template used. - args = { - "div_id": "" if div_id is None else div_id, - "background_image": background_image, - "card_clicked": onclick, - "copy_path_button": btn_copy_path, - "description": description, - "edit_button": btn_edit_item, - "local_preview": quote_js(item["local_preview"]), - "metadata_button": btn_metadata, - "name": html.escape(item["name"]), - "save_card_preview": html.escape(f"return saveCardPreview(event, '{tabname}', '{item['local_preview']}');"), - "search_only": " search_only" if search_only else "", - "search_terms": search_terms_html, - "sort_keys": sort_keys, - "style": card_style, - "tabname": tabname, - "extra_networks_tabname": self.extra_networks_tabname, - "data_prompt": item.get("prompt", "''"), - "data_neg_prompt": item.get("negative_prompt", "''"), - "data_allow_neg": str(self.allow_negative_prompt).lower(), + data_attributes = { + "data-div-id": div_id if div_id else "", + "data-name": item.get("name", "").strip(), + "data-path": item.get("filename", "").strip(), + "data-hash": item.get("shorthash", None), + "data-prompt": item.get("prompt", "").strip(), + "data-neg-prompt": item.get("negative_prompt", "").strip(), + "data-allow-neg": self.allow_negative_prompt, } - if template: - return template.format(**args) - else: - return args + data_attributes_str = "" + for k, v in data_attributes.items(): + if isinstance(v, (bool,)): + # Boolean data attributes only need a key when true. + if v: + data_attributes_str += f"{k} " + elif v not in [None, "", "\'\'", "\"\""]: + data_attributes_str += f"{k}={v} " + + return self.card_tpl.format( + 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, + name=html.escape(item["name"].strip()), + description=description, + ) + + def generate_tree_view_data(self, tabname: str) -> str: + """Generates tree view HTML as a base64 encoded zlib compressed json string.""" + res = {} + + if self.tree: + self.build_tree_html_dict( + tree=self.tree, + res=res, + depth=0, + div_id=0, + parent_id=None, + tabname=tabname, + ) + + return base64.b64encode( + zlib.compress( + json.dumps(res, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + ) + ).decode("utf-8") def generate_tree_view_data_div(self, tabname: str) -> str: """Generates HTML for displaying folders in a tree view. @@ -552,23 +579,17 @@ class ExtraNetworksPage: Returns: HTML string generated for this tree view. """ - res = {} - - if not self.tree: - return res - - self.build_tree_html_dict( - tree=self.tree, - res=res, - depth=0, - div_id=0, - parent_id=None, - tabname=tabname, + tpl = """""" + return tpl.format( + tabname_full=f"{tabname}_{self.extra_networks_tabname}", + data=self.generate_tree_view_data(tabname), ) - res = base64.b64encode(zlib.compress(json.dumps(res, separators=(",", ":")).encode("utf-8"))).decode("utf-8") - return f'' - def create_dirs_view_html(self, tabname: str) -> str: """Generates HTML for displaying folders.""" @@ -592,7 +613,18 @@ class ExtraNetworksPage: ]) return dirs_html - def generate_cards_view_data_div(self, tabname: str, *, none_message) -> str: + def generate_cards_view_data(self, tabname: str) -> str: + res = {} + for i, item in enumerate(self.items.values()): + res[i] = self.create_card_html(tabname=tabname, item=item, div_id=i) + + return base64.b64encode( + zlib.compress( + json.dumps(res, separators=(",", ":"), ensure_ascii=True).encode("utf-8") + ) + ).decode("utf-8") + + def generate_cards_view_data_div(self, tabname: str, none_message: Optional[str]) -> str: """Generates HTML for the network Card View section for a tab. This HTML goes into the `extra-networks-pane.html`
with @@ -605,11 +637,8 @@ class ExtraNetworksPage: Returns: HTML formatted string. """ - res = {} - for i, item in enumerate(self.items.values()): - res[i] = self.create_item_html(tabname, item, self.card_tpl, div_id=i) + res = self.generate_cards_view_data(tabname) - res = base64.b64encode(zlib.compress(json.dumps(res, separators=(",", ":")).encode("utf-8"))).decode("utf-8") return f'' def create_html(self, tabname, *, empty=False): @@ -645,9 +674,6 @@ class ExtraNetworksPage: tree = get_tree([os.path.abspath(x) for x in roots], items=tree_items) self.tree = tree - # Generate the data payloads for tree and cards views - tree_data_div = self.generate_tree_view_data_div(tabname) - cards_data_div = self.generate_cards_view_data_div(tabname, none_message="Loading..." if empty else None) # Generate the html for displaying directory buttons dirs_html = self.create_dirs_view_html(tabname) @@ -671,8 +697,8 @@ class ExtraNetworksPage: "tree_view_style": f"flex-basis: {shared.opts.extra_networks_tree_view_default_width}px;", "cards_view_style": "flex-grow: 1;", "dirs_html": dirs_html, - "cards_data_div": cards_data_div, - "tree_data_div": tree_data_div, + + }) def create_item(self, name, index=None): diff --git a/style.css b/style.css index 53c51050d..d0ae6b41d 100644 --- a/style.css +++ b/style.css @@ -1185,12 +1185,15 @@ body.resizing .resize-handle { .clusterize-scroll { width: 100%; height: 100%; - overflow: clip auto; + /* Use scroll instead of auto so that content size doesn't change when there is no content. */ + overflow: clip scroll; } .clusterize-content { + flex: 1; outline: 0; counter-reset: clusterize-counter; + padding: var(--spacing-md); } .clusterize-extra-row { @@ -1471,15 +1474,6 @@ body.resizing .resize-handle { } } -.extra-network-tree-content { - padding: var(--spacing-md); - flex: 1; -} - -.extra-network-cards-content { - padding: var(--spacing-md); -} - /* BUTTON ELEMENTS */ /*