From 6b8374cc8466e92b893e0b31f7d26c0d6dc0d233 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Sun, 14 Apr 2024 14:43:31 -0400 Subject: [PATCH] fix some bugs. need to continue bug testing. --- javascript/clusterize.js | 53 ++++--- javascript/extraNetworks.js | 219 ++++++++++++++++++-------- javascript/extraNetworksClusterize.js | 109 ++++++------- javascript/lru_cache.js | 52 ++++++ modules/ui_extra_networks.py | 32 +++- modules/ui_gradio_extensions.py | 9 +- style.css | 3 +- 7 files changed, 316 insertions(+), 161 deletions(-) create mode 100644 javascript/lru_cache.js diff --git a/javascript/clusterize.js b/javascript/clusterize.js index daca223a7..f222aea81 100644 --- a/javascript/clusterize.js +++ b/javascript/clusterize.js @@ -47,6 +47,7 @@ class Clusterize { #element_observer = null; #element_observer_timer = null; #pointer_events_set = false; + #on_scroll_bound; constructor(args) { for (const option of Object.keys(this.options)) { @@ -56,16 +57,16 @@ class Clusterize { } if (isNullOrUndefined(this.options.callbacks.initData)) { - this.options.callbacks.initData = this.initDataDefaultCallback; + this.options.callbacks.initData = this.initDataDefaultCallback.bind(this); } if (isNullOrUndefined(this.options.callbacks.fetchData)) { - this.options.callbacks.fetchData = this.fetchDataDefaultCallback; + this.options.callbacks.fetchData = this.fetchDataDefaultCallback.bind(this); } if (isNullOrUndefined(this.options.callbacks.sortData)) { - this.options.callbacks.sortData = this.sortDataDefaultCallback; + this.options.callbacks.sortData = this.sortDataDefaultCallback.bind(this); } if (isNullOrUndefined(this.options.callbacks.filterData)) { - this.options.callbacks.filterData = this.filterDataDefaultCallback; + this.options.callbacks.filterData = this.filterDataDefaultCallback.bind(this); } // detect ie9 and lower @@ -96,6 +97,8 @@ class Clusterize { this.#scroll_top = this.scroll_elem.scrollTop; this.#max_items = args.max_items; + + this.#on_scroll_bound = this.#onScroll.bind(this); } // ==== PUBLIC FUNCTIONS ==== @@ -114,7 +117,7 @@ class Clusterize { await this.#insertToDOM(); this.scroll_elem.scrollTop = this.#scroll_top; - this.#setupEvent("scroll", this.scroll_elem, this.#onScroll); + this.#setupEvent("scroll", this.scroll_elem, this.#on_scroll_bound); this.#setupElementObservers(); this.#setupResizeObservers(); @@ -129,12 +132,13 @@ class Clusterize { this.#html(this.#generateEmptyRow().join("")); } - destroy() { - this.#teardownEvent("scroll", this.scroll_elem, this.#onScroll); + destroy() { + this.#teardownEvent("scroll", this.scroll_elem, this.#on_scroll_bound); this.#teardownElementObservers(); this.#teardownResizeObservers(); - this.clear(); + this.#html(this.#generateEmptyRow().join("")); + this.setup_has_run = false; } @@ -210,7 +214,7 @@ class Clusterize { if (!this.enabled) { return; } - return await this.options.callbacks.initData.call(this); + return await this.options.callbacks.initData(); } fetchDataDefaultCallback() { @@ -221,7 +225,7 @@ class Clusterize { if (!this.enabled) { return; } - return await this.options.callbacks.fetchData.call(this, idx_start, idx_end); + return await this.options.callbacks.fetchData(idx_start, idx_end); } sortDataDefaultCallback() { @@ -236,7 +240,7 @@ class Clusterize { this.#fixElementReferences(); // Sort is applied to the filtered data. - await this.options.callbacks.sortData.call(this); + await this.options.callbacks.sortData(); this.#recalculateDims(); await this.#insertToDOM(); } @@ -251,7 +255,7 @@ class Clusterize { } // Filter is applied to entire dataset. - const max_items = await this.options.callbacks.filterData.call(this); + const max_items = await this.options.callbacks.filterData(); await this.setMaxItems(max_items); } @@ -287,10 +291,8 @@ class Clusterize { } // 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)) { + const node = this.content_elem.querySelector(":scope > :not(.clusterize-extra-row,.clusterize-no-data)"); + if (!isElementLogError(node)) { return; } @@ -307,10 +309,13 @@ class Clusterize { // 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; + const column_gap = parseFloat(getComputedProperty(this.content_elem, "column-gap")); + const row_gap = parseFloat(getComputedProperty(this.content_elem, "row-gap")); + if (isNumber(column_gap)) { + this.options.item_width += column_gap; + } + if (isNumber(row_gap)) { + this.options.item_height += row_gap; } const inner_width = this.scroll_elem.clientWidth - content_padding.width; @@ -594,17 +599,17 @@ class Clusterize { #setupEvent(type, elem, listener) { if (elem.addEventListener) { - return elem.addEventListener(type, event => listener.call(this), false); + return elem.addEventListener(type, listener, false); } else { - return elem.attachEvent(`on${type}`, event => listener.call(this)); + return elem.attachEvent(`on${type}`, listener); } } #teardownEvent(type, elem, listener) { if (elem.removeEventListener) { - return elem.removeEventListener(type, event => listener.call(this), false); + return elem.removeEventListener(type, listener, false); } else { - return elem.detachEvent(`on${type}`, event => listener.call(this)); + return elem.detachEvent(`on${type}`, listener); } } } diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 3c2e38f54..3856df0bf 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -16,7 +16,10 @@ 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. +/** Boolean flags used along with utils.js::waitForBool(). */ +// Set true when extraNetworksSetup completes. +const extra_networks_setup_complete = {state: false}; +// Set true when we first load the UI options. const initialUiOptionsLoaded = {state: false}; class ExtraNetworksTab { @@ -32,6 +35,9 @@ class ExtraNetworksTab { txt_prompt_elem; txt_neg_prompt_elem; active_prompt_elem; + sort_mode_str = ""; + sort_dir_str = ""; + filter_str = ""; show_prompt = true; show_neg_prompt = true; compact_prompt_en = false; @@ -70,6 +76,15 @@ class ExtraNetworksTab { await this.setupTreeList(); await this.setupCardsList(); + const sort_mode_elem = this.controls_elem.querySelector(".extra-network-control--sort-mode[data-selected='']"); + isElementThrowError(sort_mode_elem); + const sort_dir_elem = this.controls_elem.querySelector(".extra-network-control--sort-dir"); + isElementThrowError(sort_dir_elem); + + this.setSortMode(sort_mode_elem.dataset.sortMode); + this.setSortDir(sort_dir_elem.dataset.sortDir); + this.setFilterStr(this.txt_search_elem.value.toLowerCase()); + this.registerPrompt(); if (this.container_elem.style.display === "none") { @@ -79,6 +94,24 @@ class ExtraNetworksTab { } } + destroy() { + this.unload(); + this.tree_list.destroy(); + this.cards_list.destroy(); + this.tree_list = null; + this.cards_list = null; + this.container_elem = null; + this.controls_elem = null; + this.txt_search_elem = null; + this.prompt_container_elem = null; + this.prompts_elem = null; + this.prompt_row_elem = null; + this.neg_prompt_row_elem = null; + this.txt_prompt_elem = null; + this.txt_neg_prompt_elem = null; + this.active_prompt_elem = null; + } + async registerPrompt() { await Promise.all([ waitForElement(`#${this.tabname}_prompt > label > textarea`).then(elem => this.txt_prompt_elem = elem), @@ -100,8 +133,8 @@ class ExtraNetworksTab { contentId: `${this.tabname_full}_tree_list_content_area`, tag: "button", callbacks: { - initData: this.onInitTreeData, - fetchData: this.onFetchTreeData, + initData: this.onInitTreeData.bind(this), + fetchData: this.onFetchTreeData.bind(this), }, }); await this.tree_list.setup(); @@ -118,13 +151,28 @@ class ExtraNetworksTab { contentId: `${this.tabname_full}_cards_list_content_area`, tag: "div", callbacks: { - initData: this.onInitCardsData, - fetchData: this.onFetchCardsData, + initData: this.onInitCardsData.bind(this), + fetchData: this.onFetchCardsData.bind(this), }, }); await this.cards_list.setup(); } + setSortMode(sort_mode_str) { + this.sort_mode_str = sort_mode_str; + this.cards_list.setSortMode(this.sort_mode_str); + } + + setSortDir(sort_dir_str) { + this.sort_dir_str = sort_dir_str; + this.cards_list.setSortDir(this.sort_dir_str); + } + + setFilterStr(filter_str) { + this.filter_str = filter_str; + this.cards_list.setFilterStr(this.filter_str); + } + movePrompt(show_prompt=true, show_neg_prompt=true) { // This function only applies when compact prompt mode is enabled. if (!this.compact_prompt_en) { @@ -142,8 +190,8 @@ class ExtraNetworksTab { this.prompts_elem.classList.toggle("extra-page-prompts-active", show_neg_prompt || show_prompt); } - async refreshSingleCard(name) { - await requestGetPromise( + refreshSingleCard(name) { + requestGet( "./sd_extra_networks/get-single-card", { tabname: this.tabname, @@ -152,13 +200,7 @@ class ExtraNetworksTab { }, (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); + this.cards_list.updateCard(name, data.html); } }, ); @@ -176,6 +218,8 @@ class ExtraNetworksTab { 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"); + // We actually want to select the tree view's column in the resize-handle-row. + // This is what we actually show/hide, not the inner elements. const div_tree = this.container_elem.querySelector( `.extra-network-content.resize-handle-col:has(> #${this.tabname_full}_tree_list_scroll_area)` ); @@ -188,6 +232,10 @@ class ExtraNetworksTab { this.tree_list.enable(); this.cards_list.enable(); await Promise.all([this.tree_list.load(true), this.cards_list.load(true)]); + // apply the previous sort/filter options + this.setSortMode(this.sort_mode_str); + this.setSortDir(this.sort_dir_str); + this.setFilterStr(this.filter_str); } async load(show_prompt, show_neg_prompt) { @@ -207,7 +255,7 @@ class ExtraNetworksTab { applyFilter() { // We only want to filter/sort the cards list. - this.cards_list.setFilterStr(this.txt_search_elem.value.toLowerCase()); + this.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. @@ -224,48 +272,80 @@ class ExtraNetworksTab { } } + async waitForServerPageReady() { + // We need to wait for the page to be ready before we can fetch data. + // After starting the server, on the first load of the page, if the user + // immediately clicks a tab, then we will try to load the card data before + // the server has even generated it. + // We use status 503 to indicate that the page isnt ready yet. + while (true) { + try { + await requestGetPromise( + "./sd_extra_networks/page-is-ready", + {extra_networks_tabname: this.extra_networks_tabname}, + ); + break; + } catch (error) { + if (error.status === 503) { + await new Promise(resolve => setTimeout(resolve, 250)); + } else { + // We do not want to continue waiting if we get an unhandled error. + throw new Error("Error checking page readiness:", error); + } + } + } + } + async onInitCardsData() { - const res = await requestGetPromise( - "./sd_extra_networks/init-cards-data", - { - tabname: this.tabname, - extra_networks_tabname: this.extra_networks_tabname, - }, + await this.waitForServerPageReady(); + + return JSON.parse( + 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, - }, + await this.waitForServerPageReady(); + + return JSON.parse( + 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( + 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( + 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) { @@ -484,27 +564,13 @@ function extraNetworksShowMetadata(text) { } function extraNetworksRefreshSingleCard(tabname, extra_networks_tabname, name) { - requestGet( - "./sd_extra_networks/get-single-card", - {tabname: tabname, extra_networks_tabname: extra_networks_tabname, name: name}, - (data) => { - if (data && data.html) { - const card = gradioApp().querySelector(`${tabname}_${extra_networks_tabname}_cards > .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); - } - }, - ); + const tab = extra_networks_tabs[`${tabname}_${extra_networks_tabname}`]; + tab.refreshSingleCard(name); } async function extraNetworksRefreshTab(tabname_full) { /** called from python when user clicks the extra networks refresh tab button */ - extra_networks_tabs[tabname_full].refresh(); + await extra_networks_tabs[tabname_full].refresh(); } // ==== EVENT HANDLING ==== @@ -535,9 +601,10 @@ function extraNetworksUnrelatedTabSelected() { async function extraNetworksTabSelected(tabname_full, show_prompt, show_neg_prompt) { /** called from python when user selects an extra networks tab */ + await waitForKeyInObject({obj: extra_networks_tabs, k: tabname_full}); 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); + await v.load(show_prompt=show_prompt, show_neg_prompt=show_neg_prompt); } else { v.unload(); } @@ -563,6 +630,14 @@ function extraNetworksBtnDirsViewItemOnClick(event, tabname_full) { // update search input with selected button's path. elem.dataset.selected = ""; txt_search_elem.value = elem.textContent.trim(); + + // Select the corresponding tree view button. + if ("selected" in elem.dataset) { + const tree_row = tab.container_elem.querySelector(`.tree-list-item[data-path="${elem.textContent.trim()}"]`); + if (isElement(tree_row)) { + tab.tree_list.onRowSelected(tree_row.dataset.divId, tree_row); + } + } }; const _deselect_button = (elem) => { @@ -603,7 +678,7 @@ function extraNetworksControlSortModeOnClick(event, tabname_full) { const sort_mode_str = event.currentTarget.dataset.sortMode.toLowerCase(); - tab.cards_list.setSortMode(sort_mode_str); + tab.setSortMode(sort_mode_str); } function extraNetworksControlSortDirOnClick(event, tabname_full) { @@ -624,7 +699,7 @@ function extraNetworksControlSortDirOnClick(event, tabname_full) { event.currentTarget.dataset.sortDir = sort_dir_str; event.currentTarget.setAttribute("title", `Sort ${sort_dir_str}`); - tab.cards_list.setSortDir(sort_dir_str); + tab.setSortDir(sort_dir_str); } function extraNetworksControlTreeViewOnClick(event, tabname_full) { @@ -672,9 +747,6 @@ function extraNetworksControlRefreshOnClick(event, tabname_full) { * event handler that refreshes the page. So what this function here does * is it manually raises a `click` event on that button. */ - // reset states - initialUiOptionsLoaded.state = false; - // We want to reset all tabs lists on refresh click so that the viewing area // shows that it is loading new data. for (tab of Object.values(extra_networks_tabs)) { @@ -721,6 +793,17 @@ function extraNetworksTreeDirectoryOnClick(event, btn, tabname_full) { } else { // user clicked anywhere else on the row tab.tree_list.onRowSelected(div_id, btn); + // Select the corresponding dirs view button. + if ("selected" in btn.dataset) { + tab.container_elem.querySelectorAll(".extra-network-dirs-view-button").forEach(elem => { + if (elem.textContent.trim() === btn.dataset.path) { + elem.dataset.selected = ""; + } else { + delete elem.dataset.selected; + } + }); + + } tab.updateSearch("selected" in btn.dataset ? btn.dataset.path : ""); } } @@ -839,13 +922,17 @@ async function extraNetworksSetupTab(tabname) { } async function extraNetworksSetup() { + extra_networks_setup_complete.state = false; await waitForBool(initialUiOptionsLoaded); await Promise.all([ extraNetworksSetupTab('txt2img'), extraNetworksSetupTab('img2img'), ]); + extraNetworksSetupEventDelegators(); + + extra_networks_setup_complete.state = true; } onUiLoaded(extraNetworksSetup); diff --git a/javascript/extraNetworksClusterize.js b/javascript/extraNetworksClusterize.js index 4f5919c95..ec3546c7e 100644 --- a/javascript/extraNetworksClusterize.js +++ b/javascript/extraNetworksClusterize.js @@ -10,47 +10,6 @@ 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 = []; @@ -73,7 +32,6 @@ 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) { @@ -86,7 +44,7 @@ class ExtraNetworksClusterize extends Clusterize { // 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.setMaxItems(Object.keys(this.data_obj).length); - await this.options.callbacks.sortData.call(this); + await this.options.callbacks.sortData(); } async setup() { @@ -94,6 +52,12 @@ class ExtraNetworksClusterize extends Clusterize { return; } + if (this.lru instanceof LRUCache) { + this.lru.clear(); + } else { + this.lru = new LRUCache(); + } + await this.reinitData(); if (this.enabled) { @@ -101,6 +65,23 @@ class ExtraNetworksClusterize extends Clusterize { } } + destroy() { + if (this.lru instanceof LRUCache) { + this.lru.destroy(); + this.lru = null; + } + this.data_obj = {}; + this.data_obj_keys_sorted = []; + super.destroy(); + } + + clear() { + this.data_obj = {}; + this.data_obj_keys_sorted = []; + this.lru.clear(); + super.clear(); + } + async load(force_init_data) { if (!this.enabled) { return; @@ -115,13 +96,6 @@ class ExtraNetworksClusterize extends Clusterize { } } - clear() { - this.data_obj = {}; - this.data_obj_keys_sorted = []; - this.lru.clear(); - super.clear(); - } - setSortMode(sort_mode_str) { if (this.sort_mode_str === sort_mode_str) { return; @@ -173,6 +147,9 @@ class ExtraNetworksClusterize extends Clusterize { } async fetchDivIds(div_ids) { + if (isNullOrUndefinedLogError(this.lru)) { + return []; + } 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)); @@ -182,7 +159,7 @@ class ExtraNetworksClusterize extends Clusterize { if (missing_div_ids.length !== 0) { Object.assign( data, - await this.options.callbacks.fetchData.call(this, missing_div_ids), + await this.options.callbacks.fetchData(missing_div_ids), ); } @@ -270,15 +247,17 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { override = override === true; if (!isNullOrUndefined(this.selected_div_id) && div_id !== this.selected_div_id) { + const prev_elem = this.content_elem.querySelector(`[data-div-id="${this.selected_div_id}"]`); // deselect current selection if exists on page - const prev_elem = this.content_elem.querySelector(`div[data-div-id="${this.selected_div_id}"]`); if (isElement(prev_elem)) { delete prev_elem.dataset.selected; + this.data_obj[prev_elem.dataset.divId].selected = false; } } elem.toggleAttribute("data-selected"); this.selected_div_id = "selected" in elem.dataset ? div_id : null; + this.data_obj[elem.dataset.divId].selected = "selected" in elem.dataset; } getMaxRowWidth() { @@ -344,7 +323,7 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { expanded: bool, } */ - this.data_obj = await this.options.callbacks.initData.call(this); + this.data_obj = await this.options.callbacks.initData(); } async fetchData(idx_start, idx_end) { @@ -429,7 +408,29 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { sort_: string, (for various sort modes) } */ - this.data_obj = await this.options.callbacks.initData.call(this); + this.data_obj = await this.options.callbacks.initData(); + } + + updateCard(name, new_html) { + const parsed_html = htmlStringToElement(new_html); + + const old_card = this.content_elem.querySelector(`.card[data-name="${name}"]`); + if (!isElementLogError(old_card)) { + return; + } + + const div_id = old_card.dataset.divId; + + // replace new html's data attributes with the current ones + for (const [k, v] of Object.entries(old_card.dataset)) { + parsed_html.dataset[k] = v; + } + + // replace the element in DOM with our new element + old_card.replaceWith(parsed_html); + + // update the internal cache with the new html + this.lru.set(String(div_id), new_html); } async fetchData(idx_start, idx_end) { diff --git a/javascript/lru_cache.js b/javascript/lru_cache.js new file mode 100644 index 000000000..efe0c58b0 --- /dev/null +++ b/javascript/lru_cache.js @@ -0,0 +1,52 @@ +const LRU_CACHE_MAX_ITEMS_DEFAULT = 250; +class LRUCache { + /** Least Recently Used cache implementation. + * + * Source: https://stackoverflow.com/a/46432113 + */ + constructor(max = LRU_CACHE_MAX_ITEMS_DEFAULT) { + isNumberThrowError(max); + + this.max = max; + this.cache = new Map(); + } + + clear() { + this.cache.clear(); + } + + destroy() { + this.clear(); + this.cache = null; + } + + size() { + return this.cache.size; + } + + get(key) { + let item = this.cache.get(key); + if (!isNullOrUndefined(item)) { + this.cache.delete(key); + this.cache.set(key, item); + } + return item; + } + + set(key, val) { + 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) { + return this.cache.has(key); + } + + first() { + return this.cache.keys().next().value; + } +} \ No newline at end of file diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index d55cf58b0..b6ed6f29f 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -71,7 +71,6 @@ class TreeListItem(ListItem): Attributes: visible [bool]: Whether the item should be shown in the list. expanded [bool]: Whether the item children should be shown. - selected [bool]: Whether the item is selected by user. """ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -79,7 +78,6 @@ class TreeListItem(ListItem): self.node: Optional[DirectoryTreeNode] = None self.visible: bool = False self.expanded: bool = False - self.selected: bool = False class DirectoryTreeNode: @@ -241,8 +239,23 @@ def init_cards_data(tabname: str = "", extra_networks_tabname: str = "") -> JSON data = page.generate_cards_view_data(tabname) + if data is None: + return JSONResponse({}, status_code=503) + return JSONResponse(data, status_code=200) +def page_is_ready(extra_networks_tabname: str = "") -> JSONResponse: + page = get_page_by_name(extra_networks_tabname) + + try: + items_list = [x for x in page.list_items()] + if len(page.items) == len(items_list): + return JSONResponse({}, status_code=200) + + return JSONResponse({"error": "page not ready"}, status_code=503) + except Exception as exc: + return JSONResponse({"error": str(exc)}, status_code=500) + def get_metadata(extra_networks_tabname: str = "", item: str = "") -> JSONResponse: try: page = get_page_by_name(extra_networks_tabname) @@ -287,6 +300,7 @@ def add_pages_to_demo(app): app.add_api_route("/sd_extra_networks/init-cards-data", init_cards_data, methods=["GET"]) app.add_api_route("/sd_extra_networks/fetch-tree-data", fetch_tree_data, methods=["GET"]) app.add_api_route("/sd_extra_networks/fetch-cards-data", fetch_cards_data, methods=["GET"]) + app.add_api_route("/sd_extra_networks/page-is-ready", page_is_ready, methods=["GET"]) def quote_js(s): s = s.replace('\\', '\\\\') @@ -593,8 +607,8 @@ class ExtraNetworksPage: show_files = shared.opts.extra_networks_tree_view_show_files is True for div_id, node in div_id_to_node.items(): - self.tree[div_id] = TreeListItem(div_id, "") - self.tree[div_id].node = node + tree_item = TreeListItem(div_id, "") + tree_item.node = node parent_id = None if node.parent is not None: parent_id = path_to_div_id.get(node.parent.abspath, None) @@ -603,8 +617,8 @@ class ExtraNetworksPage: 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( + dir_is_empty = all(not x.is_dir for x in node.children) + tree_item.html = self.build_tree_html_dict_row( tabname=tabname, label=os.path.basename(node.abspath), btn_type="dir", @@ -619,6 +633,7 @@ class ExtraNetworksPage: "data-expanded": node.parent is None, # Expand root directories }, ) + self.tree[div_id] = tree_item else: # file if not show_files: # Don't add file if files are disabled in the options. @@ -629,7 +644,7 @@ class ExtraNetworksPage: 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( + tree_item.html = self.build_tree_html_dict_row( tabname=tabname, label=html.escape(item_name), btn_type="file", @@ -648,6 +663,7 @@ class ExtraNetworksPage: item=node.item, onclick_extra=onclick, ) + self.tree[div_id] = tree_item res = {} @@ -740,7 +756,7 @@ class ExtraNetworksPage: 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) + self.tree_roots[abspath].build(tree_items if shared.opts.extra_networks_tree_view_show_files else {}) # Generate the html for displaying directory buttons dirs_html = self.create_dirs_view_html(tabname) diff --git a/modules/ui_gradio_extensions.py b/modules/ui_gradio_extensions.py index 71ad13b73..f5278d22f 100644 --- a/modules/ui_gradio_extensions.py +++ b/modules/ui_gradio_extensions.py @@ -16,14 +16,7 @@ def javascript_html(): script_js = os.path.join(script_path, "script.js") head += f'\n' - # We want the utils.js script to be imported first since it can be used by multiple other - # scripts. This resolves dependency issues caused by html \n' - - # Now add all remaining .js scripts excluding the utils.js file. - for script in [x for x in js_scripts if x.filename != "utils.js"]: + for script in scripts.list_scripts("javascript", ".js"): head += f'\n' for script in scripts.list_scripts("javascript", ".mjs"): diff --git a/style.css b/style.css index f5b0998d8..21597a4bc 100644 --- a/style.css +++ b/style.css @@ -1192,7 +1192,8 @@ body.resizing .resize-handle { .clusterize-content { flex: 1; outline: 0; - gap: var(--spacing-sm); + /* need to manually set the gap to 0 to fix item dimension calcs. */ + gap: 0; counter-reset: clusterize-counter; padding: var(--spacing-md); }