This commit is contained in:
Sj-Si 2025-12-21 19:59:37 -05:00 committed by GitHub
commit 68579b9f78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 8027 additions and 1629 deletions

View file

@ -1,11 +1,16 @@
import os
import html
import datetime
import math
import matplotlib as mpl
import colorsys
import network
import networks
from modules import shared, ui_extra_networks
from modules.ui_extra_networks import quote_js
from ui_edit_user_metadata import LoraUserMetadataEditor
from ui_edit_user_metadata import LoraUserMetadataEditor, build_tags
class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
@ -14,6 +19,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
def refresh(self):
networks.list_available_networks()
super().refresh()
def create_item(self, name, index=None, enable_filter=True):
lora_on_disk = networks.available_networks.get(name)
@ -43,10 +49,10 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
self.read_user_metadata(item)
activation_text = item["user_metadata"].get("activation text")
preferred_weight = item["user_metadata"].get("preferred weight", 0.0)
item["prompt"] = quote_js(f"<lora:{alias}:") + " + " + (str(preferred_weight) if preferred_weight else "opts.extra_networks_default_multiplier") + " + " + quote_js(">")
prompt = f"<lora:{alias}:{str(preferred_weight) if preferred_weight else shared.opts.extra_networks_default_multiplier}>"
if activation_text:
item["prompt"] += " + " + quote_js(" " + activation_text)
prompt += f" {activation_text}"
item["prompt"] = quote_js(prompt)
negative_prompt = item["user_metadata"].get("negative text")
item["negative_prompt"] = quote_js("")
@ -88,3 +94,148 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
def create_user_metadata_editor(self, ui, tabname):
return LoraUserMetadataEditor(ui, tabname, self)
def get_model_detail_metadata_table(self, model_name: str) -> str:
res = super().get_model_detail_metadata_table(model_name)
metadata = self.metadata.get(model_name)
if metadata is None:
metadata = {}
keys = {
'ss_output_name': "Output name:",
'ss_sd_model_name': "Model:",
'ss_clip_skip': "Clip skip:",
'ss_network_module': "Kohya module:",
}
params = []
for k, lbl in keys.items():
v = metadata.get(k, None)
if v is not None and str(v) != "None":
params.append((lbl, html.escape(v)))
ss_training_started_at = metadata.get('ss_training_started_at')
if ss_training_started_at:
date_trained = datetime.datetime.utcfromtimestamp(
float(ss_training_started_at)
).strftime('%Y-%m-%d %H:%M')
params.append(("Date trained:", date_trained))
ss_bucket_info = metadata.get("ss_bucket_info")
if ss_bucket_info and "buckets" in ss_bucket_info:
resolutions = {}
for _, bucket in ss_bucket_info["buckets"].items():
resolution = bucket["resolution"]
resolution = f'{resolution[1]}x{resolution[0]}'
resolutions[resolution] = resolutions.get(resolution, 0) + int(bucket["count"])
resolutions_list = sorted(resolutions.keys(), key=resolutions.get, reverse=True)
resolutions_text = html.escape(", ".join(resolutions_list))
resolutions_text = (
"<div class='styled-scrollbar' style='overflow-x: auto'>"
f"{resolutions_text}"
"</div>"
)
params.append(("Resolutions:", resolutions_text))
image_count = 0
for v in metadata.get("ss_dataset_dirs", {}).values():
image_count += int(v.get("img_count", 0))
if image_count:
params.append(("Dataset size:", image_count))
tbl_metadata = "".join([f"<tr><th>{tr[0]}</th><td>{tr[1]}</td>" for tr in params])
return res + tbl_metadata
def get_model_detail_extra_html(self, model_name: str) -> str:
"""Generates HTML to show in the details view."""
res = ""
item = self.items.get(model_name, {})
metadata = item.get("metadata", {}) or {}
user_metadata = item.get("user_metadata", {}) or {}
sd_version = item.get("sd_version", None)
preferred_weight = user_metadata.get("preferred weight", None)
activation_text = user_metadata.get("activation text", None)
negative_text = user_metadata.get("negative text", None)
rows = []
if sd_version is not None:
rows.append(("SD Version:", sd_version))
if preferred_weight is not None:
rows.append(("Preferred weight:", preferred_weight))
if activation_text is not None:
rows.append(("Activation text:", activation_text))
if negative_text is not None:
rows.append(("Negative propmt:", negative_text))
rows_html = "".join([f"<tr><th>{tr[0]}</th><td>{tr[1]}</td>" for tr in rows])
if rows_html:
res += "<h3>User Metadata</h3>"
res += f"<table><tbody>{rows_html}</tbody></table>"
tags = build_tags(metadata)
if tags is None or len(tags) == 0:
res += "<h3>Model Tags</h3>"
res += "<div class='model-info--tags'>Metadata contains no tags</div>"
return res
min_tag = min(int(x[1]) for x in tags)
max_tag = max(int(x[1]) for x in tags)
cmap = mpl.colormaps["coolwarm"]
def _clamp(x: float, min_val: float, max_val: float) -> float:
return max(min_val, min(x, max_val))
def _get_fg_color(r, g, b) -> str:
return "#000000" if (r * 0.299 + g * 0.587 + b * 0.114) > 0.5 else "#FFFFFF"
tag_elems = []
for (tag_name, tag_count) in tags:
# Normalize tag count
tag_count = int(tag_count)
if min_tag == max_tag: # Prevent DivideByZero error.
cmap_idx = cmap.N // 2
else:
cmap_idx = math.floor(
(tag_count - min_tag) / (max_tag - min_tag) * (cmap.N - 1)
)
# Get the bg color based on tag count and a contrasting fg color.
base_color = cmap(cmap_idx)
base_color = [_clamp(x, 0, 1) for x in base_color]
base_fg_color = _get_fg_color(*base_color[:3])
# Now get a slightly darker background for the tag count bg color.
h, lum, s = colorsys.rgb_to_hls(*base_color[:3])
lum = max(min(lum * 0.7, 1.0), 0.0)
dark_color = colorsys.hls_to_rgb(h, lum, s)
dark_color = [_clamp(x, 0, 1) for x in dark_color]
dark_fg_color = _get_fg_color(*dark_color[:3])
# Convert the colors to a hex string.
base_color = mpl.colors.rgb2hex(base_color)
dark_color = mpl.colors.rgb2hex(dark_color)
# Finally, generate the HTML for this tag.
tag_style = f"background: {base_color};"
name_style = f"color: {base_fg_color};"
count_style = f"background: {dark_color}; color: {dark_fg_color};"
tag_elems.append((
f"<span class='model-info--tag' style='{tag_style}'>"
f"<span class='model-info--tag-name' style='{name_style}'>{tag_name}</span>"
f"<span class='model-info--tag-count' style='{count_style}'>{tag_count}</span>"
"</span>"
))
res += "<h3>Model Tags</h3>"
res += f"<div class='model-info--tags'>{''.join(tag_elems)}</div>"
return res

View file

@ -0,0 +1,11 @@
<div class="tree-list-item-action--chevron {extra_classes}">
<svg class="chevron-icon-single" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M 4 4 H 12 V 12" />
</svg>
<svg class="chevron-icon-double" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M 5 3 H 13 V 11" />
<path d="M 1 7 H 9 V 15" />
</svg>
</div>

View file

@ -0,0 +1,2 @@
<div class="copy-path-button card-button" title="Copy path to clipboard" data-clipboard-text="{clipboard_text}">
</div>

View file

@ -0,0 +1,2 @@
<button class='lg secondary gradio-button custom-button extra-network-dirs-view-button {extra_class}' title="{title}"
{data_attributes}>{label}</button>

View file

@ -0,0 +1 @@
<div class="edit-button card-button" title="Edit metadata"></div>

View file

@ -0,0 +1 @@
<div class="metadata-button card-button" title="Show internal metadata"></div>

View file

@ -1,9 +1,8 @@
<div class="card" style="{style}" onclick="{card_clicked}" data-name="{name}" {sort_keys}>
{background_image}
<div class="button-row">{copy_path_button}{metadata_button}{edit_button}</div>
<div class="actions">
<div class="additional">{search_terms}</div>
<span class="name">{name}</span>
<span class="description">{description}</span>
</div>
<div class="card" style="{style}" {data_attributes}>
{background_image}
{button_row}
<div class="actions">
<span class="name">{name}</span>
<span class="description" {description_data_attributes}>{description}</span>
</div>
</div>

View file

@ -1,5 +0,0 @@
<div class="copy-path-button card-button"
title="Copy path to clipboard"
onclick="extraNetworksCopyCardPath(event)"
data-clipboard-text="{filename}">
</div>

View file

@ -1,4 +0,0 @@
<div class="edit-button card-button"
title="Edit metadata"
onclick="extraNetworksEditUserMetadata(event, '{tabname}', '{extra_networks_tabname}')">
</div>

View file

@ -1,4 +0,0 @@
<div class="metadata-button card-button"
title="Show internal metadata"
onclick="extraNetworksRequestMetadata(event, '{extra_networks_tabname}')">
</div>

View file

@ -0,0 +1,17 @@
<div class="extra-network-content--dets-view-model-info">
<div class="model-info--header">
<h1 class="model-info--name">{name}</h1>
<button class="extra-network-control model-info--close" title="Close model details">
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M 2 2 L 14 14 M 2 14 L 14 2" />
</svg>
</button>
</div>
<p class="model-info--description" {description_data_attributes}>{description}</p>
<h3>Model Metadata</h3>
<table class="model-info--metadata">
<tbody>{metadata_table}</tbody>
</table>
{model_specific}
</div>

View file

@ -1,8 +0,0 @@
<div class='nocards'>
<h1>Nothing here. Add some content to the following directories:</h1>
<ul>
{dirs}
</ul>
</div>

View file

@ -1,8 +0,0 @@
<div class="extra-network-pane-content-dirs">
<div id='{tabname}_{extra_networks_tabname}_dirs' class='extra-network-dirs'>
{dirs_html}
</div>
<div id='{tabname}_{extra_networks_tabname}_cards' class='extra-network-cards'>
{items_html}
</div>
</div>

View file

@ -1,8 +0,0 @@
<div class="extra-network-pane-content-tree resize-handle-row">
<div id='{tabname}_{extra_networks_tabname}_tree' class='extra-network-tree' style='flex-basis: {extra_networks_tree_view_default_width}px'>
{tree_html}
</div>
<div id='{tabname}_{extra_networks_tabname}_cards' class='extra-network-cards' style='flex-grow: 1;'>
{items_html}
</div>
</div>

View file

@ -1,81 +1,211 @@
<div id='{tabname}_{extra_networks_tabname}_pane' class='extra-network-pane {tree_view_div_default_display_class}'>
<div class="extra-network-control" id="{tabname}_{extra_networks_tabname}_controls" style="display:none" >
<div id='{tabname}_{extra_networks_tabname}_pane' class='extra-network-pane' data-tabname="{tabname}"
data-extra-networks-tabname="{extra_networks_tabname}" data-tabname-full="{tabname}_{extra_networks_tabname}">
<div class="extra-network-controls hidden" data-tabname-full="{tabname}_{extra_networks_tabname}">
<div class="extra-network-control--search">
<input
id="{tabname}_{extra_networks_tabname}_extra_search"
class="extra-network-control--search-text"
type="search"
placeholder="Search"
>
<svg class="extra-network-control--search-icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
<circle cx="6" cy="6" r="5" />
<line x1="9.75" y1="9.75" x2="13" y2="13" />
</svg>
<input class="extra-network-control--search-text" type="text" placeholder="Search">
<button role="button" class="extra-network-control extra-network-control--search-clear">
<svg class="extra-network-control--search-clear-icon" viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
<path fill="none" stroke="currentColor" d="M 5 5 L 11 11 M 5 11 L 11 5" />
<circle fill="none" stroke="currentColor" cx="8" cy="8" r="7" />
</svg>
</button>
</div>
<small>Sort: </small>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_path"
class="extra-network-control--sort{sort_path_active}"
data-sortkey="default"
title="Sort by path"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
<div class="extra-network-controls--container">
<div class="extra-network-controls--header">Clear Filters</div>
<div class="extra-network-controls--buttons">
<button class="extra-network-control extra-network-control--clear-filters"
title="Clear all directory filters">
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="1">
<path d="M 2 2 V 4 L 6 8 V 13 L 9 14 V 12 M 12 5 L 13 4 V 2 H 2" />
<path d="M 9 10 L 12 7 M 9 7 L 12 10" />
</svg>
</button>
</div>
</div>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_name"
class="extra-network-control--sort{sort_name_active}"
data-sortkey="name"
title="Sort by name"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
<div class="extra-network-controls--container">
<div class="extra-network-controls--header">Sort Mode</div>
<div class="extra-network-controls--buttons">
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by path"
data-sort-mode="path" {btn_sort_mode_path_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="1">
<rect x="0.5" y="4" width="15" height="10" rx="1" />
<path d="M 0.5 5 L 0.5 3 Q 0.5 2 1.5 2 L 6 2 Q 7 2 7 3 L 7 4" />
<circle cx="7.5" cy="8.5" r="2" />
<line x1="9" y1="10" x2="10.5" y2="11.5" />
</svg>
</button>
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by name"
data-sort-mode="name" {btn_sort_mode_name_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path stroke-width="1" d="M 2 12 L 6 3 L 10 12 M 3.5 9 L 8.5 9" />
<path stroke-width="1" d="M 8 3 L 15 3 M 9.5 6 L 15 6 M 11 9 L 15 9 M 12.5 12 L 15 12" />
</svg>
</button>
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by date created"
data-sort-mode="date_created" {btn_sort_mode_date_created_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
<rect x="1" y="2" width="14" height="12" rx="2" fill="none" />
<line x1="1" y1="6" x2="15" y2="6" />
<line x1="8" y1="8" x2="8" y2="12" />
<line x1="6" y1="10" x2="10" y2="10" />
<line x1="4" y1="1" x2="4" y2="3" />
<line x1="8" y1="1" x2="8" y2="3" />
<line x1="12" y1="1" x2="12" y2="3" />
</svg>
</button>
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by date modified"
data-sort-mode="date_modified" {btn_sort_mode_date_modified_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-width="1">
<g stroke-linecap="round" stroke-linejoin="round">
<rect x="1" y="2" width="14" height="12" rx="2" />
<line x1="1" y1="6" x2="15" y2="6" />
<line x1="4" y1="1" x2="4" y2="3" />
<line x1="8" y1="1" x2="8" y2="3" />
<line x1="12" y1="1" x2="12" y2="3" />
</g>
<g stroke-linecap="square" stroke-linejoin="square">
<path d="M 4 9 H 10 L 12 10 L 10 11 H 4 Z M 6 9 V 10" />
</g>
</svg>
</button>
</div>
</div>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_date_created"
class="extra-network-control--sort{sort_date_created_active}"
data-sortkey="date_created"
title="Sort by date created"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
<div class="extra-network-controls--container">
<div class="extra-network-controls--header">Sort Order</div>
<div class="extra-network-controls--buttons">
<button class="extra-network-control extra-network-control--sort-dir" title="Sort ascending"
data-sort-dir="ascending" {btn_sort_dir_ascending_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path fill="currentColor" d="M 8 1 L 3 7 L 13 7 Z" />
<path fill="none" stroke="var(--input-placeholder-color)" d="M 8 15 L 3 9 L 13 9 Z" />
</svg>
</button>
<button class="extra-network-control extra-network-control--sort-dir" title="Sort descending"
data-sort-dir="descending" {btn_sort_dir_descending_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path fill="none" stroke="var(--input-placeholder-color)" d="M 8 1 L 3 7 L 13 7 Z" />
<path fill="currentColor" d="M 8 15 L 3 9 L 13 9 Z" />
</svg>
</button>
</div>
</div>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_date_modified"
class="extra-network-control--sort{sort_date_modified_active}"
data-sortkey="date_modified"
title="Sort by date modified"
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
<div class="extra-network-controls--container">
<div class="extra-network-controls--header">View Mode</div>
<div class="extra-network-controls--buttons">
<button class="extra-network-control extra-network-control--dirs-view" title="Enable Directory View"
{btn_dirs_view_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" stroke="none">
<rect x="0" y="6" width="4" height="4" rx="1" />
<rect x="6" y="6" width="4" height="4" rx="1" />
<rect x="12" y="6" width="4" height="4" rx="1" />
</svg>
</button>
<button class="extra-network-control extra-network-control--tree-view" title="Enable Tree View"
{btn_tree_view_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" stroke="none">
<rect x="3" y="0" width="4" height="4" rx="1" />
<rect x="9" y="6" width="4" height="4" rx="1" />
<rect x="9" y="12" width="4" height="4" rx="1" />
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
d="M 5 5 V 14 H 8 M 5 8 H 8" />
</svg>
</button>
<button class="extra-network-control extra-network-control--card-view" title="Enable Card View"
{btn_card_view_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" stroke="none">
<rect x="3" y="3" width="4" height="4" rx="1" />
<rect x="3" y="9" width="4" height="4" rx="1" />
<rect x="9" y="3" width="4" height="4" rx="1" />
<rect x="9" y="9" width="4" height="4" rx="1" />
</svg>
</button>
<button class="extra-network-control extra-network-control--dets-view" title="Enable Card Detail View"
{btn_dets_view_data_attributes}>
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round">
<path d="M 3 3 H 12 M 3 6 H 12 M 3 9 H 12 M 3 12 H 12" />
</svg>
</button>
</div>
</div>
<small> </small>
<div
id="{tabname}_{extra_networks_tabname}_extra_sort_dir"
class="extra-network-control--sort-dir"
data-sortdir="{data_sortdir}"
title="Sort ascending"
onclick="extraNetworksControlSortDirOnClick(event, '{tabname}', '{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--sort-dir-icon"></i>
</div>
<small> </small>
<div
id="{tabname}_{extra_networks_tabname}_extra_tree_view"
class="extra-network-control--tree-view {tree_view_btn_extra_class}"
title="Enable Tree View"
onclick="extraNetworksControlTreeViewOnClick(event, '{tabname}', '{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--tree-view-icon"></i>
</div>
<div
id="{tabname}_{extra_networks_tabname}_extra_refresh"
class="extra-network-control--refresh"
title="Refresh page"
onclick="extraNetworksControlRefreshOnClick(event, '{tabname}', '{extra_networks_tabname}');"
>
<i class="extra-network-control--icon extra-network-control--refresh-icon"></i>
<div class="extra-network-controls--container">
<div class="extra-network-controls--header">Refresh</div>
<div class="extra-network-controls--buttons">
<button class="extra-network-control extra-network-control--refresh" title="Refresh tab">
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round">
<path d="M 8 14 A 6 6 0 1 1 14 8 L 12 6.5 M 14 8 L 15 5.5 " />
</svg>
</button>
</div>
</div>
</div>
<div class="extra-network-content extra-network-content--container resize-grid">
<div id="{tabname}_{extra_networks_tabname}_dirs_view_row" class="resize-grid--row"
style="{dirs_view_row_style}" {dirs_view_row_data_attributes}>
<div id="{tabname}_{extra_networks_tabname}_dirs_view_cell" class="resize-grid--cell" data-min-size="0px" style="flex: 1 1 0px;">
<div class="extra-network-content extra-network-content--dirs-view styled-scrollbar">
{dirs_html}
</div>
</div>
</div>
<div id="{tabname}_{extra_networks_tabname}_main_row" class="resize-grid--row" style="{main_row_style}"
{main_row_data_attributes}>
<div id="{tabname}_{extra_networks_tabname}_tree_view_cell" class="resize-grid--cell"
style="{tree_view_cell_style}" {tree_view_cell_data_attributes}>
<div class="extra-network-content extra-network-content--tree-view">
<div id='{tabname}_{extra_networks_tabname}_tree_list_loading_splash'
class='extra-network-list-splash'>
{tree_list_loading_splash_content}</div>
<div id='{tabname}_{extra_networks_tabname}_tree_list_no_data_splash'
class='extra-network-list-splash hidden'>{tree_list_no_data_splash_content}</div>
<div id='{tabname}_{extra_networks_tabname}_tree_list_scroll_area'
class='styled-scrollbar clusterize-scroll'>
<div id='{tabname}_{extra_networks_tabname}_tree_list_content_area'
class='extra-network-tree-content clusterize-content'></div>
</div>
</div>
</div>
<div id="{tabname}_{extra_networks_tabname}_card_view_cell" class="resize-grid--cell"
style="{card_view_cell_style}" {card_view_cell_data_attributes}>
<div class="extra-network-content extra-network-content--card-view extra-network-cards">
<div id='{tabname}_{extra_networks_tabname}_card_list_loading_splash'
class='extra-network-list-splash'>
{card_list_loading_splash_content}</div>
<div id='{tabname}_{extra_networks_tabname}_card_list_no_data_splash'
class='extra-network-list-splash hidden'>{card_list_no_data_splash_content}
</div>
<div id='{tabname}_{extra_networks_tabname}_card_list_scroll_area'
class='styled-scrollbar clusterize-scroll'>
<div id='{tabname}_{extra_networks_tabname}_card_list_content_area'
class='extra-network-card-content clusterize-content'></div>
</div>
</div>
</div>
<div id="{tabname}_{extra_networks_tabname}_dets_view_cell" class="resize-grid--cell"
style="{dets_view_cell_style}" {dets_view_cell_data_attributes}>
<div class="extra-network-content extra-network-content--dets-view styled-scrollbar"></div>
</div>
</div>
</div>
{pane_content}
</div>

View file

@ -1,23 +0,0 @@
<span data-filterable-item-text hidden>{search_terms}</span>
<div class="tree-list-content {subclass}"
type="button"
onclick="extraNetworksTreeOnClick(event, '{tabname}', '{extra_networks_tabname}');{onclick_extra}"
data-path="{data_path}"
data-hash="{data_hash}"
>
<span class='tree-list-item-action tree-list-item-action--leading'>
{action_list_item_action_leading}
</span>
<span class="tree-list-item-visual tree-list-item-visual--leading">
{action_list_item_visual_leading}
</span>
<span class="tree-list-item-label tree-list-item-label--truncate">
{action_list_item_label}
</span>
<span class="tree-list-item-visual tree-list-item-visual--trailing">
{action_list_item_visual_trailing}
</span>
<span class="tree-list-item-action tree-list-item-action--trailing">
{action_list_item_action_trailing}
</span>
</div>

View file

@ -0,0 +1,11 @@
<div class="tree-list-item tree-list-item--{btn_type}" title={btn_title} {data_attributes}>
{indent_spans}
<button>
<span data-filterable-item-text hidden>{search_terms}</span>
<span class='tree-list-item-action tree-list-item-action--leading'>{action_list_item_action_leading}</span>
<span class="tree-list-item-visual tree-list-item-visual--leading">{action_list_item_visual_leading}</span>
<span class="tree-list-item-label tree-list-item-label--truncate">{action_list_item_label}</span>
<span class="tree-list-item-visual tree-list-item-visual--trailing">{action_list_item_visual_trailing}</span>
<span class="tree-list-item-action tree-list-item-action--trailing">{action_list_item_action_trailing}</span>
</button>
</div>

622
javascript/clusterize.js Normal file
View file

@ -0,0 +1,622 @@
/* eslint-disable */
/*
Heavily modified Clusterize.js v1.0.0.
Original: http://NeXTs.github.com/Clusterize.js/
This has been modified to allow for an asynchronous data loader implementation.
This differs from the original Clusterize.js which would store the entire dataset
in an array and load from that; this caused a large memory overhead in the client.
*/
// Many operations can be lenghty. Try to limit their frequency by debouncing.
const SCROLL_DEBOUNCE_TIME_MS = 50;
const RESIZE_OBSERVER_DEBOUNCE_TIME_MS = 100; // should be <= refresh debounce time
const ELEMENT_OBSERVER_DEBOUNCE_TIME_MS = 100;
const REFRESH_DEBOUNCE_TIME_MS = 100;
class Clusterize {
scroll_elem = null;
content_elem = null;
scroll_id = null;
content_id = null;
config = {
block_height: null,
block_width: null,
cols_in_block: 1,
cluster_height: null,
cluster_width: null,
is_mac: navigator.userAgent.toLowerCase().indexOf("mac") > -1,
item_height: null,
item_width: null,
max_items: 0,
max_rows: 0,
rows_in_block: 50,
rows_in_cluster: 50 * 5, // default is rows_in_block * blocks_in_cluster
};
options = {
blocks_in_cluster: 5,
tag: "div",
no_data_class: "clusterize-no-data",
no_data_html: "No Data",
error_class: "clusterize-error",
error_html: "Data Error",
keep_parity: true,
callbacks: {},
};
state = {
cache: {},
curr_cluster: 0,
enabled: false,
scroll_top: 0,
setup_has_run: false,
pointer_events_set: false,
};
#scroll_debounce_timer = 0;
#refresh_debounce_timer = null;
#resize_observer = null;
#resize_observer_timer = null;
#element_observer = null;
#element_observer_timer = null;
#on_scroll_bound;
constructor(args) {
for (const option of Object.keys(this.options)) {
if (keyExists(args, option)) {
this.options[option] = args[option];
}
}
if (isNullOrUndefined(this.options.callbacks.initData)) {
this.options.callbacks.initData = this.initDataDefaultCallback.bind(this);
}
if (isNullOrUndefined(this.options.callbacks.fetchData)) {
this.options.callbacks.fetchData = this.fetchDataDefaultCallback.bind(this);
}
if (isNullOrUndefined(this.options.callbacks.sortData)) {
this.options.callbacks.sortData = this.sortDataDefaultCallback.bind(this);
}
if (isNullOrUndefined(this.options.callbacks.filterData)) {
this.options.callbacks.filterData = this.filterDataDefaultCallback.bind(this);
}
this.scroll_elem = args["scrollId"] ? document.getElementById(args["scrollId"]) : args["scrollElem"];
isElementThrowError(this.scroll_elem);
this.scroll_id = this.scroll_elem.id;
this.content_elem = args["contentId"] ? document.getElementById(args["contentId"]) : args["contentElem"];
isElementThrowError(this.content_elem);
this.content_id = this.content_elem.id;
if (!this.content_elem.hasAttribute("tabindex")) {
this.content_elem.setAttribute("tabindex", 0);
}
this.state.scroll_top = this.scroll_elem.scrollTop;
this.config.max_items = args.max_items;
this.#on_scroll_bound = this.#onScroll.bind(this);
}
// ==== PUBLIC FUNCTIONS ====
enable(state) {
// if no state is passed, we enable by default.
this.state.enabled = state !== false;
}
async setup() {
if (this.state.setup_has_run || !this.state.enabled) {
return;
}
this.#fixElementReferences();
// We use margins to control the scrollbar's size and float our content.
this.content_elem.style.marginTop = "0px";
this.content_elem.style.marginBottom = "0px";
await this.#insertToDOM();
this.#setupEvent("scroll", this.scroll_elem, this.#on_scroll_bound);
this.#setupElementObservers();
this.#setupResizeObservers();
this.state.setup_has_run = true;
}
clear() {
if (!this.state.setup_has_run || !this.state.enabled) {
return;
}
this.#html(this.#generateEmptyRow().join(""));
}
destroy() {
this.#teardownEvent("scroll", this.scroll_elem, this.#on_scroll_bound);
this.#teardownElementObservers();
this.#teardownResizeObservers();
this.#html(this.#generateEmptyRow().join(""));
this.state.setup_has_run = false;
}
async refresh(force) {
if (!this.state.setup_has_run || !this.state.enabled) {
return;
}
// Refresh can be a longer operation so we want to debounce it to
// avoid refreshing too often.
clearTimeout(this.#refresh_debounce_timer);
this.#refresh_debounce_timer = setTimeout(
async () => {
if (!isElement(this.content_elem.offsetParent)) {
return;
}
if (this.#recalculateDims() || force) {
await this.update()
}
},
REFRESH_DEBOUNCE_TIME_MS,
)
}
async update() {
if (!this.state.setup_has_run || !this.state.enabled) {
return;
}
this.state.scroll_top = this.scroll_elem.scrollTop;
// fixes #39
if (this.config.max_rows * this.config.item_height < this.state.scroll_top) {
this.scroll_elem.scrollTop = 0;
this.state.curr_cluster = 0;
}
await this.#insertToDOM();
}
getRowsAmount() {
return this.config.max_rows;
}
getScrollProgress() {
return this.state.scroll_top / (this.config.max_rows * this.config.item_height) * 100 || 0;
}
async setMaxItems(max_items) {
/** Sets the new max number of items.
*
* This is used to control the scroll bar's length.
*
* Returns whether the number of max items changed.
*/
if (!this.state.setup_has_run || !this.state.enabled) {
this.config.max_items = max_items;
return this.config.max_items !== max_items;
}
this.config.max_items = max_items;
}
// ==== PRIVATE FUNCTIONS ====
initDataDefaultCallback() {
return Promise.resolve({});
}
async initData() {
if (!this.state.enabled) {
return;
}
return await this.options.callbacks.initData();
}
fetchDataDefaultCallback() {
return Promise.resolve([]);
}
async fetchData(idx_start, idx_end) {
if (!this.state.enabled) {
return;
}
try {
return await this.options.callbacks.fetchData(idx_start, idx_end);
} catch (error) {
throw error;
}
}
sortDataDefaultCallback() {
return Promise.resolve();
}
async sortData() {
if (!this.state.setup_has_run || !this.state.enabled) {
return;
}
this.#fixElementReferences();
// Sort is applied to the filtered data.
await this.options.callbacks.sortData();
this.#recalculateDims();
await this.#insertToDOM();
}
filterDataDefaultCallback() {
return Promise.resolve(0);
}
async filterData() {
if (!this.state.setup_has_run || !this.state.enabled) {
return;
}
// Filter is applied to entire dataset.
const max_items = await this.options.callbacks.filterData();
await this.setMaxItems(max_items);
await this.refresh(true);
await this.sortData();
}
#exploreEnvironment(rows, cache) {
this.options.content_tag = this.content_elem.tagName.toLowerCase();
if (isNullOrUndefined(rows) || !rows.length) {
return;
}
// Temporarily add one row so that we can calculate row dimensions.
if (this.content_elem.children.length <= 1) {
cache.data = this.#html(rows[0]);
}
if (!this.options.tag) {
this.options.tag = this.content_elem.children[0].tagName.toLowerCase();
}
this.#recalculateDims();
}
#recalculateDims() {
const prev_config = JSON.stringify(this.config);
this.config.block_height = 0;
this.config.block_width = 0;
this.config.cluster_height = 0;
this.config.cluster_width = 0;
this.config.item_height = 0;
this.config.item_width = 0;
if (!this.config.max_items) {
return;
}
// Get the first element that isn't one of our placeholder rows.
const node = this.content_elem.querySelector(
`:scope > :not(.clusterize-extra-row,.${this.options.no_data_class})`
);
if (!isElement(node)) {
// dont attempt to compute dims if we have no data.
return;
}
const node_dims = getComputedDims(node);
this.config.item_height = node_dims.height;
this.config.item_width = node_dims.width;
// consider table's browser spacing
if (this.options.tag === "tr" && getComputedProperty(this.content_elem, "borderCollapse") !== "collapse") {
const spacing = parseInt(getComputedProperty(this.content_elem, "borderSpacing"), 10) || 0;
this.config.item_height += spacing;
this.config.item_width += spacing;
}
// Update rows in block to match the number of elements that can fit in the view.
const content_padding = getComputedPaddingDims(this.content_elem);
const column_gap = parseFloat(getComputedProperty(this.content_elem, "column-gap"));
const row_gap = parseFloat(getComputedProperty(this.content_elem, "row-gap"));
if (isNumber(column_gap)) {
this.config.item_width += column_gap;
}
if (isNumber(row_gap)) {
this.config.item_height += row_gap;
}
const inner_width = this.scroll_elem.clientWidth - content_padding.width;
const inner_height = this.scroll_elem.clientHeight - content_padding.height;
// Since we don't allow horizontal scrolling, we want to round down for columns.
const cols_in_block = Math.floor(inner_width / this.config.item_width);
// Round up for rows so that we don't cut rows off from the view.
const rows_in_block = Math.ceil(inner_height / this.config.item_height);
// Always need at least 1 row/col in block
this.config.cols_in_block = Math.max(1, cols_in_block);
this.config.rows_in_block = Math.max(1, rows_in_block);
this.config.block_height = this.config.item_height * this.config.rows_in_block;
this.config.block_width = this.config.item_width * this.config.cols_in_block;
this.config.rows_in_cluster = this.options.blocks_in_cluster * this.config.rows_in_block;
this.config.cluster_height = this.options.blocks_in_cluster * this.config.block_height;
this.config.cluster_width = this.config.block_width;
this.config.max_rows = Math.ceil(this.config.max_items / this.config.cols_in_block);
this.config.max_clusters = Math.ceil(this.config.max_rows / this.config.rows_in_cluster);
return prev_config !== JSON.stringify(this.config);
}
#generateEmptyRow({is_error}={}) {
const row = document.createElement(is_error ? "div" : this.options.tag);
row.className = is_error ? this.options.error_class : this.options.no_data_class;
if (this.options.tag === "tr") {
const td = document.createElement("td");
td.colSpan = 100;
td.innerHTML = is_error ? this.options.error_html : this.options.no_data_html;
row.appendChild(td);
} else {
row.innerHTML = is_error ? this.options.error_html : this.options.no_data_html;
}
return [row.outerHTML];
}
#getClusterNum() {
this.state.scroll_top = this.scroll_elem.scrollTop;
const cluster_divider = this.config.cluster_height - this.config.block_height;
const current_cluster = Math.floor(this.state.scroll_top / cluster_divider);
return Math.min(current_cluster, this.config.max_clusters);
}
async #generate() {
const rows_start = Math.max(0, (this.config.rows_in_cluster - this.config.rows_in_block) * this.#getClusterNum());
const rows_end = rows_start + this.config.rows_in_cluster;
const top_offset = Math.max(0, rows_start * this.config.item_height);
const bottom_offset = Math.max(0, (this.config.max_rows - rows_end) * this.config.item_height);
const rows_above = top_offset < 1 ? rows_start + 1 : rows_start;
const idx_start = Math.max(0, rows_start * this.config.cols_in_block);
const idx_end = Math.min(this.config.max_items, rows_end * this.config.cols_in_block);
let this_cluster_rows = await this.fetchData(idx_start, idx_end);
if (!Array.isArray(this_cluster_rows) || !this_cluster_rows.length) {
console.error(`Failed to fetch data for idx range (${idx_start},${idx_end})`);
this_cluster_rows = [];
}
if (this_cluster_rows.length < this.config.rows_in_block) {
return {
top_offset: 0,
bottom_offset: 0,
rows_above: 0,
rows: this_cluster_rows.length ? this_cluster_rows : this.#generateEmptyRow({is_error: true}),
};
}
return {
top_offset: top_offset,
bottom_offset: bottom_offset,
rows_above: rows_above,
rows: this_cluster_rows,
};
}
async #insertToDOM() {
if (!this.config.cluster_height || !this.config.cluster_width) {
// We need to fetch a single item so that we can calculate the dimensions
// for our list.
const rows = await this.fetchData(0, 1);
if (!Array.isArray(rows) || !rows.length) {
// This implies there is no data for this list. Not an error.
// Errors should be handled in the fetchData callback, not here.
this.#html(this.#generateEmptyRow().join(""));
return;
} else {
this.#html(rows.join(""));
this.#exploreEnvironment(rows, this.state.cache);
// Remove the temporary item from the data since we calculated its size.
this.#html(this.#generateEmptyRow().join(""));
}
}
const data = await this.#generate();
let this_cluster_rows = [];
for (let i = 0; i < data.rows.length; i += this.config.cols_in_block) {
const new_row = data.rows.slice(i, i + this.config.cols_in_block).join("");
this_cluster_rows.push(new_row);
}
this_cluster_rows = this_cluster_rows.join("");
const this_cluster_content_changed = this.#checkChanges("data", this_cluster_rows, this.state.cache);
const top_offset_changed = this.#checkChanges("top", data.top_offset, this.state.cache);
const only_bottom_offset_changed = this.#checkChanges("bottom", data.bottom_offset, this.state.cache);
const layout = [];
if (this_cluster_content_changed || top_offset_changed) {
if (this.options.callbacks.clusterWillChange) {
this.options.callbacks.clusterWillChange();
}
if (data.top_offset && this.options.keep_parity) {
layout.push(this.#renderExtraTag("keep-parity"));
}
layout.push(this_cluster_rows)
this.#html(layout.join(""));
if (this.options.content_tag === "ol") {
this.content_elem.setAttribute("start", data.rows_above);
}
this.content_elem.style["counter-increment"] = `clusterize-counter ${data.rows_above - 1}`;
if (this.options.callbacks.clusterChanged) {
this.options.callbacks.clusterChanged();
}
}
// Update the margins to fix scrollbar position.
this.content_elem.style.marginTop = `${data.top_offset}px`;
this.content_elem.style.marginBottom = `${data.bottom_offset}px`;
}
#html(data) {
this.content_elem.innerHTML = data;
// Parse items flagged as containing Shadow DOM entries.
convertElementShadowDOM(this.content_elem, "[data-parse-as-shadow-dom]");
return this.content_elem.innerHTML;
}
#renderExtraTag(class_name, height) {
const tag = document.createElement(this.options.tag);
const clusterize_prefix = "clusterize-";
tag.className = [
`${clusterize_prefix}extra-row`,
`${clusterize_prefix}${class_name}`,
].join(" ");
if (isNumber(height)) {
tag.style.height = `${height}px`;
}
return tag.outerHTML;
}
#getChildNodes(tag) {
const child_nodes = tag.children;
const nodes = [];
for (let i = 0, j = child_nodes.length; i < j; i++) {
nodes.push(child_nodes[i]);
}
return nodes;
}
#checkChanges(type, value, cache) {
const changed = value !== cache[type];
cache[type] = value;
return changed;
}
// ==== EVENT HANDLERS ====
async #onScroll() {
if (this.config.is_mac) {
if (!this.state.pointer_events_set) {
this.content_elem.style.pointerEvents = "none";
this.state.pointer_events_set = true;
clearTimeout(this.#scroll_debounce_timer);
this.#scroll_debounce_timer = setTimeout(() => {
this.content_elem.style.pointerEvents = "auto";
this.state.pointer_events_set = false;
}, SCROLL_DEBOUNCE_TIME_MS);
}
}
if (this.state.curr_cluster !== (this.state.curr_cluster = this.#getClusterNum())) {
await this.#insertToDOM();
}
if (this.options.callbacks.scrollingProgress) {
this.options.callbacks.scrollingProgress(this.getScrollProgress());
}
}
async #onResize() {
await this.refresh();
}
#fixElementReferences() {
if (!isElement(this.scroll_elem) || !isElement(this.content_elem)) {
return;
}
// Element is already in DOM. Don't need to do anything.
if (isElement(this.content_elem.offsetParent)) {
return;
}
// If association for elements is broken, replace them with instance version.
document.getElementById(this.scroll_id).replaceWith(this.scroll_elem);
}
#setupElementObservers() {
/** Listens for changes to the scroll and content elements.
*
* During testing, the scroll/content elements would frequently get removed from
* the DOM. This instance stores a reference to these elements
* which breaks whenever these elements are removed from the DOM. To fix this,
* we need to check for these changes and re-attach our stores elements by
* replacing the ones in the DOM with the ones in our clusterize instance.
*/
this.#element_observer = new MutationObserver((mutations) => {
const scroll_elem = document.getElementById(this.scroll_id);
if (isElement(scroll_elem) && scroll_elem !== this.scroll_elem) {
clearTimeout(this.#element_observer_timer);
this.#element_observer_timer = setTimeout(
this.#fixElementReferences,
ELEMENT_OBSERVER_DEBOUNCE_TIME_MS,
);
}
const content_elem = document.getElementById(this.content_id);
if (isElement(content_elem) && content_elem !== this.content_elem) {
clearTimeout(this.#element_observer_timer);
this.#element_observer_timer = setTimeout(
this.#fixElementReferences,
ELEMENT_OBSERVER_DEBOUNCE_TIME_MS,
);
}
});
const options = { subtree: true, childList: true, attributes: true };
this.#element_observer.observe(document, options);
}
#teardownElementObservers() {
if (!isNullOrUndefined(this.#element_observer)) {
this.#element_observer.takeRecords();
this.#element_observer.disconnect();
}
this.#element_observer = null;
}
#setupResizeObservers() {
/** Handles any updates to the size of both the Scroll and Content elements. */
this.#resize_observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target.id === this.scroll_id || entry.target.id === this.content_id) {
// debounce the event
clearTimeout(this.#resize_observer_timer);
this.#resize_observer_timer = setTimeout(
() => this.#onResize(),
RESIZE_OBSERVER_DEBOUNCE_TIME_MS,
);
}
}
});
this.#resize_observer.observe(this.scroll_elem);
this.#resize_observer.observe(this.content_elem);
}
#teardownResizeObservers() {
if (!isNullOrUndefined(this.#resize_observer)) {
this.#resize_observer.disconnect();
}
if (!isNullOrUndefined(this.#resize_observer_timer)) {
clearTimeout(this.#resize_observer_timer);
}
this.#resize_observer = null;
this.#resize_observer_timer = null;
}
// ==== HELPER FUNCTIONS ====
#setupEvent(type, elem, listener) {
if (elem.addEventListener) {
return elem.addEventListener(type, listener, false);
} else {
return elem.attachEvent(`on${type}`, listener);
}
}
#teardownEvent(type, elem, listener) {
if (elem.removeEventListener) {
return elem.removeEventListener(type, listener, false);
} else {
return elem.detachEvent(`on${type}`, listener);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,667 @@
// Prevent eslint errors on functions defined in other files.
/*global
Clusterize,
getValueThrowError,
INT_COLLATOR,
STR_COLLATOR,
LRUCache,
isString,
isNullOrUndefined,
isNullOrUndefinedLogError,
isElement,
isElementLogError,
keyExistsLogError,
htmlStringToElement,
convertElementShadowDOM,
*/
/*eslint no-undef: "error"*/
// number of list html items to store in cache.
const EXTRA_NETWORKS_CLUSTERIZE_LRU_CACHE_SIZE = 1000;
class NotImplementedError extends Error {
constructor(...params) {
super(...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, NotImplementedError);
}
this.name = "NotImplementedError";
}
}
class ExtraNetworksClusterize extends Clusterize {
data_obj = {};
data_obj_keys_sorted = [];
lru = null;
sort_reverse = false;
default_sort_fn = this.sortByDivId;
sort_fn = this.default_sort_fn;
tabname = "";
extra_networks_tabname = "";
initial_load = false;
// Override base class defaults
default_sort_mode_str = "divId";
default_sort_dir_str = "ascending";
default_filter_str = "";
sort_mode_str = this.default_sort_mode_str;
sort_dir_str = this.default_sort_dir_str;
filter_str = this.default_filter_str;
directory_filters = {};
constructor(args) {
super(args);
this.tabname = getValueThrowError(args, "tabname");
this.extra_networks_tabname = getValueThrowError(args, "extra_networks_tabname");
}
sortByDivId(data) {
/** Sort data_obj keys (div_id) as numbers. */
return Object.keys(data).sort(INT_COLLATOR.compare);
}
async reinitData() {
await this.initData();
// can't use super class' sort since it relies on setup being run first.
// but we do need to make sure to sort the new data before continuing.
const max_items = Object.keys(this.data_obj).filter(k => this.data_obj[k].visible).length;
await this.setMaxItems(max_items);
await this.refresh(true);
await this.options.callbacks.sortData();
}
async setup() {
if (this.state.setup_has_run || !this.state.enabled) {
return;
}
if (this.lru instanceof LRUCache) {
this.lru.clear();
} else {
this.lru = new LRUCache(EXTRA_NETWORKS_CLUSTERIZE_LRU_CACHE_SIZE);
}
await this.reinitData();
if (this.state.enabled) {
await super.setup();
}
}
destroy() {
this.initial_load = false;
this.data_obj = {};
this.data_obj_keys_sorted = [];
if (this.lru instanceof LRUCache) {
this.lru.destroy();
this.lru = null;
}
super.destroy();
}
clear() {
this.initial_load = false;
this.data_obj = {};
this.data_obj_keys_sorted = [];
if (this.lru instanceof LRUCache) {
this.lru.clear();
}
super.clear();
}
async load(force_init_data) {
if (!this.state.enabled) {
return;
}
this.initial_load = true;
if (!this.state.setup_has_run) {
await this.setup();
} else if (force_init_data) {
await this.reinitData();
} else {
await this.refresh();
}
}
setSortMode(sort_mode_str) {
if (this.sort_mode_str === sort_mode_str) {
return;
}
this.sort_mode_str = sort_mode_str;
this.sortData();
}
setSortDir(sort_dir_str) {
const reverse = (sort_dir_str === "descending");
if (this.sort_reverse === reverse) {
return;
}
this.sort_dir_str = sort_dir_str;
this.sort_reverse = reverse;
this.sortData();
}
setFilterStr(filter_str) {
if (isString(filter_str) && this.filter_str !== filter_str) {
this.filter_str = filter_str;
} else if (isNullOrUndefined(filter_str)) {
this.filter_str = this.default_filter_str;
}
this.filterData();
}
setDirectoryFilters(filters) {
if (isNullOrUndefined(filters)) {
this.directory_filters = {};
return;
}
this.directory_filters = JSON.parse(JSON.stringify(filters));
}
addDirectoryFilter(div_id, filter_str, recurse) {
this.directory_filters[div_id] = {filter_str: filter_str, recurse: recurse};
}
removeDirectoryFilter(div_id) {
delete this.directory_filters[div_id];
}
clearDirectoryFilters({excluded_div_ids} = {}) {
if (isString(excluded_div_ids)) {
excluded_div_ids = [excluded_div_ids];
}
if (!Array.isArray(excluded_div_ids)) {
excluded_div_ids = [];
}
for (const div_id of Object.keys(this.directory_filters)) {
if (excluded_div_ids.includes(div_id)) {
continue;
}
delete this.directory_filters[div_id];
}
}
getDirectoryFilters() {
return this.directory_filters;
}
async initDataDefaultCallback() {
throw new NotImplementedError();
}
idxRangeToDivIds(idx_start, idx_end) {
return this.data_obj_keys_sorted.slice(idx_start, idx_end);
}
async fetchDivIds(div_ids) {
if (isNullOrUndefinedLogError(this.lru)) {
return [];
}
if (Object.keys(this.data_obj).length === 0) {
return [];
}
const lru_keys = Array.from(this.lru.cache.keys());
const cached_div_ids = div_ids.filter(x => lru_keys.includes(x));
const missing_div_ids = div_ids.filter(x => !lru_keys.includes(x));
const data = {};
// Fetch any div IDs not in the LRU Cache using our callback.
if (missing_div_ids.length !== 0) {
const fetched_data = await this.options.callbacks.fetchData(missing_div_ids);
if (Object.keys(fetched_data).length !== missing_div_ids.length) {
// expected data. got nothing.
return {};
}
Object.assign(data, fetched_data);
}
// Now load any cached IDs from the LRU Cache
for (const div_id of cached_div_ids) {
if (!keyExistsLogError(this.data_obj, div_id)) {
continue;
}
if (this.data_obj[div_id].visible) {
data[div_id] = this.lru.get(div_id);
}
}
return data;
}
async fetchDataDefaultCallback() {
throw new NotImplementedError();
}
async sortDataDefaultCallback() {
// we want to apply the sort to the visible items only.
const filtered = Object.fromEntries(
Object.entries(this.data_obj).filter(([k, v]) => v.visible)
);
this.data_obj_keys_sorted = this.sort_fn(filtered);
if (this.sort_reverse) {
this.data_obj_keys_sorted = this.data_obj_keys_sorted.reverse();
}
}
async filterDataDefaultCallback() {
throw new NotImplementedError();
}
updateHtml(elem, new_html) {
const existing = this.lru.get(String(elem.dataset.divId));
if (new_html) {
if (existing === new_html) {
return;
}
const parsed_html = htmlStringToElement(new_html);
convertElementShadowDOM(parsed_html, "[data-parse-as-shadow-dom]");
// replace the element in DOM with our new element
elem.replaceWith(parsed_html);
// update the internal cache with the new html
this.lru.set(String(elem.dataset.divId), new_html);
} else {
if (existing === elem.outerHTML) {
return;
}
this.lru.set(String(elem.dataset.divId), elem.outerHTML);
}
}
}
class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
prev_selected_div_id = null;
constructor(args) {
super({...args});
this.selected_div_ids = new Set();
}
clear() {
this.prev_selected_div_id = null;
this.selected_div_ids.clear();
super.clear();
}
setRowSelected(elem) {
if (!isElement(elem)) {
return;
}
this.updateHtml(elem);
this.selected_div_ids.add(elem.dataset.divId);
this.prev_selected_div_id = elem.dataset.divId;
}
setRowDeselected(elem) {
if (!isElement(elem)) {
return;
}
this.updateHtml(elem);
this.selected_div_ids.delete(elem.dataset.divId);
this.prev_selected_div_id = null;
}
clearSelectedRows({excluded_div_ids} = {}) {
if (isString(excluded_div_ids)) {
excluded_div_ids = [excluded_div_ids];
}
if (!Array.isArray(excluded_div_ids)) {
excluded_div_ids = [];
}
this.selected_div_ids.clear();
for (const div_id of excluded_div_ids) {
this.selected_div_ids.add(div_id);
}
if (!excluded_div_ids.includes(this.prev_selected_div_id)) {
this.prev_selected_div_id = null;
}
}
getMaxRowWidth() {
/** Calculates the width of the widest row in the list. */
if (!this.state.enabled) {
// Inactive list is not displayed on screen. Can't calculate size.
return;
}
if (this.content_elem.children.length === 0) {
// If there is no data then just skip.
return;
}
let max_width = 0;
for (let i = 0; i < this.content_elem.children.length; i += this.config.cols_in_block) {
let row_width = 0;
for (let j = 0; j < this.config.cols_in_block; j++) {
const child = this.content_elem.children[i + j];
if (!(child.classList.contains("tree-list-item"))) {
continue;
}
// Child first element is the indent div. Just use offset for this
// since we do some overlapping with ::after in CSS.
row_width += child.children[0].offsetWidth;
// Button is second element. We want entire scroll width of this one.
// But first we need to allow it to shrink to content.
const prev_css_text = child.children[1].cssText;
child.children[1].style.flex = "0 1 auto";
row_width += child.children[1].scrollWidth;
// Add the button label's overflow to the width.
const lbl = child.querySelector(".tree-list-item-label");
row_width += lbl.scrollWidth - lbl.offsetWidth;
// Revert changes to element style.
if (!prev_css_text) {
child.children[1].removeAttribute("style");
} else {
child.children[1].cssText = prev_css_text;
}
}
max_width = Math.max(row_width, max_width);
}
if (max_width <= 0) {
return;
}
// Adds the scroll_elem's scrollbar and padding to the result.
// If scrollbar isn't visible, then only the element border/padding is added.
max_width += this.scroll_elem.offsetWidth - this.content_elem.offsetWidth;
return max_width;
}
async expandAllRows(div_id) {
/** Recursively expands all directories below the passed div_id. */
if (!keyExistsLogError(this.data_obj, div_id)) {
return;
}
const _expand = (parent_id) => {
const this_obj = this.data_obj[parent_id];
this_obj.visible = true;
this_obj.expanded = true;
for (const child_id of this_obj.children) {
_expand(child_id);
}
};
this.data_obj[div_id].expanded = true;
for (const child_id of this.data_obj[div_id].children) {
_expand(child_id);
}
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
await this.setMaxItems(new_len);
await this.refresh(true);
await this.sortData();
}
async collapseAllRows(div_id) {
/** Recursively collapses all directories below the passed div_id. */
if (!keyExistsLogError(this.data_obj, div_id)) {
return;
}
const _collapse = (parent_id) => {
const this_obj = this.data_obj[parent_id];
this_obj.visible = false;
this_obj.expanded = false;
for (const child_id of this_obj.children) {
_collapse(child_id);
}
};
this.data_obj[div_id].expanded = false;
for (const child_id of this.data_obj[div_id].children) {
_collapse(child_id);
}
// Deselect current selected div id if it was just hidden.
if (this.selected_div_ids.has(div_id) && !this.data_obj[div_id].visible) {
this.selected_div_ids.delete(div_id);
if (this.prev_selected_div_id === div_id) {
this.prev_selected_div_id = null;
}
}
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
await this.setMaxItems(new_len);
await this.refresh(true);
await this.sortData();
}
getChildrenDivIds(div_id, {recurse} = {}) {
const res = JSON.parse(JSON.stringify(this.data_obj[div_id].children));
if (recurse === true) {
for (const child_id of this.data_obj[div_id].children) {
res.push(...this.getChildrenDivIds(child_id, {recurse: recurse}));
}
}
return res;
}
async toggleRowExpanded(div_id) {
/** Toggles a row between expanded and collapses states. */
if (!keyExistsLogError(this.data_obj, div_id)) {
return;
}
// Toggle state
this.data_obj[div_id].expanded = !this.data_obj[div_id].expanded;
const _set_visibility = (parent_id, visible) => {
const this_obj = this.data_obj[parent_id];
this_obj.visible = visible;
for (const child_id of this_obj.children) {
_set_visibility(child_id, visible && this_obj.expanded);
}
};
for (const child_id of this.data_obj[div_id].children) {
_set_visibility(child_id, this.data_obj[div_id].expanded);
}
// Deselect current selected div id if it was just hidden.
if (this.selected_div_ids.has(div_id) && !this.data_obj[div_id].visible) {
this.selected_div_ids.delete(div_id);
if (this.prev_selected_div_id === div_id) {
this.prev_selected_div_id = null;
}
}
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
await this.setMaxItems(new_len);
await this.refresh(true);
await this.sortData();
}
async initData() {
/*Expects an object like the following:
{
parent: null or div_id,
children: array of div_id's,
visible: bool,
expanded: bool,
}
*/
this.data_obj = await this.options.callbacks.initData();
}
async fetchData(idx_start, idx_end) {
if (!this.state.enabled) {
return [];
}
if (Object.keys(this.data_obj).length === 0) {
return [];
}
const data = await this.fetchDivIds(this.idxRangeToDivIds(idx_start, idx_end));
const data_ids_sorted = Object.keys(data).sort((a, b) => {
return this.data_obj_keys_sorted.indexOf(a) - this.data_obj_keys_sorted.indexOf(b);
});
const res = [];
for (const div_id of data_ids_sorted) {
if (!keyExistsLogError(this.data_obj, div_id)) {
continue;
}
const html_str = data[div_id];
const elem = isElement(html_str) ? html_str : htmlStringToElement(html_str);
// Roots come expanded by default. Need to delete if it exists.
delete elem.dataset.expanded;
if (this.data_obj[div_id].expanded) {
elem.dataset.expanded = "";
}
delete elem.dataset.selected;
if (this.selected_div_ids.has(div_id)) {
elem.dataset.selected = "";
}
this.lru.set(String(div_id), elem.outerHTML);
res.push(elem.outerHTML);
}
return res;
}
async filterDataDefaultCallback() {
// just return the number of visible objects in our data.
return Object.values(this.data_obj).filter(v => v.visible).length;
}
}
class ExtraNetworksClusterizeCardList extends ExtraNetworksClusterize {
constructor(args) {
super({...args});
}
sortByPath(data) {
return Object.keys(data).sort((a, b) => {
return INT_COLLATOR.compare(data[a].sort_path, data[b].sort_path);
});
}
sortByName(data) {
return Object.keys(data).sort((a, b) => {
return INT_COLLATOR.compare(data[a].sort_name, data[b].sort_name);
});
}
sortByDateCreated(data) {
return Object.keys(data).sort((a, b) => {
return INT_COLLATOR.compare(data[a].sort_date_created, data[b].sort_date_created);
});
}
sortByDateModified(data) {
return Object.keys(data).sort((a, b) => {
return INT_COLLATOR.compare(data[a].sort_date_modified, data[b].sort_date_modified);
});
}
async initData() {
/*Expects an object like the following:
{
search_keys: array of strings,
search_only: bool,
sort_<mode>: string, (for various sort modes)
}
*/
this.data_obj = await this.options.callbacks.initData();
}
async fetchData(idx_start, idx_end) {
if (!this.state.enabled) {
return [];
}
const data = await this.fetchDivIds(this.idxRangeToDivIds(idx_start, idx_end));
const data_ids_sorted = Object.keys(data).sort((a, b) => {
return this.data_obj_keys_sorted.indexOf(a) - this.data_obj_keys_sorted.indexOf(b);
});
const res = [];
for (const div_id of data_ids_sorted) {
res.push(data[div_id]);
this.lru.set(div_id, data[div_id]);
}
return res;
}
async sortData() {
switch (this.sort_mode_str) {
case "name":
this.sort_fn = this.sortByName;
break;
case "path":
this.sort_fn = this.sortByPath;
break;
case "date_created":
this.sort_fn = this.sortByDateCreated;
break;
case "date_modified":
this.sort_fn = this.sortByDateModified;
break;
default:
this.sort_fn = this.default_sort_fn;
break;
}
await super.sortData();
}
async filterDataDefaultCallback() {
/** Filters data by a string and returns number of items after filter. */
let n_visible = 0;
for (const [div_id, v] of Object.entries(this.data_obj)) {
let visible = true;
// Apply the directory filters.
if (!Object.keys(this.directory_filters).length) {
v.visible = true;
} else {
v.visible = Object.values(this.directory_filters).some((filter) => {
if (filter.recurse) {
return v.rel_parent_dir.startsWith(filter.filter_str);
} else {
return v.rel_parent_dir === filter.filter_str;
}
});
}
if (!v.visible) {
continue;
}
// Narrow the filtered items based on the search string.
// Custom filter for items marked search_only=true.
if (v.search_only) {
if (Object.keys(this.directory_filters).length || this.filter_str.length >= 4) {
visible = v.search_terms.toLowerCase().indexOf(this.filter_str.toLowerCase()) !== -1;
} else {
visible = false;
}
} else {
// All other filters treated case insensitive.
visible = v.search_terms.toLowerCase().indexOf(this.filter_str.toLowerCase()) !== -1;
}
this.data_obj[div_id].visible = visible;
if (visible) {
n_visible++;
}
}
return n_visible;
}
}

59
javascript/lru_cache.js Normal file
View file

@ -0,0 +1,59 @@
// Prevent eslint errors on functions defined in other files.
/*global
isNumberThrowError,
isNullOrUndefined,
*/
/*eslint no-undef: "error"*/
const LRU_CACHE_MAX_ITEMS_DEFAULT = 250;
class LRUCache {
/** Least Recently Used cache implementation.
*
* Source: https://stackoverflow.com/a/46432113
*/
constructor(max = LRU_CACHE_MAX_ITEMS_DEFAULT) {
isNumberThrowError(max);
this.max = max;
this.cache = new Map();
}
clear() {
this.cache.clear();
}
destroy() {
this.clear();
this.cache = null;
}
size() {
return this.cache.size;
}
get(key) {
let item = this.cache.get(key);
if (!isNullOrUndefined(item)) {
this.cache.delete(key);
this.cache.set(key, item);
}
return item;
}
set(key, val) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size === this.max) {
this.cache.delete(this.first());
}
this.cache.set(key, val);
}
has(key) {
return this.cache.has(key);
}
first() {
return this.cache.keys().next().value;
}
}

1300
javascript/resizeGrid.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,178 @@
<html>
<head>
<style>
body {
background: #333333;
margin: 50px;
}
.my-content {
display: block;
width: 100%;
height: 100%;
}
#cell_0_0 {
background: #ffb3ba;
}
#cell_0_1 {
background: #ffdfba;
}
#cell_1_0 {
background: #baffc9;
}
#cell_1_1 {
background: #bae1ff;
}
.hidden {
display: none !important;
}
body.resizing {
cursor: col-resize !important;
}
body.resizing * {
pointer-events: none !important;
}
body.resizing .resize-grid--handle {
pointer-events: initial !important;
}
body.resizing.resize-grid-col {
cursor: col-resize !important;
}
body.resizing.resize-grid-row {
cursor: row-resize !important;
}
.container {
position: relative;
display: flex;
width: 50vw;
height: 70vh;
overflow: hidden;
border: 1px solid white;
padding: 10px;
min-width: 0;
min-height: 0;
resize: both;
}
.resize-grid,
.resize-grid--row,
.resize-grid--col,
.resize-grid--cell {
display: flex;
flex-wrap: nowrap;
overflow: hidden;
gap: 0 !important;
min-width: 0;
min-height: 0;
}
.resize-grid {
width: 100%;
height: 100%;
}
.resize-grid--row {
flex-direction: row;
}
.resize-grid--col {
flex-direction: column;
}
.resize-grid--handle {
position: relative;
height: 100%;
width: 100%;
z-index: 0;
background: transparent;
}
.resize-grid--handle::after {
content: "";
position: absolute;
z-index: -1;
}
.resize-grid--row-handle {
cursor: row-resize;
}
.resize-grid--row-handle::after {
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
border-top: 1px dashed white;
}
.resize-grid--col-handle {
cursor: col-resize;
}
.resize-grid--col-handle::after {
top: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
border-left: 1px dashed white;
}
</style>
</head>
<body>
<div class="container">
<div class="resize-grid">
<div class="resize-grid--row" style="flex: 1 0 50px;" data-min-size="25px">
<div class="resize-grid--cell" style="flex: 0 0 50px;" data-min-size="25px">
<div id="cell_0_0" class="my-content">0</div>
</div>
<div class="resize-grid--cell" style="flex: 1 0 50px;" data-min-size="25px">
<div id="cell_0_1" class="my-content">1</div>
</div>
</div>
<div class="resize-grid--row" style="flex: 0 0 50px;" data-min-size="25px">
<div class="resize-grid--cell" style="flex: 1 0 50px;" data-min-size="25px">
<div id="cell_1_0" class="my-content">2</div>
</div>
<div class="resize-grid--cell" style="flex: 0 0 50px;" data-min-size="25px">
<div id="cell_1_1" class="my-content">3</div>
</div>
</div>
</div>
</div>
<button class="toggle-button" data-grid-elem-id="cell_0_0">HIDE cell_0_0</button>
<button class="toggle-button" data-grid-elem-id="cell_0_1">HIDE cell_0_1</button>
<button class="toggle-button" data-grid-elem-id="cell_1_0">HIDE cell_1_0</button>
<button class="toggle-button" data-grid-elem-id="cell_1_1">HIDE cell_1_1</button>
<script type="text/javascript" src="utils.js"></script>
<script type="text/javascript" src="resizeGrid.js"></script>
<script type="text/javascript">
const grid_elem = document.querySelector(".resize-grid");
const grid = resizeGridSetup(grid_elem);
const btns = document.querySelectorAll(".toggle-button");
btns.forEach(btn => {
btn.addEventListener("click", onToggleButton);
});
function onToggleButton(event) {
const btn = event.target.closest("button");
const id = btn.dataset.gridElemId;
const grid_elem = document.querySelector(`#${id}`);
grid.toggle({ elem: grid_elem });
btn.textContent = btn.textContent === `HIDE ${id}` ? `SHOW ${id}` : `HIDE ${id}`;
}
</script>
</body>
</html>

View file

@ -1,8 +1,8 @@
(function() {
const GRADIO_MIN_WIDTH = 320;
const PAD = 16;
const DEBOUNCE_TIME = 100;
const DOUBLE_TAP_DELAY = 200; //ms
const DEBOUNCE_TIME_MS = 250;
const DOUBLE_TAP_DELAY_MS = 250;
const R = {
tracking: false,
@ -18,7 +18,7 @@
let parents = [];
function setLeftColGridTemplate(el, width) {
el.style.gridTemplateColumns = `${width}px 16px 1fr`;
el.style.gridTemplateColumns = `${width}px ${PAD}px 1fr`;
}
function displayResizeHandle(parent) {
@ -58,6 +58,9 @@
evt.stopPropagation();
parent.style.gridTemplateColumns = parent.style.originalGridTemplateColumns;
// Fire a custom event so user can perform additional tasks on double click.
parent.dispatchEvent(new CustomEvent("resizeHandleDblClick", {bubbles: true}));
}
const leftCol = parent.firstElementChild;
@ -101,7 +104,7 @@
if (evt.changedTouches.length !== 1) return;
const currentTime = new Date().getTime();
if (R.lastTapTime && currentTime - R.lastTapTime <= DOUBLE_TAP_DELAY) {
if (R.lastTapTime && currentTime - R.lastTapTime <= DOUBLE_TAP_DELAY_MS) {
onDoubleClick(evt);
return;
}
@ -152,7 +155,13 @@
} else {
delta = R.screenX - evt.changedTouches[0].screenX;
}
const leftColWidth = Math.max(Math.min(R.leftColStartWidth - delta, R.parent.offsetWidth - R.parent.minRightColWidth - PAD), R.parent.minLeftColWidth);
const leftColWidth = Math.max(
Math.min(
R.leftColStartWidth - delta,
R.parent.offsetWidth - R.parent.minRightColWidth - PAD,
),
R.parent.minLeftColWidth,
);
setLeftColGridTemplate(R.parent, leftColWidth);
}
});
@ -173,6 +182,13 @@
R.tracking = false;
document.body.classList.remove('resizing');
// Fire a custom event at end of resizing.
R.parent.dispatchEvent(
new CustomEvent("resizeHandleResized", {
bubbles: true,
}),
);
}
});
});
@ -185,7 +201,7 @@
for (const parent of parents) {
afterResize(parent);
}
}, DEBOUNCE_TIME);
}, DEBOUNCE_TIME_MS);
});
setupResizeHandle = setup;

742
javascript/utils.js Normal file
View file

@ -0,0 +1,742 @@
/** Collators used for sorting. */
const INT_COLLATOR = new Intl.Collator([], {numeric: true});
const STR_COLLATOR = new Intl.Collator("en", {numeric: true, sensitivity: "base"});
/** Helper functions for checking types and simplifying logging/error handling. */
function isNumber(x) {
return typeof x === "number" && isFinite(x);
}
function isNumberLogError(x) {
if (isNumber(x)) {
return true;
}
console.error(`expected number, got: ${typeof x}`);
return false;
}
function isNumberThrowError(x) {
if (isNumber(x)) {
return;
}
throw new Error(`expected number, got: ${typeof x}`);
}
function isString(x) {
return typeof x === "string" || x instanceof String;
}
function isStringLogError(x) {
if (isString(x)) {
return true;
}
console.error(`expected string, got: ${typeof x}`);
return false;
}
function isStringThrowError(x) {
if (isString(x)) {
return;
}
throw new Error(`expected string, got: ${typeof x}`);
}
function isNull(x) {
return x === null;
}
function isUndefined(x) {
return typeof x === "undefined" || x === undefined;
}
// checks both null and undefined for simplicity sake.
function isNullOrUndefined(x) {
return isNull(x) || isUndefined(x);
}
function isNullOrUndefinedLogError(x) {
if (isNullOrUndefined(x)) {
console.error("Variable is null/undefined.");
return true;
}
return false;
}
function isNullOrUndefinedThrowError(x) {
if (!isNullOrUndefined(x)) {
return;
}
throw new Error("Variable is null/undefined.");
}
function isElement(x) {
return x instanceof Element;
}
function isElementLogError(x) {
if (isElement(x)) {
return true;
}
console.error(`expected element type, got: ${typeof x}`);
return false;
}
function isElementThrowError(x) {
if (isElement(x)) {
return;
}
throw new Error(`expected element type, got: ${typeof x}`);
}
function isFunction(x) {
return typeof x === "function";
}
function isFunctionLogError(x) {
if (isFunction(x)) {
return true;
}
console.error(`expected function type, got: ${typeof x}`);
return false;
}
function isFunctionThrowError(x) {
if (isFunction(x)) {
return;
}
throw new Error(`expected function type, got: ${typeof x}`);
}
function isObject(x) {
return typeof x === "object" && !Array.isArray(x);
}
function isObjectLogError(x) {
if (isObject(x)) {
return true;
}
console.error(`expected object type, got: ${typeof x}`);
return false;
}
function isObjectThrowError(x) {
if (isObject(x)) {
return;
}
throw new Error(`expected object type, got: ${typeof x}`);
}
function keyExists(obj, k) {
return isObject(obj) && isString(k) && k in obj;
}
function keyExistsLogError(obj, k) {
if (keyExists(obj, k)) {
return true;
}
console.error(`key does not exist in object: ${k}`);
return false;
}
function keyExistsThrowError(obj, k) {
if (keyExists(obj, k)) {
return;
}
throw new Error(`key does not exist in object: ${k}`);
}
function getValue(obj, k) {
/** Returns value of object for given key if it exists, otherwise returns null. */
if (keyExists(obj, k)) {
return obj[k];
}
return null;
}
function getValueLogError(obj, k) {
if (keyExistsLogError(obj, k)) {
return obj[k];
}
return null;
}
function getValueThrowError(obj, k) {
keyExistsThrowError(obj, k);
return obj[k];
}
function getElementByIdLogError(selector) {
const elem = gradioApp().getElementById(selector);
isElementLogError(elem);
return elem;
}
function getElementByIdThrowError(selector) {
const elem = gradioApp().getElementById(selector);
isElementThrowError(elem);
return elem;
}
function querySelectorLogError(selector) {
const elem = gradioApp().querySelector(selector);
isElementLogError(elem);
return elem;
}
function querySelectorThrowError(selector) {
const elem = gradioApp().querySelector(selector);
isElementThrowError(elem);
return elem;
}
const validateArrayType = (arr, type_check_fn) => {
/** Validates that a variable is an array with members of a specified type.
* `type_check_fn` must accept array elements as arguments and return whether
* they match the expected type.
* `arr` will be wrapped in an array if it is not already an array.
*/
isNullOrUndefinedThrowError(type_check_fn);
if (isNullOrUndefined(arr)) {
return [];
}
if (!Array.isArray(arr) && type_check_fn(arr)) {
return [arr];
} else if (Array.isArray(arr) && arr.every((x) => type_check_fn(x))) {
return arr;
} else {
throw new Error('Invalid array types:', arr);
}
};
/** Functions for getting dimensions of elements. */
function getStyle(elem) {
return window.getComputedStyle ? window.getComputedStyle(elem) : elem.currentStyle;
}
function getComputedProperty(elem, prop) {
return getStyle(elem)[prop];
}
function getComputedPropertyDims(elem, prop) {
/** Returns the top/left/bottom/right float dimensions of an element for the specified property. */
const style = getStyle(elem);
return {
top: parseFloat(style.getPropertyValue(`${prop}-top`)),
left: parseFloat(style.getPropertyValue(`${prop}-left`)),
bottom: parseFloat(style.getPropertyValue(`${prop}-bottom`)),
right: parseFloat(style.getPropertyValue(`${prop}-right`)),
};
}
function getComputedMarginDims(elem) {
/** Returns the width/height of the computed margin of an element. */
const dims = getComputedPropertyDims(elem, "margin");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
}
function getComputedPaddingDims(elem) {
/** Returns the width/height of the computed padding of an element. */
const dims = getComputedPropertyDims(elem, "padding");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
}
function getComputedBorderDims(elem) {
/** Returns the width/height of the computed border of an element. */
// computed border will always start with the pixel width so thankfully
// the parseFloat() conversion will just give us the width and ignore the rest.
// Otherwise we'd have to use border-<pos>-width instead.
const dims = getComputedPropertyDims(elem, "border");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
}
function getComputedDims(elem) {
/** Returns the full width and height of an element including its margin, padding, and border. */
const width = elem.scrollWidth;
const height = elem.scrollHeight;
const margin = getComputedMarginDims(elem);
const padding = getComputedPaddingDims(elem);
const border = getComputedBorderDims(elem);
return {
width: width + margin.width + padding.width + border.width,
height: height + margin.height + padding.height + border.height,
};
}
/** Functions for asynchronous operations. */
function waitForElement(selector, timeout_ms) {
/** Promise that waits for an element to exist in DOM. */
return new Promise((resolve, reject) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
return resolve(document.querySelector(selector));
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
if (isNumber(timeout_ms) && timeout_ms !== 0) {
setTimeout(() => {
observer.takeRecords();
observer.disconnect();
return reject(`timed out waiting for element: "${selector}"`);
}, timeout_ms);
}
});
}
function waitForBool(o, timeout_ms) {
/** Promise that waits for a boolean to be true.
*
* `o` must be an Object of the form:
* { state: <bool value> }
*
* If timeout_ms is null/undefined or 0, waits forever.
*
* Resolves when (state === true)
* Rejects when state is not True before timeout_ms.
*/
let wait_timer;
return new Promise((resolve, reject) => {
if (isNumber(timeout_ms) && timeout_ms !== 0) {
setTimeout(() => {
clearTimeout(wait_timer);
return reject("timed out waiting for bool");
}, timeout_ms);
}
(function _waitForBool() {
if (o.state) {
return resolve();
}
wait_timer = setTimeout(_waitForBool, 100);
})();
});
}
function waitForKeyInObject(o, timeout_ms) {
/** Promise that waits for a key to exist in an object.
*
* `o` must be an Object of the form:
* {
* obj: <object to watch for key>,
* k: <key to watch for>,
* }
*
* If timeout_ms is null/undefined or 0, waits forever.
*
* Resolves when (k in obj).
* Rejects when k is not found in obj before timeout_ms.
*/
let wait_timer;
return new Promise((resolve, reject) => {
if (isNumber(timeout_ms) && timeout_ms !== 0) {
setTimeout(() => {
clearTimeout(wait_timer);
return reject(`timed out waiting for key: ${o.k}`);
}, timeout_ms);
}
(function _waitForKeyInObject() {
if (o.k in o.obj) {
return resolve();
}
wait_timer = setTimeout(_waitForKeyInObject, 100);
})();
});
}
function waitForValueInObject(o, timeout_ms) {
/** Promise that waits for a key value pair in an Object.
*
* `o` must be an Object of the form:
* {
* obj: <object containing value>,
* k: <key in object>,
* v: <value at key for comparison>
* }
*
* If timeout_ms is null/undefined or 0, waits forever.
*
* Resolves when obj[k] == v
*/
let wait_timer;
return new Promise((resolve, reject) => {
if (isNumber(timeout_ms) && timeout_ms !== 0) {
setTimeout(() => {
clearTimeout(wait_timer);
return reject(`timed out waiting for value: ${o.k}: ${o.v}`);
}, timeout_ms);
}
waitForKeyInObject({k: o.k, obj: o.obj}, timeout_ms).then(() => {
(function _waitForValueInObject() {
if (o.k in o.obj && o.obj[o.k] == o.v) {
return resolve();
}
setTimeout(_waitForValueInObject, 100);
})();
}).catch((error) => {
return reject(error);
});
});
}
/** Requests */
class FetchError extends Error {
constructor(...args) {
super(...args);
this.name = this.constructor.name;
}
}
class Fetch4xxError extends FetchError {
constructor(...args) {
super(...args);
}
}
class Fetch5xxError extends FetchError {
constructor(...args) {
super(...args);
}
}
class FetchRetryLimitError extends FetchError {
constructor(...args) {
super(...args);
}
}
class FetchTimeoutError extends FetchError {
constructor(...args) {
super(...args);
}
}
class FetchWithRetryAndBackoffTimeoutError extends FetchError {
constructor(...args) {
super(...args);
}
}
async function fetchWithRetryAndBackoff(url, data, args = {}) {
/** Wrapper around `fetch` with retries, backoff, and timeout.
*
* Uses a Decorrelated jitter backoff strategy.
* https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
*
* Args:
* url: Primary URL to fetch.
* data: Data to append to the URL when making the request.
* args:
* method: The HTTP request method to use.
* timeout_ms: Max allowed time before this function fails.
* fetch_timeout_ms: Max allowed time for individual `fetch` calls.
* min_delay_ms: Min time that a delay between requests can be.
* max_delay_ms: Max time that a delay between reqeusts can be.
* response_handler: A callback function that returns a promise.
* This function is sent the response from the `fetch` call.
* If not specified, all status codes >= 400 are handled as errors.
* This is useful for handling requests whose responses from the server
* are erronious but the HTTP status is 200.
*/
args.method = args.method || "GET";
args.timeout_ms = args.timeout_ms || 30000;
args.min_delay_ms = args.min_delay_ms || 100;
args.max_delay_ms = args.max_delay_ms || 3000;
args.fetch_timeout_ms = args.fetch_timeout_ms || 10000;
// The default response handler function for `fetch` call responses.
const response_handler = (response) => new Promise((resolve, reject) => {
if (response.ok) {
return response.json().then(json => {
return resolve(json);
});
} else {
if (response.status >= 400 && response.status < 500) {
throw new Fetch4xxError("client error:", response);
}
if (response.status >= 500 && response.status < 600) {
throw new Fetch5xxError("server error:", response);
}
return reject(response);
}
});
args.response_handler = args.response_handler || response_handler;
const url_args = Object.entries(data).map(([k, v]) => {
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
}).join("&");
url = `${url}?${url_args}`;
let controller;
let retry = true;
const randrange = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
const get_jitter = (base, max, prev) => {
return Math.min(max, randrange(base, prev * 3));
};
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Timeout for individual `fetch` calls.
const fetch_timeout = (ms, promise) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return reject(new FetchTimeoutError("Fetch timed out."));
}, ms);
return promise.then(resolve, reject);
});
};
// Timeout for all retries.
const run_timeout = (ms, promise) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
retry = false;
controller.abort();
return reject(new FetchWithRetryAndBackoffTimeoutError("Request timed out."));
}, ms);
return promise.then(resolve, reject);
});
};
const run = async(delay_ms) => {
if (!retry) {
// Retry is controlled externally via `run_timeout`. This function's promise
// is also handled via that timeout so we can just return here.
return;
}
try {
controller = new AbortController();
const fetch_opts = {method: args.method, signal: controller.signal};
const response = await fetch_timeout(args.fetch_timeout_ms, fetch(url, fetch_opts));
return await args.response_handler(response);
} catch (error) {
controller.abort();
// dont bother with anything else if told to not retry.
if (!retry) {
return;
}
if (error instanceof Fetch4xxError) {
throw error;
}
if (error instanceof Fetch5xxError) {
throw error;
}
// Any other errors mean we need to retry the request.
delay_ms = get_jitter(args.min_delay_ms, args.max_delay_ms, delay_ms);
await delay(delay_ms);
return await run(delay_ms);
}
};
return await run_timeout(args.timeout_ms, run(args.min_delay_ms));
}
function requestGet(url, data, handler, errorHandler) {
var xhr = new XMLHttpRequest();
var args = Object.keys(data).map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]);
}).join('&');
xhr.open("GET", url + "?" + args, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var js = JSON.parse(xhr.responseText);
handler(js);
} catch (error) {
console.error(error);
errorHandler();
}
} else {
errorHandler();
}
}
};
var js = JSON.stringify(data);
xhr.send(js);
}
function requestGetPromise(url, data, timeout_ms) {
/**Asynchronous `GET` request that returns a promise.
*
* The result will be of the format {status: int, response: JSON object}.
* Thus, the xhr.responseText that we receive is expected to be a JSON string.
* Acceptable status codes for successful requests are 200 <= status < 300.
*/
if (!isNumber(timeout_ms)) {
timeout_ms = 1000;
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const args = Object.entries(data).map(([k, v]) => {
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
}).join("&");
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
return resolve({status: xhr.status, response: JSON.parse(xhr.responseText)});
} else {
return reject({status: xhr.status, response: JSON.parse(xhr.responseText)});
}
};
xhr.onerror = () => {
return reject({status: xhr.status, response: JSON.parse(xhr.responseText)});
};
xhr.ontimeout = () => {
return reject({status: 408, response: {detail: `Request timeout: ${url}`}});
};
const payload = JSON.stringify(data);
xhr.open("GET", `${url}?${args}`, true);
xhr.timeout = timeout_ms;
xhr.send(payload);
});
}
/** Misc helper functions. */
function clamp(x, min, max) {
return Math.max(min, Math.min(x, max));
}
function htmlStringToElement(s) {
/** Converts an HTML string into an Element type. */
let parser = new DOMParser();
let tmp = parser.parseFromString(s, "text/html");
return tmp.body.firstElementChild;
}
function htmlStringToFragment(s) {
/** Converts an HTML string into a DocumentFragment. */
return document.createRange().createContextualFragment(s);
}
function convertInnerHtmlToShadowDOM(elem) {
/** Inplace conversion of innerHTML of an element into a Shadow DOM.
*
* If the innerHTML is not valid HTML then the innerHTML is left unchanged.
*/
const parsed_str = new DOMParser().parseFromString(elem.innerHTML, "text/html").documentElement.textContent;
const parsed_elem = htmlStringToElement(parsed_str);
if (!isNullOrUndefined(parsed_elem)) {
elem.innerHTML = "";
const shadow = elem.attachShadow({mode: "open"});
shadow.appendChild(parsed_elem);
}
}
function convertElementShadowDOM(elem, selector) {
/** Inplace conversion of Shadow DOM of all children matching the passed selector.
*
* `selector` defaults to [data-parse-as-shadow-dom] if not a valid string.
*
* NOTE: Nested Shadow DOMs are untested but will likely not work.
*/
if (!isString(selector)) {
selector = "[data-parse-as-shadow-dom]";
}
let children = Array.from(elem.querySelectorAll(selector));
children = children.filter(x => x.innerHTML !== "");
for (const child of children) {
convertInnerHtmlToShadowDOM(child);
}
}
function toggleCss(key, css, enable) {
var style = document.getElementById(key);
if (enable && !style) {
style = document.createElement('style');
style.id = key;
style.type = 'text/css';
document.head.appendChild(style);
}
if (style && !enable) {
document.head.removeChild(style);
}
if (style) {
style.innerHTML == '';
style.appendChild(document.createTextNode(css));
}
}
function copyToClipboard(s) {
/** Copies the passed string to the clipboard. */
isStringThrowError(s);
navigator.clipboard.writeText(s);
}
function attrPromise({elem, attr, timeout_ms} = {}) {
timeout_ms = timeout_ms || 0;
return new Promise((resolve, reject) => {
let res = false;
const observer_config = {attributes: true, attributeOldValue: true};
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (isString(attr) && mutation.attributeName === attr) {
res = true;
observer.disconnect();
resolve(elem, elem.getAttribute(attr));
}
if (!isString(attr)) {
res = true;
observer.disconnect();
resolve(elem);
}
});
});
if (timeout_ms > 0) {
setTimeout(() => {
if (!res) {
reject(elem);
}
}, timeout_ms);
}
if (isString(attr)) {
observer_config.attributeFilter = [attr];
}
observer.observe(elem, observer_config);
});
}
function waitForVisible(elem, callback) {
new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
callback(elem);
observer.disconnect();
}
});
}).observe(elem);
if (!callback) return new Promise(resolve => callback = resolve);
}
function cssRelativeUnitToPx(css_value, target) {
// https://stackoverflow.com/a/66569574
// doesnt work on `%` unit.
target = target || document.body;
const units = {
px: x => x, // no conversion needed here
rem: x => x * parseFloat(getComputedStyle(document.documentElement).fontSize),
em: x => x * parseFloat(getComputedStyle(target).fontSize),
vw: x => x / 100 * window.innerWidth,
vh: x => x / 100 * window.innerHeight,
};
const re = new RegExp(`^([-+]?(?:\\d+(?:\\.\\d+)?))(${Object.keys(units).join('|')})$`, 'i');
const matches = css_value.toString().trim().match(re);
if (matches) {
const value = Number(matches[1]);
const unit = matches[2].toLocaleLowerCase();
if (unit in units) {
return units[unit](value);
}
}
return css_value;
}

View file

@ -273,9 +273,12 @@ options_templates.update(options_section(('interrogate', "Interrogate"), {
}))
options_templates.update(options_section(('extra_networks', "Extra Networks", "sd"), {
"extra_networks_show_hidden_directories": OptionInfo(True, "Show hidden directories").info("directory is hidden if its name starts with \".\"."),
"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_show_hidden_directories_buttons": OptionInfo(False, "Show buttons for hidden directories in the directory and tree views.").info("a directory is hidden if its name starts with a period (\".\").").needs_reload_ui(),
"extra_networks_show_hidden_models_cards": OptionInfo("Never", "Show cards for models in hidden directories", gr.Radio, {"choices": ["Always", "When searched", "Never"]}).info("\"When searched\" will only show cards when the search string has 4 characters or more and the search string matches either the model name or the hidden directory name (or any of its subdirectories).").needs_reload_ui(),
"extra_networks_show_hidden_models_in_tree_view": OptionInfo(False, "Show entries for models inside hidden directories in the tree view.").info("This option only applies if the \"Show buttons for hidden directories\" option is enabled.").needs_reload_ui(),
"extra_networks_tree_view_expand_depth_default": OptionInfo(0, "Expand all directories in the tree view up to this depth by default.").info("0 collapses all, -1 expands all.").needs_reload_ui(),
"extra_networks_directory_filter_click_behavior": OptionInfo("Click", "Filter directory recursively left mouse button action.", gr.Radio, {"choices": ["Click", "Long Press"]}).info("Sets the default left mouse button action required to filter a directory recursively (show children in all subdirectories) vs filtering to only show direct children of the selected directory."),
"extra_networks_card_details_click_behavior": OptionInfo("Click", "Show card details left mouse button action.", gr.Radio, {"choices": ["Click", "Long Press"]}).info("Sets the default left mouse button action when clicking a card."),
"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_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"),
@ -284,14 +287,21 @@ options_templates.update(options_section(('extra_networks', "Extra Networks", "s
"extra_networks_card_description_is_html": OptionInfo(False, "Treat card description as HTML"),
"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_style": OptionInfo("Dirs", "Extra Networks directory view style", gr.Radio, {"choices": ["Tree", "Dirs"]}).needs_reload_ui(),
"extra_networks_tree_view_default_enabled": OptionInfo(True, "Show the Extra Networks directory view by default").needs_reload_ui(),
"extra_networks_tree_view_default_width": OptionInfo(180, "Default width for the Extra Networks directory tree view", gr.Number).needs_reload_ui(),
"extra_networks_dirs_view_default_enabled": OptionInfo(True, "Show the Extra Networks directory view by default.").needs_reload_ui(),
"extra_networks_tree_view_default_enabled": OptionInfo(False, "Show the Extra Networks tree view by default").needs_reload_ui(),
"extra_networks_card_view_default_enabled": OptionInfo(True, "Show the Extra Networks card view by default").needs_reload_ui(),
"extra_networks_dets_view_default_enabled": OptionInfo(False, "Show the Extra Networks card details view by default").needs_reload_ui(),
"extra_networks_tree_view_show_files": OptionInfo(True, "Show files in tree view.").info("Disabling this option will remove file entries from the tree view and only show directories.").needs_reload_ui(),
"extra_networks_dirs_view_default_height": OptionInfo(90, "Default height for the Directory Buttons view", gr.Number).info("in pixels").needs_reload_ui(),
"extra_networks_tree_view_default_width": OptionInfo(180, "Default width for the Extra Networks directory tree view", gr.Number).info("in pixels").needs_reload_ui(),
"extra_networks_dets_view_default_width": OptionInfo(180, "Default width for the Extra Networks card details view", gr.Number).info("in pixels").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"),
"textual_inversion_add_hashes_to_infotext": OptionInfo(True, "Add Textual Inversion hashes to infotext"),
"sd_hypernetwork": OptionInfo("None", "Add hypernetwork to prompt", gr.Dropdown, lambda: {"choices": ["None", *shared.hypernetworks]}, refresh=shared_items.reload_hypernetworks),
"extra_networks_long_press_time_ms": OptionInfo(800, "Hold time required to register a long click").info("in milliseconds").info("default 800"),
"extra_networks_dbl_press_time_ms": OptionInfo(500, "Time between clicks to register a double click").info("in milliseconds").info("default 500"),
"textual_inversion_image_embedding_data_cache": OptionInfo(False, 'Cache the data of image embeddings').info('potentially increase TI load time at the cost some disk space'),
}))

View file

@ -318,7 +318,7 @@ def setup_dialog(button_show, dialog, *, button_close=None):
fn=lambda: gr.update(visible=True),
inputs=[],
outputs=[dialog],
).then(fn=None, _js="function(){ popupId('" + dialog.elem_id + "'); }")
).then(fn=None, _js=f"function(){{popupId('{dialog.elem_id}');}}")
if button_close:
button_close.click(fn=None, _js="closePopup")

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
import html
import os
from modules import shared, ui_extra_networks, sd_models
@ -13,6 +12,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage):
def refresh(self):
shared.refresh_checkpoints()
super().refresh()
def create_item(self, name, index=None, enable_filter=True):
checkpoint: sd_models.CheckpointInfo = sd_models.checkpoint_aliases.get(name)
@ -30,7 +30,6 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage):
"preview": self.find_preview(path),
"description": self.find_description(path),
"search_terms": search_terms,
"onclick": html.escape(f"return selectCheckpoint({ui_extra_networks.quote_js(name)})"),
"local_preview": f"{path}.{shared.opts.samples_format}",
"metadata": checkpoint.metadata,
"sort_keys": {'default': index, **self.get_sort_keys(checkpoint.filename)},

View file

@ -11,6 +11,7 @@ class ExtraNetworksPageHypernetworks(ui_extra_networks.ExtraNetworksPage):
def refresh(self):
shared.reload_hypernetworks()
super().refresh()
def create_item(self, name, index=None, enable_filter=True):
full_path = shared.hypernetworks.get(name)
@ -30,7 +31,7 @@ class ExtraNetworksPageHypernetworks(ui_extra_networks.ExtraNetworksPage):
"preview": self.find_preview(path),
"description": self.find_description(path),
"search_terms": search_terms,
"prompt": quote_js(f"<hypernet:{name}:") + " + opts.extra_networks_default_multiplier + " + quote_js(">"),
"prompt": quote_js(f"<hypernet:{name}:{shared.opts.extra_networks_default_multiplier}>"),
"local_preview": f"{path}.preview.{shared.opts.samples_format}",
"sort_keys": {'default': index, **self.get_sort_keys(path + ext)},
}

View file

@ -11,6 +11,7 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage):
def refresh(self):
sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings(force_reload=True)
super().refresh()
def create_item(self, name, index=None, enable_filter=True):
embedding = sd_hijack.model_hijack.embedding_db.word_embeddings.get(name)

View file

@ -148,7 +148,18 @@ class UserMetadataEditor:
def setup_save_handler(self, button, func, components):
button\
.click(fn=func, inputs=[self.edit_name_input, *components], outputs=[])\
.then(fn=None, _js="function(name){closePopup(); extraNetworksRefreshSingleCard(" + json.dumps(self.page.name) + "," + json.dumps(self.tabname) + ", name);}", inputs=[self.edit_name_input], outputs=[])
.then(
fn=None,
_js=(
"function(name){"
"closePopup(); "
"extraNetworksRefreshSingleCard("
f"'{self.tabname}', '{self.page.extra_networks_tabname}', name"
");}"
),
inputs=[self.edit_name_input],
outputs=[],
)
def create_editor(self):
self.create_default_editor_elems()
@ -199,7 +210,12 @@ class UserMetadataEditor:
outputs=[self.html_preview, self.html_status]
).then(
fn=None,
_js="function(name){extraNetworksRefreshSingleCard(" + json.dumps(self.page.name) + "," + json.dumps(self.tabname) + ", name);}",
_js=(
"function(name){"
"extraNetworksRefreshSingleCard("
f"'{self.tabname}', '{self.page.extra_networks_tabname}', name"
");}"
),
inputs=[self.edit_name_input],
outputs=[]
)

View file

@ -120,4 +120,8 @@ class UiPromptStyles:
inputs=[self.main_ui_prompt, self.main_ui_negative_prompt, self.dropdown],
outputs=[self.main_ui_prompt, self.main_ui_negative_prompt, self.dropdown],
show_progress=False,
).then(fn=None, _js="function(){update_"+self.tabname+"_tokens(); closePopup();}", show_progress=False)
).then(
fn=None,
_js=f"function(){{update_{self.tabname}_tokens(); closePopup();}}",
show_progress=False,
)

1092
style.css

File diff suppressed because it is too large Load diff