diff --git a/javascript/clusterize.js b/javascript/clusterize.js index 62f25ae2e..595b4e351 100644 --- a/javascript/clusterize.js +++ b/javascript/clusterize.js @@ -17,10 +17,32 @@ class Clusterize { content_elem = null; scroll_id = null; content_id = null; - #options = {}; + default_sort_mode_str = ""; + default_sort_dir_str = "ascending"; + default_filter_str = ""; + sort_mode_str = this.default_sort_mode_str; + sort_dir_str = this.default_sort_dir_str; + filter_str = this.default_filter_str; + options = { + rows_in_block: 50, + cols_in_block: 1, + blocks_in_cluster: 5, + tag: null, + show_no_data_row: true, + no_data_class: "clusterize-no-data", + no_data_text: "No data", + keep_parity: true, + callbacks: { + initData: this.initDataDefault, + fetchData: this.fetchDataDefault, + sortData: this.sortDataDefault, + filterData: this.filterDataDefault, + }, + }; #is_mac = null; #ie = null; - #n_rows = null; + #max_items = null; + #max_rows = null; #cache = {}; #scroll_top = 0; #last_cluster = false; @@ -30,59 +52,46 @@ class Clusterize { #element_observer = null; #element_observer_timer = null; #pointer_events_set = false; - #sort_mode = ""; - #sort_dir = ""; constructor(args) { - const defaults = { - rows_in_block: 50, - blocks_in_cluster: 4, - tag: null, - show_no_data_row: true, - no_data_class: 'clusterize-no-data', - no_data_text: 'No data', - keep_parity: true, - callbacks: {} - }; + for (const option of Object.keys(this.options)) { + if (keyExists(args, option)) { + this.options[option] = args[option]; + } + } - const options = [ - 'rows_in_block', - 'blocks_in_cluster', - 'show_no_data_row', - 'no_data_class', - 'no_data_text', - 'keep_parity', - 'tag', - 'callbacks', - ]; + if (!isNullOrUndefined(this.options.callbacks.initData)) { + this.options.callbacks.initData = this.initDataDefault; + } + if (!isNullOrUndefined(this.options.callbacks.fetchData)) { + this.options.callbacks.fetchData = this.fetchDataDefault; + } + if (!isNullOrUndefined(this.options.callbacks.sortData)) { + this.options.callbacks.sortData = this.sortDataDefault; + } + if (!isNullOrUndefined(this.options.callbacks.filterData)) { + this.options.callbacks.filterData = this.filterDataDefault; + } // detect ie9 and lower // https://gist.github.com/padolsey/527683#comment-786682 this.#ie = (function () { for (var v = 3, - el = document.createElement('b'), + el = document.createElement("b"), all = el.all || []; - el.innerHTML = '', + el.innerHTML = ``, all[0]; ) { } return v > 4 ? v : document.documentMode; }()) - this.#is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1; - - for (let i = 0, option; option = options[i]; i++) { - this.#options[option] = !isNullOrUndefined(args[option]) ? args[option] : defaults[option]; - } + this.#is_mac = navigator.platform.toLowerCase().indexOf("mac") + 1; this.scroll_elem = args["scrollId"] ? document.getElementById(args["scrollId"]) : args["scrollElem"]; - if (!isElement(this.scroll_elem)) { - throw new Error("Error! Could not find scroll element"); - } + isElementThrowError(this.scroll_elem); this.scroll_id = this.scroll_elem.id; this.content_elem = args["contentId"] ? document.getElementById(args["contentId"]) : args["contentElem"]; - if (!isElement(this.content_elem)) { - throw new Error("Error! Could not find content element"); - } + isElementThrowError(this.content_elem); this.content_id = this.content_elem.id; if (!this.content_elem.hasAttribute("tabindex")) { @@ -91,20 +100,7 @@ class Clusterize { this.#scroll_top = this.scroll_elem.scrollTop; - if (!isNumber(args.n_rows)) { - throw new Error("Invalid argument. n_rows expected number, got:", typeof args.n_rows); - } - this.#n_rows = args.n_rows; - - if (!this.#options.callbacks.fetchData) { - this.#options.callbacks.fetchData = this.#fetchDataDefault; - } - if (!this.#options.callbacks.sortData) { - this.#options.callbacks.sortData = this.#sortDataDefault; - } - if (!this.#options.callbacks.filterData) { - this.#options.callbacks.filterData = this.#filterDataDefault; - } + this.#max_items = args.max_items; } // ==== PUBLIC FUNCTIONS ==== @@ -117,23 +113,27 @@ class Clusterize { this.#setupResizeObservers(); } + clear() { + this.#html(this.#generateEmptyRow().join("")); + } + destroy() { this.#teardownEvent("scroll", this.scroll_elem, this.#onScroll); this.#teardownElementObservers(); this.#teardownResizeObservers(); - this.#html(this.#generateEmptyRow().join("")); + this.clear(); } - refresh(force) { + async refresh(force) { if (this.#getRowsHeight() || force) { - this.update() + await this.update() } } async update() { this.#scroll_top = this.scroll_elem.scrollTop; // fixes #39 - if (this.#n_rows * this.#options.item_height < this.#scroll_top) { + if (this.#max_rows * this.options.item_height < this.#scroll_top) { this.scroll_elem.scrollTop = 0; this.#last_cluster = 0; } @@ -143,119 +143,166 @@ class Clusterize { } getRowsAmount() { - return this.#n_rows; + return this.#max_rows; } getScrollProgress() { - return this.#options.scroll_top / (this.#n_rows * this.#options.item_height) * 100 || 0; + return this.options.scroll_top / (this.#max_rows * this.options.item_height) * 100 || 0; } - async filterData(filter) { + async setMaxItems(max_items) { + if (max_items === this.#max_items) { + // No change. do nothing. + return; + } + + // If the number of items changed, we need to update the cluster. + this.#max_items = max_items; + await this.refresh(true); + + // Apply sort to the updated data. + await this.sortData(this.sort_mode_str, this.sort_dir_str); + } + + async filterData(filter_str) { + if (this.filter_str === filter_str) { + return; + } + + this.filter_str = isNullOrUndefined(filter_str) ? this.default_filter_str : filter_str; + // Filter is applied to entire dataset. - const n_rows = await this.#options.callbacks.filterData(filter); - // If the number of rows changed after filter, we need to update the cluster. - if (n_rows !== this.#n_rows) { - this.#n_rows = n_rows; + const max_items = await this.options.callbacks.filterData(this.filter_str); + // If the number of items changed after filter, we need to update the cluster. + if (max_items !== this.#max_items) { + this.#max_items = max_items; this.refresh(true); } // Apply sort to the new filtered data. - await this.sortData(this.#sort_mode, this.#sort_dir); + await this.sortData(this.sort_mode_str, this.sort_dir_str); } - async sortData(mode, dir) { + async sortData(sort_mode_str, sort_dir_str) { + if (this.sort_mode_str === sort_mode_str && this.sort_dir_str === sort_dir_str) { + return; + } + + this.sort_mode_str = isNullOrUndefined(sort_mode_str) ? this.default_sort_mode_str : sort_mode_str; + this.sort_dir_str = isNullOrUndefined(sort_dir_str) ? this.default_sort_dir_str : sort_dir_str; + // Sort is applied to the filtered data. - - // update instance sort settings to the passed values. - this.#sort_mode = mode; - this.#sort_dir = dir; - - await this.#options.callbacks.sortData(this.#sort_mode, this.#sort_dir === "descending"); + await this.options.callbacks.sortData(this.sort_mode_str, this.sort_dir_str === "descending"); await this.#insertToDOM(); } // ==== PRIVATE FUNCTIONS ==== + initDataDefault() { + return Promise.resolve({}); + } - #fetchDataDefault() { + fetchDataDefault() { return Promise.resolve([]); } - #sortDataDefault() { - return Promise.resolve([]); + sortDataDefault() { + return Promise.resolve(); } - #filterDataDefault() { - return Promise.resolve([]); + filterDataDefault() { + return Promise.resolve(0); } #exploreEnvironment(rows, cache) { - this.#options.content_tag = this.content_elem.tagName.toLowerCase(); + this.options.content_tag = this.content_elem.tagName.toLowerCase(); if (!rows.length) { return; } - if (this.#ie && this.#ie <= 9 && !this.#options.tag) { - this.#options.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase(); + if (this.#ie && this.#ie <= 9 && !this.options.tag) { + this.options.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase(); } if (this.content_elem.children.length <= 1) { cache.data = this.#html(rows[0] + rows[0] + rows[0]); } - if (!this.#options.tag) { - this.#options.tag = this.content_elem.children[0].tagName.toLowerCase(); + if (!this.options.tag) { + this.options.tag = this.content_elem.children[0].tagName.toLowerCase(); } this.#getRowsHeight(); } #getRowsHeight() { - const prev_item_height = this.#options.item_height; - const prev_rows_in_block = this.#options.rows_in_block; + 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; - this.#options.cluster_height = 0; - if (!this.#n_rows) { + this.options.cluster_height = 0; + this.options.cluster_width = 0; + if (!this.#max_items) { return; } - const nodes = this.content_elem.children; + const nodes = this.content_elem.querySelectorAll(":not(.clusterize-row)"); if (!nodes.length) { return; } + const node = nodes[Math.floor(nodes.length / 2)]; - this.#options.item_height = node.offsetHeight; + 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") { - this.#options.item_height += parseInt(getStyle("borderSpacing", this.content_elem), 10) || 0; + if (this.options.tag === "tr" && getStyle("borderCollapse", this.content_elem) !== "collapse") { + const spacing = parseInt(getStyle("borderSpacing", this.content_elem), 10) || 0; + this.options.item_height += spacing; + this.options.item_width += spacing; } // consider margins and margins collapsing - if (this.#options.tag !== "tr") { + 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; - this.#options.item_height += Math.max(margin_top, margin_bottom); + 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 scroll element view. - this.#options.rows_in_block = parseInt(this.scroll_elem.clientHeight / this.#options.item_height); + this.options.rows_in_block = calcRowsPerCol(this.scroll_elem, node); + this.options.cols_in_block = calcColsPerRow(this.content_elem, node); - this.#options.block_height = this.#options.item_height * this.#options.rows_in_block; - this.#options.rows_in_cluster = this.#options.blocks_in_cluster * this.#options.rows_in_block; - this.#options.cluster_height = this.#options.blocks_in_cluster * this.#options.block_height; - return prev_item_height !== this.#options.item_height || prev_rows_in_block !== this.#options.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; + this.options.rows_in_cluster = this.options.blocks_in_cluster * this.options.rows_in_block; + 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); + + return ( + prev_item_height !== this.options.item_height || + prev_item_width !== this.options.item_width || + prev_rows_in_block !== this.options.rows_in_block || + prev_cols_in_block !== this.options.cols_in_block + ); } #getClusterNum() { - this.#options.scroll_top = this.scroll_elem.scrollTop; - const cluster_divider = this.#options.cluster_height - this.#options.block_height; - const current_cluster = Math.floor(this.#options.scroll_top / cluster_divider); - const max_cluster = Math.floor((this.#n_rows * this.#options.item_height) / cluster_divider); + this.options.scroll_top = this.scroll_elem.scrollTop; + const cluster_divider = this.options.cluster_height - this.options.block_height; + const current_cluster = Math.floor(this.options.scroll_top / cluster_divider); + const max_cluster = Math.floor((this.#max_rows * this.options.item_height) / cluster_divider); return Math.min(current_cluster, max_cluster); } #generateEmptyRow() { - if (!this.#options.tag || !this.#options.show_no_data_row) { + if (!this.options.tag || !this.options.show_no_data_row) { return []; } - const empty_row = document.createElement(this.#options.tag); - const no_data_content = document.createTextNode(this.#options.no_data_text); - empty_row.className = this.#options.no_data_class; - if (this.#options.tag === "tr") { + const empty_row = document.createElement(this.options.tag); + const no_data_content = document.createTextNode(this.options.no_data_text); + empty_row.className = this.options.no_data_class; + if (this.options.tag === "tr") { const td = document.createElement("td"); // fixes #53 td.colSpan = 100; @@ -268,13 +315,15 @@ class Clusterize { } async #generate() { - const items_start = Math.max((this.#options.rows_in_cluster - this.#options.rows_in_block) * this.#getClusterNum(), 0); - const items_end = items_start + this.#options.rows_in_cluster; - const top_offset = Math.max(items_start * this.#options.item_height, 0); - const bottom_offset = Math.max((this.#n_rows - items_end) * this.#options.item_height, 0); - const rows_above = top_offset < 1 ? items_start + 1 : items_start; + const rows_start = Math.max(0, (this.options.rows_in_cluster - this.options.rows_in_block) * this.#getClusterNum()); + const rows_end = rows_start + this.options.rows_in_cluster; + const top_offset = Math.max(0, rows_start * this.options.item_height); + const bottom_offset = Math.max(0, (this.#max_rows - rows_end) * this.options.item_height); + const rows_above = top_offset < 1 ? rows_start + 1 : rows_start; - const this_cluster_rows = await this.#options.callbacks.fetchData(items_start, items_end); + const idx_start = Math.max(0, rows_start * this.options.cols_in_block); + const idx_end = rows_end * this.options.cols_in_block; + const this_cluster_rows = await this.options.callbacks.fetchData(idx_start, idx_end); return { top_offset: top_offset, bottom_offset: bottom_offset, @@ -284,13 +333,18 @@ class Clusterize { } async #insertToDOM() { - if (!this.#options.cluster_height) { - const rows = await this.#options.callbacks.fetchData(0, 1); + if (!this.options.cluster_height || !this.options.cluster_width) { + const rows = await this.options.callbacks.fetchData(0, 1); this.#exploreEnvironment(rows, this.#cache); } const data = await this.#generate(); - const this_cluster_rows = data.rows.join(""); + 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 = this_cluster_rows.join(""); const this_cluster_content_changed = this.#checkChanges("data", this_cluster_rows, this.#cache); const top_offset_changed = this.#checkChanges("top", data.top_offset, this.#cache); const only_bottom_offset_changed = this.#checkChanges("bottom", data.bottom_offset, this.#cache); @@ -298,16 +352,16 @@ class Clusterize { if (this_cluster_content_changed || top_offset_changed) { if (data.top_offset) { - this.#options.keep_parity && layout.push(this.#renderExtraTag("keep-parity")); + 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(); + this.options.callbacks.clusterWillChange && this.options.callbacks.clusterWillChange(); this.#html(layout.join("")); - this.#options.content_tag === "ol" && this.content_elem.setAttribute("start", data.rows_above); + this.options.content_tag === "ol" && this.content_elem.setAttribute("start", data.rows_above); this.content_elem.style["counter-increment"] = `clusterize-counter ${data.rows_above - 1}`; - this.#options.callbacks.clusterChanged && this.#options.callbacks.clusterChanged(); + this.options.callbacks.clusterChanged && this.options.callbacks.clusterChanged(); } else if (only_bottom_offset_changed) { this.content_elem.lastChild.style.height = `${data.bottom_offset}px`; } @@ -315,7 +369,7 @@ class Clusterize { #html(data) { const content_elem = this.content_elem; - if (this.#ie && this.#ie <= 9 && this.#options.tag === "tr") { + if (this.#ie && this.#ie <= 9 && this.options.tag === "tr") { const div = document.createElement("div"); let last; div.innerHTML = `${data}
`; @@ -332,7 +386,7 @@ class Clusterize { } #renderExtraTag(class_name, height) { - const tag = document.createElement(this.#options.tag); + const tag = document.createElement(this.options.tag); const clusterize_prefix = "clusterize-"; tag.className = [ `${clusterize_prefix}extra-row`, @@ -374,8 +428,8 @@ class Clusterize { if (this.#last_cluster !== (this.#last_cluster = this.#getClusterNum())) { await this.#insertToDOM(); } - if (this.#options.callbacks.scrollingProgress) { - this.#options.callbacks.scrollingProgress(this.getScrollingProgress()); + if (this.options.callbacks.scrollingProgress) { + this.options.callbacks.scrollingProgress(this.getScrollProgress()); } } diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 97ec58f71..744906502 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -20,53 +20,69 @@ const extraPageUserMetadataEditors = {}; // A flag used by the `waitForBool` promise to determine when we first load Ui Options. const initialUiOptionsLoaded = {state: false}; -function toggleCss(key, css, enable) { - var style = document.getElementById(key); - if (enable && !style) { - style = document.createElement('style'); - style.id = key; - style.type = 'text/css'; - document.head.appendChild(style); +// + +const popup = contents => { + if (!globalPopup) { + globalPopup = document.createElement('div'); + globalPopup.classList.add('global-popup'); + + var close = document.createElement('div'); + close.classList.add('global-popup-close'); + close.addEventListener("click", closePopup); + close.title = "Close"; + globalPopup.appendChild(close); + + globalPopupInner = document.createElement('div'); + globalPopupInner.classList.add('global-popup-inner'); + globalPopup.appendChild(globalPopupInner); + + gradioApp().querySelector('.main').appendChild(globalPopup); } - if (style && !enable) { - document.head.removeChild(style); + + globalPopupInner.innerHTML = ''; + globalPopupInner.appendChild(contents); + + globalPopup.style.display = "flex"; +}; + +const popupId = id => { + if (!storedPopupIds[id]) { + storedPopupIds[id] = gradioApp().getElementById(id); } - if (style) { - style.innerHTML == ''; - style.appendChild(document.createTextNode(css)); + + popup(storedPopupIds[id]); +}; + +const closePopup = () => { + if (!globalPopup) return; + globalPopup.style.display = "none"; +}; + + +// ==== GENERAL EXTRA NETWORKS FUNCTIONS ==== + +const extraNetworksClusterizersLoadTab = async ( + { + tabname_full = "", + selected = false, + fetch_data = false, + } +) => { + if (!keyExistsLogError(clusterizers, tabname_full)) { + return; } -} -function extraNetworksRefreshTab(tabname_full) { - // 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"); + for (const v of Object.values(clusterizers[tabname_full])) { + if (fetch_data) { + await v.initDataDefault(); + } else { + await v.refresh(true); + } + } +}; - 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)); - - waitForKeyInObject({k: tabname_full, obj: clusterizers}) - .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) { +const extraNetworksRegisterPromptForTab = (tabname, id) => { var textarea = gradioApp().querySelector(`#${id} > label > textarea`); if (!activePromptTextarea[tabname]) { @@ -76,76 +92,9 @@ function extraNetworksRegisterPromptForTab(tabname, id) { textarea.addEventListener("focus", function() { activePromptTextarea[tabname] = textarea; }); -} +}; -function extraNetworksSetupTabContent(tabname, pane, controls_div) { - const tabname_full = pane.id; - const extra_networks_tabname = tabname_full.replace(`${tabname}_`, ""); - let controls; - - Promise.all([ - waitForElement(`#${tabname_full}_pane .extra-network-controls`).then(elem => controls = elem), - waitForElement(`#${tabname_full}_pane .extra-network-content--dirs-view`), - waitForElement(`#${tabname_full}_tree_list_scroll_area > #${tabname_full}_tree_list_content_area`), - waitForElement(`#${tabname_full}_cards_list_scroll_area > #${tabname_full}_cards_list_content_area`), - ]).then(() => { - // Insert the controls into the page. - // add an ID since we will be moving this element elsewhere. - controls.id = `${tabname_full}_controls`; - controls_div.insertBefore(controls, null); - - // Now that we have our elements in DOM, we create the clusterize lists. - clusterizers[tabname_full] = { - tree_list: new ExtraNetworksClusterizeTreeList({ - 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({ - 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 - }); - })(); - }); -} - -function extraNetworksSetupTab(tabname) { - let this_tab; - let tab_nav; - let controls_div; - Promise.all([ - waitForElement(`#${tabname}_extra_tabs`).then((elem) => this_tab = elem), - waitForElement(`#${tabname}_extra_tabs > div.tab-nav`).then((elem) => tab_nav = elem), - ]).then(() => { - controls_div = document.createElement("div"); - controls_div.classList.add("extra-network-controls-div"); - tab_nav.appendChild(controls_div); - tab_nav.insertBefore(controls_div, null); - this_tab.querySelectorAll(`:scope > .tabitem[id^="${tabname}_"]`).forEach((elem) => { - extraNetworksSetupTabContent(tabname, elem, controls_div); - }); - extraNetworksRegisterPromptForTab(tabname, `${tabname}_prompt`); - extraNetworksRegisterPromptForTab(tabname, `${tabname}_neg_prompt`); - }); -} - -function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt) { +const 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`); @@ -168,264 +117,69 @@ function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePromp if (elem) { elem.classList.toggle('extra-page-prompts-active', showNegativePrompt || showPrompt); } -} +}; - -function extraNetworksShowControlsForPage(tabname, tabname_full) { +const 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); }); -} +}; +const extraNetworksRemoveFromPrompt = (textarea, text, is_neg) => { + let match = text.match(is_neg ? re_extranet_neg : re_extranet); + let replaced = false; + let res; + let prefix = opts.extra_networks_add_text_separator; -function extraNetworksUnrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate) - extraNetworksMovePromptToTab(tabname, '', false, false); - extraNetworksShowControlsForPage(tabname, null); -} - -function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt, tabname_full) { // called from python when user selects an extra networks tab - extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt); - extraNetworksShowControlsForPage(tabname, tabname_full); - - waitForKeyInObject({k: tabname_full, obj: clusterizers}) - .then(() => { - extraNetworkClusterizersLoadTab({ - tabname_full: tabname_full, - selected: true, - fetch_data: false, - }); - }); -} - -function extraNetworksApplyFilter(tabname_full) { - if (!(tabname_full in clusterizers)) { - console.error(`${tabname_full} not in clusterizers.`); - return; - } - - const pane = gradioApp().getElementById(`${tabname_full}_pane`); - 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.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. - // 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; - } -} - -function extraNetworksClusterizersEnable(tabname_full) { - /** Enables the selected tab's clusterize lists and disables all others. */ - // iterate over tabnames - for (const [_tabname_full, tab_clusterizers] of Object.entries(clusterizers)) { - // iterate over clusterizers in tab - for (const v of Object.values(tab_clusterizers)) { - v.enable(_tabname_full === tabname_full); - } - } -} - -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() => { - if (selected) { - extraNetworksClusterizersEnable(tabname_full); - } - for (const v of Object.values(clusterizers[tabname_full])) { - if (fetch_data) { - await v.setup(); - } else { - await v.load(); + if (match) { + const content = match[1]; + const postfix = match[2]; + let idx = -1; + res = textarea.value.replaceAll( + is_neg ? re_extranet_g_neg : re_extranet_g, + (found, net, pos) => { + match = found.match(is_neg ? re_extranet_neg : re_extranet); + if (match[1] === content) { + replaced = true; + idx = pos; + return ""; } + return found; + }, + ); + if (idx >= 0) { + if (postfix && res.slice(idx, postfix.length) === postfix) { + res = res.slice(0, idx) + res.slice(idx + postfix.length); } - })().then(() => { - return resolve(); - }).catch(error => { - console.error("Error loading tab:", error); - return reject(error); - }); - }); -} - -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 (!(tabname_full in clusterizers)) { - return; - } - - const row = pane.querySelector(".resize-handle-row"); - if (!isElementLogError(row)) { - return; - } - - const left_col = row.firstElementChild; - if (!isElementLogError(left_col)) { - return; - } - - if (left_col.classList.contains("hidden")) { - // If the left column is hidden then we don't want to do anything. - 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 extraNetworksSetupEventDelegators() { - /** Sets up event delegators for all extraNetworks tabs. - * - * These event handlers are not tied to any specific elements on the page. - * We do this because elements within each tab may be removed and replaced - * which would break references to elements in DOM and thus prevent any event - * listeners from firing. - */ - - window.addEventListener("resizeHandleDblClick", event => { - // See resizeHandle.js::onDoubleClick() for event detail. - event.stopPropagation(); - extraNetworksAutoSetTreeWidth(event.target.closest(".extra-network-pane")); - }); - - // 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); - }); - - // Debounce search text input. This way we only search after user is done typing. - const search_input_debounce = debounce((tabname_full) => { - extraNetworksApplyFilter(tabname_full); - }, SEARCH_INPUT_DEBOUNCE_TIME_MS); - - window.addEventListener("keyup", event => { - const controls = event.target.closest(".extra-network-controls"); - if (isElement(controls)) { - const tabname_full = controls.dataset.tabnameFull; - const target = event.target.closest(".extra-network-control--search-text"); - if (isElement(target)) { - search_input_debounce.call(target, tabname_full); - } - } - }); -} - -function setupExtraNetworks() { - waitForBool(initialUiOptionsLoaded).then(() => { - extraNetworksSetupTab('txt2img'); - extraNetworksSetupTab('img2img'); - extraNetworksSetupEventDelegators(); - }); -} - -function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) { - var m = text.match(isNeg ? re_extranet_neg : re_extranet); - var replaced = false; - var newTextareaText; - var extraTextBeforeNet = opts.extra_networks_add_text_separator; - if (m) { - var extraTextAfterNet = m[2]; - var partToSearch = m[1]; - var foundAtPosition = -1; - newTextareaText = textarea.value.replaceAll(isNeg ? re_extranet_g_neg : re_extranet_g, function(found, net, pos) { - m = found.match(isNeg ? re_extranet_neg : re_extranet); - if (m[1] == partToSearch) { - replaced = true; - foundAtPosition = pos; - return ""; - } - return found; - }); - if (foundAtPosition >= 0) { - if (extraTextAfterNet && newTextareaText.substr(foundAtPosition, extraTextAfterNet.length) == extraTextAfterNet) { - newTextareaText = newTextareaText.substr(0, foundAtPosition) + newTextareaText.substr(foundAtPosition + extraTextAfterNet.length); - } - if (newTextareaText.substr(foundAtPosition - extraTextBeforeNet.length, extraTextBeforeNet.length) == extraTextBeforeNet) { - newTextareaText = newTextareaText.substr(0, foundAtPosition - extraTextBeforeNet.length) + newTextareaText.substr(foundAtPosition); + if (res.slice(idx - prefix.length, prefix.length) === prefix) { + res = res.slice(0, idx - prefix.length) + res.slice(idx); } } } else { - newTextareaText = textarea.value.replaceAll(new RegExp(`((?:${extraTextBeforeNet})?${text})`, "g"), ""); - replaced = (newTextareaText != textarea.value); + res = textarea.value.replaceAll(new RegExp(`((?:${extraTextBeforeNet})?${text})`, "g"), ""); + replaced = (res !== textarea.value); } if (replaced) { - textarea.value = newTextareaText; + textarea.value = res; return true; } return false; -} +}; -function updatePromptArea(text, textArea, isNeg) { - if (!tryToRemoveExtraNetworkFromPrompt(textArea, text, isNeg)) { - textArea.value = textArea.value + opts.extra_networks_add_text_separator + text; +const extraNetworksUpdatePrompt = (textarea, text, is_neg) => { + if (!extraNetworksRemoveFromPrompt(textarea, text, is_neg)) { + textarea.value = textarea.value + opts.extra_networks_add_text_separator + text; } - updateInput(textArea); -} + updateInput(textarea); +}; -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 { - updatePromptArea(elem.dataset.prompt, prompt_elem); - } -} - -function saveCardPreview(event, tabname, filename) { - var textarea = gradioApp().querySelector(`#${tabname}_preview_filename > label > textarea`); - var button = gradioApp().getElementById(`${tabname}_save_preview`); +const extraNetworksSaveCardPreview = (event, tabname, filename) => { + const textarea = gradioApp().querySelector(`#${tabname}_preview_filename > label > textarea`); + const button = gradioApp().getElementById(`${tabname}_save_preview`); textarea.value = filename; updateInput(textarea); @@ -434,267 +188,9 @@ function saveCardPreview(event, tabname, filename) { event.stopPropagation(); event.preventDefault(); -} +}; -function extraNetworksTreeProcessFileClick(event, btn, tabname_full) { - /** - * Processes `onclick` events when user clicks on files in tree. - * - * @param event The generated event. - * @param btn The clicked `tree-list-item` button. - * @param tabname_full The full active tabname. - * i.e. txt2img_lora, img2img_checkpoints, etc. - */ - // NOTE: Currently unused. - return; -} - -function extraNetworksTreeProcessDirectoryClick(event, btn, tabname_full) { - /** - * Processes `onclick` events when user clicks on directories in tree. - * - * Here is how the tree reacts to clicks for various states: - * unselected unopened directory: Directory is selected and expanded. - * unselected opened directory: Directory is selected. - * selected opened directory: Directory is collapsed and deselected. - * chevron is clicked: Directory is expanded or collapsed. Selected state unchanged. - * - * @param event The generated event. - * @param btn The clicked `tree-list-item` button. - * @param tabname_full The full active tabname. - * i.e. txt2img_lora, img2img_checkpoints, etc. - */ - // This is the actual target that the user clicked on within the target button. - // We use this to detect if the chevron was clicked. - var true_targ = event.target; - const div_id = btn.dataset.divId; - - function _updateSearch(_search_text) { - // Update search input with select button's path. - const txt_search = gradioApp().querySelector(`#${tabname_full}_controls .extra-network-control--search-text`); - txt_search.value = _search_text; - updateInput(txt_search); - extraNetworksApplyFilter(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. - let prev_selected_elem = gradioApp().querySelector(".tree-list-item[data-selected='']"); - clusterizers[tabname_full].tree_list.onRowExpandClick(div_id, btn); - let selected_elem = gradioApp().querySelector(".tree-list-item[data-selected='']"); - if (isElement(prev_selected_elem) && !isElement(selected_elem)) { - // if a selected element was removed, clear filter. - _updateSearch(""); - } - } else { - // user clicked anywhere else on row. - clusterizers[tabname_full].tree_list.onRowSelected(div_id, btn); - _updateSearch("selected" in btn.dataset ? btn.dataset.path : ""); - } -} - -function extraNetworksTreeOnClick(event, tabname_full) { - /** - * Handles `onclick` events for buttons within an `extra-network-tree .tree-list--tree`. - * - * Determines whether the clicked button in the tree is for a file entry or a directory - * then calls the appropriate function. - * - * @param event The generated event. - * @param tabname_full The full active tabname. - * i.e. txt2img_lora, img2img_checkpoints, etc. - */ - let btn = event.target.closest(".tree-list-item"); - if (btn.dataset.treeEntryType === "file") { - extraNetworksTreeProcessFileClick(event, btn, tabname_full); - } else { - extraNetworksTreeProcessDirectoryClick(event, btn, tabname_full); - } - event.stopPropagation(); -} - -function extraNetworksDirsOnClick(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`); - function _deselect_all() { - // deselect all buttons - gradioApp().querySelectorAll(".extra-network-dirs-view-button").forEach((elem) => { - delete elem.dataset.selected; - }); - } - - function _select_button(elem) { - _deselect_all(); - // Update search input with select button's path. - elem.dataset.selected = ""; - txt_search.value = elem.textContent.trim(); - } - - function _deselect_button(elem) { - delete elem.dataset.selected; - txt_search.value = ""; - } - - - if ("selected" in event.target.dataset) { - _deselect_button(event.target); - } else { - _select_button(event.target); - } - - updateInput(txt_search); - extraNetworksApplyFilter(tabname_full); -} - -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( - new CustomEvent( - "extra-network-control--search-clear", - { - bubbles: true, - detail: {tabname_full: 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 => { - delete elem.dataset.selected; - }); - - event.currentTarget.dataset.selected = ""; - - if (tabname_full in clusterizers) { - clusterizers[tabname_full].cards_list.setSortMode( - event.currentTarget.dataset.sortMode.toLowerCase() - ); - extraNetworksApplyFilter(tabname_full); - } -} - -function extraNetworksControlSortDirOnClick(event, tabname_full) { - /** Handles `onclick` events for the Sort Direction button. - * - * Modifies the data attributes of the Sort Direction button to cycle between - * ascending and descending sort directions. - */ - if (event.currentTarget.dataset.sortDir.toLowerCase() == "ascending") { - event.currentTarget.dataset.sortDir = "descending"; - event.currentTarget.setAttribute("title", "Sort descending"); - } else { - event.currentTarget.dataset.sortDir = "ascending"; - event.currentTarget.setAttribute("title", "Sort ascending"); - } - - if (tabname_full in clusterizers) { - clusterizers[tabname_full].cards_list.setSortDir(event.currentTarget.dataset.sortDir.toLowerCase()); - extraNetworksApplyFilter(tabname_full); - } -} - -function extraNetworksControlTreeViewOnClick(event, tabname_full) { - /** Handles `onclick` events for the Tree View button. - * - * Toggles the tree view in the extra networks pane. - */ - let show; - if ("selected" in event.currentTarget.dataset) { - delete event.currentTarget.dataset.selected; - show = false; - } else { - event.currentTarget.dataset.selected = ""; - show = true; - } - - gradioApp().getElementById(`${tabname_full}_tree_list_scroll_area`).parentElement.classList.toggle("hidden", !show); - clusterizers[tabname_full].tree_list.enable(show); -} - -function extraNetworksControlDirsViewOnClick(event, tabname_full) { - /** Handles `onclick` events for the Dirs View button. - * - * Toggles the directory view in the extra networks pane. - */ - let show; - if ("selected" in event.currentTarget.dataset) { - delete event.currentTarget.dataset.selected; - show = false; - } else { - event.currentTarget.dataset.selected = ""; - show = true; - } - - const pane = gradioApp().getElementById(`${tabname_full}_pane`); - pane.querySelector(".extra-network-content--dirs-view").classList.toggle("hidden", !show); -} - -function extraNetworksControlRefreshOnClick(event, tabname_full) { - /** Handles `onclick` events for the Refresh Page button. - * - * In order to actually call the python functions in `ui_extra_networks.py` - * to refresh the page, we created an empty gradio button in that file with an - * 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 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")); -} - -function closePopup() { - if (!globalPopup) return; - globalPopup.style.display = "none"; -} - -function popup(contents) { - if (!globalPopup) { - globalPopup = document.createElement('div'); - globalPopup.classList.add('global-popup'); - - var close = document.createElement('div'); - close.classList.add('global-popup-close'); - close.addEventListener("click", closePopup); - close.title = "Close"; - globalPopup.appendChild(close); - - globalPopupInner = document.createElement('div'); - globalPopupInner.classList.add('global-popup-inner'); - globalPopup.appendChild(globalPopupInner); - - gradioApp().querySelector('.main').appendChild(globalPopup); - } - - globalPopupInner.innerHTML = ''; - globalPopupInner.appendChild(contents); - - globalPopup.style.display = "flex"; -} - -function popupId(id) { - if (!storedPopupIds[id]) { - storedPopupIds[id] = gradioApp().getElementById(id); - } - - popup(storedPopupIds[id]); -} - -function extraNetworksFlattenMetadata(obj) { +const extraNetworksFlattenMetadata = obj => { const result = {}; // Convert any stringified JSON objects to actual objects @@ -743,9 +239,9 @@ function extraNetworksFlattenMetadata(obj) { } return result; -} +}; -function extraNetworksShowMetadata(text) { +const extraNetworksShowMetadata = text => { try { let parsed = JSON.parse(text); if (parsed && typeof parsed === 'object') { @@ -764,98 +260,406 @@ function extraNetworksShowMetadata(text) { popup(elem); 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); +const 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; - 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}); - } + new_card.style.display = ""; + card.parentElement.insertBefore(new_card, card); + card.parentElement.removeChild(card); } - }; - xhr.send(JSON.stringify(data)); - }); -} + }, + ); +}; -function requestGet(url, data, handler, errorHandler) { - var xhr = new XMLHttpRequest(); - var args = Object.keys(data).map(function(k) { - return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]); - }).join('&'); - xhr.open("GET", url + "?" + args, true); +const extraNetworksRefreshTab = async 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"); - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - try { - var js = JSON.parse(xhr.responseText); - handler(js); - } catch (error) { - console.error(error); - errorHandler(); - } - } else { - errorHandler(); - } - } - }; - var js = JSON.stringify(data); - xhr.send(js); -} + 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)`); -function extraNetworksCopyPathToClipboard(event, path) { - navigator.clipboard.writeText(path); - event.stopPropagation(); -} + // 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)); -async function extraNetworksRequestListData(tabname, extra_networks_tabname, class_name) { + 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, + }); + } +}; + +const 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`; +}; + +const 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.filterData(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; + } +}; + +// ==== EVENT HANDLING ==== + +const extraNetworksInitCardsData = async () => { return await requestGetPromise( - "./sd_extra_networks/get-list-data", + "./sd_extra_networks/init-cards-data", { tabname: tabname, extra_networks_tabname: extra_networks_tabname, - list_type: class_name, }, ); -} +}; -function extraNetworksRequestMetadata(extra_networks_tabname, card_name) { - var showError = function() { +const extraNetworksInitTreeData = async () => { + return await requestGetPromise( + "./sd_extra_networks/init-tree-data", + { + tabname: tabname, + extra_networks_tabname: extra_networks_tabname, + }, + ); +}; + +const extraNetworksOnInitData = async class_name => { + if (class_name === "ExtraNetworksClusterizeTreeList") { + return await extraNetworksInitCardsData(); + } else if (class_name === "ExtraNetworksClusterizeCardsList") { + return await extraNetworksInitTreeData(); + } +}; + +const extraNetworksFetchCardsData = async (extra_networks_tabname, div_ids) => { + return await requestGetPromise( + "./sd_extra_networks/fetch-cards-data", + { + extra_networks_tabname: extra_networks_tabname, + div_ids: div_ids, + }, + ); +}; + +const extraNetworksFetchTreeData = async (extra_networks_tabname, div_ids) => { + return await requestGetPromise( + "./sd_extra_networks/fetch-tree-data", + { + extra_networks_tabname: extra_networks_tabname, + div_ids: div_ids, + }, + ); +}; + +const extraNetworksOnFetchData = async (class_name, extra_networks_tabname, div_ids) => { + if (class_name === "ExtraNetworksClusterizeTreeList") { + return await extraNetworksFetchCardsData(extra_networks_tabname, div_ids); + } else if (class_name === "ExtraNetworksClusterizeCardsList") { + return await extraNetworksFetchTreeData(extra_networks_tabname, div_ids); + } +}; + +const extraNetworksFetchMetadata = (extra_networks_tabname, card_name) => { + const _showError = () => { extraNetworksShowMetadata("there was an error getting metadata"); }; - requestGet("./sd_extra_networks/metadata", {page: extra_networks_tabname, item: card_name}, function(data) { - if (data && data.metadata) { - extraNetworksShowMetadata(data.metadata); - } else { - showError(); + requestGet( + "./sd_extra_networks/metadata", + {extra_networks_tabname: extra_networks_tabname, item: card_name}, + function(data) { + if (data && data.metadata) { + extraNetworksShowMetadata(data.metadata); + } else { + _showError(); + } + }, + _showError, + ); +}; + +const extraNetworksUnrelatedTabSelected = tabname => { + /** called from python when user selects an unrelated tab (generate) */ + extraNetworksMovePromptToTab(tabname, '', false, false); + extraNetworksShowControlsForPage(tabname, null); +}; + +const extraNetworksTabSelected = async (tabname, id, showPrompt, showNegativePrompt, tabname_full) => { + /** 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); +}; + +const 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 _deselect_all_buttons = () => { + gradioApp().querySelectorAll(".extra-network-dirs-view-button").forEach((elem) => { + delete elem.dataset.selected; + }); + }; + + const _select_button = elem => { + _deselect_all_buttons(); + // Update search input with select button's path. + elem.dataset.selected = ""; + txt_search.value = elem.textContent.trim(); + }; + + const _deselect_button = elem => { + delete elem.dataset.selected; + txt_search.value = ""; + }; + + if ("selected" in event.target.dataset) { + _deselect_button(event.target); + } else { + _select_button(event.target); + } + + updateInput(txt_search); + extraNetworksApplyFilter(tabname_full); +}; + +const 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( + new CustomEvent( + "extra-network-control--search-clear", + {bubbles: true, detail: {tabname_full: tabname_full}}, + ) + ); +}; + +const extraNetworksControlSortModeOnClick = (event, tabname_full) => { + /** Handles `onclick` events for Sort Mode buttons. */ + event.currentTarget.parentElement.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.sort_mode_str = sort_mode_str; + extraNetworksApplyFilter(tabname_full); +}; + +const extraNetworksControlSortDirOnClick = (event, tabname_full) => { + /** Handles `onclick` events for the Sort Direction button. + * + * Modifies the data attributes of the Sort Direction button to cycle between + * ascending and descending sort directions. + */ + 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}`); + return; + } + + let sort_dir_str = curr_sort_dir_str === "ascending" ? "descending" : "ascending"; + 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.sort_dir_str = sort_dir_str; + extraNetworksApplyFilter(tabname_full); +}; + +const extraNetworksControlTreeViewOnClick = (event, tabname_full) => { + /** Handles `onclick` events for the Tree View button. + * + * Toggles the tree view in the extra networks pane. + */ + let show; + if ("selected" in event.currentTarget.dataset) { + delete event.currentTarget.dataset.selected; + show = false; + } else { + event.currentTarget.dataset.selected = ""; + 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 extraNetworksControlDirsViewOnClick = (event, tabname_full) => { + /** Handles `onclick` events for the Dirs View button. + * + * Toggles the directory view in the extra networks pane. + */ + const show = !("selected" in event.currentTarget.dataset); + if (show) { + event.currentTarget.dataset.selected = ""; + } else { + delete event.currentTarget.dataset.selected; + } + + const pane = gradioApp().getElementById(`${tabname_full}_pane`); + pane.querySelector(".extra-network-content--dirs-view").classList.toggle("hidden", !show); +}; + +const extraNetworksControlRefreshOnClick = (event, tabname_full) => { + /** Handles `onclick` events for the Refresh Page button. + * + * In order to actually call the python functions in `ui_extra_networks.py` + * to refresh the page, we created an empty gradio button in that file with an + * 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 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.clear(); } - }, showError); -} + } + + // Fire an event for this button click. + gradioApp().getElementById(`${tabname_full}_extra_refresh_internal`).dispatchEvent(new Event("click")); +}; + +const 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) { + extraNetworksUpdatePrompt(prompt_elem, elem.dataset.prompt); + extraNetworksUpdatePrompt(neg_prompt_elem, elem.dataset.negPrompt); + } else if ("allowNeg" in elem.dataset) { + extraNetworksUpdatePrompt(activePromptTextarea[tabname], elem.dataset.prompt); + } else { + extraNetworksUpdatePrompt(prompt_elem, elem.dataset.prompt); + } +}; + +const extraNetworksTreeFileOnClick = (event, btn, tabname_full) => { + return; +}; + +const extraNetworksTreeDirectoryOnClick = (event, btn, tabname_full) => { + return; +}; + +const extraNetworksTreeOnClick = (event, tabname_full) => { + const btn = event.target.closest(".tree-list-item"); + if (!isElementLogError(btn)) { + return; + } + + if (btn.dataset.treeEntryType === "file") { + extraNetworksTreeFileOnClick(event, btn, tabname_full); + } else { + extraNetworksTreeDirectoryOnClick(event, btn, tabname_full); + } -function extraNetworksMetadataButtonOnClick(event, extra_networks_tabname, card_name) { - extraNetworksRequestMetadata(extra_networks_tabname, card_name); event.stopPropagation(); -} +}; -function extraNetworksEditUserMetadata(tabname_full, card_name) { +const extraNetworksBtnShowMetadataOnClick = (event, extra_networks_tabname, card_name) => { + extraNetworksFetchMetadata(extra_networks_tabname, card_name); + event.stopPropagation(); +}; + +const extraNetworksBtnEditMetadataOnClick = (event, tabname_full, card_name) => { const id = `${tabname_full}_edit_user_metadata`; let editor = extraPageUserMetadataEditors[id]; - if (!editor) { + if (isNullOrUndefined(editor)) { editor = {}; editor.page = gradioApp().getElementById(id); editor.nameTextarea = gradioApp().querySelector(`#${id}_name textarea`); @@ -869,34 +673,135 @@ function extraNetworksEditUserMetadata(tabname_full, card_name) { editor.button.click(); popup(editor.page); -} +}; -function extraNetworksEditItemOnClick(event, tabname_full, card_name) { - extraNetworksEditUserMetadata(tabname_full, card_name); +const extraNetworksBtnCopyPathOnClick = (event, path) => { + copyToClipboard(path); event.stopPropagation(); -} +}; -function extraNetworksRefreshSingleCard(page, tabname, name) { - requestGet("./sd_extra_networks/get-single-card", {page: page, tabname: tabname, name: name}, function(data) { - if (data && data.html) { - var card = gradioApp().querySelector(`#${tabname}_${page.replace(" ", "_")}_cards > .card[data-name="${name}"]`); +// ==== MAIN SETUP ==== - var newDiv = document.createElement('DIV'); - newDiv.innerHTML = data.html; - var newCard = newDiv.firstElementChild; +const extraNetworksSetupEventDelegators = () => { + /** Sets up event delegators for all extraNetworks tabs. + * + * These event handlers are not tied to any specific elements on the page. + * We do this because elements within each tab may be removed and replaced + * which would break references to elements in DOM and thus prevent any event + * listeners from firing. + */ - newCard.style.display = ''; - card.parentElement.insertBefore(newCard, card); - card.parentElement.removeChild(card); + window.addEventListener("resizeHandleDblClick", event => { + // See resizeHandle.js::onDoubleClick() for event detail. + event.stopPropagation(); + extraNetworksAutoSetTreeWidth(event.target.closest(".extra-network-pane")); + }); + + // 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); + }); + + // Debounce search text input. This way we only search after user is done typing. + const search_input_debounce = debounce((tabname_full) => { + extraNetworksApplyFilter(tabname_full); + }, SEARCH_INPUT_DEBOUNCE_TIME_MS); + + window.addEventListener("keyup", event => { + const controls = event.target.closest(".extra-network-controls"); + if (isElement(controls)) { + const tabname_full = controls.dataset.tabnameFull; + const target = event.target.closest(".extra-network-control--search-text"); + if (isElement(target)) { + search_input_debounce.call(target, tabname_full); + } } }); -} -window.addEventListener("keydown", function(event) { - if (event.key == "Escape") { - closePopup(); + window.addEventListener("keydown", event => { + if (event.key === "Escape") { + closePopup(); + } + }); +}; + +const extraNetworksSetupTabContent = async (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`); + const tree_scroll_elem = await waitForElement(`#${tabname_full}_tree_list_scroll_area`); + const tree_content_elem = await waitForElement(`#${tabname_full}_tree_list_content_area`); + const cards_scroll_elem = await waitForElement(`#${tabname_full}_cards_list_scroll_area`); + const cards_content_elem = await waitForElement(`#${tabname_full}_cards_list_content_area`); + await waitForElement(`#${tabname_full}_pane .extra-network-content--dirs-view`); + + console.log("BEFORE:", tree_scroll_elem, cards_scroll_elem); + 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, + scrollElem: tree_scroll_elem, + contentElem: tree_content_elem, + tag: "div", + callbacks: { + initData: extraNetworksOnInitData, + fetchData: extraNetworksOnFetchData, + }, + }), + cards_list: new ExtraNetworksClusterizeCardsList({ + tabname: tabname, + extra_networks_tabname: extra_networks_tabname, + scrollElem: cards_scroll_elem, + contentElem: cards_content_elem, + tag: "div", + callbacks: { + initData: extraNetworksOnInitData, + fetchData: extraNetworksOnFetchData, + }, + }), + }; + + if (pane.style.display !== "none") { + extraNetworksShowControlsForPage(tabname, tabname_full); } -}); -onUiLoaded(setupExtraNetworks); + await extraNetworksClusterizersLoadTab({ + tabname_full: tabname_full, + selected: false, + fetch_data: true, + }); +}; + +const extraNetworksSetupTab = async (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"); + 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); + } + extraNetworksRegisterPromptForTab(tabname, `${tabname}_prompt`); + extraNetworksRegisterPromptForTab(tabname, `${tabname}_neg_prompt`); +}; + +const extraNetworksSetup = async () => { + await waitForBool(initialUiOptionsLoaded); + + extraNetworksSetupTab('txt2img'); + extraNetworksSetupTab('img2img'); + extraNetworksSetupEventDelegators(); +}; + +onUiLoaded(extraNetworksSetup); onOptionsChanged(() => initialUiOptionsLoaded.state = true); diff --git a/javascript/extraNetworksClusterize.js b/javascript/extraNetworksClusterize.js new file mode 100644 index 000000000..37f7214bb --- /dev/null +++ b/javascript/extraNetworksClusterize.js @@ -0,0 +1,337 @@ +class NotImplementedError extends Error { + constructor(...params) { + super(...params); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, NotImplementedError); + } + + this.name = "NotImplementedError"; + } +} + +class ExtraNetworksClusterize extends Clusterize { + data_obj = {}; + data_obj_keys_sorted = []; + sort_reverse = false; + default_sort_fn = this.sortByDivId; + sort_fn = this.default_sort_fn; + tabname = ""; + extra_networks_tabname = ""; + + // Override base class defaults + default_sort_mode_str = "divId"; + default_sort_dir_str = "ascending"; + default_filter_str = ""; + sort_mode_str = this.default_sort_mode_str; + sort_dir_str = this.default_sort_dir_str; + filter_str = this.default_filter_str; + + constructor(...args) { + super(...args); + + // finish initialization + this.tabname = getValueThrowError(...args, "tabname"); + this.extra_networks_tabname = getValueThrowError(...args, "extra_networks_tabname"); + } + + sortByDivId() { + /** Sort data_obj keys (div_id) as numbers. */ + this.data_obj_keys_sorted = Object.keys(this.data_obj).sort(INT_COLLATOR.compare); + } + + clear() { + this.data_obj = {}; + this.data_obj_keys_sorted = []; + super.clear(); + } + + async initDataDefault() { + /**Fetches the initial data. + * + * This data should be minimal and only contain div IDs and other necessary + * information such as sort keys and terms for filtering. + */ + throw new NotImplementedError(); + } + + async fetchDataDefault(idx_start, idx_end) { + throw new NotImplementedError(); + } + + async sortDataDefault(sort_mode_str, sort_dir_str) { + this.sort_mode_str = sort_mode_str; + this.sort_dir_str = sort_dir_str; + this.sort_reverse = sort_dir_str === "descending"; + + this.data_obj_keys_sorted = this.sort_fn(this.data_obj); + if (this.sort_reverse) { + this.data_obj_keys_sorted = this.data_obj_keys_sorted.reverse(); + } + } + + async filterDataDefault(filter_str) { + throw new NotImplementedError(); + } +} + +class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { + selected_div_id = null; + + constructor(args) { + args.no_data_text = "Directory is empty."; + super(args); + + } + + clear() { + this.selected_div_id = null; + super.clear(); + } + + 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. + */ + let res = ""; + var style = getComputedStyle(document.body); + let bg = style.getPropertyValue("--body-background-fill"); + let fg = style.getPropertyValue("--border-color-primary"); + let text_size = style.getPropertyValue("--button-large-text-size"); + for (let i = 1; i <= depth; i++) { + res += `calc((${i} * ${text_size}) - (${text_size} * 0.6)) 0 0 ${bg} inset,`; + res += `calc((${i} * ${text_size}) - (${text_size} * 0.4)) 0 0 ${fg} inset`; + res += (i + 1 > depth) ? "" : ", "; + } + return res; + } + + #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); + } + } + } + + onRowSelected(div_id, elem, override) { + /** Selects a row and deselects all others. */ + if (!isElementLogError(elem)) { + return; + } + + if (!keyExistsLogError(this.data_obj, div_id)) { + return; + } + + override = override === true; + + if (!isNullOrUndefined(this.selected_div_id) && 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; + } + } + + elem.toggleAttribute("data-selected"); + this.selected_div_id = "selected" in elem.dataset ? div_id : null; + } + + async onRowExpandClick(div_id, elem) { + /** Expands or collapses a row to show/hide children. */ + if (!keyExistsLogError(this.data_obj, div_id)){ + return; + } + + // Toggle state + this.data_obj[div_id].expanded = !this.data_obj[div_id].expanded; + + 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() + + await this.setMaxItems(Object.values(this.data_obj).filter(v => v.visible).length); + } + + async initDataDefault() { + /*Expects an object like the following: + { + parent: null or div_id, + children: array of div_id's, + visible: bool, + expanded: bool, + } + */ + console.log("BLAH:", this.options.callbacks.initData); + this.data_obj = await this.options.callbacks.initData(this.constructor.name); + } + + async fetchDataDefault(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; + } + } + + const data = await this.options.callbacks.fetchData( + this.constructor.name, + this.extra_networks_tabname, + div_ids, + ); + + // 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. + const style = getComputedStyle(document.body); + const text_size = style.getPropertyValue("--button-large-text-size"); + + const res = []; + for (const [div_id, item] of Object.entries(data)) { + const parsed_html = htmlStringToElement(item); + const depth = Number(parsed_html.dataset.depth); + parsed_html.style.paddingLeft = `calc(${depth} * ${text_size})`; + parsed_html.style.boxShadow = this.getBoxShadow(depth); + if (this.data_obj[div_id].expanded) { + parsed_html.dataset.expanded = ""; + } + if (div_id === this.selected_div_id) { + parsed_html.dataset.selected = ""; + } + res.push(parsed_html.outerHTML); + } + + return rows; + } + + async sortDataDefault(sort_mode, sort_dir) { + throw new NotImplementedError(); + } + + async filterDataDefault(filter_str) { + // just return the number of visible objects in our data. + return Object.values(this.data_obj).filter(v => v.visible).length; + } +} + +class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { + constructor(args) { + args.no_data_text = "No files matching filter."; + super(args); + } + + sortByName(data) { + return Object.keys(data).sort((a, b) => { + return STR_COLLATOR.compare(data[a].sort_name, data[b].sort_name); + }); + } + + sortByPath(data) { + return Object.keys(data).sort((a, b) => { + return STR_COLLATOR.compare(data[a].sort_path, data[b].sort_path); + }); + } + + sortByDateCreated(data) { + return Object.keys(data).sort((a, b) => { + return INT_COLLATOR.compare(data[a].sort_date_created, data[b].sort_date_created); + }); + } + + sortByDateModified(data) { + return Object.keys(data).sort((a, b) => { + return INT_COLLATOR.compare(data[a].sort_date_modified, data[b].sort_date_modified); + }); + } + + async initDataDefault() { + /*Expects an object like the following: + { + search_keys: array of strings, + sort_: string, (for various sort modes) + } + */ + console.log("HERE:", this.options.callbacks); + this.data_obj = await this.options.callbacks.initData(this.constructor.name); + } + + async fetchDataDefault(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; + } + } + + const data = await this.options.callbacks.fetchData( + this.constructor.name, + this.extra_networks_tabname, + div_ids, + ); + + return Object.values(data); + } + + async sortDataDefault(sort_mode_str, sort_dir_str) { + switch (sort_mode_str) { + case "name": + this.sort_fn = this.sortByName; + break; + case "path": + this.sort_fn = this.sortByPath; + break; + case "created": + this.sort_fn = this.sortByDateCreated; + break; + case "modified": + this.sort_fn = this.sortByDateModified; + break; + default: + this.sort_fn = this.default_sort_fn; + break; + } + await super.sortDataDefault(sort_mode_str, sort_dir_str) + } + + async filterDataDefault(filter_str) { + /** Filters data by a string and returns number of items after filter. */ + if (isString(filter_str)) { + this.filter_str = filter_str.toLowerCase(); + } else if (isNullOrUndefined(this.filter_str)) { + this.filter_str = this.default_filter_str; + } + + 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; + if (v.search_only && this.filter_str.length < 4) { + visible = false; + } + this.data_obj[div_id].visible = visible; + if (visible) { + n_visible++; + } + } + + return n_visible; + } +} \ No newline at end of file diff --git a/javascript/extraNetworksClusterizeList.js b/javascript/extraNetworksClusterizeList.js deleted file mode 100644 index 960f9bd54..000000000 --- a/javascript/extraNetworksClusterizeList.js +++ /dev/null @@ -1,942 +0,0 @@ -// Prevent eslint errors on functions defined in other files. -/*global - isString, - isStringLogError, - isNull, - isUndefined, - isNullOrUndefined, - isNullOrUndefinedLogError, - isElement, - isElementLogError, - isFunction, - isFunctionLogError, - getElementByIdLogError, - querySelectorLogError, - waitForElement, - Clusterize -*/ -/*eslint no-undef: "error"*/ - -const JSON_UPDATE_DEBOUNCE_TIME_MS = 250; -const RESIZE_DEBOUNCE_TIME_MS = 250; -// Collators used for sorting. -const INT_COLLATOR = new Intl.Collator([], {numeric: true}); -const STR_COLLATOR = new Intl.Collator("en", {numeric: true, sensitivity: "base"}); - -class InvalidCompressedJsonDataError extends Error { - constructor(message, options) { - super(message, options); - } -} - -async function decompress(base64string) { - /** Decompresses a base64 encoded ZLIB compressed string. */ - try { - if (isNullOrUndefined(base64string) || base64string === "") { - throw new Error("invalid base64 string"); - } - const ds = new DecompressionStream("deflate"); - const writer = ds.writable.getWriter(); - const bytes = Uint8Array.from(atob(base64string), c => c.charCodeAt(0)); - writer.write(bytes); - writer.close(); - const arrayBuffer = await new Response(ds.readable).arrayBuffer(); - return await JSON.parse(new TextDecoder().decode(arrayBuffer)); - } catch (error) { - throw new InvalidCompressedJsonDataError(error); - } -} - -class ExtraNetworksClusterize { - /** Base class for a clusterize list. Cannot be used directly. */ - constructor( - { - tabname, - extra_networks_tabname, - scroll_id, - content_id, - data_request_callback, - rows_in_block = 10, - blocks_in_cluster = 4, - show_no_data_row = true, - callbacks = {}, - } = { - rows_in_block: 10, - blocks_in_cluster: 4, - show_no_data_row: true, - callbacks: {}, - } - ) { - // Do not continue if any of the required parameters are invalid. - if (!isStringLogError(tabname)) { - return; - } - if (!isStringLogError(extra_networks_tabname)) { - return; - } - if (!isStringLogError(scroll_id)) { - return; - } - if (!isStringLogError(content_id)) { - return; - } - if (!isFunctionLogError(data_request_callback)) { - return; - } - - 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; - this.callbacks = callbacks; - - this.clusterize = null; - - this.scroll_elem = null; - this.content_elem = null; - - this.element_observer = null; - - this.resize_observer = null; - this.resize_observer_timer = null; - - // Used to control logic. Many functions immediately return when disabled. - this.enabled = false; - - // Stores the current encoded string so we can compare against future versions. - this.encoded_str = ""; - - this.no_data_text = "No results."; - this.no_data_class = "clusterize-no-data"; - - 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; - } - - 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; - } - - async setup() { - return new Promise(resolve => { - // 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(); - }); - }); - } - - 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) => { - Promise.resolve(encoded_str) - .then(v => decompress(v)) - .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 => { - return resolve(); - }); - } - - sortByDivId() { - /** Sort data_obj keys (div_id) as numbers. */ - this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => INT_COLLATOR.compare(a, b)); - } - - updateDivContent(div_id, content) { - /** Updates an element's html in the dataset. - * - * NOTE: This function only updates the dataset. Calling function must call - * 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. - */ - if (!(div_id in this.data_obj)) { - console.error("div_id not in data_obj:", div_id); - } else if (isElement(content)) { - this.data_obj[div_id].html = content.outerHTML; - return true; - } else if (isString(content)) { - this.data_obj[div_id].html = content; - return true; - } else { - console.error("Invalid content:", div_id, content); - } - - return false; - } - - getMaxRowWidth() { - console.error("getMaxRowWidth:: Not implemented in base class. Must be overridden."); - return; - } - - 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); - // Fix resize observers since they are bound to each element individually. - if (!isNullOrUndefined(this.resize_observer)) { - this.resize_observer.disconnect(); - this.resize_observer.observe(this.scroll_elem); - this.resize_observer.observe(this.content_elem); - } - // Make sure to refresh forcefully after updating the dom. - this.refresh(true); - return true; - } - return false; - } - - onResize(elem_id) { - /** Callback whenever one of our visible elements is resized. */ - 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.scroll_id: - this.repair(); - break; - case this.content_id: - this.repair(); - break; - default: - break; - } - } - - setupElementObservers() { - /** Listens for changes to the data, scroll, and content elements. - * - * During testing, the scroll/content elements would frequently get removed from - * the DOM. Our clusterize instance stores a reference to these elements - * which breaks whenever these elements are removed from the DOM. To fix this, - * we need to check for these changes and re-attach our stores elements by - * replacing the ones in the DOM with the ones in our clusterize instance. - * - * We also use an observer to detect whenever the data element gets a new set - * of JSON data so that we can update our dataset. - */ - this.element_observer = new MutationObserver((mutations) => { - // don't waste time if this object isn't enabled. - if (!this.enabled) { - return; - } - - let scroll_elem = gradioApp().getElementById(this.scroll_id); - if (scroll_elem && scroll_elem !== this.scroll_elem) { - this.onElementDetached(scroll_elem.id); - } - - let content_elem = gradioApp().getElementById(this.content_id); - if (content_elem && content_elem !== this.content_elem) { - this.onElementDetached(content_elem.id); - } - }); - this.element_observer.observe(gradioApp(), {subtree: true, childList: true, attributes: true}); - } - - 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) { - if (entry.target.id === this.scroll_id || entry.target.id === this.content_id) { - // debounce the event - clearTimeout(this.resize_observer_timer); - this.resize_observer_timer = setTimeout(() => this.onResize(entry.id), RESIZE_DEBOUNCE_TIME_MS); - } - } - }); - - this.resize_observer.observe(this.scroll_elem); - 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 - * its dimensions have changed. - */ - if (isNullOrUndefined(this.clusterize)) { - return; - } - - // Only allow boolean variables. default to false. - if (force !== true) { - force = false; - } - this.clusterize.refresh(force); - } - - rowCount() { - /** Gets the total (not only visible) row count in the clusterize instance. */ - return this.clusterize.getRowsAmount(); - } - - 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. - 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...
"; - } - } -} - -class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { - /** Subclass used to display a directories/files in the Tree View. */ - constructor(...args) { - super(...args); - - this.no_data_text = "No directories/files"; - 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. - */ - let res = ""; - var style = getComputedStyle(document.body); - let bg = style.getPropertyValue("--body-background-fill"); - let fg = style.getPropertyValue("--border-color-primary"); - let text_size = style.getPropertyValue("--button-large-text-size"); - for (let i = 1; i <= depth; i++) { - res += `calc((${i} * ${text_size}) - (${text_size} * 0.6)) 0 0 ${bg} inset,`; - res += `calc((${i} * ${text_size}) - (${text_size} * 0.4)) 0 0 ${fg} inset`; - res += (i + 1 > depth) ? "" : ", "; - } - return res; - } - - updateJson(json) { - /** Processes JSON object and adds each entry to our data object. */ - return new Promise(resolve => { - var style = getComputedStyle(document.body); - let text_size = style.getPropertyValue("--button-large-text-size"); - for (const [k, v] of Object.entries(json)) { - let div_id = k; - let parsed_html = htmlStringToElement(v); - // parent_id = -1 if item is at root level - let parent_id = "parentId" in parsed_html.dataset ? parsed_html.dataset.parentId : -1; - let expanded = "expanded" in parsed_html.dataset; - let selected = "selected" in parsed_html.dataset; - let depth = Number(parsed_html.dataset.depth); - parsed_html.style.paddingLeft = `calc(${depth} * ${text_size})`; - parsed_html.style.boxShadow = this.getBoxShadow(depth); - - // Add the updated html to the data object. - this.data_obj[div_id] = { - html: parsed_html.outerHTML, - active: parent_id === -1, // always show root - expanded: expanded || (parent_id === -1), // always expand root - selected: selected, - parent: parent_id, - children: [], // populated later - }; - - // maybe not necessary. - parsed_html = null; - } - - // Build list of children for each element in dataset. - for (const [k, v] of Object.entries(this.data_obj)) { - if (v.parent === -1) { - continue; - } else if (!(v.parent in this.data_obj)) { - console.error("parent not in data:", v.parent); - } else { - this.data_obj[v.parent].children.push(k); - } - } - - // Handle expanding of rows on initial load - for (const [k, v] of Object.entries(this.data_obj)) { - if (v.parent === -1) { - // Always show root level. - this.data_obj[k].active = true; - } else if (this.data_obj[v.parent].expanded && this.data_obj[v.parent].active) { - // Parent is both active and expanded. show child - this.data_obj[k].active = true; - } else { - this.data_obj[k].active = false; - } - } - return resolve(); - }); - } - - removeChildRows(div_id) { - /** Removes rows from the list that are children of the passed div. - * The rows aren't removed from the data object, just set to active=false - * so they aren't displayed. - */ - for (const child_id of this.data_obj[div_id].children) { - this.data_obj[child_id].active = false; - if (this.data_obj[child_id].selected) { - // deselect the child only if it is selected. - let elem = htmlStringToElement(this.data_obj[child_id].html); - delete elem.dataset.selected; - this.data_obj[child_id].selected = false; - this.updateDivContent(child_id, elem); - } - this.removeChildRows(child_id); - } - } - - addChildRows(div_id) { - /** Adds rows to the list that are children of the passed div. - * The rows aren't added to the data object, just set to active=true - * so they are displayed. - */ - for (const child_id of this.data_obj[div_id].children) { - this.data_obj[child_id].active = true; - if (this.data_obj[child_id].expanded) { - this.addChildRows(child_id); - } - } - } - - onRowExpandClick(div_id, elem) { - /** Toggles expand/collapse of a row's children. */ - if ("expanded" in elem.dataset) { - this.data_obj[div_id].expanded = false; - delete elem.dataset.expanded; - this.removeChildRows(div_id); - } else { - this.data_obj[div_id].expanded = true; - elem.dataset.expanded = ""; - this.addChildRows(div_id); - } - this.updateDivContent(div_id, elem); - if (this.calculateDims()) { - this.rebuild(this.getFilteredRows()); - } else { - this.update(this.getFilteredRows()); - } - } - - _setRowSelectedState(div_id, elem, new_state) { - if (new_state) { - elem.dataset.selected = ""; - } else { - delete elem.dataset.selected; - } - this.data_obj[div_id].selected = new_state; - this.updateDivContent(div_id, elem); - } - - onRowSelected(div_id, elem, override) { - /** Selects a row and deselects all others. */ - if (!isElementLogError(elem)) { - return; - } - if (!(div_id in this.data_obj)) { - console.error("div_id not in dataset:", div_id); - return; - } - - if (!isNullOrUndefined(override)) { - override = (override === true); - } - - if (!isNullOrUndefined(this.selected_div_id) && div_id !== this.selected_div_id) { - // deselect the current selected row - let prev_elem = htmlStringToElement(this.data_obj[this.selected_div_id].html); - this._setRowSelectedState(this.selected_div_id, prev_elem, false); - - // select the new row - this._setRowSelectedState(div_id, elem, true); - this.selected_div_id = div_id; - } else { - // toggle the passed row's selected state. - if (this.data_obj[div_id].selected) { - this._setRowSelectedState(div_id, elem, false); - this.selected_div_id = null; - } else { - this._setRowSelectedState(div_id, elem, true); - this.selected_div_id = div_id; - } - } - - this.update(this.getFilteredRows()); - } - - getMaxRowWidth() { - /** 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; - } - if (this.rowCount() === 0) { - // If there is no data then just skip. - return false; - } - - let max_width = 0; - for (let i = 0; i < this.content_elem.children.length; i += this.n_cols) { - let row_width = 0; - for (let j = 0; j < this.n_cols; j++) { - const child = this.content_elem.children[i + j]; - const child_style = window.getComputedStyle(child, null); - const prev_style = child.style.cssText; - const n_cols = child_style.getPropertyValue("grid-template-columns").split(" ").length; - child.style.gridTemplateColumns = `repeat(${n_cols}, max-content)`; - row_width += child.scrollWidth; - // Restore previous style. - child.style.cssText = prev_style; - } - max_width = Math.max(max_width, row_width); - } - if (max_width <= 0) { - return; - } - - // Adds the scroll element's border and the scrollbar's width to the result. - // If scrollbar isn't visible, then only the element border is added. - max_width += this.scroll_elem.offsetWidth - this.scroll_elem.clientWidth; - return max_width; - } -} - -class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { - /** Subclass used to display cards in the Cards View. */ - constructor(...args) { - super(...args); - - this.no_data_text = "No files matching filter."; - 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 => { - this.data_obj = {}; - for (const [i, [k, v]] of Object.entries(Object.entries(json))) { - let div_id = k; - let parsed_html = htmlStringToElement(v); - let search_only = isElement(parsed_html.querySelector(".search_only")); - let search_terms_elem = parsed_html.querySelector(".search_terms"); - let search_terms = ""; - if (isElement(search_terms_elem)) { - search_terms = Array.prototype.map.call( - parsed_html.querySelectorAll(".search_terms"), - (elem) => { - return elem.textContent.toLowerCase(); - } - ).join(" "); - } - - // Add the updated html to the data object. - this.data_obj[div_id] = { - active: true, - html: v, - sort_name: parsed_html.dataset.sortName, - sort_path: parsed_html.dataset.sortPath, - sort_created: parsed_html.dataset.sortCreated, - sort_modified: parsed_html.dataset.sortModified, - search_only: search_only, - search_terms: search_terms, - }; - - // maybe not necessary - parsed_html = null; - } - return resolve(); - }); - } - - sortByName() { - this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => { - return STR_COLLATOR.compare( - this.data_obj[a].sort_name, - this.data_obj[b].sort_name, - ); - }); - } - - sortByPath() { - this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => { - return STR_COLLATOR.compare( - this.data_obj[a].sort_path, - this.data_obj[b].sort_path, - ); - }); - } - - sortByCreated() { - this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => { - return INT_COLLATOR.compare( - this.data_obj[a].sort_created, - this.data_obj[b].sort_created, - ); - }); - } - - sortByModified() { - this.data_obj_keys_sorted = Object.keys(this.data_obj).sort((a, b) => { - return INT_COLLATOR.compare( - this.data_obj[a].sort_modified, - this.data_obj[b].sort_modified, - ); - }); - } - - setSortMode(sort_mode_str) { - this.sort_mode_str = sort_mode_str; - } - - setSortDir(sort_dir_str) { - this.sort_dir_str = sort_dir_str; - } - - sortData() { - this.sort_reverse = this.sort_dir_str === "descending"; - - switch (this.sort_mode_str) { - case "name": - this.sort_fn = this.sortByName; - break; - case "path": - this.sort_fn = this.sortByPath; - break; - case "created": - this.sort_fn = this.sortByCreated; - break; - case "modified": - this.sort_fn = this.sortByModified; - break; - default: - this.sort_fn = this.sortByDivId; - break; - } - super.sortData(); - } - - applyFilter(filter_str) { - /** Filters our data object by setting each member's `active` attribute then sorts the result. */ - if (!isNullOrUndefined(filter_str)) { - this.filter_str = filter_str.toLowerCase(); - } else if (isNullOrUndefined(this.filter_str)) { - this.filter_str = this.default_filter_str; - } - - for (const [k, v] of Object.entries(this.data_obj)) { - let visible = v.search_terms.indexOf(this.filter_str) != -1; - if (v.search_only && this.filter_str.length < 4) { - visible = false; - } - this.data_obj[k].active = visible; - } - - super.applyFilter(); - } - - getMaxRowWidth() { - /** Calculates the width of the widest pseudo-row in the list. */ - if (!this.enabled) { - // Inactive list is not displayed on screen. Can't calculate size. - return false; - } - if (this.rowCount() === 0) { - // If there is no data then just skip. - return false; - } - - let max_width = 0; - for (let i = 0; i < this.content_elem.children.length; i += this.n_cols) { - let row_width = 0; - for (let j = 0; j < this.n_cols; j++) { - row_width += getComputedDims(this.content_elem.children[i + j]).width; - } - max_width = Math.max(max_width, row_width); - } - if (max_width <= 0) { - return; - } - - // Add the container's padding to the result. - max_width += getComputedPaddingDims(this.content_elem).width; - // Add the scrollbar's width to the result. Will add 0 if scrollbar isnt present. - max_width += this.scroll_elem.offsetWidth - this.scroll_elem.clientWidth; - return max_width; - } -} diff --git a/javascript/utils.js b/javascript/utils.js index fe213d6b3..88694c93c 100644 --- a/javascript/utils.js +++ b/javascript/utils.js @@ -5,29 +5,29 @@ const isNumberLogError = x => { if (isNumber(x)) { return true; } - console.error("expected number, got:", typeof x); + console.error(`expected number, got: ${typeof x}`); return false; -} +}; const isNumberThrowError = x => { if (isNumber(x)) { return; } - throw new Error("expected number, got:", typeof x); -} + throw new Error(`expected number, got: ${typeof x}`); +}; const isString = x => typeof x === "string" || x instanceof String; const isStringLogError = x => { if (isString(x)) { return true; } - console.error("expected string, got:", typeof x); + console.error(`expected string, got: ${typeof x}`); return false; }; const isStringThrowError = x => { if (isString(x)) { return; } - throw new Error("expected string, got:", typeof x); + throw new Error(`expected string, got: ${typeof x}`); }; const isNull = x => x === null; @@ -53,14 +53,14 @@ const isElementLogError = x => { if (isElement(x)) { return true; } - console.error("expected element type, got:", typeof x); + console.error(`expected element type, got: ${typeof x}`); return false; }; const isElementThrowError = x => { if (isElement(x)) { return; } - throw new Error("expected element type, got:", typeof x); + throw new Error(`expected element type, got: ${typeof x}`); }; const isFunction = x => typeof x === "function"; @@ -68,14 +68,62 @@ const isFunctionLogError = x => { if (isFunction(x)) { return true; } - console.error("expected function type, got:", typeof x); + console.error(`expected function type, got: ${typeof x}`); return false; }; const isFunctionThrowError = x => { if (isFunction(x)) { return; } - throw new Error("expected function type, got:", typeof x); + throw new Error(`expected function type, got: ${typeof x}`); +}; + +const isObject = x => typeof x === "object" && !Array.isArray(x); +const isObjectLogError = x => { + if (isObject(x)) { + return true; + } + console.error(`expected object type, got: ${typeof x}`); + return false; +}; +const isObjectThrowError = x => { + if (isObject(x)) { + return; + } + throw new Error(`expected object type, got: ${typeof x}`); +}; + +const keyExists = (obj, k) => isObject(obj) && isString(k) && k in obj; +const keyExistsLogError = (obj, k) => { + if (keyExists(obj, k)) { + return true; + } + console.error(`key does not exist in object: ${k}`); + return false; +}; +const keyExistsThrowError = (obj, k) => { + if (keyExists(obj, k)) { + return; + } + throw new Error(`key does not exist in object: ${k}`) +}; + +const getValue = (obj, k) => { + /** Returns value of object for given key if it exists, otherwise returns null. */ + if (keyExists(obj, k)) { + return obj[k]; + } + return null; +}; +const getValueLogError = (obj, k) => { + if (keyExistsLogError(obj, k)) { + return obj[k]; + } + return null; +}; +const getValueThrowError = (obj, k) => { + keyExistsThrowError(obj, k); + return obj[k]; }; const getElementByIdLogError = selector => { @@ -156,17 +204,17 @@ const getComputedDims = elem => { }; }; -const calcColsPerRow = function(parent, child) { +const calcColsPerRow = function (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); + return parseInt(parent_inner_width / getComputedDims(child).width, 10); }; -const calcRowsPerCol = function(parent, child) { +const calcRowsPerCol = function (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); + return parseInt(parent_inner_height / getComputedDims(child).height, 10); }; /** Functions for asynchronous operations. */ @@ -271,7 +319,7 @@ const waitForValueInObject = o => { * Resolves when obj[k] == v */ return new Promise(resolve => { - waitForKeyInObject({k: o.k, obj: o.obj}).then(() => { + waitForKeyInObject({ k: o.k, obj: o.obj }).then(() => { (function _waitForValueInObject() { if (o.k in o.obj && o.obj[o.k] == o.v) { @@ -283,17 +331,93 @@ const waitForValueInObject = o => { }); }; +/** Requests */ + +const requestGet = (url, data, handler, errorHandler) => { + var xhr = new XMLHttpRequest(); + var args = Object.keys(data).map(function (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 { + var js = JSON.parse(xhr.responseText); + handler(js); + } catch (error) { + console.error(error); + errorHandler(); + } + } else { + errorHandler(); + } + } + }; + var js = JSON.stringify(data); + xhr.send(js); +}; + +const 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)); + }); +}; + /** Misc helper functions. */ const clamp = (x, min, max) => Math.max(min, Math.min(x, max)); const getStyle = (prop, elem) => { return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop]; -} +}; -const htmlStringToElement = function(str) { +const htmlStringToElement = function (str) { /** Converts an HTML string into an Element type. */ let parser = new DOMParser(); let tmp = parser.parseFromString(str, "text/html"); return tmp.body.firstElementChild; +}; + +const toggleCss = (key, css, enable) => { + var style = document.getElementById(key); + if (enable && !style) { + style = document.createElement('style'); + style.id = key; + style.type = 'text/css'; + document.head.appendChild(style); + } + if (style && !enable) { + document.head.removeChild(style); + } + if (style) { + style.innerHTML == ''; + style.appendChild(document.createTextNode(css)); + } +}; + +const copyToClipboard = s => { + /** Copies the passed string to the clipboard. */ + isStringThrowError(s); + navigator.clipboard.writeText(s); }; \ No newline at end of file diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 85b8ee12e..51937b73d 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -4,12 +4,12 @@ import urllib.parse from base64 import b64decode from io import BytesIO from pathlib import Path -from typing import Optional, Union +from typing import Optional, Union, Any, Callable from dataclasses import dataclass import zlib import base64 import re -from starlette.responses import JSONResponse, PlainTextResponse +from starlette.responses import Response, FileResponse, 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 @@ -40,6 +40,112 @@ class ExtraNetworksItem: item: dict +class ListItem: + """ + Attributes: + id [str]: The ID of this list item. + html [str]: The HTML string for this item. + """ + def __init__(self, _id: str, _html: str) -> None: + self.id = _id + self.html = _html + + +class CardListItem(ListItem): + """ + Attributes: + visible [bool]: Whether the item should be shown in the list. + sort_keys [dict]: Nested dict where keys are sort modes and values are sort keys. + """ + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self.visible: bool = False + self.sort_keys = {} + self.search_terms = [] + + +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) + + self.node: Optional[DirectoryTreeNode] = None + self.visible: bool = False + self.expanded: bool = False + self.selected: bool = False + + +class DirectoryTreeNode: + def __init__( + self, + root_dir: str, + abspath: str, + parent: Optional["DirectoryTreeNode"] = None, + ) -> None: + self.abspath = abspath + self.root_dir = root_dir + self.parent = parent + + self.is_dir = False + self.item = None + self.relpath = os.path.relpath(self.abspath, self.root_dir) + self.children: list["DirectoryTreeNode"] = [] + + # If a parent is passed, then we add this instance to the parent's children. + if self.parent is not None: + self.parent.add_child(self) + + def add_child(self, child: "DirectoryTreeNode") -> None: + self.children.append(child) + + def build(self, items: dict[str, dict]) -> None: + """Builds a tree of nodes as children of this instance. + + Args: + items: A dictionary where keys are absolute filepaths for directories/files. + The values are dictionaries representing extra networks items. + """ + self.is_dir = os.path.isdir(self.abspath) + 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) + 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. + + Args: + res: The dictionary result updated in place. On initial call, should be passed + as an empty dictionary. + dirs_only: Whether to only add directories to the result. + + Raises: + KeyError: If any nodes in the tree have the same ID. + """ + 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 + + for child in self.children: + child.flatten(res, dirs_only) + + def apply(self, fn: Callable) -> None: + """Recursively calls passed function with instance for entire tree.""" + fn(self) + 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. @@ -100,10 +206,15 @@ def register_page(page): allowed_dirs.clear() allowed_dirs.update(set(sum([x.allowed_directories_for_previews() for x in extra_pages], []))) +def get_page_by_name(extra_networks_tabname: str = "") -> "ExtraNetworksPage": + for page in extra_pages: + if page.extra_networks_tabname == extra_networks_tabname: + return page + + raise HTTPException(status_code=404, detail=f"Page not found: {extra_networks_tabname}") + def fetch_file(filename: str = ""): - from starlette.responses import FileResponse - if not os.path.isfile(filename): raise HTTPException(status_code=404, detail="File not found") @@ -118,12 +229,8 @@ def fetch_file(filename: str = ""): return FileResponse(filename, headers={"Accept-Ranges": "bytes"}) -def fetch_cover_images(page: str = "", item: str = "", index: int = 0): - from starlette.responses import Response - - page = next(iter([x for x in extra_pages if x.name == page]), None) - if page is None: - raise HTTPException(status_code=404, detail="File not found") +def fetch_cover_images(extra_networks_tabname: str = "", item: str = "", index: int = 0): + page = get_page_by_name(extra_networks_tabname) metadata = page.metadata.get(item) if metadata is None: @@ -142,70 +249,82 @@ def fetch_cover_images(page: str = "", item: str = "", index: int = 0): except Exception as err: raise ValueError(f"File cannot be fetched: {item}. Failed to load cover image.") from err +def init_tree_data(tabname: str = "", extra_networks_tabname: str = "") -> JSONResponse: + page = get_page_by_name(extra_networks_tabname) -def get_list_data( - tabname: str = "", + data = page.generate_tree_view_data(tabname) + + return JSONResponse(data, status_code=200) + +def fetch_tree_data( 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. + div_ids: Optional[list[str]] = None, +) -> JSONResponse: + page = get_page_by_name(extra_networks_tabname) - 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`. + if div_ids is None: + return JSONResponse({}) - 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 + res = {} + for div_id in div_ids: + if div_id in page.tree: + res[div_id] = page.tree[div_id].html - 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) + return JSONResponse(res) -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: +def fetch_cards_data( + extra_networks_tabname: str = "", + div_ids: Optional[list[str]] = None, +) -> JSONResponse: + page = get_page_by_name(extra_networks_tabname) + + if div_ids is None: + return JSONResponse({}) + + res = {} + for div_id in div_ids: + if div_id in page.cards: + res[div_id] = page.cards[div_id].html + + return JSONResponse(res) + + +def init_cards_data(tabname: str = "", extra_networks_tabname: str = "") -> JSONResponse: + page = get_page_by_name(extra_networks_tabname) + + data = page.generate_cards_view_data(tabname) + + return JSONResponse(data, status_code=200) + +def get_metadata(extra_networks_tabname: str = "", item: str = "") -> JSONResponse: + try: + page = get_page_by_name(extra_networks_tabname) + except HTTPException: return JSONResponse({}) metadata = page.metadata.get(item) if metadata is None: return JSONResponse({}) - metadata = {i:metadata[i] for i in metadata if i != 'ssmd_cover_images'} # those are cover images, and they are too big to display in UI as text + # 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'} return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)}) -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) +def get_single_card(tabname: str = "", extra_networks_tabname: str = "", name: str = "") -> JSONResponse: + page = get_page_by_name(extra_networks_tabname) try: item = page.create_item(name, enable_filter=False) page.items[name] = item - except Exception as e: - errors.display(e, "creating item for extra network") - item = page.items.get(name) + except Exception as exc: + errors.display(exc, "creating item for extra network") + item = page.items.get(name, None) + + if item is None: + return JSONResponse({}) page.read_user_metadata(item, use_cache=False) item_html = page.create_card_html(tabname=tabname, item=item) @@ -217,7 +336,11 @@ def add_pages_to_demo(app): 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"]) + #app.add_api_route("/sd_extra_networks/init-tree-data", init_tree_data, methods=["GET"]) + #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"]) + def quote_js(s): @@ -236,18 +359,26 @@ class ExtraNetworksPage: self.allow_negative_prompt = False self.metadata = {} self.items = {} + self.cards = {} self.tree = {} + self.tree_roots = {} self.lister = util.MassFileLister() # HTML Templates self.pane_tpl = shared.html("extra-networks-pane.html") - self.pane_content_tree_tpl = shared.html("extra-networks-pane-tree.html") - self.pane_content_dirs_tpl = shared.html("extra-networks-pane-dirs.html") self.card_tpl = shared.html("extra-networks-card.html") self.tree_row_tpl = shared.html("extra-networks-tree-row.html") - self.btn_copy_path_tpl = shared.html("extra-networks-copy-path-button.html") - self.btn_metadata_tpl = shared.html("extra-networks-metadata-button.html") - self.btn_edit_item_tpl = shared.html("extra-networks-edit-item-button.html") - self.btn_dirs_view_tpl = shared.html("extra-networks-dirs-view-button.html") + self.btn_copy_path_tpl = shared.html("extra-networks-btn-copy-path.html") + self.btn_show_metadata_tpl = shared.html("extra-networks-btn-show-metadata.html") + self.btn_edit_metadata_tpl = shared.html("extra-networks-btn-edit-metadata.html") + self.btn_dirs_view_item_tpl = shared.html("extra-networks-btn-dirs-view-item.html") + # Sorted lists + # These just store ints so it won't use hardly any memory to just sort ahead + # of time for each sort mode. These are lists of keys for each file. + self.keys_sorted = {} + self.keys_by_name = [] + self.keys_by_path = [] + self.keys_by_created = [] + self.keys_by_modified = [] def refresh(self): pass @@ -281,8 +412,8 @@ class ExtraNetworksPage: tabname: str, label: str, btn_type: str, - data_attributes: Optional[dict] = None, btn_title: Optional[str] = None, + data_attributes: Optional[dict] = None, dir_is_empty: bool = False, item: Optional[dict] = None, onclick_extra: Optional[str] = None, @@ -340,92 +471,6 @@ class ExtraNetworksPage: res = re.sub(" +", " ", res.replace("\n", "")) return res - def build_tree_html_dict( - self, - tree: dict, - res: dict, - tabname: str, - div_id: int, - depth: int, - parent_id: Optional[int] = None, - ) -> int: - for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])): - if not isinstance(v, (ExtraNetworksItem,)): - # dir - if div_id in res: - raise KeyError("div_id already in res:", div_id) - - dir_is_empty = True - for _v in v.values(): - if shared.opts.extra_networks_tree_view_show_files: - dir_is_empty = False - break - elif not isinstance(_v, (ExtraNetworksItem,)): - dir_is_empty = False - break - else: - dir_is_empty = True - - res[div_id] = self.build_tree_html_dict_row( - tabname=tabname, - label=os.path.basename(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, - res=res, - depth=depth + 1, - div_id=div_id + 1, - parent_id=div_id, - tabname=tabname, - ) - div_id = last_div_id - else: - # file - if not shared.opts.extra_networks_tree_view_show_files: - # Don't add file if showing files is disabled in options. - continue - - if div_id in res: - raise KeyError("div_id already in res:", div_id) - - onclick = v.item.get("onclick", None) - if onclick is None: - # Don't quote prompt/neg_prompt since they are stored as js strings already. - onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');") - - res[div_id] = self.build_tree_html_dict_row( - tabname=tabname, - label=html.escape(v.item.get("name", "").strip()), - btn_type="file", - 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 get_button_row(self, tabname: str, item: dict) -> str: metadata = item.get("metadata", None) name = item.get("name", "") @@ -434,14 +479,14 @@ class ExtraNetworksPage: 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( + btn_edit_item = self.btn_edit_metadata_tpl.format( tabname=tabname, extra_networks_tabname=self.extra_networks_tabname, name=name, ) btn_metadata = "" if metadata: - btn_metadata = self.btn_metadata_tpl.format( + btn_metadata = self.btn_show_metadata_tpl.format( extra_networks_tabname=self.extra_networks_tabname, name=name, ) @@ -457,7 +502,7 @@ class ExtraNetworksPage: self, tabname: str, item: dict, - div_id: Optional[int] = None, + div_id: Optional[str] = None, ) -> str: """Generates HTML for a single ExtraNetworks Item. @@ -469,44 +514,37 @@ class ExtraNetworksPage: Returns: 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}%;" - style = style_height + style_width + style_font_size - background_image = f'' if preview else '' + style = f"font-size: {shared.opts.extra_networks_card_text_scale*100}%;" + if shared.opts.extra_networks_card_height: + style += f"height: {shared.opts.extra_networks_card_height}px;" + if shared.opts.extra_networks_card_width: + style += f"width: {shared.opts.extra_networks_card_width}px;" + + background_image = None + preview = html.escape(item.get("preview", "")) + if preview: + background_image = f'' onclick = item.get("onclick", None) if onclick is None: - # Don't quote prompt/neg_prompt since they are stored as js strings already. onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');") button_row = self.get_button_row(tabname, item) - local_path = "" filename = item.get("filename", "") - for reldir in self.allowed_directories_for_previews(): - absdir = os.path.abspath(reldir) - - if filename.startswith(absdir): - local_path = filename[len(absdir):] - - # if this is true, the item must not be shown in the default view, and must instead only be - # shown when searching for it + # if this is true, the item must not be shown in the default view, + # and must instead only be shown when searching for it if shared.opts.extra_networks_hidden_models == "Always": search_only = False else: - search_only = "/." in local_path or "\\." in local_path + search_only = filename.startswith(".") if search_only and shared.opts.extra_networks_hidden_models == "Never": return "" - sort_keys = " ".join( - [ - f'data-sort-{k}="{html.escape(str(v))}"' - for k, v in item.get("sort_keys", {}).items() - ] - ).strip() + sort_keys = {} + for sort_mode, sort_key in item.get("sort_keys", {}).items(): + sort_keys[sort_mode.strip().lower()] = html.escape(str(sort_key)) search_terms_html = "" search_terms_tpl = "" @@ -518,7 +556,10 @@ class ExtraNetworksPage: } ) - description = (item.get("description", "") or "" if shared.opts.extra_networks_card_show_desc else "") + description = "" + if shared.opts.extra_networks_card_show_desc: + description = item.get("description", "") + if not shared.opts.extra_networks_card_description_is_html: description = html.escape(description) @@ -530,6 +571,7 @@ class ExtraNetworksPage: "data-prompt": item.get("prompt", "").strip(), "data-neg-prompt": item.get("negative_prompt", "").strip(), "data-allow-neg": self.allow_negative_prompt, + **{f"data-sort-{sort_mode}": sort_key for sort_mode, sort_key in sort_keys}, } data_attributes_str = "" @@ -553,97 +595,163 @@ class ExtraNetworksPage: description=description, ) - def generate_tree_view_data(self, tabname: str) -> str: - """Generates tree view HTML as a base64 encoded zlib compressed json string.""" + def generate_cards_view_data(self, tabname: str) -> dict: + for i, item in enumerate(self.items.values()): + div_id = str(i) + card_html = self.create_card_html(tabname=tabname, item=item, div_id=div_id) + sort_keys = { + k.strip().lower().replace(" ", "_"): html.escape(str(v)) + for k, v in item.get("sort_keys", {}).items() + } + search_terms = item.get("search_terms", []) + self.cards[div_id] = CardListItem(div_id, card_html) + self.cards[div_id].sort_keys = sort_keys + self.cards[div_id].search_terms = search_terms + + # Sort cards for all sort modes + sort_modes = ["name", "path", "date_created", "date_modified"] + for mode in sort_modes: + self.keys_sorted[mode] = sorted( + self.cards.keys(), + key=lambda k: shared.natural_sort_key(self.cards[k].sort_keys[mode]), + ) + + res = {} + for div_id, card_item in self.cards.items(): + res[div_id] = { + **{f"sort_{mode}": key for mode, key in card_item.sort_keys.items()}, + "search_terms": card_item.search_terms, + } + return res + + def generate_tree_view_data(self, tabname: str) -> dict: + if not self.tree_roots: + return {} + + # Flatten each root into a single dict + tree = {} + for node in self.tree_roots.values(): + subtree = {} + node.flatten(subtree) + tree.update(subtree) + + path_to_div_id = {} + div_id_to_node = {} # reverse mapping + # First assign div IDs to each node. Used for parent ID lookup later. + for i, path in enumerate(sorted(tree.keys(), key=shared.natural_sort_key)): + div_id = str(i) + path_to_div_id[path] = div_id + div_id_to_node[div_id] = tree[path] + + 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 + parent_id = None + 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) + self.tree[div_id].html = self.build_tree_html_dict_row( + tabname=tabname, + label=os.path.basename(node.abspath), + btn_type="dir", + btn_title=node.abspath, + 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": node.depth, + "data-path": node.abspath, + "data-expanded": node.parent is None, # Expand root directories + }, + ) + else: # file + if not show_files: + # Don't add file if files are disabled in the options. + continue + + onclick = node.item.get("onclick", None) + if onclick is None: + onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');") + + item_name = node.item.get("name", "").strip() + self.tree[div_id].html = self.build_tree_html_dict_row( + tabname=tabname, + label=html.escape(item_name), + btn_type="file", + data_attributes={ + "data-div-id": div_id, + "data-parent-id": parent_id, + "data-tree-entry-type": "file", + "data-name": item_name, + "data-depth": node.depth, + "data-path": node.item.get("filename", "").strip(), + "data-hash": node.item.get("shorthash", None), + "data-prompt": node.item.get("prompt", "").strip(), + "data-neg-prompt": node.item.get("negative_prompt", "").strip(), + "data-allow-neg": self.allow_negative_prompt, + }, + item=node.item, + onclick_extra=onclick, + ) + res = {} - if self.tree: - self.build_tree_html_dict( - tree=self.tree, - res=res, - depth=0, - div_id=0, - parent_id=None, - tabname=tabname, - ) + # Expand all root directories and set them to active so they are displayed. + for path in self.tree_roots.keys(): + div_id = path_to_div_id[path] + self.tree[div_id].expanded = True + self.tree[div_id].visible = True + # Set all direct children to active + for child_node in self.tree[div_id].node.children: + self.tree[child_node.id].visible = True - return base64.b64encode( - zlib.compress( - json.dumps(res, separators=(",", ":"), ensure_ascii=True).encode("utf-8") - ) - ).decode("utf-8") + for div_id, tree_item in self.tree.items(): + # Expand root nodes and make them visible. + expanded = tree_item.node.parent is None + visible = tree_item.node.parent is None + parent = None + if tree_item.node.parent is not None: + parent = path_to_div_id[tree_item.node.parent.abspath] + # Direct children of root nodes should be visible by default. + if tree_item.node.parent.node is None: + visible = True - def generate_tree_view_data_div(self, tabname: str) -> str: - """Generates HTML for displaying folders in a tree view. + res[div_id] = { + "parent": parent, + "children": [path_to_div_id[child.abspath] for child in tree_item.node.children], + "visible": visible, + "expanded": expanded, + } + return res - Args: - tabname: The name of the active tab. - - Returns: - HTML string generated for this tree view. - """ - tpl = """""" - return tpl.format( - tabname_full=f"{tabname}_{self.extra_networks_tabname}", - data=self.generate_tree_view_data(tabname), - ) def create_dirs_view_html(self, tabname: str) -> str: """Generates HTML for displaying folders.""" + # Flatten each root into a single dict. Only get the directories for buttons. + tree = {} + for node in self.tree_roots.values(): + subtree = {} + node.flatten(subtree, dirs_only=True) + tree.update(subtree) - def _get_dirs_buttons(tree: dict, res: list) -> None: - """ Builds a list of directory names from a tree. """ - for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])): - if not isinstance(v, (ExtraNetworksItem,)): - # dir - res.append(k) - _get_dirs_buttons(tree=v, res=res) - - dirs = [] - _get_dirs_buttons(tree=self.tree, res=dirs) - + # Sort the tree nodes by relative paths + dir_nodes = list(sorted( + tree.values(), + key=lambda x: shared.natural_sort_key(x.relpath), + )) dirs_html = "".join([ - self.btn_dirs_view_tpl.format(**{ - "extra_class": "search-all" if d == "" else "", + self.btn_dirs_view_item_tpl.format(**{ + "extra_class": "search-all" if node.relpath == "" else "", "tabname_full": f"{tabname}_{self.extra_networks_tabname}", - "path": html.escape(d), - }) for d in dirs + "path": html.escape(node.relpath), + }) for node in dir_nodes ]) return dirs_html - 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 - `id='{tabname}_{extra_networks_tabname}_cards`. - - Args: - tabname: The name of the active tab. - none_message: HTML text to show when there are no cards. - - Returns: - HTML formatted string. - """ - res = self.generate_cards_view_data(tabname) - - return f'' - def create_html(self, tabname, *, empty=False): """Generates an HTML string for the current pane. @@ -672,10 +780,13 @@ class ExtraNetworksPage: self.read_user_metadata(item) # Setup the tree dictionary. - roots = self.allowed_directories_for_previews() - tree_items = {v["filename"]: ExtraNetworksItem(v) for v in self.items.values()} - tree = get_tree([os.path.abspath(x) for x in roots], items=tree_items) - self.tree = tree + tree_items = {v["filename"]: v for v in self.items.values()} + # Create a DirectoryTreeNode for each root directory since they might not share + # a common path. + for path in self.allowed_directories_for_previews(): + abspath = os.path.abspath(path) + self.tree_roots[abspath] = DirectoryTreeNode(os.path.dirname(abspath), abspath, None) + self.tree_roots[abspath].build(tree_items) # Generate the html for displaying directory buttons dirs_html = self.create_dirs_view_html(tabname) @@ -744,7 +855,7 @@ class ExtraNetworksPage: file = f"{path}.safetensors" if self.lister.exists(file) and 'ssmd_cover_images' in metadata and len(list(filter(None, json.loads(metadata['ssmd_cover_images'])))) > 0: - return f"./sd_extra_networks/cover-images?page={self.extra_networks_tabname}&item={name}" + return f"./sd_extra_networks/cover-images?extra_networks_tabname={self.extra_networks_tabname}&item={name}" return None @@ -839,13 +950,23 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=f"{tabname}_preview_filename", visible=False) for tab in unrelated_tabs: - tab.select(fn=None, _js=f"function(){{extraNetworksUnrelatedTabSelected('{tabname}');}}", inputs=[], outputs=[], show_progress=False) + tab.select( + fn=None, + _js=f"fujnction(){{extraNetworksUnrelatedTabSelected('{tabname}');}}", + inputs=[], + outputs=[], + show_progress=False, + ) for page, tab in zip(ui.stored_extra_pages, related_tabs): jscode = ( - "function(){" - f"extraNetworksTabSelected('{tabname}', '{tabname}_{page.extra_networks_tabname}_prompts', {str(page.allow_prompt).lower()}, {str(page.allow_negative_prompt).lower()}, '{tabname}_{page.extra_networks_tabname}');" - "}" + "function(){extraNetworksTabSelected(" + f"'{tabname}', " + f"'{tabname}_{page.extra_networks_tabname}_prompts', " + f"{str(page.allow_prompt).lower()}, " + f"{str(page.allow_negative_prompt).lower()}, " + f"'{tabname}_{page.extra_networks_tabname}'" + ");}" ) tab.select(fn=None, _js=jscode, inputs=[], outputs=[], show_progress=False)