From df8aa69a99e38ae59a4e599b9dff11eccf3490f4 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Mon, 8 Jan 2024 14:10:03 -0500 Subject: [PATCH 01/20] Add tree-view display for extra networks. --- html/extra-networks-card-minimal.html | 3 + html/extra-networks-card.html | 1 + javascript/extraNetworks.js | 5 + modules/shared_options.py | 1 + modules/ui_extra_networks.py | 352 ++++++++++++++++++-------- style.css | 92 +++++++ 6 files changed, 355 insertions(+), 99 deletions(-) create mode 100644 html/extra-networks-card-minimal.html diff --git a/html/extra-networks-card-minimal.html b/html/extra-networks-card-minimal.html new file mode 100644 index 00000000000..a6a54d9f4ac --- /dev/null +++ b/html/extra-networks-card-minimal.html @@ -0,0 +1,3 @@ +
+ {name}{copy_path_button}{metadata_button}{edit_button} +
diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html index 39674666f1e..d76011d737c 100644 --- a/html/extra-networks-card.html +++ b/html/extra-networks-card.html @@ -1,6 +1,7 @@
{background_image}
+ {copy_path_button} {metadata_button} {edit_button}
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 98a7abb745c..40309d5575c 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -337,6 +337,11 @@ function requestGet(url, data, handler, errorHandler) { xhr.send(js); } +function extraNetworksCopyCardPath(event, path) { + navigator.clipboard.writeText(path); + event.stopPropagation(); +} + function extraNetworksRequestMetadata(event, extraPage, cardName) { var showError = function() { extraNetworksShowMetadata("there was an error getting metadata"); diff --git a/modules/shared_options.py b/modules/shared_options.py index d2e86ff10b3..e698c26498c 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -238,6 +238,7 @@ "extra_networks_dir_button_function": OptionInfo(False, "Add a '/' to the beginning of directory buttons").info("Buttons will display the contents of the selected directory without acting as a search filter."), "extra_networks_hidden_models": OptionInfo("When searched", "Show cards for models in hidden directories", gr.Radio, {"choices": ["Always", "When searched", "Never"]}).info('"When searched" option will only show the item when the search string has 4 characters or more'), "extra_networks_default_multiplier": OptionInfo(1.0, "Default multiplier for extra networks", gr.Slider, {"minimum": 0.0, "maximum": 2.0, "step": 0.01}), + "extra_networks_tree_view": OptionInfo(False, "Show extra networks using a directory tree view.").needs_reload_ui(), "extra_networks_card_width": OptionInfo(0, "Card width for Extra Networks").info("in pixels"), "extra_networks_card_height": OptionInfo(0, "Card height for Extra Networks").info("in pixels"), "extra_networks_card_text_scale": OptionInfo(1.0, "Card text scale", gr.Slider, {"minimum": 0.0, "maximum": 2.0, "step": 0.01}).info("1 = original size"), diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index fe5d3ba3338..8667617b9f5 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -2,6 +2,8 @@ import os.path import urllib.parse from pathlib import Path +from typing import Optional, Union +from dataclasses import dataclass from modules import shared, ui_extra_networks_user_metadata, errors, extra_networks from modules.images import read_info_from_image, save_image_with_geninfo @@ -15,10 +17,8 @@ extra_pages = [] allowed_dirs = set() - default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"] - @functools.cache def allowed_preview_extensions_with_extra(extra_extensions=None): return set(default_allowed_preview_extensions) | set(extra_extensions or []) @@ -28,6 +28,58 @@ def allowed_preview_extensions(): return allowed_preview_extensions_with_extra((shared.opts.samples_format, )) +@dataclass +class ExtraNetworksItem: + """Wrapper for dictionaries representing ExtraNetworks items.""" + item: dict + + +def get_tree(paths: Union[str, list[str]], items: dict[str, ExtraNetworksItem]) -> dict: + """Recursively builds a directory tree. + + Args: + paths: Path or list of paths to directories. These paths are treated as roots from which + the tree will be built. + items: A dictionary associating filepaths to an ExtraNetworksItem instance. + + Returns: + The result directory tree. + """ + if isinstance(paths, (str,)): + paths = [paths] + + def _get_tree(_paths: list[str]): + _res = {} + for path in _paths: + if os.path.isdir(path): + dir_items = os.listdir(path) + # Ignore empty directories. + if not dir_items: + continue + dir_tree = _get_tree([os.path.join(path, x) for x in dir_items]) + # We only want to store non-empty folders in the tree. + if dir_tree: + _res[os.path.basename(path)] = dir_tree + else: + if path not in items: + continue + # Add the ExtraNetworksItem to the result. + _res[os.path.basename(path)] = items[path] + return _res + + res = {} + # Handle each root directory separately. + # Each root WILL have a key/value at the root of the result dict though + # the value can be an empty dict if the directory is empty. We want these + # placeholders for empty dirs so we can inform the user later. + for path in paths: + # Wrap the path in a list since that is what the `_get_tree` expects. + res[path] = _get_tree([path]) + if res[path]: + res[path] = res[path][os.path.basename(path)] + + return res + def register_page(page): """registers extra networks page for the UI; recommend doing it in on_before_ui() callback for extensions""" @@ -80,7 +132,7 @@ def get_single_card(page: str = "", tabname: str = "", name: str = ""): item = page.items.get(name) page.read_user_metadata(item) - item_html = page.create_html_for_item(item, tabname) + item_html = page.create_item_html(tabname, item) return JSONResponse({"html": item_html}) @@ -96,13 +148,15 @@ def quote_js(s): s = s.replace('"', '\\"') return f'"{s}"' - class ExtraNetworksPage: def __init__(self, title): self.title = title self.name = title.lower() self.id_page = self.name.replace(" ", "_") - self.card_page = shared.html("extra-networks-card.html") + if shared.opts.extra_networks_tree_view: + self.card_page = shared.html("extra-networks-card-minimal.html") + else: + self.card_page = shared.html("extra-networks-card.html") self.allow_prompt = True self.allow_negative_prompt = False self.metadata = {} @@ -136,12 +190,141 @@ def search_terms_from_path(self, filename, possible_directories=None): return "" - def create_html(self, tabname): - items_html = '' + def create_item_html(self, tabname: str, item: dict) -> str: + """Generates HTML for a single ExtraNetworks Item + + Args: + tabname: The name of the active tab. + item: Dictionary containing item information. + + Returns: + HTML string generated for this item. + Can be empty if the item is not meant to be shown. + """ + metadata = item.get("metadata") + if metadata: + self.metadata[item["name"]] = metadata + + if "user_metadata" not in item: + self.read_user_metadata(item) + + preview = item.get("preview", None) + height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else '' + width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else '' + background_image = f'' if preview else '' + + onclick = item.get("onclick", None) + if onclick is None: + onclick = '"' + html.escape(f"""return cardClicked({quote_js(tabname)}, {item["prompt"]}, {"true" if self.allow_negative_prompt else "false"})""") + '"' + + copy_path_button = f"
" + + metadata_button = "" + metadata = item.get("metadata") + if metadata: + metadata_button = f"" + + edit_button = f"
" + + 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 shared.opts.extra_networks_hidden_models == "Always": + search_only = False + else: + search_only = "/." in local_path or "\\." in local_path + + 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() + + # Some items here might not be used depending on HTML template used. + args = { + "background_image": background_image, + "card_clicked": onclick, + "copy_path_button": copy_path_button, + "description": (item.get("description") or "" if shared.opts.extra_networks_card_show_desc else ""), + "edit_button": edit_button, + "local_preview": quote_js(item["local_preview"]), + "metadata_button": metadata_button, + "name": html.escape(item["name"]), + "prompt": item.get("prompt", None), + "save_card_preview": '"' + html.escape(f"""return saveCardPreview(event, {quote_js(tabname)}, {quote_js(item["local_preview"])})""") + '"', + "search_only": " search_only" if search_only else "", + "search_term": item.get("search_term", ""), + "sort_keys": sort_keys, + "style": f"'display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%'", + "tabname": quote_js(tabname), + } + + return self.card_page.format(**args) + + def create_tree_view_html(self, tabname: str) -> str: + """Generates HTML for displaying folders in a tree view. + + Args: + tabname: The name of the active tab. + + Returns: + HTML string generated for this tree view. + """ + self_name_id = self.name.replace(" ", "_") + res = f"
" self.metadata = {} + self.items = {x["name"]: x for x in self.list_items()} + 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) + + if not tree: + return res + "
" + + file_template = "
  • {}
  • " + dir_template = ( + "
    " + "{}" + "{}" + "
    " + ) + + def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> str: + """Recursively builds HTML for a tree.""" + _res = "
    " + + return res + + def create_card_view_html(self, tabname): + items_html = "" + self.metadata = {} subdirs = {} + for parentdir in [os.path.abspath(x) for x in self.allowed_directories_for_previews()]: for root, dirs, _ in sorted(os.walk(parentdir, followlinks=True), key=lambda x: shared.natural_sort_key(x[0])): for dirname in sorted(dirs, key=shared.natural_sort_key): @@ -171,40 +354,45 @@ def create_html(self, tabname): if subdirs: subdirs = {"": 1, **subdirs} - subdirs_html = "".join([f""" - -""" for subdir in subdirs]) + subdirs_html_template = ( + "" + ) + subdirs_html = "".join( + [ + subdirs_html_template.format( + " search-all" if subdir == "" else "", + tabname, + html.escape(subdir if subdir != "" else "all"), + ) for subdir in subdirs + ] + ) self.items = {x["name"]: x for x in self.list_items()} for item in self.items.values(): - metadata = item.get("metadata") - if metadata: - self.metadata[item["name"]] = metadata - - if "user_metadata" not in item: - self.read_user_metadata(item) - - items_html += self.create_html_for_item(item, tabname) + items_html += self.create_item_html(tabname, item) - if items_html == '': + if items_html == "": dirs = "".join([f"
  • {x}
  • " for x in self.allowed_directories_for_previews()]) items_html = shared.html("extra-networks-no-cards.html").format(dirs=dirs) self_name_id = self.name.replace(" ", "_") - res = f""" -
    -{subdirs_html} -
    -
    -{items_html} -
    -""" + res = ( + f"
    {subdirs_html}
    " + f"
    {items_html}
    " + ) return res + def create_html(self, tabname): + if shared.opts.extra_networks_tree_view: + return self.create_tree_view_html(tabname) + else: + return self.create_card_view_html(tabname) + def create_item(self, name, index=None): raise NotImplementedError() @@ -214,66 +402,6 @@ def list_items(self): def allowed_directories_for_previews(self): return [] - def create_html_for_item(self, item, tabname): - """ - Create HTML for card item in tab tabname; can return empty string if the item is not meant to be shown. - """ - - preview = item.get("preview", None) - - onclick = item.get("onclick", None) - if onclick is None: - onclick = '"' + html.escape(f"""return cardClicked({quote_js(tabname)}, {item["prompt"]}, {"true" if self.allow_negative_prompt else "false"})""") + '"' - - height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else '' - width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else '' - background_image = f'' if preview else '' - metadata_button = "" - metadata = item.get("metadata") - if metadata: - metadata_button = f"
    " - - edit_button = f"
    " - - 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 shared.opts.extra_networks_hidden_models == "Always": - search_only = False - else: - search_only = "/." in local_path or "\\." in local_path - - 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() - - args = { - "background_image": background_image, - "style": f"'display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%'", - "prompt": item.get("prompt", None), - "tabname": quote_js(tabname), - "local_preview": quote_js(item["local_preview"]), - "name": html.escape(item["name"]), - "description": (item.get("description") or "" if shared.opts.extra_networks_card_show_desc else ""), - "card_clicked": onclick, - "save_card_preview": '"' + html.escape(f"""return saveCardPreview(event, {quote_js(tabname)}, {quote_js(item["local_preview"])})""") + '"', - "search_term": item.get("search_term", ""), - "metadata_button": metadata_button, - "edit_button": edit_button, - "search_only": " search_only" if search_only else "", - "sort_keys": sort_keys, - } - - return self.card_page.format(**args) - def get_sort_keys(self, path): """ List of default keys used for sorting in the UI. @@ -360,7 +488,6 @@ def tab_name_score(name): return sorted(pages, key=lambda x: tab_scores[x.name]) - def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): from modules.ui import switch_values_symbol @@ -381,7 +508,6 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): elem_id = f"{tabname}_{page.id_page}_cards_html" page_elem = gr.HTML('Loading...', elem_id=elem_id) ui.pages.append(page_elem) - page_elem.change(fn=lambda: None, _js='function(){applyExtraNetworkFilter(' + quote_js(tabname) + '); return []}', inputs=[], outputs=[]) editor = page.create_user_metadata_editor(ui, tabname) @@ -390,30 +516,60 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): related_tabs.append(tab) + tab_controls = {} + edit_search = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", elem_classes="search", placeholder="Search...", visible=False, interactive=True) dropdown_sort = gr.Dropdown(choices=['Path', 'Name', 'Date Created', 'Date Modified', ], value=shared.opts.extra_networks_card_order_field, elem_id=tabname+"_extra_sort", elem_classes="sort", multiselect=False, visible=False, show_label=False, interactive=True, label=tabname+"_extra_sort_order") button_sortorder = ToolButton(switch_values_symbol, elem_id=tabname+"_extra_sortorder", elem_classes=["sortorder"] + ([] if shared.opts.extra_networks_card_order == "Ascending" else ["sortReverse"]), visible=False, tooltip="Invert sort order") button_refresh = gr.Button('Refresh', elem_id=tabname+"_extra_refresh", visible=False) checkbox_show_dirs = gr.Checkbox(True, label='Show dirs', elem_id=tabname+"_extra_show_dirs", elem_classes="show-dirs", visible=False) + + tab_controls["edit_search"] = edit_search + tab_controls["dropdown_sort"] = dropdown_sort + tab_controls["button_sortorder"] = button_sortorder + tab_controls["button_refresh"] = button_refresh + tab_controls["checkbox_show_dirs"] = checkbox_show_dirs ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False) ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=tabname+"_preview_filename", visible=False) - tab_controls = [edit_search, dropdown_sort, button_sortorder, button_refresh, checkbox_show_dirs] - for tab in unrelated_tabs: - tab.select(fn=lambda: [gr.update(visible=False) for _ in tab_controls], _js='function(){ extraNetworksUrelatedTabSelected("' + tabname + '"); }', inputs=[], outputs=tab_controls, show_progress=False) + tab.select( + fn=lambda: [gr.update(visible=False) for _ in tab_controls], + _js="function(){ extraNetworksUrelatedTabSelected('" + tabname + "'); }", + inputs=[], + outputs=list(tab_controls.values()), + show_progress=False, + ) + + visible_controls = list(tab_controls.keys()) + if shared.opts.extra_networks_tree_view: + visible_controls = ["button_refresh"] for page, tab in zip(ui.stored_extra_pages, related_tabs): allow_prompt = "true" if page.allow_prompt else "false" allow_negative_prompt = "true" if page.allow_negative_prompt else "false" - jscode = 'extraNetworksTabSelected("' + tabname + '", "' + f"{tabname}_{page.id_page}_prompts" + '", ' + allow_prompt + ', ' + allow_negative_prompt + ');' - - tab.select(fn=lambda: [gr.update(visible=True) for _ in tab_controls], _js='function(){ ' + jscode + ' }', inputs=[], outputs=tab_controls, show_progress=False) + jscode = ( + "extraNetworksTabSelected(" + f"'{tabname}', " + f"'{tabname}_{page.id_page}_prompts', " + f"'{allow_prompt}', " + f"'{allow_negative_prompt}'" + ");" + ) + + tab.select( + fn=lambda: [gr.update(visible=k in visible_controls) for k in tab_controls], + _js="function(){ " + jscode + " }", + inputs=[], + outputs=list(tab_controls.values()), + show_progress=False, + ) dropdown_sort.change(fn=lambda: None, _js="function(){ applyExtraNetworkSort('" + tabname + "'); }") + def pages_html(): if not ui.pages_contents: return refresh() @@ -478,5 +634,3 @@ def save_preview(index, images, filename): for editor in ui.user_metadata_editors: editor.setup_ui(gallery) - - diff --git a/style.css b/style.css index ee39a57b73e..680a5f837b7 100644 --- a/style.css +++ b/style.css @@ -1157,3 +1157,95 @@ body.resizing .resize-handle { left: 7.5px; border-left: 1px dashed var(--border-color-primary); } + +.extra-network-cards .card .copy-path-button:before { + content: "⎘"; +} + +.extra-network-cards .card-minimal .button-column { + display: inline-flex; + visibility: hidden; + color: white; + padding-left: 0.5rem; + padding-right: 0.5rem; + align-items: center; +} + +.extra-network-cards .card-minimal:hover .button-column { + visibility: visible; +} + +.extra-network-cards .card-minimal .copy-path-button:before { + content: "⎘"; +} + +.extra-network-cards .card-minimal .metadata-button:before{ + content: "🛈"; +} + +.extra-network-cards .card-minimal .edit-button:before{ + content: "🛠"; +} + +.extra-network-cards .card-minimal .card-button { + color: white; + text-shadow: 2px 2px 3px black; + font-size: 1rem; + width: 1.5rem; +} + +.extra-network-cards .card-minimal .card-button:hover { + color: red; +} + +.extra-network-cards .card-minimal { + display: inline-flex; + position: relative; + overflow: hidden; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + line-break: anywhere; +} + +.file-item { + list-style-type: '📄'; +} + +/* prevents clicking/collapsing of details tags when disabled attribute is used*/ +details[disabled] summary { + pointer-events: none; + user-select: none; +} + +details.folder-item > summary { + list-style-type: '📁'; +} + +details.folder-item[open] > summary { + list-style-type: '📂'; +} + +.file-item, +.folder-item, +.folder-item-summary { + display: block; + font-size: 1rem; + padding: 0.05rem; + cursor: pointer; + user-select: none; +} + +.folder-item-summary:hover, +.file-item:hover { + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; + background-color: var(--neutral-200); +} + +.dark .folder-item-summary:hover, +.dark .file-item:hover { + -webkit-transition: all 0.05s ease-in-out; + transition: all 0.05s ease-in-out; + background-color: var(--neutral-800); +} From 67a70ad112e1b0fb262852ab830896302dbd306a Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Mon, 8 Jan 2024 14:11:24 -0500 Subject: [PATCH 02/20] fix indentation --- html/extra-networks-card.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html index d76011d737c..7770094da14 100644 --- a/html/extra-networks-card.html +++ b/html/extra-networks-card.html @@ -1,7 +1,7 @@
    {background_image}
    - {copy_path_button} + {copy_path_button} {metadata_button} {edit_button}
    From 34fc215249e2bc0acc66cda47319e40b6e46a05f Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Mon, 8 Jan 2024 14:23:01 -0500 Subject: [PATCH 03/20] fix linting --- modules/ui_extra_networks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 8667617b9f5..ab484a5dc0b 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -216,7 +216,7 @@ def create_item_html(self, tabname: str, item: dict) -> str: onclick = item.get("onclick", None) if onclick is None: onclick = '"' + html.escape(f"""return cardClicked({quote_js(tabname)}, {item["prompt"]}, {"true" if self.allow_negative_prompt else "false"})""") + '"' - + copy_path_button = f"
    " metadata_button = "" @@ -523,7 +523,7 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): button_sortorder = ToolButton(switch_values_symbol, elem_id=tabname+"_extra_sortorder", elem_classes=["sortorder"] + ([] if shared.opts.extra_networks_card_order == "Ascending" else ["sortReverse"]), visible=False, tooltip="Invert sort order") button_refresh = gr.Button('Refresh', elem_id=tabname+"_extra_refresh", visible=False) checkbox_show_dirs = gr.Checkbox(True, label='Show dirs', elem_id=tabname+"_extra_show_dirs", elem_classes="show-dirs", visible=False) - + tab_controls["edit_search"] = edit_search tab_controls["dropdown_sort"] = dropdown_sort tab_controls["button_sortorder"] = button_sortorder @@ -560,7 +560,7 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): ) tab.select( - fn=lambda: [gr.update(visible=k in visible_controls) for k in tab_controls], + fn=lambda: [gr.update(visible=k in visible_controls) for k in tab_controls], _js="function(){ " + jscode + " }", inputs=[], outputs=list(tab_controls.values()), From 3db6938caa719aaa38b52edecf42740ef62b0c3c Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Wed, 10 Jan 2024 18:11:48 -0500 Subject: [PATCH 04/20] begin redesign of tree module. --- html/extra-networks-card-minimal.html | 3 +- html/extra-networks-pane.html | 11 ++ html/extra-networks-tree-directory.html | 4 + html/extra-networks-tree-file.html | 1 + javascript/extraNetworks.js | 22 +++ modules/shared_options.py | 1 - modules/ui_extra_networks.py | 164 ++++++++++--------- style.css | 204 +++++++++++++----------- 8 files changed, 248 insertions(+), 162 deletions(-) create mode 100644 html/extra-networks-pane.html create mode 100644 html/extra-networks-tree-directory.html create mode 100644 html/extra-networks-tree-file.html diff --git a/html/extra-networks-card-minimal.html b/html/extra-networks-card-minimal.html index a6a54d9f4ac..d66df7dfb3f 100644 --- a/html/extra-networks-card-minimal.html +++ b/html/extra-networks-card-minimal.html @@ -1,3 +1,4 @@
    - {name}{copy_path_button}{metadata_button}{edit_button} + {name} + {copy_path_button}{metadata_button}{edit_button}
    diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html new file mode 100644 index 00000000000..93bad698ddb --- /dev/null +++ b/html/extra-networks-pane.html @@ -0,0 +1,11 @@ +
    + {subdirs_html} +
    +
    +
    + {tree_html} +
    +
    + {items_html} +
    +
    \ No newline at end of file diff --git a/html/extra-networks-tree-directory.html b/html/extra-networks-tree-directory.html new file mode 100644 index 00000000000..cec15588635 --- /dev/null +++ b/html/extra-networks-tree-directory.html @@ -0,0 +1,4 @@ +
    +{folder_name} +{content} +
    \ No newline at end of file diff --git a/html/extra-networks-tree-file.html b/html/extra-networks-tree-file.html new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/html/extra-networks-tree-file.html @@ -0,0 +1 @@ + diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 40309d5575c..33f45c8e4d0 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -253,6 +253,28 @@ function saveCardPreview(event, tabname, filename) { event.preventDefault(); } +function extraNetworksFolderClick(event, tabs_id) { + var els = document.querySelectorAll(".folder-item-summary.selected"); + [...els].forEach(el => { + el.classList.remove("selected"); + }); + event.target.classList.add("selected"); + + var searchTextArea = gradioApp().querySelector("#" + tabs_id + ' > label > textarea'); + var text = event.target.classList.contains("search-all") ? "" : event.target.firstChild.textContent.trim(); + searchTextArea.value = text; + updateInput(searchTextArea); + + if (event.target.parentElement.open) { + // before close + console.log("closed"); + } else { + // before open + console.log("opened"); + //console.log("Opened:", event.target.parentElement); + } +} + function extraNetworksSearchButton(tabs_id, event) { var searchTextarea = gradioApp().querySelector("#" + tabs_id + ' > label > textarea'); var button = event.target; diff --git a/modules/shared_options.py b/modules/shared_options.py index e698c26498c..d2e86ff10b3 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -238,7 +238,6 @@ "extra_networks_dir_button_function": OptionInfo(False, "Add a '/' to the beginning of directory buttons").info("Buttons will display the contents of the selected directory without acting as a search filter."), "extra_networks_hidden_models": OptionInfo("When searched", "Show cards for models in hidden directories", gr.Radio, {"choices": ["Always", "When searched", "Never"]}).info('"When searched" option will only show the item when the search string has 4 characters or more'), "extra_networks_default_multiplier": OptionInfo(1.0, "Default multiplier for extra networks", gr.Slider, {"minimum": 0.0, "maximum": 2.0, "step": 0.01}), - "extra_networks_tree_view": OptionInfo(False, "Show extra networks using a directory tree view.").needs_reload_ui(), "extra_networks_card_width": OptionInfo(0, "Card width for Extra Networks").info("in pixels"), "extra_networks_card_height": OptionInfo(0, "Card height for Extra Networks").info("in pixels"), "extra_networks_card_text_scale": OptionInfo(1.0, "Card text scale", gr.Slider, {"minimum": 0.0, "maximum": 2.0, "step": 0.01}).info("1 = original size"), diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index ab484a5dc0b..6318594fa5b 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -73,10 +73,11 @@ def _get_tree(_paths: list[str]): # the value can be an empty dict if the directory is empty. We want these # placeholders for empty dirs so we can inform the user later. for path in paths: + short_path = os.path.basename(path) # Wrap the path in a list since that is what the `_get_tree` expects. - res[path] = _get_tree([path]) - if res[path]: - res[path] = res[path][os.path.basename(path)] + res[short_path] = _get_tree([path]) + if res[short_path]: + res[short_path] = res[short_path][os.path.basename(path)] return res @@ -153,10 +154,9 @@ def __init__(self, title): self.title = title self.name = title.lower() self.id_page = self.name.replace(" ", "_") - if shared.opts.extra_networks_tree_view: - self.card_page = shared.html("extra-networks-card-minimal.html") - else: - self.card_page = shared.html("extra-networks-card.html") + self.extra_networks_pane_template = shared.html("extra-networks-pane.html") + self.card_page_template = shared.html("extra-networks-card.html") + self.card_page_minimal_template = shared.html("extra-networks-card-minimal.html") self.allow_prompt = True self.allow_negative_prompt = False self.metadata = {} @@ -182,15 +182,14 @@ def link_preview(self, filename): def search_terms_from_path(self, filename, possible_directories=None): abspath = os.path.abspath(filename) - for parentdir in (possible_directories if possible_directories is not None else self.allowed_directories_for_previews()): - parentdir = os.path.abspath(parentdir) + parentdir = os.path.dirname(os.path.abspath(parentdir)) if abspath.startswith(parentdir): - return abspath[len(parentdir):].replace('\\', '/') + return os.path.relpath(abspath, parentdir) return "" - def create_item_html(self, tabname: str, item: dict) -> str: + def create_item_html(self, tabname: str, item: dict, template: Optional[str] = None) -> str: """Generates HTML for a single ExtraNetworks Item Args: @@ -265,7 +264,10 @@ def create_item_html(self, tabname: str, item: dict) -> str: "tabname": quote_js(tabname), } - return self.card_page.format(**args) + if template: + return template.format(**args) + else: + return self.card_page.format(**args) def create_tree_view_html(self, tabname: str) -> str: """Generates HTML for displaying folders in a tree view. @@ -276,53 +278,67 @@ def create_tree_view_html(self, tabname: str) -> str: Returns: HTML string generated for this tree view. """ - self_name_id = self.name.replace(" ", "_") - res = f"
    " - - self.metadata = {} - self.items = {x["name"]: x for x in self.list_items()} + res = f"" + # Generate HTML for the tree. 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) if not tree: - return res + "
    " + return res - file_template = "
  • {}
  • " + file_template = "
  • {card}
  • " dir_template = ( - "
    " - "{}" - "{}" + "
    " + "" + "{folder_name}" + "" + "
      {content}
    " "
    " ) def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> str: """Recursively builds HTML for a tree.""" - _res = "
      " + _res = "" if not data: - return "
      • DIRECTORY IS EMPTY
      " + return "
    • DIRECTORY IS EMPTY
    • " for k, v in sorted(data.items(), key=lambda x: shared.natural_sort_key(x[0])): if isinstance(v, (ExtraNetworksItem,)): - _res += file_template.format(self.create_item_html(tabname, v.item)) + item_html = self.create_item_html(tabname, v.item, self.card_page_minimal_template) + _res += file_template.format(**{"card": item_html}) else: - _res += dir_template.format("", k, _build_tree(v)) + tmp = dir_template.format( + **{ + "attributes": "", + "tabname": tabname, + "folder_name": k, + "content": _build_tree(v), + } + ) + _res += tmp + return _res - res += "
        " # Add each root directory to the tree. for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])): # If root is empty, append the "disabled" attribute to the template details tag. - res += dir_template.format("open" if v else "open disabled", k, _build_tree(v)) + res += "
          " + res += dir_template.format( + **{ + "attributes": "open" if v else "open", + "tabname": tabname, + "folder_name": k, + "content": _build_tree(v), + } + ) + res += "
        " res += "
      " - res += "
    " return res - def create_card_view_html(self, tabname): - items_html = "" - self.metadata = {} + def create_subdirs_html(self, tabname): subdirs = {} for parentdir in [os.path.abspath(x) for x in self.allowed_directories_for_previews()]: @@ -355,43 +371,53 @@ def create_card_view_html(self, tabname): subdirs = {"": 1, **subdirs} subdirs_html_template = ( - "" ) - subdirs_html = "".join( + return "".join( [ subdirs_html_template.format( - " search-all" if subdir == "" else "", - tabname, - html.escape(subdir if subdir != "" else "all"), + **{ + "classes": "search-all" if not subdir else "", + "tabname": tabname, + "content": html.escape(subdir if subdir else "all"), + } ) for subdir in subdirs ] ) + def create_card_view_html(self, tabname): + res = "" self.items = {x["name"]: x for x in self.list_items()} for item in self.items.values(): - items_html += self.create_item_html(tabname, item) + res += self.create_item_html(tabname, item, self.card_page_template) - if items_html == "": + if res == "": dirs = "".join([f"
  • {x}
  • " for x in self.allowed_directories_for_previews()]) - items_html = shared.html("extra-networks-no-cards.html").format(dirs=dirs) - - self_name_id = self.name.replace(" ", "_") - - res = ( - f"
    {subdirs_html}
    " - f"
    {items_html}
    " - ) + res = shared.html("extra-networks-no-cards.html").format(dirs=dirs) return res def create_html(self, tabname): - if shared.opts.extra_networks_tree_view: - return self.create_tree_view_html(tabname) - else: - return self.create_card_view_html(tabname) + self.metadata = {} + self.items = {x["name"]: x for x in self.list_items()} + + tree_view_html = self.create_tree_view_html(tabname) + subdirs_html = self.create_subdirs_html(tabname) + card_view_html = self.create_card_view_html(tabname) + network_type_id = self.name.replace(" ", "_") + + return self.extra_networks_pane_template.format( + **{ + "tabname": tabname, + "network_type_id": network_type_id, + "tree_html": tree_view_html, + "subdirs_html": subdirs_html, + "items_html": card_view_html, + } + ) def create_item(self, name, index=None): raise NotImplementedError() @@ -516,19 +542,19 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): related_tabs.append(tab) - tab_controls = {} - edit_search = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", elem_classes="search", placeholder="Search...", visible=False, interactive=True) dropdown_sort = gr.Dropdown(choices=['Path', 'Name', 'Date Created', 'Date Modified', ], value=shared.opts.extra_networks_card_order_field, elem_id=tabname+"_extra_sort", elem_classes="sort", multiselect=False, visible=False, show_label=False, interactive=True, label=tabname+"_extra_sort_order") button_sortorder = ToolButton(switch_values_symbol, elem_id=tabname+"_extra_sortorder", elem_classes=["sortorder"] + ([] if shared.opts.extra_networks_card_order == "Ascending" else ["sortReverse"]), visible=False, tooltip="Invert sort order") button_refresh = gr.Button('Refresh', elem_id=tabname+"_extra_refresh", visible=False) checkbox_show_dirs = gr.Checkbox(True, label='Show dirs', elem_id=tabname+"_extra_show_dirs", elem_classes="show-dirs", visible=False) - tab_controls["edit_search"] = edit_search - tab_controls["dropdown_sort"] = dropdown_sort - tab_controls["button_sortorder"] = button_sortorder - tab_controls["button_refresh"] = button_refresh - tab_controls["checkbox_show_dirs"] = checkbox_show_dirs + tab_controls = [ + edit_search, + dropdown_sort, + button_sortorder, + button_refresh, + checkbox_show_dirs, + ] ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False) ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=tabname+"_preview_filename", visible=False) @@ -538,32 +564,28 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): fn=lambda: [gr.update(visible=False) for _ in tab_controls], _js="function(){ extraNetworksUrelatedTabSelected('" + tabname + "'); }", inputs=[], - outputs=list(tab_controls.values()), + outputs=tab_controls, show_progress=False, ) - visible_controls = list(tab_controls.keys()) - if shared.opts.extra_networks_tree_view: - visible_controls = ["button_refresh"] - for page, tab in zip(ui.stored_extra_pages, related_tabs): allow_prompt = "true" if page.allow_prompt else "false" allow_negative_prompt = "true" if page.allow_negative_prompt else "false" jscode = ( "extraNetworksTabSelected(" - f"'{tabname}', " - f"'{tabname}_{page.id_page}_prompts', " - f"'{allow_prompt}', " - f"'{allow_negative_prompt}'" + f"'{tabname}', " + f"'{tabname}_{page.id_page}_prompts', " + f"'{allow_prompt}', " + f"'{allow_negative_prompt}'" ");" ) tab.select( - fn=lambda: [gr.update(visible=k in visible_controls) for k in tab_controls], + fn=lambda: [gr.update(visible=True) for _ in tab_controls], _js="function(){ " + jscode + " }", inputs=[], - outputs=list(tab_controls.values()), + outputs=tab_controls, show_progress=False, ) diff --git a/style.css b/style.css index 680a5f837b7..4f285c68b06 100644 --- a/style.css +++ b/style.css @@ -863,7 +863,7 @@ footer { margin-bottom: 1em; } -.extra-network-cards{ +.extra-network-pane{ height: calc(100vh - 24rem); overflow: clip scroll; resize: vertical; @@ -908,53 +908,75 @@ footer { width: auto; } -.extra-network-cards .nocards{ +.extra-network-pane .nocards{ margin: 1.25em 0.5em 0.5em 0.5em; } -.extra-network-cards .nocards h1{ +.extra-network-pane .nocards h1{ font-size: 1.5em; margin-bottom: 1em; } -.extra-network-cards .nocards li{ +.extra-network-pane .nocards li{ margin-left: 0.5em; } +.extra-network-pane :is(.card, .card-minimal) .button-row{ + display: inline-flex; + visibility: hidden; + color: white; +} -.extra-network-cards .card .button-row{ - display: none; +.extra-network-pane .card .button-row { position: absolute; - color: white; right: 0; - z-index: 1 + z-index: 1; } -.extra-network-cards .card:hover .button-row{ - display: flex; + +.extra-network-pane .card-minimal .button-row { + padding-left: 0.5rem; + padding-right: 0.5rem; + align-items: center; } -.extra-network-cards .card .card-button{ +.extra-network-pane :is(.card:hover, .card-minimal:hover) .button-row{ + visibility: visible; +} + +.extra-network-pane .card-button{ color: white; } -.extra-network-cards .card .metadata-button:before{ +.extra-network-pane .copy-path-button:before { + content: "⎘"; +} + +.extra-network-pane .metadata-button:before{ content: "🛈"; } -.extra-network-cards .card .edit-button:before{ +.extra-network-pane .edit-button:before{ content: "🛠"; } -.extra-network-cards .card .card-button { +.extra-network-pane .card-button { + width: 1.5em; text-shadow: 2px 2px 3px black; + color: white; padding: 0.25em 0.1em; - font-size: 200%; - width: 1.5em; } -.extra-network-cards .card .card-button:hover{ + +.extra-network-pane .card-button:hover{ color: red; } +.extra-network-pane .card .card-button { + font-size: 2rem; +} + +.extra-network-pane .card-minimal .card-button { + font-size: 1rem; +} .standalone-card-preview.card .preview{ position: absolute; @@ -963,7 +985,7 @@ footer { height:100%; } -.extra-network-cards .card, .standalone-card-preview.card{ +.extra-network-pane .card, .standalone-card-preview.card{ display: inline-block; margin: 0.5rem; width: 16rem; @@ -980,15 +1002,15 @@ footer { background-image: url('./file=html/card-no-preview.png') } -.extra-network-cards .card:hover{ +.extra-network-pane .card:hover{ box-shadow: 0 0 2px 0.3em rgba(0, 128, 255, 0.35); } -.extra-network-cards .card .actions .additional{ +.extra-network-pane .card .actions .additional{ display: none; } -.extra-network-cards .card .actions{ +.extra-network-pane .card .actions{ position: absolute; bottom: 0; left: 0; @@ -999,45 +1021,45 @@ footer { text-shadow: 0 0 0.2em black; } -.extra-network-cards .card .actions *{ +.extra-network-pane .card .actions *{ color: white; } -.extra-network-cards .card .actions .name{ +.extra-network-pane .card .actions .name{ font-size: 1.7em; font-weight: bold; line-break: anywhere; } -.extra-network-cards .card .actions .description { +.extra-network-pane .card .actions .description { display: block; max-height: 3em; white-space: pre-wrap; line-height: 1.1; } -.extra-network-cards .card .actions .description:hover { +.extra-network-pane .card .actions .description:hover { max-height: none; } -.extra-network-cards .card .actions:hover .additional{ +.extra-network-pane .card .actions:hover .additional{ display: block; } -.extra-network-cards .card ul{ +.extra-network-pane .card ul{ margin: 0.25em 0 0.75em 0.25em; cursor: unset; } -.extra-network-cards .card ul a{ +.extra-network-pane .card ul a{ cursor: pointer; } -.extra-network-cards .card ul a:hover{ +.extra-network-pane .card ul a:hover{ color: red; } -.extra-network-cards .card .preview{ +.extra-network-pane .card .preview{ position: absolute; object-fit: cover; width: 100%; @@ -1158,94 +1180,98 @@ body.resizing .resize-handle { border-left: 1px dashed var(--border-color-primary); } -.extra-network-cards .card .copy-path-button:before { - content: "⎘"; -} - -.extra-network-cards .card-minimal .button-column { +.extra-network-pane .card-minimal { display: inline-flex; - visibility: hidden; - color: white; - padding-left: 0.5rem; - padding-right: 0.5rem; - align-items: center; + flex-grow: 1; + position: relative; + overflow: hidden; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + line-break: anywhere; } -.extra-network-cards .card-minimal:hover .button-column { - visibility: visible; +/* Pushes buttons to right */ +.extra-network-pane .card-minimal .name { + flex-grow: 1; } -.extra-network-cards .card-minimal .copy-path-button:before { - content: "⎘"; +.file-item, +.folder-item, +.folder-item-summary { + padding-left: 0.05rem; + cursor: pointer; + user-select: none; + font-size: 1rem; } -.extra-network-cards .card-minimal .metadata-button:before{ - content: "🛈"; +.extra-network-pane .extra-network-tree .folder-item-summary:hover, +.extra-network-pane .extra-network-tree .file-item:hover { + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; + background-color: var(--neutral-200); } -.extra-network-cards .card-minimal .edit-button:before{ - content: "🛠"; +.dark .extra-network-pane .extra-network-tree .folder-item-summary:hover, +.dark .extra-network-pane .extra-network-tree .file-item:hover { + -webkit-transition: all 0.05s ease-in-out; + transition: all 0.05s ease-in-out; + background-color: var(--neutral-800); } -.extra-network-cards .card-minimal .card-button { - color: white; - text-shadow: 2px 2px 3px black; - font-size: 1rem; - width: 1.5rem; +/* prevents clicking/collapsing of details tags when disabled attribute is used*/ +.extra-network-pane .extra-network-tree details[disabled] summary { + pointer-events: none; + user-select: none; } -.extra-network-cards .card-minimal .card-button:hover { - color: red; +.extra-network-pane .extra-network-tree details.folder-item > summary { + list-style-type: '📁'; + text-overflow: ellipsis; } -.extra-network-cards .card-minimal { - display: inline-flex; - position: relative; - overflow: hidden; - cursor: pointer; - font-size: 1rem; - font-weight: bold; - line-break: anywhere; +.extra-network-pane .extra-network-tree details.folder-item[open] > summary { + list-style-type: '📂'; + text-overflow: ellipsis; } -.file-item { - list-style-type: '📄'; +.extra-network-pane .extra-network tree ul.folder-container { + list-style: none; + font-size: 1rem; + text-overflow: ellipsis; } -/* prevents clicking/collapsing of details tags when disabled attribute is used*/ -details[disabled] summary { - pointer-events: none; - user-select: none; +.extra-network-pane .extra-network-tree li.file-item { + display: flex; + position: relative; + align-items: center; } -details.folder-item > summary { - list-style-type: '📁'; +.extra-network-pane .extra-network-tree li.file-item::before { + content: '📄'; + font-size: 0.85rem; + vertical-align: middle; } -details.folder-item[open] > summary { - list-style-type: '📂'; +.extra-network-pane { + display: flex; } -.file-item, -.folder-item, -.folder-item-summary { +.extra-network-pane .extra-network-subdirs { display: block; +} +.extra-network-pane .extra-network-tree { font-size: 1rem; - padding: 0.05rem; - cursor: pointer; - user-select: none; + width: 25%; } - -.folder-item-summary:hover, -.file-item:hover { - -webkit-transition: all 0.1s ease-in-out; - transition: all 0.1s ease-in-out; - background-color: var(--neutral-200); +.extra-network-pane .extra-network-cards { + flex-grow: 1; } -.dark .folder-item-summary:hover, -.dark .file-item:hover { - -webkit-transition: all 0.05s ease-in-out; - transition: all 0.05s ease-in-out; +.dark .extra-network-tree .folder-item-summary.selected{ background-color: var(--neutral-800); } + +.extra-network-tree .folder-item-summary.selected { + background-color: var(--neutral-200); +} From 0726a6e12e85a37d1e514f5603acf9f058c11783 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Thu, 11 Jan 2024 15:06:57 -0500 Subject: [PATCH 05/20] Finish base layout. Fix bugs. Need to test for stability and clean up. --- .../Lora/ui_extra_networks_lora.py | 5 +- html/extra-networks-card.html | 4 +- html/extra-networks-pane.html | 3 - javascript/extraNetworks.js | 48 ++++---- modules/ui_extra_networks.py | 113 ++++++------------ modules/ui_extra_networks_checkpoints.py | 5 +- modules/ui_extra_networks_hypernets.py | 6 +- .../ui_extra_networks_textual_inversion.py | 5 +- style.css | 24 ++-- 9 files changed, 89 insertions(+), 124 deletions(-) diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index df02c663b12..db612fa2ba3 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -24,13 +24,16 @@ def create_item(self, name, index=None, enable_filter=True): alias = lora_on_disk.get_alias() + search_terms = [self.search_terms_from_path(lora_on_disk.filename)] + if lora_on_disk.hash: + search_terms.append(lora_on_disk.hash) item = { "name": name, "filename": lora_on_disk.filename, "shorthash": lora_on_disk.shorthash, "preview": self.find_preview(path), "description": self.find_description(path), - "search_term": self.search_terms_from_path(lora_on_disk.filename) + " " + (lora_on_disk.hash or ""), + "search_terms": search_terms, "local_preview": f"{path}.{shared.opts.samples_format}", "metadata": lora_on_disk.metadata, "sort_keys": {'default': index, **self.get_sort_keys(lora_on_disk.filename)}, diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html index 7770094da14..d163fe370a5 100644 --- a/html/extra-networks-card.html +++ b/html/extra-networks-card.html @@ -6,9 +6,7 @@ {edit_button}
    -
    - -
    +
    {search_terms}
    {name} {description}
    diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index 93bad698ddb..20cf6686755 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -1,6 +1,3 @@ -
    - {subdirs_html} -
    {tree_html} diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 33f45c8e4d0..4cc67fd18a3 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -24,8 +24,6 @@ function setupExtraNetworksForTab(tabname) { var sort = gradioApp().getElementById(tabname + '_extra_sort'); var sortOrder = gradioApp().getElementById(tabname + '_extra_sortorder'); var refresh = gradioApp().getElementById(tabname + '_extra_refresh'); - var showDirsDiv = gradioApp().getElementById(tabname + '_extra_show_dirs'); - var showDirs = gradioApp().querySelector('#' + tabname + '_extra_show_dirs input'); var promptContainer = gradioApp().querySelector('.prompt-container-compact#' + tabname + '_prompt_container'); var negativePrompt = gradioApp().querySelector('#' + tabname + '_neg_prompt'); @@ -33,14 +31,14 @@ function setupExtraNetworksForTab(tabname) { tabs.appendChild(sort); tabs.appendChild(sortOrder); tabs.appendChild(refresh); - tabs.appendChild(showDirsDiv); var applyFilter = function() { var searchTerm = search.value.toLowerCase(); gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) { var searchOnly = elem.querySelector('.search_only'); - var text = elem.querySelector('.name').textContent.toLowerCase() + " " + elem.querySelector('.search_term').textContent.toLowerCase(); + + var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms'), function(t) { return t.textContent.toLowerCase() }).join(" "); var visible = text.indexOf(searchTerm) != -1; @@ -100,15 +98,6 @@ function setupExtraNetworksForTab(tabname) { extraNetworksApplySort[tabname] = applySort; extraNetworksApplyFilter[tabname] = applyFilter; - - var showDirsUpdate = function() { - var css = '#' + tabname + '_extra_tabs .extra-network-subdirs { display: none; }'; - toggleCss(tabname + '_extra_show_dirs_style', css, !showDirs.checked); - localSet('extra-networks-show-dirs', showDirs.checked ? 1 : 0); - }; - showDirs.checked = localGet('extra-networks-show-dirs', 1) == 1; - showDirs.addEventListener("change", showDirsUpdate); - showDirsUpdate(); } function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt) { @@ -136,14 +125,23 @@ function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePromp } } +function clearSearch(tabname) { + // Clear search box. + var tab_id = tabname + "_extra_search"; + var searchTextarea = gradioApp().querySelector("#" + tab_id + ' > label > textarea'); + searchTextarea.value = ""; + updateInput(searchTextarea); +} + -function extraNetworksUrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate) +function extraNetworksUnrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate) extraNetworksMovePromptToTab(tabname, '', false, false); + clearSearch(tabname); } function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt) { // called from python when user selects an extra networks tab extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt); - + clearSearch(tabname); } function applyExtraNetworkFilter(tabname) { @@ -254,6 +252,15 @@ function saveCardPreview(event, tabname, filename) { } function extraNetworksFolderClick(event, tabs_id) { + // If folder is open but not selected, we don't want to collapse it. Instead + // we override the removal of the "open" attribute so that the folder is + // only selected but remains open. Since this is a toggle event, removing + // the "open" attribute instead forces the event to add it back which keeps it open. + if (event.target.parentElement.open && !event.target.classList.contains("selected")) { + // before event handler removes "open" + event.target.parentElement.removeAttribute("open"); + } + var els = document.querySelectorAll(".folder-item-summary.selected"); [...els].forEach(el => { el.classList.remove("selected"); @@ -261,18 +268,9 @@ function extraNetworksFolderClick(event, tabs_id) { event.target.classList.add("selected"); var searchTextArea = gradioApp().querySelector("#" + tabs_id + ' > label > textarea'); - var text = event.target.classList.contains("search-all") ? "" : event.target.firstChild.textContent.trim(); + var text = event.target.classList.contains("search-all") ? "" : event.target.getAttribute("data-path"); searchTextArea.value = text; updateInput(searchTextArea); - - if (event.target.parentElement.open) { - // before close - console.log("closed"); - } else { - // before open - console.log("opened"); - //console.log("Opened:", event.target.parentElement); - } } function extraNetworksSearchButton(tabs_id, event) { diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 6318594fa5b..2e226ba0029 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -48,23 +48,24 @@ def get_tree(paths: Union[str, list[str]], items: dict[str, ExtraNetworksItem]) if isinstance(paths, (str,)): paths = [paths] - def _get_tree(_paths: list[str]): + def _get_tree(_paths: list[str], _root: str): _res = {} for path in _paths: + relpath = os.path.relpath(path, _root) if os.path.isdir(path): dir_items = os.listdir(path) # Ignore empty directories. if not dir_items: continue - dir_tree = _get_tree([os.path.join(path, x) for x in dir_items]) + dir_tree = _get_tree([os.path.join(path, x) for x in dir_items], _root) # We only want to store non-empty folders in the tree. if dir_tree: - _res[os.path.basename(path)] = dir_tree + _res[relpath] = dir_tree else: if path not in items: continue # Add the ExtraNetworksItem to the result. - _res[os.path.basename(path)] = items[path] + _res[relpath] = items[path] return _res res = {} @@ -73,11 +74,13 @@ def _get_tree(_paths: list[str]): # the value can be an empty dict if the directory is empty. We want these # placeholders for empty dirs so we can inform the user later. for path in paths: - short_path = os.path.basename(path) + root = os.path.dirname(path) + relpath = os.path.relpath(path, root) # Wrap the path in a list since that is what the `_get_tree` expects. - res[short_path] = _get_tree([path]) - if res[short_path]: - res[short_path] = res[short_path][os.path.basename(path)] + res[relpath] = _get_tree([path], root) + if res[relpath]: + # We need to pull the inner path out one for these root dirs. + res[relpath] = res[relpath][relpath] return res @@ -245,6 +248,17 @@ def create_item_html(self, tabname: str, item: dict, template: Optional[str] = N sort_keys = " ".join([f'data-sort-{k}="{html.escape(str(v))}"' for k, v in item.get("sort_keys", {}).items()]).strip() + search_terms_html = "" + search_term_template = "{search_term}" + for search_term in item.get("search_terms", []): + search_terms_html += search_term_template.format( + **{ + "style": "display: none;", + "class": "search_terms" + (" search_only" if search_only else ""), + "search_term": search_term, + } + ) + # Some items here might not be used depending on HTML template used. args = { "background_image": background_image, @@ -258,7 +272,7 @@ def create_item_html(self, tabname: str, item: dict, template: Optional[str] = N "prompt": item.get("prompt", None), "save_card_preview": '"' + html.escape(f"""return saveCardPreview(event, {quote_js(tabname)}, {quote_js(item["local_preview"])})""") + '"', "search_only": " search_only" if search_only else "", - "search_term": item.get("search_term", ""), + "search_terms": search_terms_html, "sort_keys": sort_keys, "style": f"'display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%'", "tabname": quote_js(tabname), @@ -278,7 +292,7 @@ def create_tree_view_html(self, tabname: str) -> str: Returns: HTML string generated for this tree view. """ - res = f"" + res = "" # Generate HTML for the tree. roots = self.allowed_directories_for_previews() @@ -291,7 +305,8 @@ def create_tree_view_html(self, tabname: str) -> str: file_template = "
  • {card}
  • " dir_template = ( "
    " - "" + "" "{folder_name}" "" "
      {content}
    " @@ -309,16 +324,15 @@ def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> str: item_html = self.create_item_html(tabname, v.item, self.card_page_minimal_template) _res += file_template.format(**{"card": item_html}) else: - tmp = dir_template.format( + _res += dir_template.format( **{ "attributes": "", "tabname": tabname, - "folder_name": k, + "folder_name": os.path.basename(k), + "data_path": k, "content": _build_tree(v), } ) - _res += tmp - return _res # Add each root directory to the tree. @@ -329,65 +343,15 @@ def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> str: **{ "attributes": "open" if v else "open", "tabname": tabname, - "folder_name": k, + "folder_name": os.path.basename(k), + "data_path": k, "content": _build_tree(v), } ) res += "" res += "" - return res - def create_subdirs_html(self, tabname): - subdirs = {} - - for parentdir in [os.path.abspath(x) for x in self.allowed_directories_for_previews()]: - for root, dirs, _ in sorted(os.walk(parentdir, followlinks=True), key=lambda x: shared.natural_sort_key(x[0])): - for dirname in sorted(dirs, key=shared.natural_sort_key): - x = os.path.join(root, dirname) - - if not os.path.isdir(x): - continue - - subdir = os.path.abspath(x)[len(parentdir):].replace("\\", "/") - - if shared.opts.extra_networks_dir_button_function: - if not subdir.startswith("/"): - subdir = "/" + subdir - else: - while subdir.startswith("/"): - subdir = subdir[1:] - - is_empty = len(os.listdir(x)) == 0 - if not is_empty and not subdir.endswith("/"): - subdir = subdir + "/" - - if ("/." in subdir or subdir.startswith(".")) and not shared.opts.extra_networks_show_hidden_directories: - continue - - subdirs[subdir] = 1 - - if subdirs: - subdirs = {"": 1, **subdirs} - - subdirs_html_template = ( - "" - ) - return "".join( - [ - subdirs_html_template.format( - **{ - "classes": "search-all" if not subdir else "", - "tabname": tabname, - "content": html.escape(subdir if subdir else "all"), - } - ) for subdir in subdirs - ] - ) - def create_card_view_html(self, tabname): res = "" self.items = {x["name"]: x for x in self.list_items()} @@ -405,7 +369,6 @@ def create_html(self, tabname): self.items = {x["name"]: x for x in self.list_items()} tree_view_html = self.create_tree_view_html(tabname) - subdirs_html = self.create_subdirs_html(tabname) card_view_html = self.create_card_view_html(tabname) network_type_id = self.name.replace(" ", "_") @@ -414,7 +377,6 @@ def create_html(self, tabname): "tabname": tabname, "network_type_id": network_type_id, "tree_html": tree_view_html, - "subdirs_html": subdirs_html, "items_html": card_view_html, } ) @@ -534,7 +496,12 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): elem_id = f"{tabname}_{page.id_page}_cards_html" page_elem = gr.HTML('Loading...', elem_id=elem_id) ui.pages.append(page_elem) - page_elem.change(fn=lambda: None, _js='function(){applyExtraNetworkFilter(' + quote_js(tabname) + '); return []}', inputs=[], outputs=[]) + page_elem.change( + fn=lambda: None, + _js=f"function(){{applyExtraNetworkFilter({tabname}_extra_search); return []}}", + inputs=[], + outputs=[], + ) editor = page.create_user_metadata_editor(ui, tabname) editor.create_ui() @@ -542,18 +509,16 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): related_tabs.append(tab) - edit_search = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", elem_classes="search", placeholder="Search...", visible=False, interactive=True) + edit_search = gr.Textbox('', show_label=False, elem_id=f"{tabname}_extra_search", elem_classes="search", placeholder="Search...", visible=False, interactive=True) dropdown_sort = gr.Dropdown(choices=['Path', 'Name', 'Date Created', 'Date Modified', ], value=shared.opts.extra_networks_card_order_field, elem_id=tabname+"_extra_sort", elem_classes="sort", multiselect=False, visible=False, show_label=False, interactive=True, label=tabname+"_extra_sort_order") button_sortorder = ToolButton(switch_values_symbol, elem_id=tabname+"_extra_sortorder", elem_classes=["sortorder"] + ([] if shared.opts.extra_networks_card_order == "Ascending" else ["sortReverse"]), visible=False, tooltip="Invert sort order") button_refresh = gr.Button('Refresh', elem_id=tabname+"_extra_refresh", visible=False) - checkbox_show_dirs = gr.Checkbox(True, label='Show dirs', elem_id=tabname+"_extra_show_dirs", elem_classes="show-dirs", visible=False) tab_controls = [ edit_search, dropdown_sort, button_sortorder, button_refresh, - checkbox_show_dirs, ] ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False) @@ -562,7 +527,7 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): for tab in unrelated_tabs: tab.select( fn=lambda: [gr.update(visible=False) for _ in tab_controls], - _js="function(){ extraNetworksUrelatedTabSelected('" + tabname + "'); }", + _js=f"function(){{ extraNetworksUnrelatedTabSelected('{tabname}'); }}", inputs=[], outputs=tab_controls, show_progress=False, diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index 1693e71f16f..e7976ba1260 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -21,13 +21,16 @@ def create_item(self, name, index=None, enable_filter=True): return path, ext = os.path.splitext(checkpoint.filename) + search_terms = [self.search_terms_from_path(checkpoint.filename)] + if checkpoint.sha256: + search_terms.append(checkpoint.sha256) return { "name": checkpoint.name_for_extra, "filename": checkpoint.filename, "shorthash": checkpoint.shorthash, "preview": self.find_preview(path), "description": self.find_description(path), - "search_term": self.search_terms_from_path(checkpoint.filename) + " " + (checkpoint.sha256 or ""), + "search_terms": search_terms, "onclick": '"' + html.escape(f"""return selectCheckpoint({quote_js(name)})""") + '"', "local_preview": f"{path}.{shared.opts.samples_format}", "metadata": checkpoint.metadata, diff --git a/modules/ui_extra_networks_hypernets.py b/modules/ui_extra_networks_hypernets.py index c96c4fa3b12..2fb4bd190a1 100644 --- a/modules/ui_extra_networks_hypernets.py +++ b/modules/ui_extra_networks_hypernets.py @@ -20,14 +20,16 @@ def create_item(self, name, index=None, enable_filter=True): path, ext = os.path.splitext(full_path) sha256 = sha256_from_cache(full_path, f'hypernet/{name}') shorthash = sha256[0:10] if sha256 else None - + search_terms = [self.search_terms_from_path(path)] + if sha256: + search_terms.append(sha256) return { "name": name, "filename": full_path, "shorthash": shorthash, "preview": self.find_preview(path), "description": self.find_description(path), - "search_term": self.search_terms_from_path(path) + " " + (sha256 or ""), + "search_terms": search_terms, "prompt": quote_js(f""), "local_preview": f"{path}.preview.{shared.opts.samples_format}", "sort_keys": {'default': index, **self.get_sort_keys(path + ext)}, diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py index 1b334fda174..deb7cb8733b 100644 --- a/modules/ui_extra_networks_textual_inversion.py +++ b/modules/ui_extra_networks_textual_inversion.py @@ -18,13 +18,16 @@ def create_item(self, name, index=None, enable_filter=True): return path, ext = os.path.splitext(embedding.filename) + search_terms = [self.search_terms_from_path(embedding.filename)] + if embedding.hash: + search_terms.append(embedding.hash) return { "name": name, "filename": embedding.filename, "shorthash": embedding.shorthash, "preview": self.find_preview(path), "description": self.find_description(path), - "search_term": self.search_terms_from_path(embedding.filename) + " " + (embedding.hash or ""), + "search_terms": search_terms, "prompt": quote_js(embedding.name), "local_preview": f"{path}.preview.{shared.opts.samples_format}", "sort_keys": {'default': index, **self.get_sort_keys(embedding.filename)}, diff --git a/style.css b/style.css index 4f285c68b06..70d80d6a170 100644 --- a/style.css +++ b/style.css @@ -878,16 +878,8 @@ footer { margin: 0.3em; } -.extra-network-subdirs{ - padding: 0.2em 0.35em; -} - -.extra-network-subdirs button{ - margin: 0 0.15em; -} .extra-networks .tab-nav .search, -.extra-networks .tab-nav .sort, -.extra-networks .tab-nav .show-dirs +.extra-networks .tab-nav .sort { margin: 0.3em; align-self: center; @@ -1196,6 +1188,10 @@ body.resizing .resize-handle { flex-grow: 1; } +.folder-container { + margin-left: 1.5em !important; +} + .file-item, .folder-item, .folder-item-summary { @@ -1235,7 +1231,7 @@ body.resizing .resize-handle { text-overflow: ellipsis; } -.extra-network-pane .extra-network tree ul.folder-container { +.extra-network-pane .extra-network-tree ul.folder-container { list-style: none; font-size: 1rem; text-overflow: ellipsis; @@ -1257,15 +1253,15 @@ body.resizing .resize-handle { display: flex; } -.extra-network-pane .extra-network-subdirs { - display: block; -} .extra-network-pane .extra-network-tree { font-size: 1rem; - width: 25%; + min-width: 25%; + max-width: 25%; + border: 1px solid var(--block-border-color); } .extra-network-pane .extra-network-cards { flex-grow: 1; + border: 1px solid var(--block-border-color); } .dark .extra-network-tree .folder-item-summary.selected{ From 02e6963325e5221e0efb96a63f3dc849550489b7 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Sat, 13 Jan 2024 13:16:39 -0500 Subject: [PATCH 06/20] continue cleanup and redesign. --- html/extra-networks-tree-button.html | 11 + javascript/extraNetworks.js | 294 ++++++++++++++++++--------- modules/ui_extra_networks.py | 177 +++++++++++++--- style.css | 277 +++++++++++++++++++------ 4 files changed, 572 insertions(+), 187 deletions(-) create mode 100644 html/extra-networks-tree-button.html diff --git a/html/extra-networks-tree-button.html b/html/extra-networks-tree-button.html new file mode 100644 index 00000000000..920330f7a7d --- /dev/null +++ b/html/extra-networks-tree-button.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 3e3b03f320e..cce2246899e 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -16,88 +16,110 @@ function toggleCss(key, css, enable) { } function setupExtraNetworksForTab(tabname) { - gradioApp().querySelector('#' + tabname + '_extra_tabs').classList.add('extra-networks'); + var this_tab = gradioApp().querySelector('#' + tabname + '_extra_tabs'); + this_tab.classList.add('extra-networks'); - var tabs = gradioApp().querySelector('#' + tabname + '_extra_tabs > div'); - var searchDiv = gradioApp().getElementById(tabname + '_extra_search'); - var search = searchDiv.querySelector('textarea'); - var sort = gradioApp().getElementById(tabname + '_extra_sort'); - var sortOrder = gradioApp().getElementById(tabname + '_extra_sortorder'); - var refresh = gradioApp().getElementById(tabname + '_extra_refresh'); - var promptContainer = gradioApp().querySelector('.prompt-container-compact#' + tabname + '_prompt_container'); - var negativePrompt = gradioApp().querySelector('#' + tabname + '_neg_prompt'); - - tabs.appendChild(searchDiv); - tabs.appendChild(sort); - tabs.appendChild(sortOrder); - tabs.appendChild(refresh); - - var applyFilter = function() { - var searchTerm = search.value.toLowerCase(); - - gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) { - var searchOnly = elem.querySelector('.search_only'); - - var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms'), function(t) { return t.textContent.toLowerCase() }).join(" "); - - var visible = text.indexOf(searchTerm) != -1; - - if (searchOnly && searchTerm.length < 4) { - visible = false; - } - - elem.style.display = visible ? "" : "none"; - }); - - applySort(); - }; - - var applySort = function() { - var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card'); - - var reverse = sortOrder.classList.contains("sortReverse"); - var sortKey = sort.querySelector("input").value.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; - sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1); - var sortKeyStore = sortKey + "-" + (reverse ? "Descending" : "Ascending") + "-" + cards.length; + function registerPrompt(tabname, id) { + var textarea = gradioApp().querySelector("#" + id + " > label > textarea"); - if (sortKeyStore == sort.dataset.sortkey) { - return; + if (!activePromptTextarea[tabname]) { + activePromptTextarea[tabname] = textarea; } - sort.dataset.sortkey = sortKeyStore; - cards.forEach(function(card) { - card.originalParentElement = card.parentElement; + textarea.addEventListener("focus", function() { + activePromptTextarea[tabname] = textarea; }); - var sortedCards = Array.from(cards); - sortedCards.sort(function(cardA, cardB) { - var a = cardA.dataset[sortKey]; - var b = cardB.dataset[sortKey]; - if (!isNaN(a) && !isNaN(b)) { - return parseInt(a) - parseInt(b); + } + + this_tab.querySelectorAll(":scope > [id^='" + tabname + "_']").forEach(function(elem) { + var tab_id = elem.getAttribute("id"); + + var tabs = gradioApp().querySelector('#' + tabname + '_extra_tabs > div'); + var searchDiv = gradioApp().QuerySelector("#" + tab_id + "_extra_search"); + console.log("HERE:", tab_id + "_extra_search", searchDiv); + var search = searchDiv.value; + var sort = gradioApp().getElementById(tabname + '_extra_sort'); + var sortOrder = gradioApp().getElementById(tabname + '_extra_sortorder'); + var refresh = gradioApp().getElementById(tabname + '_extra_refresh'); + var promptContainer = gradioApp().querySelector('.prompt-container-compact#' + tabname + '_prompt_container'); + var negativePrompt = gradioApp().querySelector('#' + tabname + '_neg_prompt'); + tabs.appendChild(searchDiv); + tabs.appendChild(sort); + tabs.appendChild(sortOrder); + tabs.appendChild(refresh); + var applyFilter = function() { + var searchTerm = search.value.toLowerCase(); + + gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) { + var searchOnly = elem.querySelector('.search_only'); + + var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms'), function(t) { return t.textContent.toLowerCase() }).join(" "); + + var visible = text.indexOf(searchTerm) != -1; + + if (searchOnly && searchTerm.length < 4) { + visible = false; + } + + elem.style.display = visible ? "" : "none"; + }); + + applySort(); + }; + + var applySort = function() { + var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card'); + + var reverse = sortOrder.classList.contains("sortReverse"); + var sortKey = sort.querySelector("input").value.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; + sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1); + var sortKeyStore = sortKey + "-" + (reverse ? "Descending" : "Ascending") + "-" + cards.length; + + if (sortKeyStore == sort.dataset.sortkey) { + return; } - - return (a < b ? -1 : (a > b ? 1 : 0)); - }); - if (reverse) { - sortedCards.reverse(); - } - cards.forEach(function(card) { - card.remove(); - }); - sortedCards.forEach(function(card) { - card.originalParentElement.appendChild(card); + sort.dataset.sortkey = sortKeyStore; + + cards.forEach(function(card) { + card.originalParentElement = card.parentElement; + }); + var sortedCards = Array.from(cards); + sortedCards.sort(function(cardA, cardB) { + var a = cardA.dataset[sortKey]; + var b = cardB.dataset[sortKey]; + if (!isNaN(a) && !isNaN(b)) { + return parseInt(a) - parseInt(b); + } + + return (a < b ? -1 : (a > b ? 1 : 0)); + }); + if (reverse) { + sortedCards.reverse(); + } + cards.forEach(function(card) { + card.remove(); + }); + sortedCards.forEach(function(card) { + card.originalParentElement.appendChild(card); + }); + }; + + search.addEventListener("input", applyFilter); + sortOrder.addEventListener("click", function() { + sortOrder.classList.toggle("sortReverse"); + applySort(); }); - }; + applyFilter(); + + extraNetworksApplySort[tab_id] = applySort; + extraNetworksApplyFilter[tab_id] = applyFilter; - search.addEventListener("input", applyFilter); - sortOrder.addEventListener("click", function() { - sortOrder.classList.toggle("sortReverse"); - applySort(); + registerPrompt(tab_id, tab_id + "_prompt"); + registerPrompt(tab_id, tab_id + "_neg_prompt"); }); - applyFilter(); - extraNetworksApplySort[tabname] = applySort; - extraNetworksApplyFilter[tabname] = applyFilter; + + } function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt) { @@ -136,12 +158,12 @@ function clearSearch(tabname) { function extraNetworksUnrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate) extraNetworksMovePromptToTab(tabname, '', false, false); - clearSearch(tabname); + //clearSearch(tabname); } function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt) { // called from python when user selects an extra networks tab extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt); - clearSearch(tabname); + //clearSearch(tabname); } function applyExtraNetworkFilter(tabname) { @@ -159,23 +181,6 @@ var activePromptTextarea = {}; function setupExtraNetworks() { setupExtraNetworksForTab('txt2img'); setupExtraNetworksForTab('img2img'); - - function registerPrompt(tabname, id) { - var textarea = gradioApp().querySelector("#" + id + " > label > textarea"); - - if (!activePromptTextarea[tabname]) { - activePromptTextarea[tabname] = textarea; - } - - textarea.addEventListener("focus", function() { - activePromptTextarea[tabname] = textarea; - }); - } - - registerPrompt('txt2img', 'txt2img_prompt'); - registerPrompt('txt2img', 'txt2img_neg_prompt'); - registerPrompt('img2img', 'img2img_prompt'); - registerPrompt('img2img', 'img2img_neg_prompt'); } onUiLoaded(setupExtraNetworks); @@ -262,6 +267,106 @@ function saveCardPreview(event, tabname, filename) { event.preventDefault(); } +function extraNetworksTreeProcessFileClick(event, btn, tabname, tab_id) { + /** + * Processes `onclick` events when user clicks on files in tree. + * + * @param event The generated event. + * @param btn The clicked `action-list-item` button. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param tab_id The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + */ + var par = btn.parentElement; + var search_id = tabname + "_" + tab_id + "_extra_search"; + var type = par.getAttribute("data-tree-entry-type"); + var path = par.getAttribute("data-path"); +} + +function extraNetworksTreeProcessDirectoryClick(event, btn) { + /** + * Processes `onclick` events when user clicks on directories in tree. + * + * Here is how the tree reacts to clicks for various states: + * unselected unopened directory: Diretory 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 `action-list-item` button. + */ + var ul = btn.nextElementSibling; + // 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; + + function _expand_or_collapse(_ul, _btn) { + // Expands
      if it is collapsed, collapses otherwise. Updates button attributes. + if (_ul.hasAttribute("data-hidden")) { + _ul.removeAttribute("data-hidden"); + _btn.setAttribute("expanded", "true"); + } else { + _ul.setAttribute("data-hidden", ""); + _btn.setAttribute("expanded", "false"); + } + } + + function _remove_selected_from_all() { + // Removes the `selected` attribute from all buttons. + var sels = document.querySelectorAll("button.action-list-content"); + [...sels].forEach(el => { + el.removeAttribute("selected"); + }) + } + + function _select_button(_btn) { + // Removes `selected` attribute from all buttons then adds to passed button. + _remove_selected_from_all(); + _btn.setAttribute("selected", ""); + } + + // If user clicks on the chevron, then we do not select the folder. + if (true_targ.matches(".action-list-item-action--leading, .action-list-item-action-chevron")) { + _expand_or_collapse(ul, btn); + } else { + // User clicked anywhere else on the button. + if (btn.hasAttribute("selected") && !ul.hasAttribute("data-hidden")) { + // If folder is select and open, collapse and deselect button. + _expand_or_collapse(ul, btn); + btn.removeAttribute("selected"); + } else if (!(!btn.hasAttribute("selected") && !ul.hasAttribute("data-hidden"))) { + // If folder is open and not selected, then we don't collapse; just select. + // NOTE: Double inversion sucks but it is the clearest way to show the branching here. + _expand_or_collapse(ul, btn); + _select_button(btn); + } else { + // All other cases, just select the button. + _select_button(btn); + } + + } +} + +function extraNetworksTreeOnClick(event, tabname, tab_id) { + /** + * Handles `onclick` events for buttons within an `extra-network-tree .action-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 The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param tab_id The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + */ + var btn = event.currentTarget; + var par = btn.parentElement; + if (par.getAttribute("data-tree-entry-type") === "file") { + extraNetworksTreeProcessFileClick(event, btn, tabname, tab_id); + } else { + extraNetworksTreeProcessDirectoryClick(event, btn); + } +} + function extraNetworksFolderClick(event, tabs_id) { // If folder is open but not selected, we don't want to collapse it. Instead // we override the removal of the "open" attribute so that the folder is @@ -434,3 +539,10 @@ window.addEventListener("keydown", function(event) { closePopup(); } }); + +function testprint(e) { + console.log(e); +} + +const testinput = gradioApp().querySelector("#txt2img_lora_extra_search"); +testinput.addEventListener("input", testprint); \ No newline at end of file diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 093ac7b4f0c..9cf5b57fbcb 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -19,6 +19,90 @@ allowed_dirs = set() default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"] +tree_tpl = ( + "" + "
        " + "{content}" + "
      " +) + +tree_ul_tpl = ( + "
        " + "{content}" + "
      " +) + +tree_li_dir_tpl = ( + "
    • " + "{content}" + "
    • " +) +tree_li_file_tpl = ( + "
    • " + "{content}" + "
    • " +) + +tree_btn_dir_tpl = ( + "" +) + +tree_btn_file_action_buttons_tpl = ( + "
      " + "
      " + "
      " + "
      " + "
      " + "
      " +) + +tree_btn_file_tpl = ( + "" + "" +) + + @functools.cache def allowed_preview_extensions_with_extra(extra_extensions=None): return set(default_allowed_preview_extensions) | set(extra_extensions or []) @@ -160,6 +244,7 @@ def __init__(self, title): self.extra_networks_pane_template = shared.html("extra-networks-pane.html") self.card_page_template = shared.html("extra-networks-card.html") self.card_page_minimal_template = shared.html("extra-networks-card-minimal.html") + self.tree_button_template = shared.html("extra-networks-tree-button.html") self.allow_prompt = True self.allow_negative_prompt = False self.metadata = {} @@ -279,7 +364,9 @@ def create_item_html(self, tabname: str, item: dict, template: Optional[str] = N "search_terms": search_terms_html, "sort_keys": sort_keys, "style": f"'display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%'", - "tabname": quote_js(tabname), + "tabname": tabname, + "tab_id": self.id_page, + } if template: @@ -306,55 +393,81 @@ def create_tree_view_html(self, tabname: str) -> str: if not tree: return res - file_template = "
    • {card}
    • " - dir_template = ( - "
      " - "" - "{folder_name}" - "" - "
        {content}
      " - "
      " - ) - def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> str: """Recursively builds HTML for a tree.""" _res = "" if not data: - return "
    • DIRECTORY IS EMPTY
    • " + return ( + "
      " + "Directory is empty" + "
      " + ) for k, v in sorted(data.items(), key=lambda x: shared.natural_sort_key(x[0])): if isinstance(v, (ExtraNetworksItem,)): - item_html = self.create_item_html(tabname, v.item, self.card_page_minimal_template) - _res += file_template.format(**{"card": item_html}) + _action_buttons = tree_btn_file_action_buttons_tpl.format( + **{ + "path": quote_js(k), + "filename": quote_js(v.item["name"]), + "tabname": quote_js(tabname), + "tab_id": quote_js(self.id_page), + } + ) + _btn = tree_btn_file_tpl.format( + **{ + "label": v.item["name"], + "filter": v.item["search_terms"], + "tabname": tabname, + "tab_id": self.id_page, + "buttons": _action_buttons, + } + ) + _li = tree_li_file_tpl.format( + **{ + "hash": v.item["shorthash"], + "path": k, + "type": "file", + #"content": _btn, + "content": self.create_item_html(tabname, v.item, self.tree_button_template), + } + ) + _res += _li + #item_html = self.create_item_html(tabname, v.item, self.card_page_minimal_template) + #_res += file_template.format(**{"card": item_html}) else: - _res += dir_template.format( + _btn = tree_btn_dir_tpl.format( **{ - "attributes": "", + "label": os.path.basename(k), "tabname": tabname, - "folder_name": os.path.basename(k), - "data_path": k, - "content": _build_tree(v), + "tab_id": self.id_page, } ) + _ul = tree_ul_tpl.format(**{"content": _build_tree(v)}) + _li = tree_li_dir_tpl.format(**{"content": _btn + _ul, "path": k}) + _res += _li return _res # Add each root directory to the tree. for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])): # If root is empty, append the "disabled" attribute to the template details tag. - res += "
        " - res += dir_template.format( + btn = tree_btn_dir_tpl.format( **{ - "attributes": "open" if v else "open", + "label": os.path.basename(k), "tabname": tabname, - "folder_name": os.path.basename(k), - "data_path": k, - "content": _build_tree(v), + "tab_id": self.id_page, } ) - res += "
      " - res += "
    " - return res + ul = tree_ul_tpl.format(**{"content": _build_tree(v)}) + li = tree_li_dir_tpl.format(**{"content": btn + ul, "path": k}) + res += li + + return tree_tpl.format( + **{ + "content": res, + "tabname": tabname, + "tab_id": self.id_page, + } + ) def create_card_view_html(self, tabname): res = "" @@ -375,7 +488,7 @@ def create_html(self, tabname): tree_view_html = self.create_tree_view_html(tabname) card_view_html = self.create_card_view_html(tabname) - network_type_id = self.name.replace(" ", "_") + network_type_id = self.id_page return self.extra_networks_pane_template.format( **{ @@ -506,7 +619,7 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): ui.pages.append(page_elem) page_elem.change( fn=lambda: None, - _js=f"function(){{applyExtraNetworkFilter({tabname}_extra_search); return []}}", + _js=f"function(){{applyExtraNetworkFilter({tabname}_{page.id_page}_extra_search); return []}}", inputs=[], outputs=[], ) @@ -517,13 +630,11 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): related_tabs.append(tab) - edit_search = gr.Textbox('', show_label=False, elem_id=f"{tabname}_extra_search", elem_classes="search", placeholder="Search...", visible=False, interactive=True) dropdown_sort = gr.Dropdown(choices=['Path', 'Name', 'Date Created', 'Date Modified', ], value=shared.opts.extra_networks_card_order_field, elem_id=tabname+"_extra_sort", elem_classes="sort", multiselect=False, visible=False, show_label=False, interactive=True, label=tabname+"_extra_sort_order") button_sortorder = ToolButton(switch_values_symbol, elem_id=tabname+"_extra_sortorder", elem_classes=["sortorder"] + ([] if shared.opts.extra_networks_card_order == "Ascending" else ["sortReverse"]), visible=False, tooltip="Invert sort order") button_refresh = gr.Button('Refresh', elem_id=tabname+"_extra_refresh", visible=False) tab_controls = [ - edit_search, dropdown_sort, button_sortorder, button_refresh, diff --git a/style.css b/style.css index aaafaa9d7e1..8aa41088dff 100644 --- a/style.css +++ b/style.css @@ -955,15 +955,15 @@ footer { color: white; } -.extra-network-pane .copy-path-button:before { +.extra-network-pane .copy-path-button::before { content: "⎘"; } -.extra-network-pane .metadata-button:before{ +.extra-network-pane .metadata-button::before{ content: "🛈"; } -.extra-network-pane .edit-button:before{ +.extra-network-pane .edit-button::before{ content: "🛠"; } @@ -1188,102 +1188,253 @@ body.resizing .resize-handle { border-left: 1px dashed var(--border-color-primary); } -.extra-network-pane .card-minimal { - display: inline-flex; - flex-grow: 1; - position: relative; - overflow: hidden; - cursor: pointer; - font-size: 1rem; - font-weight: bold; - line-break: anywhere; +/* ========================= */ +.extra-network-pane { + display: flex; } -/* Pushes buttons to right */ -.extra-network-pane .card-minimal .name { - flex-grow: 1; +.extra-network-pane .extra-network-cards { + display: block; } -.folder-container { - margin-left: 1.5em !important; +.extra-network-pane .extra-network-tree { + display: block; + font-size: 1rem; + min-width: 25%; + border: 1px solid var(--block-border-color); + overflow: hidden; } -.file-item, -.folder-item, -.folder-item-summary { - padding-left: 0.05rem; +.extra-network-tree .action-list--tree { cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; user-select: none; - font-size: 1rem; + margin: 0; + padding: 0; } -.extra-network-pane .extra-network-tree .folder-item-summary:hover, -.extra-network-pane .extra-network-tree .file-item:hover { - -webkit-transition: all 0.1s ease-in-out; - transition: all 0.1s ease-in-out; - background-color: var(--neutral-200); +/* Remove auto indentation from tree. Will be overridden later. */ +.extra-network-tree .action-list--subgroup { + margin: 0 !important; + padding: 0 !important; + box-shadow: 0.6rem 0 0 var(--body-background-fill) inset, + 0.8rem 0 0 var(--neutral-800) inset; +} + +/* Set indentation for each depth of tree. */ +.extra-network-tree .action-list--subgroup > .action-list-item { + margin-left: 0.4rem !important; + padding-left: 0.4rem !important; } -.dark .extra-network-pane .extra-network-tree .folder-item-summary:hover, -.dark .extra-network-pane .extra-network-tree .file-item:hover { +/* Styles for tree
      elements. */ +.extra-network-tree .action-list { + +} + +/* Styles for tree
    • elements. */ +.extra-network-tree .action-list-item { + list-style: none; + position: relative; + background-color: transparent; +} + +/* Directory
        */ +.extra-network-tree .action-list-content[expanded=false]+.action-list--subgroup { + height: 0; + overflow: hidden; + visibility: hidden; + opacity: 0; +} + +.extra-network-tree .action-list-content[expanded=true]+.action-list--subgroup { + height: auto; + overflow: visible; + visibility: visible; + opacity: 1; +} + +/* File
      • */ +.extra-network-tree .action-list-item--subitem { +} + +/*
      • containing
          */ +.extra-network-tree .action-list-item--has-subitem { +} + +/* BUTTON ELEMENTS */ +/* \ No newline at end of file diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 97a97d61f2c..56c9dc4c35d 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -267,7 +267,7 @@ function extraNetworksTreeProcessFileClick(event, btn, tabname, tab_id) { * Processes `onclick` events when user clicks on files in tree. * * @param event The generated event. - * @param btn The clicked `action-list-item` button. + * @param btn The clicked `tree-list-item` button. * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. * @param tab_id The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. */ @@ -288,7 +288,7 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { * chevron is clicked: Directory is expanded or collapsed. Selected state unchanged. * * @param event The generated event. - * @param btn The clicked `action-list-item` button. + * @param btn The clicked `tree-list-item` button. * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. * @param tab_id The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. */ @@ -310,7 +310,7 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { function _remove_selected_from_all() { // Removes the `selected` attribute from all buttons. - var sels = document.querySelectorAll("button.action-list-content"); + var sels = document.querySelectorAll("button.tree-list-content"); [...sels].forEach(el => { el.removeAttribute("selected"); }); @@ -331,7 +331,7 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { // If user clicks on the chevron, then we do not select the folder. - if (true_targ.matches(".action-list-item-action--leading, .action-list-item-action-chevron")) { + if (true_targ.matches(".tree-list-item-action--leading, .tree-list-item-action-chevron")) { _expand_or_collapse(ul, btn); } else { // User clicked anywhere else on the button. @@ -356,7 +356,7 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { function extraNetworksTreeOnClick(event, tabname, tab_id) { /** - * Handles `onclick` events for buttons within an `extra-network-tree .action-list--tree`. + * 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. diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 9cf5b57fbcb..a49c6c1c54b 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -20,28 +20,28 @@ default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"] tree_tpl = ( - " \ No newline at end of file diff --git a/html/extra-networks-tree.html b/html/extra-networks-tree.html new file mode 100644 index 00000000000..4d29b1be273 --- /dev/null +++ b/html/extra-networks-tree.html @@ -0,0 +1,42 @@ +
          +
          + +
          + +
          +
          + +
          +
          + +
          +
          +
          + {tree} +
          +
          \ No newline at end of file diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 56c9dc4c35d..cf98452a296 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -37,15 +37,20 @@ function setupExtraNetworksForTab(tabname) { return; // `continue` doesn't work in `forEach` loops. This is equivalent. } - var tabs = gradioApp().querySelector('#' + tabname + '_extra_tabs > div'); - var sort = gradioApp().getElementById(tabname + '_extra_sort'); - var sortOrder = gradioApp().getElementById(tabname + '_extra_sortorder'); - var refresh = gradioApp().getElementById(tabname + '_extra_refresh'); - var promptContainer = gradioApp().querySelector('.prompt-container-compact#' + tabname + '_prompt_container'); - var negativePrompt = gradioApp().querySelector('#' + tabname + '_neg_prompt'); - tabs.appendChild(sort); - tabs.appendChild(sortOrder); - tabs.appendChild(refresh); + var sort = gradioApp().querySelector("#" + tab_id + "_extra_sort"); + if (!sort) { + return; // `continue` doesn't work in `forEach` loops. This is equivalent. + } + + var sort_dir = gradioApp().querySelector("#" + tab_id + "_extra_sort_dir"); + if (!sort_dir) { + return; // `continue` doesn't work in `forEach` loops. This is equivalent. + } + + var refresh = gradioApp().querySelector("#" + tab_id + "_extra_refresh"); + if (!refresh) { + return; // `continue` doesn't work in `forEach` loops. This is equivalent. + } var applyFilter = function() { var searchTerm = search.value.toLowerCase(); @@ -72,8 +77,8 @@ function setupExtraNetworksForTab(tabname) { var applySort = function() { var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card'); - var reverse = sortOrder.classList.contains("sortReverse"); - var sortKey = sort.querySelector("input").value.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; + var reverse = sort_dir.dataset.sortdir == "Descending"; + var sortKey = sort.dataset.sortmode.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1); var sortKeyStore = sortKey + "-" + (reverse ? "Descending" : "Ascending") + "-" + cards.length; @@ -107,10 +112,7 @@ function setupExtraNetworksForTab(tabname) { }; search.addEventListener("input", applyFilter); - sortOrder.addEventListener("click", function() { - sortOrder.classList.toggle("sortReverse"); - applySort(); - }); + applySort(); applyFilter(); extraNetworksApplySort[tab_id] = applySort; @@ -274,7 +276,7 @@ function extraNetworksTreeProcessFileClick(event, btn, tabname, tab_id) { var par = btn.parentElement; var search_id = tabname + "_" + tab_id + "_extra_search"; var type = par.getAttribute("data-tree-entry-type"); - var path = par.getAttribute("data-path"); + var path = btn.getAttribute("data-path"); } function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { @@ -310,7 +312,7 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { function _remove_selected_from_all() { // Removes the `selected` attribute from all buttons. - var sels = document.querySelectorAll("button.tree-list-content"); + var sels = document.querySelectorAll("div.tree-list-content"); [...sels].forEach(el => { el.removeAttribute("selected"); }); @@ -345,11 +347,11 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { // NOTE: Double inversion sucks but it is the clearest way to show the branching here. _expand_or_collapse(ul, btn); _select_button(btn, tabname, tab_id); - _update_search(tabname, tab_id, btn.parentElement.getAttribute("data-path")); + _update_search(tabname, tab_id, btn.getAttribute("data-path")); } else { // All other cases, just select the button. _select_button(btn, tabname, tab_id); - _update_search(tabname, tab_id, btn.parentElement.getAttribute("data-path")); + _update_search(tabname, tab_id, btn.getAttribute("data-path")); } } } @@ -374,6 +376,48 @@ function extraNetworksTreeOnClick(event, tabname, tab_id) { } } +function extraNetworksTreeSortOnClick(event, tabname, tab_id) { + var curr_mode = event.currentTarget.dataset.sortmode; + var el_sort_dir = gradioApp().querySelector("#" + tabname + "_" + tab_id + "_extra_sort_dir"); + var sort_dir = el_sort_dir.dataset.sortdir; + if (curr_mode == "path") { + event.currentTarget.dataset.sortmode = "name"; + event.currentTarget.dataset.sortkey = "sortName-" + sort_dir + "-640"; + event.currentTarget.setAttribute("title", "Sort by filename"); + } else if (curr_mode == "name") { + event.currentTarget.dataset.sortmode = "date_created"; + event.currentTarget.dataset.sortkey = "sortDate_created-" + sort_dir + "-640"; + event.currentTarget.setAttribute("title", "Sort by date created"); + } else if (curr_mode == "date_created") { + event.currentTarget.dataset.sortmode = "date_modified"; + event.currentTarget.dataset.sortkey = "sortDate_modified-" + sort_dir + "-640"; + event.currentTarget.setAttribute("title", "Sort by date modified"); + } else { + event.currentTarget.dataset.sortmode = "path"; + event.currentTarget.dataset.sortkey = "sortPath-" + sort_dir + "-640"; + event.currentTarget.setAttribute("title", "Sort by path"); + } + applyExtraNetworkSort(tabname + "_" + tab_id); +} + +function extraNetworksTreeSortDirOnClick(event, tabname, tab_id) { + var curr_dir = event.currentTarget.getAttribute("data-sortdir"); + if (curr_dir == "Ascending") { + event.currentTarget.dataset.sortdir = "Descending"; + event.currentTarget.setAttribute("title", "Sort descending"); + } else { + event.currentTarget.dataset.sortdir = "Ascending"; + event.currentTarget.setAttribute("title", "Sort ascending"); + } + applyExtraNetworkSort(tabname + "_" + tab_id); +} + +function extraNetworksTreeRefreshOnClick(event, tabname, tab_id) { + console.log("refresh clicked"); + var btn_refresh_internal = gradioApp().getElementById(tabname + "_extra_refresh_internal"); + btn_refresh_internal.dispatchEvent(new Event("click")); +} + var globalPopup = null; var globalPopupInner = null; diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index a49c6c1c54b..4ba2bea1a8c 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -19,50 +19,6 @@ allowed_dirs = set() default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"] -tree_tpl = ( - "" - "
            " - "{content}" - "
          " -) - -tree_ul_tpl = ( - "
            " - "{content}" - "
          " -) - -tree_li_dir_tpl = ( - "
        • " - "{content}" - "
        • " -) -tree_li_file_tpl = ( - "
        • " - "{content}" - "
        • " -) - -action_list_item_action_leading = ( - "" - "" - "" -) - @functools.cache def allowed_preview_extensions_with_extra(extra_extensions=None): return set(default_allowed_preview_extensions) | set(extra_extensions or []) @@ -201,9 +157,13 @@ def __init__(self, title): self.title = title self.name = title.lower() self.id_page = self.name.replace(" ", "_") - self.extra_networks_pane_template = shared.html("extra-networks-pane.html") - self.card_page_template = shared.html("extra-networks-card.html") - self.tree_button_template = shared.html("extra-networks-tree-button.html") + self.pane_tpl = shared.html("extra-networks-pane.html") + self.tree_tpl = shared.html("extra-networks-tree.html") + self.card_tpl = shared.html("extra-networks-card.html") + self.btn_tree_tpl = shared.html("extra-networks-tree-button.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.allow_prompt = True self.allow_negative_prompt = False self.metadata = {} @@ -268,12 +228,8 @@ def create_item_html( onclick = item.get("onclick", None) if onclick is None: - print("HERE") - print("TABNAME:", tabname) - print("PROMPT:", item["prompt"]) - print("NEG_PROMPT:", item.get("negative_prompt", "")) - print("ALLOW_NEG:", self.allow_negative_prompt) - onclick_js_tpl = "cardClicked('{tabname}', '{prompt}', '{neg_prompt}', '{allow_neg}');" + # Don't quote prompt/neg_prompt since they are stored as js strings already. + onclick_js_tpl = "cardClicked('{tabname}', {prompt}, {neg_prompt}, '{allow_neg}');" onclick = onclick_js_tpl.format( **{ "tabname": tabname, @@ -284,15 +240,23 @@ def create_item_html( ) onclick = html.escape(onclick) - - copy_path_button = f"
          " - - metadata_button = "" + btn_copy_path = self.btn_copy_path_tpl.format(**{"filename": item["filename"]}) + btn_metadata = "" metadata = item.get("metadata") if metadata: - metadata_button = f"" - - edit_button = f"
          " + btn_metadata = self.btn_metadata_tpl.format( + **{ + "page_id": self.id_page, + "name": html.escape(item["name"]), + } + ) + btn_edit_item = self.btn_edit_item_tpl.format( + **{ + "tabname": tabname, + "page_id": self.id_page, + "name": html.escape(item["name"]), + } + ) local_path = "" filename = item.get("filename", "") @@ -334,11 +298,11 @@ def create_item_html( args = { "background_image": background_image, "card_clicked": onclick, - "copy_path_button": copy_path_button, + "copy_path_button": btn_copy_path, "description": (item.get("description") or "" if shared.opts.extra_networks_card_show_desc else ""), - "edit_button": edit_button, + "edit_button": btn_edit_item, "local_preview": quote_js(item["local_preview"]), - "metadata_button": metadata_button, + "metadata_button": btn_metadata, "name": html.escape(item["name"]), "prompt": item.get("prompt", None), "save_card_preview": '"' + html.escape(f"""return saveCardPreview(event, {quote_js(tabname)}, {quote_js(item["local_preview"])})""") + '"', @@ -355,6 +319,57 @@ def create_item_html( else: return args + def create_tree_dir_item_html(self, tabname: str, dir_path: str, content: Optional[str] = None) -> Optional[str]: + if not content: + return None + + btn = self.btn_tree_tpl.format( + **{ + "search_terms": "", + "subclass": "tree-list-content-dir", + "tabname": tabname, + "tab_id": self.id_page, + "onclick_extra": "", + "data_path": dir_path, + "data_hash": "", + "action_list_item_action_leading": "", + "action_list_item_visual_leading": "🗀", + "action_list_item_label": os.path.basename(dir_path), + "action_list_item_visual_trailing": "", + "action_list_item_action_trailing": "", + } + ) + ul = f"
            {content}
          " + return f"
        • {btn + ul}
        • " + + def create_tree_file_item_html(self, tabname: str, item_name: str, item: dict) -> str: + item_html_args = self.create_item_html(tabname, item) + action_buttons = "".join( + [ + item_html_args["copy_path_button"], + item_html_args["metadata_button"], + item_html_args["edit_button"], + ] + ) + action_buttons = f"
          {action_buttons}
          " + btn = self.btn_tree_tpl.format( + **{ + "search_terms": "", + "subclass": "tree-list-content-file", + "tabname": tabname, + "tab_id": self.id_page, + "onclick_extra": item_html_args["card_clicked"], + "data_path": item_name, + "data_hash": item["shorthash"], + "action_list_item_action_leading": "", + "action_list_item_visual_leading": "🗎", + "action_list_item_label": item["name"], + "action_list_item_visual_trailing": "", + "action_list_item_action_trailing": action_buttons, + } + ) + return f"
        • {btn}
        • " + def create_tree_view_html(self, tabname: str) -> str: """Generates HTML for displaying folders in a tree view. @@ -385,57 +400,9 @@ def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> Optional for k, v in sorted(data.items(), key=lambda x: shared.natural_sort_key(x[0])): if isinstance(v, (ExtraNetworksItem,)): - _item_html_args = self.create_item_html(tabname, v.item) - _action_buttons = "".join( - [ - _item_html_args["copy_path_button"], - _item_html_args["metadata_button"], - _item_html_args["edit_button"], - ] - ) - _action_buttons = f"
          {_action_buttons}
          " - _btn = self.tree_button_template.format( - **{ - "search_terms": "", - "subclass": "tree-list-content-file", - "tabname": tabname, - "tab_id": self.id_page, - "onclick_extra": _item_html_args["card_clicked"], - "action_list_item_action_leading": action_list_item_action_leading, - "action_list_item_visual_leading": "🗎", - "action_list_item_label": v.item["name"], - "action_list_item_visual_trailing": "", - "action_list_item_action_trailing": _action_buttons, - } - ) - - _li = tree_li_file_tpl.format( - **{ - "hash": v.item["shorthash"], - "path": k, - "type": "file", - "content": _btn, - } - ) - _file_li.append(_li) + _file_li.append(self.create_tree_file_item_html(tabname, k, v.item)) else: - _btn = self.tree_button_template.format( - **{ - "search_terms": "", - "subclass": "tree-list-content-dir", - "tabname": tabname, - "tab_id": self.id_page, - "onclick_extra": "", - "action_list_item_action_leading": action_list_item_action_leading, - "action_list_item_visual_leading": "🗀", - "action_list_item_label": os.path.basename(k), - "action_list_item_visual_trailing": "", - "action_list_item_action_trailing": "", - } - ) - _ul = tree_ul_tpl.format(**{"content": _build_tree(v)}) - _li = tree_li_dir_tpl.format(**{"content": _btn + _ul, "path": k}) - _dir_li.append(_li) + _dir_li.append(self.create_tree_dir_item_html(tabname, k, _build_tree(v))) # Directories should always be displayed before files. return "".join(_dir_li) + "".join(_file_li) @@ -443,31 +410,15 @@ def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> Optional # Add each root directory to the tree. for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])): # If root is empty, append the "disabled" attribute to the template details tag. - btn = self.tree_button_template.format( - **{ - "search_terms": "", - "subclass": "tree-list-content-dir", - "tabname": tabname, - "tab_id": self.id_page, - "onclick_extra": "", - "action_list_item_action_leading": action_list_item_action_leading, - "action_list_item_visual_leading": "🗀", - "action_list_item_label": os.path.basename(k), - "action_list_item_visual_trailing": "", - "action_list_item_action_trailing": "", - } - ) - subtree = _build_tree(v) - if subtree: - ul = tree_ul_tpl.format(**{"content": _build_tree(v)}) - li = tree_li_dir_tpl.format(**{"content": btn + ul, "path": k}) - res += li + item_html = self.create_tree_dir_item_html(tabname, k, _build_tree(v)) + if item_html: + res += item_html - return tree_tpl.format( + return self.tree_tpl.format( **{ - "content": res, "tabname": tabname, "tab_id": self.id_page, + "tree": f"
            {res}
          " } ) @@ -475,8 +426,7 @@ def create_card_view_html(self, tabname): res = "" self.items = {x["name"]: x for x in self.list_items()} for item in self.items.values(): - print("HEEEERRE:", item) - res += self.create_item_html(tabname, item, self.card_page_template) + res += self.create_item_html(tabname, item, self.card_tpl) if res == "": dirs = "".join([f"
        • {x}
        • " for x in self.allowed_directories_for_previews()]) @@ -493,7 +443,7 @@ def create_html(self, tabname): card_view_html = self.create_card_view_html(tabname) network_type_id = self.id_page - return self.extra_networks_pane_template.format( + return self.pane_tpl.format( **{ "tabname": tabname, "network_type_id": network_type_id, @@ -612,6 +562,8 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): related_tabs = [] + button_refresh = gr.Button("Refresh", elem_id=tabname+"_extra_refresh_internal", visible=False) + for page in ui.stored_extra_pages: with gr.Tab(page.title, elem_id=f"{tabname}_{page.id_page}", elem_classes=["extra-page"]) as tab: with gr.Column(elem_id=f"{tabname}_{page.id_page}_prompts", elem_classes=["extra-page-prompts"]): @@ -633,51 +585,9 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): related_tabs.append(tab) - dropdown_sort = gr.Dropdown(choices=['Path', 'Name', 'Date Created', 'Date Modified', ], value=shared.opts.extra_networks_card_order_field, elem_id=tabname+"_extra_sort", elem_classes="sort", multiselect=False, visible=False, show_label=False, interactive=True, label=tabname+"_extra_sort_order") - button_sortorder = ToolButton(switch_values_symbol, elem_id=tabname+"_extra_sortorder", elem_classes=["sortorder"] + ([] if shared.opts.extra_networks_card_order == "Ascending" else ["sortReverse"]), visible=False, tooltip="Invert sort order") - button_refresh = gr.Button('Refresh', elem_id=tabname+"_extra_refresh", visible=False) - - tab_controls = [ - dropdown_sort, - button_sortorder, - button_refresh, - ] - ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False) ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=tabname+"_preview_filename", visible=False) - for tab in unrelated_tabs: - tab.select( - fn=lambda: [gr.update(visible=False) for _ in tab_controls], - _js=f"function(){{ extraNetworksUnrelatedTabSelected('{tabname}'); }}", - inputs=[], - outputs=tab_controls, - show_progress=False, - ) - - for page, tab in zip(ui.stored_extra_pages, related_tabs): - allow_prompt = "true" if page.allow_prompt else "false" - allow_negative_prompt = "true" if page.allow_negative_prompt else "false" - - jscode = ( - "extraNetworksTabSelected(" - f"'{tabname}', " - f"'{tabname}_{page.id_page}_prompts', " - f"'{allow_prompt}', " - f"'{allow_negative_prompt}'" - ");" - ) - - tab.select( - fn=lambda: [gr.update(visible=True) for _ in tab_controls], - _js="function(){ " + jscode + " }", - inputs=[], - outputs=tab_controls, - show_progress=False, - ) - - dropdown_sort.change(fn=lambda: None, _js="function(){ applyExtraNetworkSort('" + tabname + "'); }") - def create_html(): ui.pages_contents = [pg.create_html(ui.tabname) for pg in ui.stored_extra_pages] @@ -693,6 +603,8 @@ def refresh(): return ui.pages_contents interface.load(fn=pages_html, inputs=[], outputs=ui.pages) + # NOTE: Event is manually fired in extraNetworks.js:extraNetworksTreeRefreshOnClick() + # button is unused and hidden at all times. Only used in order to fire this event. button_refresh.click(fn=refresh, inputs=[], outputs=ui.pages) return ui diff --git a/style.css b/style.css index 2dafe97fd87..08573248654 100644 --- a/style.css +++ b/style.css @@ -1196,17 +1196,33 @@ body.resizing .resize-handle { overflow: hidden; } -.extra-network-tree .tree-list--tree { - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - margin: 0; +.extra-network-tree .tree-list { + margin: 0 0.25rem; padding: 0; - margin-left: 0.25rem; } +.extra-network-tree .tree-list .tree-list-controls { + position: relative; + display: grid; + width: 100%; + padding: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + font-size: 1rem; + text-align: left; + user-select: none; + background-color: transparent; + border: none; + transition: background 33.333ms linear; + grid-template-rows: min-content; + grid-template-areas: "tree-list-controls-col-0 tree-list-controls-col-1 tree-list-controls-col-2 tree-list-controls-col-3"; + grid-template-columns: minmax(0, auto) min-content min-content min-content; + grid-gap: 0.1rem; + align-items: start; +} + +.extra-network-tree .tree-list--tree {} + /* Remove auto indentation from tree. Will be overridden later. */ .extra-network-tree .tree-list--subgroup { margin: 0 !important; @@ -1221,9 +1237,6 @@ body.resizing .resize-handle { padding-left: 0.4rem !important; } -/* Styles for tree
            elements. */ -.extra-network-tree .tree-list {} - /* Styles for tree
          • elements. */ .extra-network-tree .tree-list-item { list-style: none; @@ -1288,26 +1301,182 @@ body.resizing .resize-handle { padding-top: 0.5rem !important; } -.dark .extra-network-tree button.tree-list-content:hover { +.dark .extra-network-tree div.tree-list-content:hover { -webkit-transition: all 0.05s ease-in-out; transition: all 0.05s ease-in-out; background-color: var(--neutral-800); } -.dark .extra-network-tree button.tree-list-content[selected] { +.dark .extra-network-tree div.tree-list-content[selected] { background-color: var(--neutral-700); } -.extra-network-tree button.tree-list-content:hover { +.extra-network-tree div.tree-list-content:hover { -webkit-transition: all 0.05s ease-in-out; transition: all 0.05s ease-in-out; background-color: var(--neutral-200); } -.extra-network-tree button.tree-list-content[selected] { +.extra-network-tree div.tree-list-content[selected] { background-color: var(--neutral-300); } +/* ==== CHEVRON ICON ACTIONS ==== */ +/* Define the animation for the arrow when it is clicked. */ +.extra-network-tree .tree-list-content-dir[expanded=false] .tree-list-item-action-chevron { + -ms-transform: rotate(135deg); + -webkit-transform: rotate(135deg); + transform: rotate(135deg); + transition: transform 0.2s; +} + +.extra-network-tree .tree-list-content-dir[expanded=true] .tree-list-item-action-chevron { + -ms-transform: rotate(225deg); + -webkit-transform: rotate(225deg); + transform: rotate(225deg); + transition: transform 0.2s; +} + +.tree-list-item-action-chevron { + display: inline-flex; + /* Uses box shadow to generate a pseudo chevron `>` icon. */ + padding: 0.3rem; + box-shadow: 0.1rem 0.1rem 0 0 var(--neutral-200) inset; + transform: rotate(135deg); +} + +/* ==== SEARCH INPUT ACTIONS ==== */ +/* Add icon to left side of */ +.extra-network-tree .tree-list-controls .tree-list-search::before { + content: "🔎︎"; + position: absolute; + margin: 0.5rem; + font-size: 1rem; + color: var(--input-placeholder-color); +} + +.extra-network-tree .tree-list-controls .tree-list-search { + display: inline-flex; + grid-area: tree-list-controls-col-0; + position: relative; + margin: 0.5rem; +} + +.extra-network-tree .tree-list-controls .tree-list-search .tree-list-search-text { + border: 1px solid var(--button-secondary-border-color); + border-radius: 0.5rem; + color: var(--button-secondary-text-color); + background-color: transparent; + width: 100%; + padding-left: 2rem; + line-height: 1rem; +} + +/* clear button (x on right side) styling */ +.extra-network-tree .tree-list-controls .tree-list-search .tree-list-search-text::-webkit-search-cancel-button { + -webkit-appearance: none; + appearance: none; + cursor: pointer; + height: 1rem; + width: 1rem; + mask-image: url('data:image/svg+xml,'); + mask-repeat: no-repeat; + mask-position: center center; + mask-size: 100%; + background-color: var(--input-placeholder-color); +} + +/* ==== SORT ICON ACTIONS ==== */ +.extra-network-tree .tree-list-controls .tree-list-sort { + grid-area: tree-list-controls-col-1; + padding: 0.25rem; + display: inline-flex; + cursor: pointer; + justify-self: center; + align-self: center; +} + +.extra-network-tree .tree-list-controls .tree-list-sort .tree-list-sort-icon { + height: 1.5rem; + width: 1.5rem; + mask-repeat: no-repeat; + mask-position: center center; + mask-size: 100%; + background-color: var(--input-placeholder-color); +} + +.extra-network-tree .tree-list-sort[data-sortmode="path"] .tree-list-sort-icon { + mask-image: url('data:image/svg+xml,'); +} + +.extra-network-tree .tree-list-sort[data-sortmode="name"] .tree-list-sort-icon { + mask-image: url('data:image/svg+xml,'); +} + +.extra-network-tree .tree-list-sort[data-sortmode="date_created"] .tree-list-sort-icon { + mask-image: url('data:image/svg+xml,'); +} + +.extra-network-tree .tree-list-sort[data-sortmode="date_modified"] .tree-list-sort-icon { + mask-image: url('data:image/svg+xml,'); +} + +/* ==== SORT DIRECTION ICON ACTIONS ==== */ +.extra-network-tree .tree-list-controls .tree-list-sort-dir { + grid-area: tree-list-controls-col-2; + padding: 0.25rem; + display: inline-flex; + cursor: pointer; + justify-self: center; + align-self: center; +} + +.extra-network-tree .tree-list-controls .tree-list-sort-dir .tree-list-sort-dir-icon { + height: 1.5rem; + width: 1.5rem; + mask-repeat: no-repeat; + mask-position: center center; + mask-size: 100%; + background-color: var(--input-placeholder-color); +} + +.extra-network-tree .tree-list-sort-dir[data-sortdir="Ascending"] .tree-list-sort-dir-icon { + mask-image: url('data:image/svg+xml,'); +} + +.extra-network-tree .tree-list-sort-dir[data-sortdir="Descending"] .tree-list-sort-dir-icon { + mask-image: url('data:image/svg+xml,'); +} + +/* ==== REFRESH ICON ACTIONS ==== */ +.extra-network-tree .tree-list-controls .tree-list-refresh { + grid-area: tree-list-controls-col-3; + padding: 0.25rem; + display: inline-flex; + cursor: pointer; + justify-self: center; + align-self: center; +} + +.extra-network-tree .tree-list-controls .tree-list-refresh .tree-list-refresh-icon { + height: 1.5rem; + width: 1.5rem; + mask-image: url('data:image/svg+xml,'); + mask-repeat: no-repeat; + mask-position: center center; + mask-size: 100%; + background-color: var(--input-placeholder-color); +} + +.extra-network-tree .tree-list-refresh-icon:active { + -ms-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + transition: transform 0.2s; +} + +/* ==== TREE GRID CONFIG ==== */ + /* Text for button. */ .extra-network-tree .tree-list-item-label { position: relative; @@ -1332,6 +1501,7 @@ body.resizing .resize-handle { align-items: right; } + /* Icon for button when it is before label. */ .extra-network-tree .tree-list-item-visual--leading { grid-area: leading-visual; @@ -1348,7 +1518,7 @@ body.resizing .resize-handle { /* Dropdown arrow for button. */ .extra-network-tree .tree-list-item-action--leading { - margin-right: 0.2rem; + margin-right: 0.5rem; margin-left: 0.2rem; } @@ -1356,30 +1526,6 @@ body.resizing .resize-handle { visibility: hidden; } -/* Define the animation for the arrow when it is clicked. */ -.extra-network-tree .tree-list-content-dir[expanded=false] .tree-list-item-action-chevron { - -ms-transform: rotate(135deg); - -webkit-transform: rotate(135deg); - transform: rotate(135deg); - transition: transform 0.2s; -} - -.extra-network-tree .tree-list-content-dir[expanded=true] .tree-list-item-action-chevron { - -ms-transform: rotate(225deg); - -webkit-transform: rotate(225deg); - transform: rotate(225deg); - transition: transform 0.2s; -} - -.tree-list-item-action-chevron { - display: inline-flex; - /* Uses box shadow to generate a pseudo chevron `>` icon. */ - padding: 0.3rem; - box-shadow: 0.1rem 0.1rem 0 0 var(--neutral-200) inset; - transform: rotate(135deg); -} - - .extra-network-tree .tree-list-item-action--leading { grid-area: leading-action; } @@ -1399,41 +1545,3 @@ body.resizing .resize-handle { .extra-network-tree .tree-list-content:hover .button-row { visibility: visible; } - -/* Add icon to left side of */ -.extra-network-tree .tree-list-search::before { - content: "🔎︎"; - position: absolute; - margin: 0.5rem; - font-size: 1rem; - color: var(--input-placeholder-color); -} - -.extra-network-tree .tree-list-search { - position: relative; - margin: 0.5rem; -} - -.extra-network-tree .tree-list-search .tree-list-search-text { - border: 1px solid var(--button-secondary-border-color); - border-radius: 0.5rem; - color: var(--button-secondary-text-color); - background-color: transparent; - width: 100%; - padding-left: 2rem; - line-height: 1rem; -} - -/* clear button (x on right side) styling */ -.extra-network-tree .tree-list-search .tree-list-search-text::-webkit-search-cancel-button { - -webkit-appearance: none; - appearance: none; - cursor: pointer; - height: 1rem; - width: 1rem; - mask-image: url('data:image/svg+xml,'); - mask-repeat: no-repeat; - mask-position: center center; - mask-size: 100%; - background-color: var(--input-placeholder-color); -} \ No newline at end of file From 1fdc18e6a01eb40889d46fab40f21aa138d64b01 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Mon, 15 Jan 2024 18:01:13 -0500 Subject: [PATCH 11/20] Run linting --- modules/ui_extra_networks.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 4ba2bea1a8c..06fa22afada 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -13,7 +13,6 @@ from fastapi.exceptions import HTTPException from modules.infotext_utils import image_from_url_text -from modules.ui_components import ToolButton extra_pages = [] allowed_dirs = set() @@ -225,7 +224,6 @@ def create_item_html( width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else '' background_image = f'' if preview else '' - onclick = item.get("onclick", None) if onclick is None: # Don't quote prompt/neg_prompt since they are stored as js strings already. @@ -239,7 +237,7 @@ def create_item_html( } ) onclick = html.escape(onclick) - + btn_copy_path = self.btn_copy_path_tpl.format(**{"filename": item["filename"]}) btn_metadata = "" metadata = item.get("metadata") @@ -551,8 +549,6 @@ def tab_name_score(name): return sorted(pages, key=lambda x: tab_scores[x.name]) def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): - from modules.ui import switch_values_symbol - ui = ExtraNetworksUi() ui.pages = [] ui.pages_contents = [] @@ -588,6 +584,9 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False) ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=tabname+"_preview_filename", visible=False) + for tab in unrelated_tabs: + tab.select(fn=None, _js='function(){ extraNetworksUrelatedTabSelected("' + tabname + '"); }', inputs=[], outputs=[], show_progress=False) + def create_html(): ui.pages_contents = [pg.create_html(ui.tabname) for pg in ui.stored_extra_pages] From 4f9626703345ced77935e6bbb06de0b4522d53b7 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Tue, 16 Jan 2024 13:35:01 -0500 Subject: [PATCH 12/20] Finish cleanup. --- html/extra-networks-card-minimal.html | 4 - html/extra-networks-card.html | 10 +- html/extra-networks-edit-item-button.html | 2 +- html/extra-networks-metadata-button.html | 2 +- html/extra-networks-pane.html | 6 +- html/extra-networks-tree-button.html | 3 +- html/extra-networks-tree.html | 14 +- javascript/extraNetworks.js | 170 +++++++++++---------- modules/ui_extra_networks.py | 162 +++++++++++++++----- modules/ui_extra_networks_user_metadata.py | 2 +- style.css | 81 +++++++--- 11 files changed, 288 insertions(+), 168 deletions(-) delete mode 100644 html/extra-networks-card-minimal.html diff --git a/html/extra-networks-card-minimal.html b/html/extra-networks-card-minimal.html deleted file mode 100644 index d66df7dfb3f..00000000000 --- a/html/extra-networks-card-minimal.html +++ /dev/null @@ -1,4 +0,0 @@ -
            - {name} - {copy_path_button}{metadata_button}{edit_button} -
            diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html index ca683dc47ec..f1d959a6733 100644 --- a/html/extra-networks-card.html +++ b/html/extra-networks-card.html @@ -1,9 +1,9 @@ -
            +
            {background_image}
            {copy_path_button}{metadata_button}{edit_button}
            -
            -
            {search_terms}
            - {name} - {description} +
            +
            {search_terms}
            + {name} + {description}
            diff --git a/html/extra-networks-edit-item-button.html b/html/extra-networks-edit-item-button.html index 7d2677d9f14..0fe43082ad1 100644 --- a/html/extra-networks-edit-item-button.html +++ b/html/extra-networks-edit-item-button.html @@ -1,4 +1,4 @@
            + onclick="extraNetworksEditUserMetadata(event, '{tabname}', '{extra_networks_tabname}', '{name}')">
            \ No newline at end of file diff --git a/html/extra-networks-metadata-button.html b/html/extra-networks-metadata-button.html index ad6d6f41662..285b5b3b658 100644 --- a/html/extra-networks-metadata-button.html +++ b/html/extra-networks-metadata-button.html @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index 20cf6686755..bf46ca16305 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -1,8 +1,8 @@ -
            -
            +
            +
            {tree_html}
            -
            +
            {items_html}
            \ No newline at end of file diff --git a/html/extra-networks-tree-button.html b/html/extra-networks-tree-button.html index 20a9b0b863f..9dc2e2a40c8 100644 --- a/html/extra-networks-tree-button.html +++ b/html/extra-networks-tree-button.html @@ -1,8 +1,7 @@
            diff --git a/html/extra-networks-tree.html b/html/extra-networks-tree.html index 4d29b1be273..23f6af1058c 100644 --- a/html/extra-networks-tree.html +++ b/html/extra-networks-tree.html @@ -2,36 +2,36 @@
            diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index cf98452a296..a3f003bf475 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -31,25 +31,15 @@ function setupExtraNetworksForTab(tabname) { var this_tab = gradioApp().querySelector('#' + tabname + '_extra_tabs'); this_tab.classList.add('extra-networks'); this_tab.querySelectorAll(":scope > [id^='" + tabname + "_']").forEach(function(elem) { - var tab_id = elem.getAttribute("id"); - var search = gradioApp().querySelector("#" + tab_id + "_extra_search"); - if (!search) { - return; // `continue` doesn't work in `forEach` loops. This is equivalent. - } - - var sort = gradioApp().querySelector("#" + tab_id + "_extra_sort"); - if (!sort) { - return; // `continue` doesn't work in `forEach` loops. This is equivalent. - } - - var sort_dir = gradioApp().querySelector("#" + tab_id + "_extra_sort_dir"); - if (!sort_dir) { - return; // `continue` doesn't work in `forEach` loops. This is equivalent. - } - - var refresh = gradioApp().querySelector("#" + tab_id + "_extra_refresh"); - if (!refresh) { - return; // `continue` doesn't work in `forEach` loops. This is equivalent. + var extra_networks_tabname = elem.id; + var search = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_search"); + var sort_mode = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_sort"); + var sort_dir = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_sort_dir"); + var refresh = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_refresh"); + + // If any of the buttons above don't exist, we want to skip this iteration of the loop. + if (!search || !sort_mode || !sort_dir || !refresh) { + return; // `return` is equivalent of `continue` but for forEach loops. } var applyFilter = function() { @@ -78,14 +68,14 @@ function setupExtraNetworksForTab(tabname) { var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card'); var reverse = sort_dir.dataset.sortdir == "Descending"; - var sortKey = sort.dataset.sortmode.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; + var sortKey = sort_mode.dataset.sortmode.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1); var sortKeyStore = sortKey + "-" + (reverse ? "Descending" : "Ascending") + "-" + cards.length; - if (sortKeyStore == sort.dataset.sortkey) { + if (sortKeyStore == sort_mode.dataset.sortkey) { return; } - sort.dataset.sortkey = sortKeyStore; + sort_mode.dataset.sortkey = sortKeyStore; cards.forEach(function(card) { card.originalParentElement = card.parentElement; @@ -115,8 +105,8 @@ function setupExtraNetworksForTab(tabname) { applySort(); applyFilter(); - extraNetworksApplySort[tab_id] = applySort; - extraNetworksApplyFilter[tab_id] = applyFilter; + extraNetworksApplySort[extra_networks_tabname] = applySort; + extraNetworksApplyFilter[extra_networks_tabname] = applyFilter; }); registerPrompt(tabname, tabname + "_prompt"); @@ -148,14 +138,6 @@ function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePromp } } -function clearSearch(tabname) { - // Clear search box. - var tab_id = tabname + "_extra_search"; - var searchTextarea = gradioApp().querySelector("#" + tab_id + ' > label > textarea'); - searchTextarea.value = ""; - updateInput(searchTextarea); -} - function extraNetworksUnrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate) extraNetworksMovePromptToTab(tabname, '', false, false); @@ -264,22 +246,20 @@ function saveCardPreview(event, tabname, filename) { event.preventDefault(); } -function extraNetworksTreeProcessFileClick(event, btn, tabname, tab_id) { +function extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname) { /** * 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 The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. - * @param tab_id The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + * @param event The generated event. + * @param btn The clicked `tree-list-item` button. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. */ - var par = btn.parentElement; - var search_id = tabname + "_" + tab_id + "_extra_search"; - var type = par.getAttribute("data-tree-entry-type"); - var path = btn.getAttribute("data-path"); + // NOTE: Currently unused. + return; } -function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { +function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_networks_tabname) { /** * Processes `onclick` events when user clicks on directories in tree. * @@ -289,10 +269,10 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { * 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 The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. - * @param tab_id The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + * @param event The generated event. + * @param btn The clicked `tree-list-item` button. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. */ var ul = btn.nextElementSibling; // This is the actual target that the user clicked on within the target button. @@ -301,12 +281,12 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { function _expand_or_collapse(_ul, _btn) { // Expands
              if it is collapsed, collapses otherwise. Updates button attributes. - if (_ul.hasAttribute("data-hidden")) { - _ul.removeAttribute("data-hidden"); - _btn.setAttribute("expanded", "true"); + if (_ul.hasAttribute("hidden")) { + _ul.removeAttribute("hidden"); + _btn.dataset.expanded = ""; } else { - _ul.setAttribute("data-hidden", ""); - _btn.setAttribute("expanded", "false"); + _ul.setAttribute("hidden", ""); + delete _btn.dataset.expanded; } } @@ -314,19 +294,19 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { // Removes the `selected` attribute from all buttons. var sels = document.querySelectorAll("div.tree-list-content"); [...sels].forEach(el => { - el.removeAttribute("selected"); + delete el.dataset.selected; }); } function _select_button(_btn) { - // Removes `selected` attribute from all buttons then adds to passed button. + // Removes `data-selected` attribute from all buttons then adds to passed button. _remove_selected_from_all(); - _btn.setAttribute("selected", ""); + _btn.dataset.selected = ""; } - function _update_search(_tabname, _tab_id, _search_text) { + function _update_search(_tabname, _extra_networks_tabname, _search_text) { // Update search input with select button's path. - var search_input_elem = gradioApp().querySelector("#" + tabname + "_" + tab_id + "_extra_search"); + var search_input_elem = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_search"); search_input_elem.value = _search_text; updateInput(search_input_elem); } @@ -337,48 +317,58 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id) { _expand_or_collapse(ul, btn); } else { // User clicked anywhere else on the button. - if (btn.hasAttribute("selected") && !ul.hasAttribute("data-hidden")) { + if ("selected" in btn.dataset && !(ul.hasAttribute("hidden"))) { // If folder is select and open, collapse and deselect button. _expand_or_collapse(ul, btn); - btn.removeAttribute("selected"); - _update_search(tabname, tab_id, ""); - } else if (!(!btn.hasAttribute("selected") && !ul.hasAttribute("data-hidden"))) { + delete btn.dataset.selected; + _update_search(tabname, extra_networks_tabname, ""); + } else if (!(!("selected" in btn.dataset) && !(ul.hasAttribute("hidden")))) { // If folder is open and not selected, then we don't collapse; just select. // NOTE: Double inversion sucks but it is the clearest way to show the branching here. _expand_or_collapse(ul, btn); - _select_button(btn, tabname, tab_id); - _update_search(tabname, tab_id, btn.getAttribute("data-path")); + _select_button(btn, tabname, extra_networks_tabname); + _update_search(tabname, extra_networks_tabname, btn.dataset.path); } else { // All other cases, just select the button. - _select_button(btn, tabname, tab_id); - _update_search(tabname, tab_id, btn.getAttribute("data-path")); + _select_button(btn, tabname, extra_networks_tabname); + _update_search(tabname, extra_networks_tabname, btn.dataset.path); } } } -function extraNetworksTreeOnClick(event, tabname, tab_id) { +function extraNetworksTreeOnClick(event, tabname, extra_networks_tabname) { /** * 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 The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. - * @param tab_id The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + * @param event The generated event. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. */ var btn = event.currentTarget; var par = btn.parentElement; - if (par.getAttribute("data-tree-entry-type") === "file") { - extraNetworksTreeProcessFileClick(event, btn, tabname, tab_id); + if (par.dataset.treeEntryType === "file") { + extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname); } else { - extraNetworksTreeProcessDirectoryClick(event, btn, tabname, tab_id); + extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_networks_tabname); } } -function extraNetworksTreeSortOnClick(event, tabname, tab_id) { +function extraNetworksTreeSortOnClick(event, tabname, extra_networks_tabname) { + /** + * Handles `onclick` events for the Sort Mode button. + * + * Modifies the data attributes of the Sort Mode button to cycle between + * various sorting modes. + * + * @param event The generated event. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + */ var curr_mode = event.currentTarget.dataset.sortmode; - var el_sort_dir = gradioApp().querySelector("#" + tabname + "_" + tab_id + "_extra_sort_dir"); + var el_sort_dir = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_sort_dir"); var sort_dir = el_sort_dir.dataset.sortdir; if (curr_mode == "path") { event.currentTarget.dataset.sortmode = "name"; @@ -397,23 +387,43 @@ function extraNetworksTreeSortOnClick(event, tabname, tab_id) { event.currentTarget.dataset.sortkey = "sortPath-" + sort_dir + "-640"; event.currentTarget.setAttribute("title", "Sort by path"); } - applyExtraNetworkSort(tabname + "_" + tab_id); + applyExtraNetworkSort(tabname + "_" + extra_networks_tabname); } -function extraNetworksTreeSortDirOnClick(event, tabname, tab_id) { - var curr_dir = event.currentTarget.getAttribute("data-sortdir"); - if (curr_dir == "Ascending") { +function extraNetworksTreeSortDirOnClick(event, tabname, extra_networks_tabname) { + /** + * 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. + * + * @param event The generated event. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + */ + if (event.currentTarget.dataset.sortdir == "Ascending") { event.currentTarget.dataset.sortdir = "Descending"; event.currentTarget.setAttribute("title", "Sort descending"); } else { event.currentTarget.dataset.sortdir = "Ascending"; event.currentTarget.setAttribute("title", "Sort ascending"); } - applyExtraNetworkSort(tabname + "_" + tab_id); + applyExtraNetworkSort(tabname + "_" + extra_networks_tabname); } -function extraNetworksTreeRefreshOnClick(event, tabname, tab_id) { - console.log("refresh clicked"); +function extraNetworksTreeRefreshOnClick(event, tabname, extra_networks_tabname) { + /** + * 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. + * + * @param event The generated event. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + */ var btn_refresh_internal = gradioApp().getElementById(tabname + "_extra_refresh_internal"); btn_refresh_internal.dispatchEvent(new Event("click")); } diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 06fa22afada..a03207b2bc0 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -155,7 +155,14 @@ class ExtraNetworksPage: def __init__(self, title): self.title = title self.name = title.lower() - self.id_page = self.name.replace(" ", "_") + # This is the actual name of the extra networks tab (not txt2img/img2img). + self.extra_networks_tabname = self.name.replace(" ", "_") + self.allow_prompt = True + self.allow_negative_prompt = False + self.metadata = {} + self.items = {} + self.lister = util.MassFileLister() + # HTML Templates self.pane_tpl = shared.html("extra-networks-pane.html") self.tree_tpl = shared.html("extra-networks-tree.html") self.card_tpl = shared.html("extra-networks-card.html") @@ -163,11 +170,6 @@ def __init__(self, title): 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.allow_prompt = True - self.allow_negative_prompt = False - self.metadata = {} - self.items = {} - self.lister = util.MassFileLister() def refresh(self): pass @@ -202,15 +204,17 @@ def create_item_html( item: dict, template: Optional[str] = None, ) -> Union[str, dict]: - """Generates HTML for a single ExtraNetworks Item + """Generates HTML for a single ExtraNetworks Item. Args: tabname: The name of the active tab. item: Dictionary containing item information. + template: Optional template string to use. Returns: - HTML string generated for this item. - Can be empty if the item is not meant to be shown. + If a template is passed: HTML string generated for this item. + Can be empty if the item is not meant to be shown. + If no template is passed: A dictionary containing the generated item's attributes. """ metadata = item.get("metadata") if metadata: @@ -244,14 +248,14 @@ def create_item_html( if metadata: btn_metadata = self.btn_metadata_tpl.format( **{ - "page_id": self.id_page, + "extra_networks_tabname": self.extra_networks_tabname, "name": html.escape(item["name"]), } ) btn_edit_item = self.btn_edit_item_tpl.format( **{ "tabname": tabname, - "page_id": self.id_page, + "extra_networks_tabname": self.extra_networks_tabname, "name": html.escape(item["name"]), } ) @@ -307,9 +311,9 @@ def create_item_html( "search_only": " search_only" if search_only else "", "search_terms": search_terms_html, "sort_keys": sort_keys, - "style": f"'display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%'", + "style": f"display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%", "tabname": tabname, - "tab_id": self.id_page, + "extra_networks_tabname": self.extra_networks_tabname, } if template: @@ -317,7 +321,32 @@ def create_item_html( else: return args - def create_tree_dir_item_html(self, tabname: str, dir_path: str, content: Optional[str] = None) -> Optional[str]: + def create_tree_dir_item_html( + self, + tabname: str, + dir_path: str, + content: Optional[str] = None, + ) -> Optional[str]: + """Generates HTML for a directory item in the tree. + + The generated HTML is of the format: + ```html +
            • +
              +
                + {content} +
              +
            • + ``` + + Args: + tabname: The name of the active tab. + dir_path: Path to the directory for this item. + content: Optional HTML string that will be wrapped by this
                . + + Returns: + HTML formatted string. + """ if not content: return None @@ -326,7 +355,7 @@ def create_tree_dir_item_html(self, tabname: str, dir_path: str, content: Option "search_terms": "", "subclass": "tree-list-content-dir", "tabname": tabname, - "tab_id": self.id_page, + "extra_networks_tabname": self.extra_networks_tabname, "onclick_extra": "", "data_path": dir_path, "data_hash": "", @@ -337,10 +366,32 @@ def create_tree_dir_item_html(self, tabname: str, dir_path: str, content: Option "action_list_item_action_trailing": "", } ) - ul = f"
                  {content}
                " - return f"
              • {btn + ul}
              • " + ul = f"" + return ( + "
              • " + f"{btn + ul}" + "
              • " + ) + + def create_tree_file_item_html(self, tabname: str, file_path: str, item: dict) -> str: + """Generates HTML for a file item in the tree. + + The generated HTML is of the format: + ```html +
              • + +
                +
              • + ``` - def create_tree_file_item_html(self, tabname: str, item_name: str, item: dict) -> str: + Args: + tabname: The name of the active tab. + file_path: The path to the file for this item. + item: Dictionary containing the item information. + + Returns: + HTML formatted string. + """ item_html_args = self.create_item_html(tabname, item) action_buttons = "".join( [ @@ -355,9 +406,9 @@ def create_tree_file_item_html(self, tabname: str, item_name: str, item: dict) - "search_terms": "", "subclass": "tree-list-content-file", "tabname": tabname, - "tab_id": self.id_page, + "extra_networks_tabname": self.extra_networks_tabname, "onclick_extra": item_html_args["card_clicked"], - "data_path": item_name, + "data_path": file_path, "data_hash": item["shorthash"], "action_list_item_action_leading": "", "action_list_item_visual_leading": "🗎", @@ -366,11 +417,17 @@ def create_tree_file_item_html(self, tabname: str, item_name: str, item: dict) - "action_list_item_action_trailing": action_buttons, } ) - return f"
              • {btn}
              • " + return ( + "
              • " + f"{btn}" + "
              • " + ) def create_tree_view_html(self, tabname: str) -> str: """Generates HTML for displaying folders in a tree view. + The generated HTML uses `extra-networks-tree.html` as a template. + Args: tabname: The name of the active tab. @@ -379,7 +436,7 @@ def create_tree_view_html(self, tabname: str) -> str: """ res = "" - # Generate HTML for the tree. + # 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) @@ -388,7 +445,17 @@ def create_tree_view_html(self, tabname: str) -> str: return res def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> Optional[str]: - """Recursively builds HTML for a tree.""" + """Recursively builds HTML for a tree. + + Args: + data: Dictionary representing a directory tree. Can be NoneType. + Data keys should be absolute paths from the root and values + should be subdirectory trees or an ExtraNetworksItem. + + Returns: + If data is not None: HTML string + Else: None + """ if not data: return None @@ -402,25 +469,36 @@ def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> Optional else: _dir_li.append(self.create_tree_dir_item_html(tabname, k, _build_tree(v))) - # Directories should always be displayed before files. + # Directories should always be displayed before files so we order them here. return "".join(_dir_li) + "".join(_file_li) # Add each root directory to the tree. for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])): - # If root is empty, append the "disabled" attribute to the template details tag. item_html = self.create_tree_dir_item_html(tabname, k, _build_tree(v)) - if item_html: + # Only add non-empty entries to the tree. + if item_html is not None: res += item_html return self.tree_tpl.format( **{ "tabname": tabname, - "tab_id": self.id_page, + "extra_networks_tabname": self.extra_networks_tabname, "tree": f"
                  {res}
                " } ) - def create_card_view_html(self, tabname): + def create_card_view_html(self, tabname: 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. + + Returns: + HTML formatted string. + """ res = "" self.items = {x["name"]: x for x in self.list_items()} for item in self.items.values(): @@ -433,20 +511,26 @@ def create_card_view_html(self, tabname): return res def create_html(self, tabname): + """Generates an HTML string for the current pane. + + The generated HTML uses `extra-networks-pane.html` as a template. + + Args: + tabname: The name of the active tab. + + Returns: + HTML formatted string. + """ self.lister.reset() self.metadata = {} self.items = {x["name"]: x for x in self.list_items()} - tree_view_html = self.create_tree_view_html(tabname) - card_view_html = self.create_card_view_html(tabname) - network_type_id = self.id_page - return self.pane_tpl.format( **{ "tabname": tabname, - "network_type_id": network_type_id, - "tree_html": tree_view_html, - "items_html": card_view_html, + "extra_networks_tabname": self.extra_networks_tabname, + "tree_html": self.create_tree_view_html(tabname), + "items_html": self.create_card_view_html(tabname), } ) @@ -561,16 +645,16 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): button_refresh = gr.Button("Refresh", elem_id=tabname+"_extra_refresh_internal", visible=False) for page in ui.stored_extra_pages: - with gr.Tab(page.title, elem_id=f"{tabname}_{page.id_page}", elem_classes=["extra-page"]) as tab: - with gr.Column(elem_id=f"{tabname}_{page.id_page}_prompts", elem_classes=["extra-page-prompts"]): + with gr.Tab(page.title, elem_id=f"{tabname}_{page.extra_networks_tabname}", elem_classes=["extra-page"]) as tab: + with gr.Column(elem_id=f"{tabname}_{page.extra_networks_tabname}_prompts", elem_classes=["extra-page-prompts"]): pass - elem_id = f"{tabname}_{page.id_page}_cards_html" + elem_id = f"{tabname}_{page.extra_networks_tabname}_cards_html" page_elem = gr.HTML('Loading...', elem_id=elem_id) ui.pages.append(page_elem) page_elem.change( fn=lambda: None, - _js=f"function(){{applyExtraNetworkFilter({tabname}_{page.id_page}_extra_search); return []}}", + _js=f"function(){{applyExtraNetworkFilter({tabname}_{page.extra_networks_tabname}_extra_search); return []}}", inputs=[], outputs=[], ) diff --git a/modules/ui_extra_networks_user_metadata.py b/modules/ui_extra_networks_user_metadata.py index 989a649b799..2ca937fd117 100644 --- a/modules/ui_extra_networks_user_metadata.py +++ b/modules/ui_extra_networks_user_metadata.py @@ -14,7 +14,7 @@ def __init__(self, ui, tabname, page): self.ui = ui self.tabname = tabname self.page = page - self.id_part = f"{self.tabname}_{self.page.id_page}_edit_user_metadata" + self.id_part = f"{self.tabname}_{self.page.extra_networks_tabname}_edit_user_metadata" self.box = None diff --git a/style.css b/style.css index 08573248654..1090e4368ab 100644 --- a/style.css +++ b/style.css @@ -879,13 +879,6 @@ footer { margin-bottom: 1em; } -.extra-network-pane{ - height: calc(100vh - 24rem); - overflow: clip scroll; - resize: vertical; - min-height: 52rem; -} - .extra-networks > div.tab-nav{ min-height: 3.4rem; } @@ -1182,23 +1175,63 @@ body.resizing .resize-handle { /* ========================= */ .extra-network-pane { display: flex; -} - -.extra-network-pane .extra-network-cards { - display: block; + height: calc(100vh - 24rem); + resize: vertical; + min-height: 52rem; } .extra-network-pane .extra-network-tree { - display: block; + flex: 1; + flex-direction: column; + display: flex; font-size: 1rem; - min-width: 25%; border: 1px solid var(--block-border-color); - overflow: hidden; } -.extra-network-tree .tree-list { - margin: 0 0.25rem; +.extra-network-pane .extra-network-cards { + flex: 3; + overflow: clip auto !important; + border: 1px solid var(--block-border-color); +} + +.extra-network-pane .extra-network-tree .tree-list { + flex: 1; + display: flex; + flex-direction: column; padding: 0; + width: 100%; + overflow: hidden; +} + +.extra-network-pane .extra-network-tree .tree-list .tree-list-container { + flex: 1; + overflow: clip auto !important; + width: 100%; +} + + +.extra-network-pane .extra-network-cards::-webkit-scrollbar, +.extra-network-pane .tree-list-container::-webkit-scrollbar { + background-color: transparent; + width: 16px; +} + +.extra-network-pane .extra-network-cards::-webkit-scrollbar-track, +.extra-network-pane .tree-list-container::-webkit-scrollbar-track { + background-color: transparent; + background-clip: content-box; +} + +.extra-network-pane .extra-network-cards::-webkit-scrollbar-thumb, +.extra-network-pane .tree-list-container::-webkit-scrollbar-thumb { + background-color: var(--border-color-primary); + border-radius: 16px; + border: 4px solid var(--background-fill-primary); +} + +.extra-network-pane .extra-network-cards::-webkit-scrollbar-button, +.extra-network-pane .tree-list-container::-webkit-scrollbar-button { + display: none; } .extra-network-tree .tree-list .tree-list-controls { @@ -1244,17 +1277,15 @@ body.resizing .resize-handle { background-color: transparent; } -/* Directory
                  visibility based on expanded attribute. */ -.extra-network-tree .tree-list-content[expanded=false]+.tree-list--subgroup { +/* Directory
                    visibility based on data-expanded attribute. */ +.extra-network-tree .tree-list-content+.tree-list--subgroup { height: 0; - overflow: hidden; visibility: hidden; opacity: 0; } -.extra-network-tree .tree-list-content[expanded=true]+.tree-list--subgroup { +.extra-network-tree .tree-list-content[data-expanded]+.tree-list--subgroup { height: auto; - overflow: visible; visibility: visible; opacity: 1; } @@ -1307,7 +1338,7 @@ body.resizing .resize-handle { background-color: var(--neutral-800); } -.dark .extra-network-tree div.tree-list-content[selected] { +.dark .extra-network-tree div.tree-list-content[data-selected] { background-color: var(--neutral-700); } @@ -1317,20 +1348,20 @@ body.resizing .resize-handle { background-color: var(--neutral-200); } -.extra-network-tree div.tree-list-content[selected] { +.extra-network-tree div.tree-list-content[data-selected] { background-color: var(--neutral-300); } /* ==== CHEVRON ICON ACTIONS ==== */ /* Define the animation for the arrow when it is clicked. */ -.extra-network-tree .tree-list-content-dir[expanded=false] .tree-list-item-action-chevron { +.extra-network-tree .tree-list-content-dir .tree-list-item-action-chevron { -ms-transform: rotate(135deg); -webkit-transform: rotate(135deg); transform: rotate(135deg); transition: transform 0.2s; } -.extra-network-tree .tree-list-content-dir[expanded=true] .tree-list-item-action-chevron { +.extra-network-tree .tree-list-content-dir[data-expanded] .tree-list-item-action-chevron { -ms-transform: rotate(225deg); -webkit-transform: rotate(225deg); transform: rotate(225deg); From ccee26b0653b4f6778c107d68df52da27446abd2 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Tue, 16 Jan 2024 14:54:07 -0500 Subject: [PATCH 13/20] fix bugs --- javascript/extraNetworks.js | 28 ++++++++----------- modules/ui_extra_networks.py | 35 ++++++++++++------------ modules/ui_extra_networks_checkpoints.py | 3 +- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index a3f003bf475..caaa3fae0d2 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -31,11 +31,12 @@ function setupExtraNetworksForTab(tabname) { var this_tab = gradioApp().querySelector('#' + tabname + '_extra_tabs'); this_tab.classList.add('extra-networks'); this_tab.querySelectorAll(":scope > [id^='" + tabname + "_']").forEach(function(elem) { - var extra_networks_tabname = elem.id; - var search = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_search"); - var sort_mode = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_sort"); - var sort_dir = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_sort_dir"); - var refresh = gradioApp().querySelector("#" + extra_networks_tabname + "_extra_refresh"); + // tabname_full = {tabname}_{extra_networks_tabname} + var tabname_full = elem.id; + var search = gradioApp().querySelector("#" + tabname_full + "_extra_search"); + var sort_mode = gradioApp().querySelector("#" + tabname_full + "_extra_sort"); + var sort_dir = gradioApp().querySelector("#" + tabname_full + "_extra_sort_dir"); + var refresh = gradioApp().querySelector("#" + tabname_full + "_extra_refresh"); // If any of the buttons above don't exist, we want to skip this iteration of the loop. if (!search || !sort_mode || !sort_dir || !refresh) { @@ -44,16 +45,13 @@ function setupExtraNetworksForTab(tabname) { var applyFilter = function() { var searchTerm = search.value.toLowerCase(); - gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) { var searchOnly = elem.querySelector('.search_only'); - var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms'), function(t) { return t.textContent.toLowerCase(); }).join(" "); var visible = text.indexOf(searchTerm) != -1; - if (searchOnly && searchTerm.length < 4) { visible = false; } @@ -66,7 +64,6 @@ function setupExtraNetworksForTab(tabname) { var applySort = function() { var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card'); - var reverse = sort_dir.dataset.sortdir == "Descending"; var sortKey = sort_mode.dataset.sortmode.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1); @@ -104,9 +101,8 @@ function setupExtraNetworksForTab(tabname) { search.addEventListener("input", applyFilter); applySort(); applyFilter(); - - extraNetworksApplySort[extra_networks_tabname] = applySort; - extraNetworksApplyFilter[extra_networks_tabname] = applyFilter; + extraNetworksApplySort[tabname_full] = applySort; + extraNetworksApplyFilter[tabname_full] = applyFilter; }); registerPrompt(tabname, tabname + "_prompt"); @@ -147,12 +143,12 @@ function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt) { extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt); } -function applyExtraNetworkFilter(tabname) { - setTimeout(extraNetworksApplyFilter[tabname], 1); +function applyExtraNetworkFilter(tabname_full) { + setTimeout(extraNetworksApplyFilter[tabname_full], 1); } -function applyExtraNetworkSort(tabname) { - setTimeout(extraNetworksApplySort[tabname], 1); +function applyExtraNetworkSort(tabname_full) { + setTimeout(extraNetworksApplySort[tabname_full], 1); } var extraNetworksApplyFilter = {}; diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index a03207b2bc0..55cd1da2772 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -237,7 +237,7 @@ def create_item_html( "tabname": tabname, "prompt": item["prompt"], "neg_prompt": item.get("negative_prompt", ""), - "allow_neg": "true" if self.allow_negative_prompt else "false" + "allow_neg": str(self.allow_negative_prompt).lower(), } ) onclick = html.escape(onclick) @@ -291,7 +291,7 @@ def create_item_html( search_terms_html += search_term_template.format( **{ "style": "display: none;", - "class": "search_terms" + (" search_only" if search_only else ""), + "class": f"search_terms{' search_only' if search_only else ''}", "search_term": search_term, } ) @@ -307,7 +307,7 @@ def create_item_html( "metadata_button": btn_metadata, "name": html.escape(item["name"]), "prompt": item.get("prompt", None), - "save_card_preview": '"' + html.escape(f"""return saveCardPreview(event, {quote_js(tabname)}, {quote_js(item["local_preview"])})""") + '"', + "save_card_preview": html.escape(f"return saveCardPreview(event, '{tabname}', '{item['local_preview']}');"), "search_only": " search_only" if search_only else "", "search_terms": search_terms_html, "sort_keys": sort_keys, @@ -369,7 +369,7 @@ def create_tree_dir_item_html( ul = f"" return ( "
                  • " - f"{btn + ul}" + f"{btn}{ul}" "
                  • " ) @@ -561,7 +561,7 @@ def find_preview(self, path): Find a preview PNG for a given path (without extension) and call link_preview on it. """ - potential_files = sum([[path + "." + ext, path + ".preview." + ext] for ext in allowed_preview_extensions()], []) + potential_files = sum([[f"{path}.{ext}", f"{path}.preview.{ext}"] for ext in allowed_preview_extensions()], []) for file in potential_files: if self.lister.exists(file): @@ -642,7 +642,7 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): related_tabs = [] - button_refresh = gr.Button("Refresh", elem_id=tabname+"_extra_refresh_internal", visible=False) + button_refresh = gr.Button("Refresh", elem_id=f"{tabname}_extra_refresh_internal", visible=False) for page in ui.stored_extra_pages: with gr.Tab(page.title, elem_id=f"{tabname}_{page.extra_networks_tabname}", elem_classes=["extra-page"]) as tab: @@ -652,24 +652,25 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): elem_id = f"{tabname}_{page.extra_networks_tabname}_cards_html" page_elem = gr.HTML('Loading...', elem_id=elem_id) ui.pages.append(page_elem) - page_elem.change( - fn=lambda: None, - _js=f"function(){{applyExtraNetworkFilter({tabname}_{page.extra_networks_tabname}_extra_search); return []}}", - inputs=[], - outputs=[], - ) - editor = page.create_user_metadata_editor(ui, tabname) editor.create_ui() ui.user_metadata_editors.append(editor) - related_tabs.append(tab) - ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False) - ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=tabname+"_preview_filename", visible=False) + ui.button_save_preview = gr.Button('Save preview', elem_id=f"{tabname}_save_preview", visible=False) + 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='function(){ extraNetworksUrelatedTabSelected("' + tabname + '"); }', inputs=[], outputs=[], show_progress=False) + tab.select(fn=None, _js=f"function(){{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()});" + f"applyExtraNetworkFilter('{tabname}_{page.extra_networks_tabname}');" + "}}" + ) + tab.select(fn=None, _js=jscode, inputs=[], outputs=[], show_progress=False) def create_html(): ui.pages_contents = [pg.create_html(ui.tabname) for pg in ui.stored_extra_pages] diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index e7976ba1260..a8c3367198f 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -2,7 +2,6 @@ import os from modules import shared, ui_extra_networks, sd_models -from modules.ui_extra_networks import quote_js from modules.ui_extra_networks_checkpoints_user_metadata import CheckpointUserMetadataEditor @@ -31,7 +30,7 @@ def create_item(self, name, index=None, enable_filter=True): "preview": self.find_preview(path), "description": self.find_description(path), "search_terms": search_terms, - "onclick": '"' + html.escape(f"""return selectCheckpoint({quote_js(name)})""") + '"', + "onclick": html.escape(f"return selectCheckpoint('{name}');"), "local_preview": f"{path}.{shared.opts.samples_format}", "metadata": checkpoint.metadata, "sort_keys": {'default': index, **self.get_sort_keys(checkpoint.filename)}, From f25c81a74462554890ac7327a30629b332db1084 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Wed, 17 Jan 2024 22:38:51 -0500 Subject: [PATCH 14/20] Fix embeddings add/remove to/from prompt on click bugs. --- javascript/extraNetworks.js | 13 +++---------- modules/ui_extra_networks.py | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index caaa3fae0d2..1e2786ab0d2 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -169,8 +169,8 @@ 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 extraTextBeforeNet = opts.extra_networks_add_text_separator; var extraTextAfterNet = m[2]; var partToSearch = m[1]; var foundAtPosition = -1; @@ -183,7 +183,6 @@ function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) { } return found; }); - if (foundAtPosition >= 0) { if (extraTextAfterNet && newTextareaText.substr(foundAtPosition, extraTextAfterNet.length) == extraTextAfterNet) { newTextareaText = newTextareaText.substr(0, foundAtPosition) + newTextareaText.substr(foundAtPosition + extraTextAfterNet.length); @@ -193,13 +192,8 @@ function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) { } } } else { - newTextareaText = textarea.value.replaceAll(new RegExp(text, "g"), function(found) { - if (found == text) { - replaced = true; - return ""; - } - return found; - }); + newTextareaText = textarea.value.replaceAll(new RegExp(`((?:${extraTextBeforeNet})?${text})`, "g"), ""); + replaced = (newTextareaText != textarea.value); } if (replaced) { @@ -211,7 +205,6 @@ function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) { } function updatePromptArea(text, textArea, isNeg) { - if (!tryToRemoveExtraNetworkFromPrompt(textArea, text, isNeg)) { textArea.value = textArea.value + opts.extra_networks_add_text_separator + text; } diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 55cd1da2772..5dd4e443f8e 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -236,7 +236,7 @@ def create_item_html( **{ "tabname": tabname, "prompt": item["prompt"], - "neg_prompt": item.get("negative_prompt", ""), + "neg_prompt": item.get("negative_prompt", "''"), "allow_neg": str(self.allow_negative_prompt).lower(), } ) From 50e444fa1d1c779b0c9c915e44ed2d78b587f277 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Thu, 18 Jan 2024 12:13:09 -0500 Subject: [PATCH 15/20] Fix missing important style. --- style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style.css b/style.css index 1090e4368ab..57c52354a0b 100644 --- a/style.css +++ b/style.css @@ -28,7 +28,7 @@ div.gradio-container{ } .hidden{ - display: none; + display: none !important; } .compact{ From 69f4f148dce0868b748b700f96942d4036e848c9 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Thu, 18 Jan 2024 12:13:33 -0500 Subject: [PATCH 16/20] Fix various bugs including refresh bug. --- javascript/extraNetworks.js | 7 +++++-- modules/ui_extra_networks.py | 31 ++++++++++++++++--------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 1e2786ab0d2..3029afec847 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -55,8 +55,11 @@ function setupExtraNetworksForTab(tabname) { if (searchOnly && searchTerm.length < 4) { visible = false; } - - elem.style.display = visible ? "" : "none"; + if (visible) { + elem.classList.remove("hidden"); + } else { + elem.classList.add("hidden"); + } }); applySort(); diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 5dd4e443f8e..656e7f181e4 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -216,22 +216,17 @@ def create_item_html( Can be empty if the item is not meant to be shown. If no template is passed: A dictionary containing the generated item's attributes. """ - metadata = item.get("metadata") - if metadata: - self.metadata[item["name"]] = metadata - - if "user_metadata" not in item: - self.read_user_metadata(item) - preview = item.get("preview", None) - height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else '' - width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else '' + style_height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else '' + style_width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else '' + style_font_size = f"font-size: {shared.opts.extra_networks_card_text_scale*100}%;" + card_style = style_height + style_width + style_font_size background_image = f'' if preview else '' onclick = item.get("onclick", None) if onclick is None: # Don't quote prompt/neg_prompt since they are stored as js strings already. - onclick_js_tpl = "cardClicked('{tabname}', {prompt}, {neg_prompt}, '{allow_neg}');" + onclick_js_tpl = "cardClicked('{tabname}', {prompt}, {neg_prompt}, {allow_neg});" onclick = onclick_js_tpl.format( **{ "tabname": tabname, @@ -286,11 +281,10 @@ def create_item_html( ).strip() search_terms_html = "" - search_term_template = "{search_term}" + search_term_template = "" for search_term in item.get("search_terms", []): search_terms_html += search_term_template.format( **{ - "style": "display: none;", "class": f"search_terms{' search_only' if search_only else ''}", "search_term": search_term, } @@ -301,7 +295,7 @@ def create_item_html( "background_image": background_image, "card_clicked": onclick, "copy_path_button": btn_copy_path, - "description": (item.get("description") or "" if shared.opts.extra_networks_card_show_desc else ""), + "description": (item.get("description", "") or "" if shared.opts.extra_networks_card_show_desc else ""), "edit_button": btn_edit_item, "local_preview": quote_js(item["local_preview"]), "metadata_button": btn_metadata, @@ -311,7 +305,7 @@ def create_item_html( "search_only": " search_only" if search_only else "", "search_terms": search_terms_html, "sort_keys": sort_keys, - "style": f"display: none; {height}{width}; font-size: {shared.opts.extra_networks_card_text_scale*100}%", + "style": card_style, "tabname": tabname, "extra_networks_tabname": self.extra_networks_tabname, } @@ -500,7 +494,6 @@ def create_card_view_html(self, tabname: str) -> str: HTML formatted string. """ res = "" - self.items = {x["name"]: x for x in self.list_items()} for item in self.items.values(): res += self.create_item_html(tabname, item, self.card_tpl) @@ -524,6 +517,14 @@ def create_html(self, tabname): self.lister.reset() self.metadata = {} self.items = {x["name"]: x for x in self.list_items()} + # Populate the instance metadata for each item. + for item in self.items.values(): + metadata = item.get("metadata") + if metadata: + self.metadata[item["name"]] = metadata + + if "user_metadata" not in item: + self.read_user_metadata(item) return self.pane_tpl.format( **{ From 2310cd66e5381fbe6b966894381c6ee7b762898f Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Sat, 20 Jan 2024 11:43:45 -0500 Subject: [PATCH 17/20] Add toggle button for tree view. Use default settings for sortmode and direction. --- html/extra-networks-pane.html | 55 ++++++++++++++++++++-- html/extra-networks-tree.html | 37 --------------- javascript/extraNetworks.js | 20 ++++++-- modules/ui_extra_networks.py | 7 +++ style.css | 86 +++++++++++++++++++++++------------ 5 files changed, 132 insertions(+), 73 deletions(-) diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index bf46ca16305..73dad2ab248 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -1,8 +1,55 @@
                    -
                    - {tree_html} +
                    + +
                    + +
                    +
                    + +
                    +
                    + +
                    +
                    + +
                    -
                    - {items_html} +
                    +
                    + {tree_html} +
                    +
                    + {items_html} +
                    \ No newline at end of file diff --git a/html/extra-networks-tree.html b/html/extra-networks-tree.html index 23f6af1058c..39649e86093 100644 --- a/html/extra-networks-tree.html +++ b/html/extra-networks-tree.html @@ -1,41 +1,4 @@
                    -
                    - -
                    - -
                    -
                    - -
                    -
                    - -
                    -
                    {tree}
                    diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 3029afec847..ce788328ce5 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -348,7 +348,7 @@ function extraNetworksTreeOnClick(event, tabname, extra_networks_tabname) { } } -function extraNetworksTreeSortOnClick(event, tabname, extra_networks_tabname) { +function extraNetworksControlSortOnClick(event, tabname, extra_networks_tabname) { /** * Handles `onclick` events for the Sort Mode button. * @@ -382,7 +382,7 @@ function extraNetworksTreeSortOnClick(event, tabname, extra_networks_tabname) { applyExtraNetworkSort(tabname + "_" + extra_networks_tabname); } -function extraNetworksTreeSortDirOnClick(event, tabname, extra_networks_tabname) { +function extraNetworksControlSortDirOnClick(event, tabname, extra_networks_tabname) { /** * Handles `onclick` events for the Sort Direction button. * @@ -403,7 +403,21 @@ function extraNetworksTreeSortDirOnClick(event, tabname, extra_networks_tabname) applyExtraNetworkSort(tabname + "_" + extra_networks_tabname); } -function extraNetworksTreeRefreshOnClick(event, tabname, extra_networks_tabname) { +function extraNetworksControlTreeViewOnClick(event, tabname, extra_networks_tabname) { + /** + * Handles `onclick` events for the Tree View button. + * + * Toggles the tree view in the extra networks pane. + * + * @param event The generated event. + * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. + * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. + */ + gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_tree").classList.toggle("hidden"); + event.currentTarget.classList.toggle("extra-network-control--enabled"); +} + +function extraNetworksControlRefreshOnClick(event, tabname, extra_networks_tabname) { /** * Handles `onclick` events for the Refresh Page button. * diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 656e7f181e4..4c8a407449e 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -526,10 +526,17 @@ def create_html(self, tabname): if "user_metadata" not in item: self.read_user_metadata(item) + data_sortdir = shared.opts.extra_networks_card_order + data_sortmode = shared.opts.extra_networks_card_order_field.lower().replace("sort", "").replace(" ", "_").rstrip("_").strip() + data_sortkey = f"{data_sortmode}-{data_sortdir}-{len(self.items)}" + return self.pane_tpl.format( **{ "tabname": tabname, "extra_networks_tabname": self.extra_networks_tabname, + "data_sortmode": data_sortmode, + "data_sortkey": data_sortkey, + "data_sortdir": data_sortdir, "tree_html": self.create_tree_view_html(tabname), "items_html": self.create_card_view_html(tabname), } diff --git a/style.css b/style.css index 57c52354a0b..f3fd1571bb1 100644 --- a/style.css +++ b/style.css @@ -1178,6 +1178,13 @@ body.resizing .resize-handle { height: calc(100vh - 24rem); resize: vertical; min-height: 52rem; + flex-direction: column; +} + +.extra-network-pane .extra-network-pane-content { + display: flex; + flex: 1; + flex-direction: row; } .extra-network-pane .extra-network-tree { @@ -1234,7 +1241,7 @@ body.resizing .resize-handle { display: none; } -.extra-network-tree .tree-list .tree-list-controls { +.extra-network-pane .extra-network-control { position: relative; display: grid; width: 100%; @@ -1248,8 +1255,7 @@ body.resizing .resize-handle { border: none; transition: background 33.333ms linear; grid-template-rows: min-content; - grid-template-areas: "tree-list-controls-col-0 tree-list-controls-col-1 tree-list-controls-col-2 tree-list-controls-col-3"; - grid-template-columns: minmax(0, auto) min-content min-content min-content; + grid-template-columns: minmax(0, auto) repeat(4, min-content); grid-gap: 0.1rem; align-items: start; } @@ -1342,16 +1348,16 @@ body.resizing .resize-handle { background-color: var(--neutral-700); } +.extra-network-tree div.tree-list-content[data-selected] { + background-color: var(--neutral-300); +} + .extra-network-tree div.tree-list-content:hover { -webkit-transition: all 0.05s ease-in-out; transition: all 0.05s ease-in-out; background-color: var(--neutral-200); } -.extra-network-tree div.tree-list-content[data-selected] { - background-color: var(--neutral-300); -} - /* ==== CHEVRON ICON ACTIONS ==== */ /* Define the animation for the arrow when it is clicked. */ .extra-network-tree .tree-list-content-dir .tree-list-item-action-chevron { @@ -1378,7 +1384,7 @@ body.resizing .resize-handle { /* ==== SEARCH INPUT ACTIONS ==== */ /* Add icon to left side of */ -.extra-network-tree .tree-list-controls .tree-list-search::before { +.extra-network-pane .extra-network-control .extra-network-control--search::before { content: "🔎︎"; position: absolute; margin: 0.5rem; @@ -1386,14 +1392,12 @@ body.resizing .resize-handle { color: var(--input-placeholder-color); } -.extra-network-tree .tree-list-controls .tree-list-search { +.extra-network-pane .extra-network-control .extra-network-control--search { display: inline-flex; - grid-area: tree-list-controls-col-0; position: relative; - margin: 0.5rem; } -.extra-network-tree .tree-list-controls .tree-list-search .tree-list-search-text { +.extra-network-pane .extra-network-control .extra-network-control--search .extra-network-control--search-text { border: 1px solid var(--button-secondary-border-color); border-radius: 0.5rem; color: var(--button-secondary-text-color); @@ -1404,7 +1408,7 @@ body.resizing .resize-handle { } /* clear button (x on right side) styling */ -.extra-network-tree .tree-list-controls .tree-list-search .tree-list-search-text::-webkit-search-cancel-button { +.extra-network-pane .extra-network-control .extra-network-control--search .extra-network-control--search-text::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; cursor: pointer; @@ -1418,8 +1422,7 @@ body.resizing .resize-handle { } /* ==== SORT ICON ACTIONS ==== */ -.extra-network-tree .tree-list-controls .tree-list-sort { - grid-area: tree-list-controls-col-1; +.extra-network-pane .extra-network-control .extra-network-control--sort { padding: 0.25rem; display: inline-flex; cursor: pointer; @@ -1427,7 +1430,7 @@ body.resizing .resize-handle { align-self: center; } -.extra-network-tree .tree-list-controls .tree-list-sort .tree-list-sort-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort .extra-network-control--sort-icon { height: 1.5rem; width: 1.5rem; mask-repeat: no-repeat; @@ -1436,25 +1439,24 @@ body.resizing .resize-handle { background-color: var(--input-placeholder-color); } -.extra-network-tree .tree-list-sort[data-sortmode="path"] .tree-list-sort-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="path"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } -.extra-network-tree .tree-list-sort[data-sortmode="name"] .tree-list-sort-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="name"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } -.extra-network-tree .tree-list-sort[data-sortmode="date_created"] .tree-list-sort-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="date_created"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } -.extra-network-tree .tree-list-sort[data-sortmode="date_modified"] .tree-list-sort-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort[data-sortmode="date_modified"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } /* ==== SORT DIRECTION ICON ACTIONS ==== */ -.extra-network-tree .tree-list-controls .tree-list-sort-dir { - grid-area: tree-list-controls-col-2; +.extra-network-pane .extra-network-control .extra-network-control--sort-dir { padding: 0.25rem; display: inline-flex; cursor: pointer; @@ -1462,7 +1464,7 @@ body.resizing .resize-handle { align-self: center; } -.extra-network-tree .tree-list-controls .tree-list-sort-dir .tree-list-sort-dir-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort-dir .extra-network-control--sort-dir-icon { height: 1.5rem; width: 1.5rem; mask-repeat: no-repeat; @@ -1471,17 +1473,43 @@ body.resizing .resize-handle { background-color: var(--input-placeholder-color); } -.extra-network-tree .tree-list-sort-dir[data-sortdir="Ascending"] .tree-list-sort-dir-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort-dir[data-sortdir="Ascending"] .extra-network-control--sort-dir-icon { mask-image: url('data:image/svg+xml,'); } -.extra-network-tree .tree-list-sort-dir[data-sortdir="Descending"] .tree-list-sort-dir-icon { +.extra-network-pane .extra-network-control .extra-network-control--sort-dir[data-sortdir="Descending"] .extra-network-control--sort-dir-icon { mask-image: url('data:image/svg+xml,'); } +/* ==== TREE VIEW ICON ACTIONS ==== */ +.extra-network-pane .extra-network-control .extra-network-control--tree-view { + padding: 0.25rem; + display: inline-flex; + cursor: pointer; + justify-self: center; + align-self: center; +} + +.extra-network-pane .extra-network-control .extra-network-control--tree-view .extra-network-control--tree-view-icon { + height: 1.5rem; + width: 1.5rem; + mask-image: url('data:image/svg+xml,'); + mask-repeat: no-repeat; + mask-position: center center; + mask-size: 100%; + background-color: var(--input-placeholder-color); +} + +.dark .extra-network-pane .extra-network-control .extra-network-control--enabled { + background-color: var(--neutral-700); +} + +.dark .extra-network-pane .extra-network-control .extra-network-control--enabled { + background-color: var(--neutral-300); +} + /* ==== REFRESH ICON ACTIONS ==== */ -.extra-network-tree .tree-list-controls .tree-list-refresh { - grid-area: tree-list-controls-col-3; +.extra-network-pane .extra-network-control .extra-network-control--refresh { padding: 0.25rem; display: inline-flex; cursor: pointer; @@ -1489,7 +1517,7 @@ body.resizing .resize-handle { align-self: center; } -.extra-network-tree .tree-list-controls .tree-list-refresh .tree-list-refresh-icon { +.extra-network-pane .extra-network-control .extra-network-control--refresh .extra-network-control--refresh-icon { height: 1.5rem; width: 1.5rem; mask-image: url('data:image/svg+xml,'); @@ -1499,7 +1527,7 @@ body.resizing .resize-handle { background-color: var(--input-placeholder-color); } -.extra-network-tree .tree-list-refresh-icon:active { +.extra-network-pane .extra-network-control .extra-network-control--refresh-icon:active { -ms-transform: rotate(180deg); -webkit-transform: rotate(180deg); transform: rotate(180deg); From b67a49441fc420f37c6bef1172a0b1ad5c42f30f Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Sat, 20 Jan 2024 13:28:37 -0500 Subject: [PATCH 18/20] Add option in settings to enable/disable tree view by default. --- html/extra-networks-pane.html | 4 ++-- modules/shared_options.py | 1 + modules/ui_extra_networks.py | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index 73dad2ab248..9f5b3ecee62 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -29,7 +29,7 @@
                    @@ -45,7 +45,7 @@
                    -
                    +
                    {tree_html}
                    diff --git a/modules/shared_options.py b/modules/shared_options.py index 63488f4e700..e0a6d977c30 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -251,6 +251,7 @@ "extra_networks_card_show_desc": OptionInfo(True, "Show description on card"), "extra_networks_card_order_field": OptionInfo("Path", "Default order field for Extra Networks cards", gr.Dropdown, {"choices": ['Path', 'Name', 'Date Created', 'Date Modified']}).needs_reload_ui(), "extra_networks_card_order": OptionInfo("Ascending", "Default order for Extra Networks cards", gr.Dropdown, {"choices": ['Ascending', 'Descending']}).needs_reload_ui(), + "extra_networks_tree_view_default_enabled": OptionInfo(False, "Enables the Extra Networks directory tree view by default").needs_reload_ui(), "extra_networks_add_text_separator": OptionInfo(" ", "Extra networks separator").info("extra text to add before <...> when adding extra network to prompt"), "ui_extra_networks_tab_reorder": OptionInfo("", "Extra networks tab order").needs_reload_ui(), "textual_inversion_print_at_load": OptionInfo(False, "Print a list of Textual Inversion embeddings when loading model"), diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 4c8a407449e..80160b847b6 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -529,6 +529,11 @@ def create_html(self, tabname): data_sortdir = shared.opts.extra_networks_card_order data_sortmode = shared.opts.extra_networks_card_order_field.lower().replace("sort", "").replace(" ", "_").rstrip("_").strip() data_sortkey = f"{data_sortmode}-{data_sortdir}-{len(self.items)}" + tree_view_btn_extra_class = "" + tree_view_div_extra_class = "hidden" + if shared.opts.extra_networks_tree_view_default_enabled: + tree_view_btn_extra_class = "extra-network-control--enabled" + tree_view_div_extra_class = "" return self.pane_tpl.format( **{ @@ -537,6 +542,8 @@ def create_html(self, tabname): "data_sortmode": data_sortmode, "data_sortkey": data_sortkey, "data_sortdir": data_sortdir, + "tree_view_btn_extra_class": tree_view_btn_extra_class, + "tree_view_div_extra_class": tree_view_div_extra_class, "tree_html": self.create_tree_view_html(tabname), "items_html": self.create_card_view_html(tabname), } From d7d3166a2749fe04f0ba1d8d0f88c8e8819a379d Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Sun, 21 Jan 2024 11:27:24 -0500 Subject: [PATCH 19/20] Fix broken scrollbars --- html/extra-networks-tree.html | 4 +--- style.css | 20 +++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/html/extra-networks-tree.html b/html/extra-networks-tree.html index 39649e86093..beec888cd9d 100644 --- a/html/extra-networks-tree.html +++ b/html/extra-networks-tree.html @@ -1,5 +1,3 @@
                    -
                    - {tree} -
                    + {tree}
                    \ No newline at end of file diff --git a/style.css b/style.css index f3fd1571bb1..ff1d907226a 100644 --- a/style.css +++ b/style.css @@ -1179,20 +1179,20 @@ body.resizing .resize-handle { resize: vertical; min-height: 52rem; flex-direction: column; + overflow: hidden; } .extra-network-pane .extra-network-pane-content { display: flex; flex: 1; - flex-direction: row; + overflow: hidden; } .extra-network-pane .extra-network-tree { flex: 1; - flex-direction: column; - display: flex; font-size: 1rem; border: 1px solid var(--block-border-color); + overflow: clip auto !important; } .extra-network-pane .extra-network-cards { @@ -1210,34 +1210,28 @@ body.resizing .resize-handle { overflow: hidden; } -.extra-network-pane .extra-network-tree .tree-list .tree-list-container { - flex: 1; - overflow: clip auto !important; - width: 100%; -} - .extra-network-pane .extra-network-cards::-webkit-scrollbar, -.extra-network-pane .tree-list-container::-webkit-scrollbar { +.extra-network-pane .extra-network-tree::-webkit-scrollbar { background-color: transparent; width: 16px; } .extra-network-pane .extra-network-cards::-webkit-scrollbar-track, -.extra-network-pane .tree-list-container::-webkit-scrollbar-track { +.extra-network-pane .extra-network-tree::-webkit-scrollbar-track { background-color: transparent; background-clip: content-box; } .extra-network-pane .extra-network-cards::-webkit-scrollbar-thumb, -.extra-network-pane .tree-list-container::-webkit-scrollbar-thumb { +.extra-network-pane .extra-network-tree::-webkit-scrollbar-thumb { background-color: var(--border-color-primary); border-radius: 16px; border: 4px solid var(--background-fill-primary); } .extra-network-pane .extra-network-cards::-webkit-scrollbar-button, -.extra-network-pane .tree-list-container::-webkit-scrollbar-button { +.extra-network-pane .extra-network-tree::-webkit-scrollbar-button { display: none; } From 26e1cd7ec47c8d234d2ea3f189b1147329c9059c Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Sun, 21 Jan 2024 11:34:08 -0500 Subject: [PATCH 20/20] Remove unnecessary template and simplify tree list. --- html/extra-networks-tree.html | 3 --- modules/ui_extra_networks.py | 11 +---------- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 html/extra-networks-tree.html diff --git a/html/extra-networks-tree.html b/html/extra-networks-tree.html deleted file mode 100644 index beec888cd9d..00000000000 --- a/html/extra-networks-tree.html +++ /dev/null @@ -1,3 +0,0 @@ -
                    - {tree} -
                    \ No newline at end of file diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 80160b847b6..157b3a6d4a9 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -164,7 +164,6 @@ def __init__(self, title): self.lister = util.MassFileLister() # HTML Templates self.pane_tpl = shared.html("extra-networks-pane.html") - self.tree_tpl = shared.html("extra-networks-tree.html") self.card_tpl = shared.html("extra-networks-card.html") self.btn_tree_tpl = shared.html("extra-networks-tree-button.html") self.btn_copy_path_tpl = shared.html("extra-networks-copy-path-button.html") @@ -420,8 +419,6 @@ def create_tree_file_item_html(self, tabname: str, file_path: str, item: dict) - def create_tree_view_html(self, tabname: str) -> str: """Generates HTML for displaying folders in a tree view. - The generated HTML uses `extra-networks-tree.html` as a template. - Args: tabname: The name of the active tab. @@ -473,13 +470,7 @@ def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> Optional if item_html is not None: res += item_html - return self.tree_tpl.format( - **{ - "tabname": tabname, - "extra_networks_tabname": self.extra_networks_tabname, - "tree": f"
                      {res}
                    " - } - ) + return f"
                      {res}
                    " def create_card_view_html(self, tabname: str) -> str: """Generates HTML for the network Card View section for a tab.