mirror of
https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
synced 2026-02-15 04:10:56 -08:00
update to use new clusterize class
This commit is contained in:
parent
806bbff5a5
commit
ec80d57e8b
6 changed files with 1636 additions and 2037 deletions
|
|
@ -4,12 +4,12 @@ import urllib.parse
|
|||
from base64 import b64decode
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from typing import Optional, Union, Any, Callable
|
||||
from dataclasses import dataclass
|
||||
import zlib
|
||||
import base64
|
||||
import re
|
||||
from starlette.responses import JSONResponse, PlainTextResponse
|
||||
from starlette.responses import Response, FileResponse, JSONResponse, PlainTextResponse
|
||||
|
||||
from modules import shared, ui_extra_networks_user_metadata, errors, extra_networks, util
|
||||
from modules.images import read_info_from_image, save_image_with_geninfo
|
||||
|
|
@ -40,6 +40,112 @@ class ExtraNetworksItem:
|
|||
item: dict
|
||||
|
||||
|
||||
class ListItem:
|
||||
"""
|
||||
Attributes:
|
||||
id [str]: The ID of this list item.
|
||||
html [str]: The HTML string for this item.
|
||||
"""
|
||||
def __init__(self, _id: str, _html: str) -> None:
|
||||
self.id = _id
|
||||
self.html = _html
|
||||
|
||||
|
||||
class CardListItem(ListItem):
|
||||
"""
|
||||
Attributes:
|
||||
visible [bool]: Whether the item should be shown in the list.
|
||||
sort_keys [dict]: Nested dict where keys are sort modes and values are sort keys.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.visible: bool = False
|
||||
self.sort_keys = {}
|
||||
self.search_terms = []
|
||||
|
||||
|
||||
class TreeListItem(ListItem):
|
||||
"""
|
||||
Attributes:
|
||||
visible [bool]: Whether the item should be shown in the list.
|
||||
expanded [bool]: Whether the item children should be shown.
|
||||
selected [bool]: Whether the item is selected by user.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.node: Optional[DirectoryTreeNode] = None
|
||||
self.visible: bool = False
|
||||
self.expanded: bool = False
|
||||
self.selected: bool = False
|
||||
|
||||
|
||||
class DirectoryTreeNode:
|
||||
def __init__(
|
||||
self,
|
||||
root_dir: str,
|
||||
abspath: str,
|
||||
parent: Optional["DirectoryTreeNode"] = None,
|
||||
) -> None:
|
||||
self.abspath = abspath
|
||||
self.root_dir = root_dir
|
||||
self.parent = parent
|
||||
|
||||
self.is_dir = False
|
||||
self.item = None
|
||||
self.relpath = os.path.relpath(self.abspath, self.root_dir)
|
||||
self.children: list["DirectoryTreeNode"] = []
|
||||
|
||||
# If a parent is passed, then we add this instance to the parent's children.
|
||||
if self.parent is not None:
|
||||
self.parent.add_child(self)
|
||||
|
||||
def add_child(self, child: "DirectoryTreeNode") -> None:
|
||||
self.children.append(child)
|
||||
|
||||
def build(self, items: dict[str, dict]) -> None:
|
||||
"""Builds a tree of nodes as children of this instance.
|
||||
|
||||
Args:
|
||||
items: A dictionary where keys are absolute filepaths for directories/files.
|
||||
The values are dictionaries representing extra networks items.
|
||||
"""
|
||||
self.is_dir = os.path.isdir(self.abspath)
|
||||
if self.is_dir:
|
||||
for x in os.listdir(self.abspath):
|
||||
child_path = os.path.join(self.abspath, x)
|
||||
DirectoryTreeNode(self.root_dir, child_path, self).build(items)
|
||||
else:
|
||||
self.item = items.get(self.abspath, None)
|
||||
|
||||
def flatten(self, res: dict, dirs_only: bool = False) -> None:
|
||||
"""Flattens the keys/values of the tree nodes into a dictionary.
|
||||
|
||||
Args:
|
||||
res: The dictionary result updated in place. On initial call, should be passed
|
||||
as an empty dictionary.
|
||||
dirs_only: Whether to only add directories to the result.
|
||||
|
||||
Raises:
|
||||
KeyError: If any nodes in the tree have the same ID.
|
||||
"""
|
||||
if self.abspath in res:
|
||||
raise KeyError(f"duplicate key: {self.abspath}")
|
||||
|
||||
if not dirs_only or (dirs_only and self.is_dir):
|
||||
res[self.abspath] = self
|
||||
|
||||
for child in self.children:
|
||||
child.flatten(res, dirs_only)
|
||||
|
||||
def apply(self, fn: Callable) -> None:
|
||||
"""Recursively calls passed function with instance for entire tree."""
|
||||
fn(self)
|
||||
for child in self.children:
|
||||
child.apply(fn)
|
||||
|
||||
|
||||
def get_tree(paths: Union[str, list[str]], items: dict[str, ExtraNetworksItem]) -> dict:
|
||||
"""Recursively builds a directory tree.
|
||||
|
||||
|
|
@ -100,10 +206,15 @@ def register_page(page):
|
|||
allowed_dirs.clear()
|
||||
allowed_dirs.update(set(sum([x.allowed_directories_for_previews() for x in extra_pages], [])))
|
||||
|
||||
def get_page_by_name(extra_networks_tabname: str = "") -> "ExtraNetworksPage":
|
||||
for page in extra_pages:
|
||||
if page.extra_networks_tabname == extra_networks_tabname:
|
||||
return page
|
||||
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {extra_networks_tabname}")
|
||||
|
||||
|
||||
def fetch_file(filename: str = ""):
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
if not os.path.isfile(filename):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
|
|
@ -118,12 +229,8 @@ def fetch_file(filename: str = ""):
|
|||
return FileResponse(filename, headers={"Accept-Ranges": "bytes"})
|
||||
|
||||
|
||||
def fetch_cover_images(page: str = "", item: str = "", index: int = 0):
|
||||
from starlette.responses import Response
|
||||
|
||||
page = next(iter([x for x in extra_pages if x.name == page]), None)
|
||||
if page is None:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
def fetch_cover_images(extra_networks_tabname: str = "", item: str = "", index: int = 0):
|
||||
page = get_page_by_name(extra_networks_tabname)
|
||||
|
||||
metadata = page.metadata.get(item)
|
||||
if metadata is None:
|
||||
|
|
@ -142,70 +249,82 @@ def fetch_cover_images(page: str = "", item: str = "", index: int = 0):
|
|||
except Exception as err:
|
||||
raise ValueError(f"File cannot be fetched: {item}. Failed to load cover image.") from err
|
||||
|
||||
def init_tree_data(tabname: str = "", extra_networks_tabname: str = "") -> JSONResponse:
|
||||
page = get_page_by_name(extra_networks_tabname)
|
||||
|
||||
def get_list_data(
|
||||
tabname: str = "",
|
||||
data = page.generate_tree_view_data(tabname)
|
||||
|
||||
return JSONResponse(data, status_code=200)
|
||||
|
||||
def fetch_tree_data(
|
||||
extra_networks_tabname: str = "",
|
||||
list_type: Optional[str] = None,
|
||||
) -> PlainTextResponse:
|
||||
"""Responds to API GET requests on /sd_extra_networks/get-list-data with list data.
|
||||
div_ids: Optional[list[str]] = None,
|
||||
) -> JSONResponse:
|
||||
page = get_page_by_name(extra_networks_tabname)
|
||||
|
||||
Args:
|
||||
tabname: The primary tab name containing the data.
|
||||
(i.e. txt2img, img2img)
|
||||
extra_networks_tabname: The selected extra networks tabname.
|
||||
(i.e. lora, hypernetworks, etc.)
|
||||
list_type: The type of list data to retrieve. This reflects the
|
||||
class name used in `extraNetworksClusterizeList.js`.
|
||||
if div_ids is None:
|
||||
return JSONResponse({})
|
||||
|
||||
Returns:
|
||||
The string data result along with a status code.
|
||||
A status_code of 501 is returned on error, 200 on success.
|
||||
"""
|
||||
res = ""
|
||||
status_code = 200
|
||||
res = {}
|
||||
for div_id in div_ids:
|
||||
if div_id in page.tree:
|
||||
res[div_id] = page.tree[div_id].html
|
||||
|
||||
page = next(iter([
|
||||
x for x in extra_pages
|
||||
if x.extra_networks_tabname == extra_networks_tabname
|
||||
]), None)
|
||||
|
||||
if page is None:
|
||||
return PlainTextResponse(res, status_code=501)
|
||||
|
||||
if list_type == "ExtraNetworksClusterizeTreeList":
|
||||
res = page.generate_tree_view_data(tabname)
|
||||
elif list_type == "ExtraNetworksClusterizeCardsList":
|
||||
res = page.generate_cards_view_data(tabname)
|
||||
else:
|
||||
status_code = 501 # HTTP_501_NOT_IMPLEMENTED
|
||||
|
||||
return PlainTextResponse(res, status_code=status_code)
|
||||
return JSONResponse(res)
|
||||
|
||||
|
||||
def get_metadata(page: str = "", item: str = "") -> JSONResponse:
|
||||
page = next(iter([x for x in extra_pages if x.name == page]), None)
|
||||
if page is None:
|
||||
def fetch_cards_data(
|
||||
extra_networks_tabname: str = "",
|
||||
div_ids: Optional[list[str]] = None,
|
||||
) -> JSONResponse:
|
||||
page = get_page_by_name(extra_networks_tabname)
|
||||
|
||||
if div_ids is None:
|
||||
return JSONResponse({})
|
||||
|
||||
res = {}
|
||||
for div_id in div_ids:
|
||||
if div_id in page.cards:
|
||||
res[div_id] = page.cards[div_id].html
|
||||
|
||||
return JSONResponse(res)
|
||||
|
||||
|
||||
def init_cards_data(tabname: str = "", extra_networks_tabname: str = "") -> JSONResponse:
|
||||
page = get_page_by_name(extra_networks_tabname)
|
||||
|
||||
data = page.generate_cards_view_data(tabname)
|
||||
|
||||
return JSONResponse(data, status_code=200)
|
||||
|
||||
def get_metadata(extra_networks_tabname: str = "", item: str = "") -> JSONResponse:
|
||||
try:
|
||||
page = get_page_by_name(extra_networks_tabname)
|
||||
except HTTPException:
|
||||
return JSONResponse({})
|
||||
|
||||
metadata = page.metadata.get(item)
|
||||
if metadata is None:
|
||||
return JSONResponse({})
|
||||
|
||||
metadata = {i:metadata[i] for i in metadata if i != 'ssmd_cover_images'} # those are cover images, and they are too big to display in UI as text
|
||||
# those are cover images, and they are too big to display in UI as text
|
||||
metadata = {i: metadata[i] for i in metadata if i != 'ssmd_cover_images'}
|
||||
|
||||
return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)})
|
||||
|
||||
|
||||
def get_single_card(page: str = "", tabname: str = "", name: str = "") -> JSONResponse:
|
||||
page = next(iter([x for x in extra_pages if x.name == page]), None)
|
||||
def get_single_card(tabname: str = "", extra_networks_tabname: str = "", name: str = "") -> JSONResponse:
|
||||
page = get_page_by_name(extra_networks_tabname)
|
||||
|
||||
try:
|
||||
item = page.create_item(name, enable_filter=False)
|
||||
page.items[name] = item
|
||||
except Exception as e:
|
||||
errors.display(e, "creating item for extra network")
|
||||
item = page.items.get(name)
|
||||
except Exception as exc:
|
||||
errors.display(exc, "creating item for extra network")
|
||||
item = page.items.get(name, None)
|
||||
|
||||
if item is None:
|
||||
return JSONResponse({})
|
||||
|
||||
page.read_user_metadata(item, use_cache=False)
|
||||
item_html = page.create_card_html(tabname=tabname, item=item)
|
||||
|
|
@ -217,7 +336,11 @@ def add_pages_to_demo(app):
|
|||
app.add_api_route("/sd_extra_networks/cover-images", fetch_cover_images, methods=["GET"])
|
||||
app.add_api_route("/sd_extra_networks/metadata", get_metadata, methods=["GET"])
|
||||
app.add_api_route("/sd_extra_networks/get-single-card", get_single_card, methods=["GET"])
|
||||
app.add_api_route("/sd_extra_networks/get-list-data", get_list_data, methods=["GET"])
|
||||
#app.add_api_route("/sd_extra_networks/init-tree-data", init_tree_data, methods=["GET"])
|
||||
#app.add_api_route("/sd_extra_networks/init-cards-data", init_cards_data, methods=["GET"])
|
||||
#app.add_api_route("/sd_extra_networks/fetch-tree-data", fetch_tree_data, methods=["GET"])
|
||||
#app.add_api_route("/sd_extra_networks/fetch-cards-data", fetch_cards_data, methods=["GET"])
|
||||
|
||||
|
||||
|
||||
def quote_js(s):
|
||||
|
|
@ -236,18 +359,26 @@ class ExtraNetworksPage:
|
|||
self.allow_negative_prompt = False
|
||||
self.metadata = {}
|
||||
self.items = {}
|
||||
self.cards = {}
|
||||
self.tree = {}
|
||||
self.tree_roots = {}
|
||||
self.lister = util.MassFileLister()
|
||||
# HTML Templates
|
||||
self.pane_tpl = shared.html("extra-networks-pane.html")
|
||||
self.pane_content_tree_tpl = shared.html("extra-networks-pane-tree.html")
|
||||
self.pane_content_dirs_tpl = shared.html("extra-networks-pane-dirs.html")
|
||||
self.card_tpl = shared.html("extra-networks-card.html")
|
||||
self.tree_row_tpl = shared.html("extra-networks-tree-row.html")
|
||||
self.btn_copy_path_tpl = shared.html("extra-networks-copy-path-button.html")
|
||||
self.btn_metadata_tpl = shared.html("extra-networks-metadata-button.html")
|
||||
self.btn_edit_item_tpl = shared.html("extra-networks-edit-item-button.html")
|
||||
self.btn_dirs_view_tpl = shared.html("extra-networks-dirs-view-button.html")
|
||||
self.btn_copy_path_tpl = shared.html("extra-networks-btn-copy-path.html")
|
||||
self.btn_show_metadata_tpl = shared.html("extra-networks-btn-show-metadata.html")
|
||||
self.btn_edit_metadata_tpl = shared.html("extra-networks-btn-edit-metadata.html")
|
||||
self.btn_dirs_view_item_tpl = shared.html("extra-networks-btn-dirs-view-item.html")
|
||||
# Sorted lists
|
||||
# These just store ints so it won't use hardly any memory to just sort ahead
|
||||
# of time for each sort mode. These are lists of keys for each file.
|
||||
self.keys_sorted = {}
|
||||
self.keys_by_name = []
|
||||
self.keys_by_path = []
|
||||
self.keys_by_created = []
|
||||
self.keys_by_modified = []
|
||||
|
||||
def refresh(self):
|
||||
pass
|
||||
|
|
@ -281,8 +412,8 @@ class ExtraNetworksPage:
|
|||
tabname: str,
|
||||
label: str,
|
||||
btn_type: str,
|
||||
data_attributes: Optional[dict] = None,
|
||||
btn_title: Optional[str] = None,
|
||||
data_attributes: Optional[dict] = None,
|
||||
dir_is_empty: bool = False,
|
||||
item: Optional[dict] = None,
|
||||
onclick_extra: Optional[str] = None,
|
||||
|
|
@ -340,92 +471,6 @@ class ExtraNetworksPage:
|
|||
res = re.sub(" +", " ", res.replace("\n", ""))
|
||||
return res
|
||||
|
||||
def build_tree_html_dict(
|
||||
self,
|
||||
tree: dict,
|
||||
res: dict,
|
||||
tabname: str,
|
||||
div_id: int,
|
||||
depth: int,
|
||||
parent_id: Optional[int] = None,
|
||||
) -> int:
|
||||
for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])):
|
||||
if not isinstance(v, (ExtraNetworksItem,)):
|
||||
# dir
|
||||
if div_id in res:
|
||||
raise KeyError("div_id already in res:", div_id)
|
||||
|
||||
dir_is_empty = True
|
||||
for _v in v.values():
|
||||
if shared.opts.extra_networks_tree_view_show_files:
|
||||
dir_is_empty = False
|
||||
break
|
||||
elif not isinstance(_v, (ExtraNetworksItem,)):
|
||||
dir_is_empty = False
|
||||
break
|
||||
else:
|
||||
dir_is_empty = True
|
||||
|
||||
res[div_id] = self.build_tree_html_dict_row(
|
||||
tabname=tabname,
|
||||
label=os.path.basename(k),
|
||||
btn_type="dir",
|
||||
btn_title=k,
|
||||
dir_is_empty=dir_is_empty,
|
||||
data_attributes={
|
||||
"data-div-id": div_id,
|
||||
"data-parent-id": parent_id,
|
||||
"data-tree-entry-type": "dir",
|
||||
"data-depth": depth,
|
||||
"data-path": k,
|
||||
"data-expanded": parent_id is None, # Expand root directories
|
||||
},
|
||||
)
|
||||
last_div_id = self.build_tree_html_dict(
|
||||
tree=v,
|
||||
res=res,
|
||||
depth=depth + 1,
|
||||
div_id=div_id + 1,
|
||||
parent_id=div_id,
|
||||
tabname=tabname,
|
||||
)
|
||||
div_id = last_div_id
|
||||
else:
|
||||
# file
|
||||
if not shared.opts.extra_networks_tree_view_show_files:
|
||||
# Don't add file if showing files is disabled in options.
|
||||
continue
|
||||
|
||||
if div_id in res:
|
||||
raise KeyError("div_id already in res:", div_id)
|
||||
|
||||
onclick = v.item.get("onclick", None)
|
||||
if onclick is None:
|
||||
# Don't quote prompt/neg_prompt since they are stored as js strings already.
|
||||
onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');")
|
||||
|
||||
res[div_id] = self.build_tree_html_dict_row(
|
||||
tabname=tabname,
|
||||
label=html.escape(v.item.get("name", "").strip()),
|
||||
btn_type="file",
|
||||
data_attributes={
|
||||
"data-div-id": div_id,
|
||||
"data-parent-id": parent_id,
|
||||
"data-tree-entry-type": "file",
|
||||
"data-name": v.item.get("name", "").strip(),
|
||||
"data-depth": depth,
|
||||
"data-path": v.item.get("filename", "").strip(),
|
||||
"data-hash": v.item.get("shorthash", None),
|
||||
"data-prompt": v.item.get("prompt", "").strip(),
|
||||
"data-neg-prompt": v.item.get("negative_prompt", "").strip(),
|
||||
"data-allow-neg": self.allow_negative_prompt,
|
||||
},
|
||||
item=v.item,
|
||||
onclick_extra=onclick,
|
||||
)
|
||||
div_id += 1
|
||||
return div_id
|
||||
|
||||
def get_button_row(self, tabname: str, item: dict) -> str:
|
||||
metadata = item.get("metadata", None)
|
||||
name = item.get("name", "")
|
||||
|
|
@ -434,14 +479,14 @@ class ExtraNetworksPage:
|
|||
button_row_tpl = '<div class="button-row">{btn_copy_path}{btn_edit_item}{btn_metadata}</div>'
|
||||
|
||||
btn_copy_path = self.btn_copy_path_tpl.format(filename=filename)
|
||||
btn_edit_item = self.btn_edit_item_tpl.format(
|
||||
btn_edit_item = self.btn_edit_metadata_tpl.format(
|
||||
tabname=tabname,
|
||||
extra_networks_tabname=self.extra_networks_tabname,
|
||||
name=name,
|
||||
)
|
||||
btn_metadata = ""
|
||||
if metadata:
|
||||
btn_metadata = self.btn_metadata_tpl.format(
|
||||
btn_metadata = self.btn_show_metadata_tpl.format(
|
||||
extra_networks_tabname=self.extra_networks_tabname,
|
||||
name=name,
|
||||
)
|
||||
|
|
@ -457,7 +502,7 @@ class ExtraNetworksPage:
|
|||
self,
|
||||
tabname: str,
|
||||
item: dict,
|
||||
div_id: Optional[int] = None,
|
||||
div_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Generates HTML for a single ExtraNetworks Item.
|
||||
|
||||
|
|
@ -469,44 +514,37 @@ class ExtraNetworksPage:
|
|||
Returns:
|
||||
HTML string generated for this item. Can be empty if the item is not meant to be shown.
|
||||
"""
|
||||
preview = item.get("preview", None)
|
||||
style_height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else ''
|
||||
style_width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else ''
|
||||
style_font_size = f"font-size: {shared.opts.extra_networks_card_text_scale*100}%;"
|
||||
style = style_height + style_width + style_font_size
|
||||
background_image = f'<img src="{html.escape(preview)}" class="preview" loading="lazy">' if preview else ''
|
||||
style = f"font-size: {shared.opts.extra_networks_card_text_scale*100}%;"
|
||||
if shared.opts.extra_networks_card_height:
|
||||
style += f"height: {shared.opts.extra_networks_card_height}px;"
|
||||
if shared.opts.extra_networks_card_width:
|
||||
style += f"width: {shared.opts.extra_networks_card_width}px;"
|
||||
|
||||
background_image = None
|
||||
preview = html.escape(item.get("preview", ""))
|
||||
if preview:
|
||||
background_image = f'<img src="{preview}" class="preview" loading="lazy">'
|
||||
|
||||
onclick = item.get("onclick", None)
|
||||
if onclick is None:
|
||||
# Don't quote prompt/neg_prompt since they are stored as js strings already.
|
||||
onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');")
|
||||
|
||||
button_row = self.get_button_row(tabname, item)
|
||||
|
||||
local_path = ""
|
||||
filename = item.get("filename", "")
|
||||
for reldir in self.allowed_directories_for_previews():
|
||||
absdir = os.path.abspath(reldir)
|
||||
|
||||
if filename.startswith(absdir):
|
||||
local_path = filename[len(absdir):]
|
||||
|
||||
# if this is true, the item must not be shown in the default view, and must instead only be
|
||||
# shown when searching for it
|
||||
# if this is true, the item must not be shown in the default view,
|
||||
# and must instead only be shown when searching for it
|
||||
if shared.opts.extra_networks_hidden_models == "Always":
|
||||
search_only = False
|
||||
else:
|
||||
search_only = "/." in local_path or "\\." in local_path
|
||||
search_only = filename.startswith(".")
|
||||
|
||||
if search_only and shared.opts.extra_networks_hidden_models == "Never":
|
||||
return ""
|
||||
|
||||
sort_keys = " ".join(
|
||||
[
|
||||
f'data-sort-{k}="{html.escape(str(v))}"'
|
||||
for k, v in item.get("sort_keys", {}).items()
|
||||
]
|
||||
).strip()
|
||||
sort_keys = {}
|
||||
for sort_mode, sort_key in item.get("sort_keys", {}).items():
|
||||
sort_keys[sort_mode.strip().lower()] = html.escape(str(sort_key))
|
||||
|
||||
search_terms_html = ""
|
||||
search_terms_tpl = "<span class='hidden {class}'>{search_term}</span>"
|
||||
|
|
@ -518,7 +556,10 @@ class ExtraNetworksPage:
|
|||
}
|
||||
)
|
||||
|
||||
description = (item.get("description", "") or "" if shared.opts.extra_networks_card_show_desc else "")
|
||||
description = ""
|
||||
if shared.opts.extra_networks_card_show_desc:
|
||||
description = item.get("description", "")
|
||||
|
||||
if not shared.opts.extra_networks_card_description_is_html:
|
||||
description = html.escape(description)
|
||||
|
||||
|
|
@ -530,6 +571,7 @@ class ExtraNetworksPage:
|
|||
"data-prompt": item.get("prompt", "").strip(),
|
||||
"data-neg-prompt": item.get("negative_prompt", "").strip(),
|
||||
"data-allow-neg": self.allow_negative_prompt,
|
||||
**{f"data-sort-{sort_mode}": sort_key for sort_mode, sort_key in sort_keys},
|
||||
}
|
||||
|
||||
data_attributes_str = ""
|
||||
|
|
@ -553,97 +595,163 @@ class ExtraNetworksPage:
|
|||
description=description,
|
||||
)
|
||||
|
||||
def generate_tree_view_data(self, tabname: str) -> str:
|
||||
"""Generates tree view HTML as a base64 encoded zlib compressed json string."""
|
||||
def generate_cards_view_data(self, tabname: str) -> dict:
|
||||
for i, item in enumerate(self.items.values()):
|
||||
div_id = str(i)
|
||||
card_html = self.create_card_html(tabname=tabname, item=item, div_id=div_id)
|
||||
sort_keys = {
|
||||
k.strip().lower().replace(" ", "_"): html.escape(str(v))
|
||||
for k, v in item.get("sort_keys", {}).items()
|
||||
}
|
||||
search_terms = item.get("search_terms", [])
|
||||
self.cards[div_id] = CardListItem(div_id, card_html)
|
||||
self.cards[div_id].sort_keys = sort_keys
|
||||
self.cards[div_id].search_terms = search_terms
|
||||
|
||||
# Sort cards for all sort modes
|
||||
sort_modes = ["name", "path", "date_created", "date_modified"]
|
||||
for mode in sort_modes:
|
||||
self.keys_sorted[mode] = sorted(
|
||||
self.cards.keys(),
|
||||
key=lambda k: shared.natural_sort_key(self.cards[k].sort_keys[mode]),
|
||||
)
|
||||
|
||||
res = {}
|
||||
for div_id, card_item in self.cards.items():
|
||||
res[div_id] = {
|
||||
**{f"sort_{mode}": key for mode, key in card_item.sort_keys.items()},
|
||||
"search_terms": card_item.search_terms,
|
||||
}
|
||||
return res
|
||||
|
||||
def generate_tree_view_data(self, tabname: str) -> dict:
|
||||
if not self.tree_roots:
|
||||
return {}
|
||||
|
||||
# Flatten each root into a single dict
|
||||
tree = {}
|
||||
for node in self.tree_roots.values():
|
||||
subtree = {}
|
||||
node.flatten(subtree)
|
||||
tree.update(subtree)
|
||||
|
||||
path_to_div_id = {}
|
||||
div_id_to_node = {} # reverse mapping
|
||||
# First assign div IDs to each node. Used for parent ID lookup later.
|
||||
for i, path in enumerate(sorted(tree.keys(), key=shared.natural_sort_key)):
|
||||
div_id = str(i)
|
||||
path_to_div_id[path] = div_id
|
||||
div_id_to_node[div_id] = tree[path]
|
||||
|
||||
show_files = shared.opts.extra_networks_tree_view_show_files is True
|
||||
for div_id, node in div_id_to_node.items():
|
||||
self.tree[div_id] = TreeListItem(div_id, "")
|
||||
self.tree[div_id].node = node
|
||||
parent_id = None
|
||||
if node.parent is not None:
|
||||
parent_id = path_to_div_id.get(node.parent.abspath, None)
|
||||
|
||||
if node.item is None: # directory
|
||||
dir_is_empty = show_files and any(x.item is not None for x in node.children)
|
||||
self.tree[div_id].html = self.build_tree_html_dict_row(
|
||||
tabname=tabname,
|
||||
label=os.path.basename(node.abspath),
|
||||
btn_type="dir",
|
||||
btn_title=node.abspath,
|
||||
dir_is_empty=dir_is_empty,
|
||||
data_attributes={
|
||||
"data-div-id": div_id,
|
||||
"data-parent-id": parent_id,
|
||||
"data-tree-entry-type": "dir",
|
||||
"data-depth": node.depth,
|
||||
"data-path": node.abspath,
|
||||
"data-expanded": node.parent is None, # Expand root directories
|
||||
},
|
||||
)
|
||||
else: # file
|
||||
if not show_files:
|
||||
# Don't add file if files are disabled in the options.
|
||||
continue
|
||||
|
||||
onclick = node.item.get("onclick", None)
|
||||
if onclick is None:
|
||||
onclick = html.escape(f"extraNetworksCardOnClick(event, '{tabname}');")
|
||||
|
||||
item_name = node.item.get("name", "").strip()
|
||||
self.tree[div_id].html = self.build_tree_html_dict_row(
|
||||
tabname=tabname,
|
||||
label=html.escape(item_name),
|
||||
btn_type="file",
|
||||
data_attributes={
|
||||
"data-div-id": div_id,
|
||||
"data-parent-id": parent_id,
|
||||
"data-tree-entry-type": "file",
|
||||
"data-name": item_name,
|
||||
"data-depth": node.depth,
|
||||
"data-path": node.item.get("filename", "").strip(),
|
||||
"data-hash": node.item.get("shorthash", None),
|
||||
"data-prompt": node.item.get("prompt", "").strip(),
|
||||
"data-neg-prompt": node.item.get("negative_prompt", "").strip(),
|
||||
"data-allow-neg": self.allow_negative_prompt,
|
||||
},
|
||||
item=node.item,
|
||||
onclick_extra=onclick,
|
||||
)
|
||||
|
||||
res = {}
|
||||
|
||||
if self.tree:
|
||||
self.build_tree_html_dict(
|
||||
tree=self.tree,
|
||||
res=res,
|
||||
depth=0,
|
||||
div_id=0,
|
||||
parent_id=None,
|
||||
tabname=tabname,
|
||||
)
|
||||
# Expand all root directories and set them to active so they are displayed.
|
||||
for path in self.tree_roots.keys():
|
||||
div_id = path_to_div_id[path]
|
||||
self.tree[div_id].expanded = True
|
||||
self.tree[div_id].visible = True
|
||||
# Set all direct children to active
|
||||
for child_node in self.tree[div_id].node.children:
|
||||
self.tree[child_node.id].visible = True
|
||||
|
||||
return base64.b64encode(
|
||||
zlib.compress(
|
||||
json.dumps(res, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
||||
)
|
||||
).decode("utf-8")
|
||||
for div_id, tree_item in self.tree.items():
|
||||
# Expand root nodes and make them visible.
|
||||
expanded = tree_item.node.parent is None
|
||||
visible = tree_item.node.parent is None
|
||||
parent = None
|
||||
if tree_item.node.parent is not None:
|
||||
parent = path_to_div_id[tree_item.node.parent.abspath]
|
||||
# Direct children of root nodes should be visible by default.
|
||||
if tree_item.node.parent.node is None:
|
||||
visible = True
|
||||
|
||||
def generate_tree_view_data_div(self, tabname: str) -> str:
|
||||
"""Generates HTML for displaying folders in a tree view.
|
||||
res[div_id] = {
|
||||
"parent": parent,
|
||||
"children": [path_to_div_id[child.abspath] for child in tree_item.node.children],
|
||||
"visible": visible,
|
||||
"expanded": expanded,
|
||||
}
|
||||
return res
|
||||
|
||||
Args:
|
||||
tabname: The name of the active tab.
|
||||
|
||||
Returns:
|
||||
HTML string generated for this tree view.
|
||||
"""
|
||||
tpl = """<div id="{tabname_full}_tree_list_data"
|
||||
class="extra-network-script-data"
|
||||
data-tabname-full={tabname_full}
|
||||
data-proxy-name=tree_list
|
||||
data-json={data}
|
||||
hidden></div>"""
|
||||
return tpl.format(
|
||||
tabname_full=f"{tabname}_{self.extra_networks_tabname}",
|
||||
data=self.generate_tree_view_data(tabname),
|
||||
)
|
||||
|
||||
def create_dirs_view_html(self, tabname: str) -> str:
|
||||
"""Generates HTML for displaying folders."""
|
||||
# Flatten each root into a single dict. Only get the directories for buttons.
|
||||
tree = {}
|
||||
for node in self.tree_roots.values():
|
||||
subtree = {}
|
||||
node.flatten(subtree, dirs_only=True)
|
||||
tree.update(subtree)
|
||||
|
||||
def _get_dirs_buttons(tree: dict, res: list) -> None:
|
||||
""" Builds a list of directory names from a tree. """
|
||||
for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])):
|
||||
if not isinstance(v, (ExtraNetworksItem,)):
|
||||
# dir
|
||||
res.append(k)
|
||||
_get_dirs_buttons(tree=v, res=res)
|
||||
|
||||
dirs = []
|
||||
_get_dirs_buttons(tree=self.tree, res=dirs)
|
||||
|
||||
# Sort the tree nodes by relative paths
|
||||
dir_nodes = list(sorted(
|
||||
tree.values(),
|
||||
key=lambda x: shared.natural_sort_key(x.relpath),
|
||||
))
|
||||
dirs_html = "".join([
|
||||
self.btn_dirs_view_tpl.format(**{
|
||||
"extra_class": "search-all" if d == "" else "",
|
||||
self.btn_dirs_view_item_tpl.format(**{
|
||||
"extra_class": "search-all" if node.relpath == "" else "",
|
||||
"tabname_full": f"{tabname}_{self.extra_networks_tabname}",
|
||||
"path": html.escape(d),
|
||||
}) for d in dirs
|
||||
"path": html.escape(node.relpath),
|
||||
}) for node in dir_nodes
|
||||
])
|
||||
return dirs_html
|
||||
|
||||
def generate_cards_view_data(self, tabname: str) -> str:
|
||||
res = {}
|
||||
for i, item in enumerate(self.items.values()):
|
||||
res[i] = self.create_card_html(tabname=tabname, item=item, div_id=i)
|
||||
|
||||
return base64.b64encode(
|
||||
zlib.compress(
|
||||
json.dumps(res, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
||||
)
|
||||
).decode("utf-8")
|
||||
|
||||
def generate_cards_view_data_div(self, tabname: str, none_message: Optional[str]) -> str:
|
||||
"""Generates HTML for the network Card View section for a tab.
|
||||
|
||||
This HTML goes into the `extra-networks-pane.html` <div> with
|
||||
`id='{tabname}_{extra_networks_tabname}_cards`.
|
||||
|
||||
Args:
|
||||
tabname: The name of the active tab.
|
||||
none_message: HTML text to show when there are no cards.
|
||||
|
||||
Returns:
|
||||
HTML formatted string.
|
||||
"""
|
||||
res = self.generate_cards_view_data(tabname)
|
||||
|
||||
return f'<div id="{tabname}_{self.extra_networks_tabname}_cards_list_data" class="extra-network-script-data" data-tabname-full={tabname}_{self.extra_networks_tabname} data-proxy-name=cards_list data-json={res} hidden></div>'
|
||||
|
||||
def create_html(self, tabname, *, empty=False):
|
||||
"""Generates an HTML string for the current pane.
|
||||
|
||||
|
|
@ -672,10 +780,13 @@ class ExtraNetworksPage:
|
|||
self.read_user_metadata(item)
|
||||
|
||||
# Setup the tree dictionary.
|
||||
roots = self.allowed_directories_for_previews()
|
||||
tree_items = {v["filename"]: ExtraNetworksItem(v) for v in self.items.values()}
|
||||
tree = get_tree([os.path.abspath(x) for x in roots], items=tree_items)
|
||||
self.tree = tree
|
||||
tree_items = {v["filename"]: v for v in self.items.values()}
|
||||
# Create a DirectoryTreeNode for each root directory since they might not share
|
||||
# a common path.
|
||||
for path in self.allowed_directories_for_previews():
|
||||
abspath = os.path.abspath(path)
|
||||
self.tree_roots[abspath] = DirectoryTreeNode(os.path.dirname(abspath), abspath, None)
|
||||
self.tree_roots[abspath].build(tree_items)
|
||||
|
||||
# Generate the html for displaying directory buttons
|
||||
dirs_html = self.create_dirs_view_html(tabname)
|
||||
|
|
@ -744,7 +855,7 @@ class ExtraNetworksPage:
|
|||
|
||||
file = f"{path}.safetensors"
|
||||
if self.lister.exists(file) and 'ssmd_cover_images' in metadata and len(list(filter(None, json.loads(metadata['ssmd_cover_images'])))) > 0:
|
||||
return f"./sd_extra_networks/cover-images?page={self.extra_networks_tabname}&item={name}"
|
||||
return f"./sd_extra_networks/cover-images?extra_networks_tabname={self.extra_networks_tabname}&item={name}"
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -839,13 +950,23 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
|
|||
ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=f"{tabname}_preview_filename", visible=False)
|
||||
|
||||
for tab in unrelated_tabs:
|
||||
tab.select(fn=None, _js=f"function(){{extraNetworksUnrelatedTabSelected('{tabname}');}}", inputs=[], outputs=[], show_progress=False)
|
||||
tab.select(
|
||||
fn=None,
|
||||
_js=f"fujnction(){{extraNetworksUnrelatedTabSelected('{tabname}');}}",
|
||||
inputs=[],
|
||||
outputs=[],
|
||||
show_progress=False,
|
||||
)
|
||||
|
||||
for page, tab in zip(ui.stored_extra_pages, related_tabs):
|
||||
jscode = (
|
||||
"function(){"
|
||||
f"extraNetworksTabSelected('{tabname}', '{tabname}_{page.extra_networks_tabname}_prompts', {str(page.allow_prompt).lower()}, {str(page.allow_negative_prompt).lower()}, '{tabname}_{page.extra_networks_tabname}');"
|
||||
"}"
|
||||
"function(){extraNetworksTabSelected("
|
||||
f"'{tabname}', "
|
||||
f"'{tabname}_{page.extra_networks_tabname}_prompts', "
|
||||
f"{str(page.allow_prompt).lower()}, "
|
||||
f"{str(page.allow_negative_prompt).lower()}, "
|
||||
f"'{tabname}_{page.extra_networks_tabname}'"
|
||||
");}"
|
||||
)
|
||||
tab.select(fn=None, _js=jscode, inputs=[], outputs=[], show_progress=False)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue