Fix bug with resizegrid resetting on tab change. Add content to details view.

This commit is contained in:
Sj-Si 2024-05-29 15:59:24 -04:00
parent dc5b155cda
commit 25e516a1d3
7 changed files with 444 additions and 18 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):
@ -89,3 +94,134 @@ 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:
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)
cmap_idx = math.floor((tag_count - min_tag) / (max_tag - min_tag) * (cmap.N - 1))
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])
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])
base_color = mpl.colors.rgb2hex(base_color)
dark_color = mpl.colors.rgb2hex(dark_color)
tag_style = f"background: {mpl.colors.rgb2hex(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,17 @@
<div class="extra-network-content--dets-view-model-info">
<div class="model-info--header">
<h1>{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>{description}</p>
<h3>Model Metadata</h3>
<table>
<tbody>{metadata_table}</tbody>
</table>
{model_specific}
</div>

View File

@ -204,7 +204,7 @@
</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"></div>
<div class="extra-network-content extra-network-content--dets-view styled-scrollbar"></div>
</div>
</div>
</div>

View File

@ -5,6 +5,7 @@
ExtraNetworksClusterizeCardList,
waitForElement,
isString,
isObject,
isElement,
isElementThrowError,
fetchWithRetryAndBackoff,
@ -395,7 +396,10 @@ class ExtraNetworksTab {
this.resize_grid.toggle({elem: div_dirs, override: this.dirs_view_en});
this.resize_grid.toggle({elem: div_tree, override: this.tree_view_en});
this.resize_grid.toggle({elem: div_card, override: this.card_view_en});
this.resize_grid.toggle({elem: div_dets, override: this.dets_view_en});
this.resize_grid.toggle({
elem: div_dets,
override: this.dets_view_en && div_dets.innerHTML !== "",
});
// apply the previous sort/filter options
await this.applyListButtonStates();
@ -462,7 +466,10 @@ class ExtraNetworksTab {
this.resize_grid.toggle({elem: div_dirs, override: this.dirs_view_en});
this.resize_grid.toggle({elem: div_tree, override: this.tree_view_en});
this.resize_grid.toggle({elem: div_card, override: this.card_view_en});
this.resize_grid.toggle({elem: div_dets, override: this.dets_view_en});
this.resize_grid.toggle({
elem: div_dets,
override: this.dets_view_en && div_dets.innerHTML !== "",
});
}
unload() {
@ -911,6 +918,45 @@ class ExtraNetworksTab {
}
this.applyDirectoryFilters();
}
showDetsView(source_elem) {
const div_dets = this.container_elem.querySelector(".extra-network-content--dets-view");
const _popup = (msg) => {
const elem = document.createElement("pre");
elem.classList.add("popup-metadata");
elem.textContent = msg;
popup(elem);
};
const _clear_details = () => {
div_dets.innerHTML = "";
};
const _show_details = (response) => {
if (!isObject(response) || !isString(response.html)) {
console.warn("Error parsing model details.");
div_dets.innerHTML = "Error parsing model details.";
return;
}
div_dets.innerHTML = response.html;
};
_clear_details();
requestGet(
"./sd_extra_networks/get-model-details",
{
extra_networks_tabname: this.extra_networks_tabname,
model_name: source_elem.dataset.name,
},
(response) => _show_details(response),
() => _popup("Error fetching model details."),
);
if (this.dets_view_en) {
this.resize_grid.toggle({elem: div_dets, override: true});
}
}
}
function popup(contents) {
@ -1309,20 +1355,30 @@ function extraNetworksControlDetsViewOnClick(event) {
/** Handles `onclick` events for the Card Details View button.
*
* Toggles the card details view in the extra networks pane.
*
* This button is unique in that we allow the user to enable/disable it
* regardless of whether we actually show the details view. This is because
* the details view only actually shows when the user has a model selected.
* Otherwise, the view is always hidden.
*/
const btn = event.target.closest(".extra-network-control--dets-view");
const controls = btn.closest(".extra-network-controls");
const tab = extra_networks_tabs[controls.dataset.tabnameFull];
btn.toggleAttribute("data-selected");
tab.dets_view_en = "selected" in btn.dataset;
const div_dets = tab.container_elem.querySelector(".extra-network-content--dets-view");
try {
tab.resize_grid.toggle({elem: div_dets, override: !("selected" in btn.dataset)});
tab.resize_grid.toggle({
elem: div_dets,
override: tab.dets_view_en && div_dets.innerHTML !== "",
});
} catch (error) {
console.warn("Error attempting to enable dets_view:", error);
return;
}
btn.toggleAttribute("data-selected");
tab.dets_view_en = "selected" in btn.dataset;
}
function extraNetworksControlRefreshOnClick(event) {
@ -1376,11 +1432,6 @@ function extraNetworksSelectModel({tab, prompt, neg_prompt, allow_neg, checkpoin
}
function extraNetworksCardOnClick(event) {
// Do not select the card if its child button-row is the target of the event.
if (event.target.closest(".button-row")) {
return;
}
const btn = event.target.closest(".card");
const pane = btn.closest(".extra-network-pane");
const tab = extra_networks_tabs[pane.dataset.tabnameFull];
@ -1398,6 +1449,34 @@ function extraNetworksCardOnClick(event) {
});
}
function extraNetworksDetsViewCloseOnClick(event) {
const btn = event.target.closest(".model-info--close");
const pane = btn.closest(".extra-network-pane");
const tab = extra_networks_tabs[pane.dataset.tabnameFull];
const div_dets = tab.container_elem.querySelector(".extra-network-content--dets-view");
div_dets.innerHTML = "";
tab.resize_grid.toggle({elem: div_dets, override: false});
}
function extraNetworksDetsViewTagOnClick(event) {
const btn = event.target.closest(".model-info--tag");
const pane = btn.closest(".extra-network-pane");
const tab = extra_networks_tabs[pane.dataset.tabnameFull];
const tag_name_elem = btn.querySelector(".model-info--tag-name");
isElementThrowError(tag_name_elem);
extraNetworksUpdatePrompt(tab.active_prompt_elem, tag_name_elem.textContent);
}
function extraNetworksCardOnLongPress(event) {
const btn = event.target.closest(".card");
const pane = btn.closest(".extra-network-pane");
const tab = extra_networks_tabs[pane.dataset.tabnameFull];
tab.showDetsView(btn);
}
function extraNetworksTreeFileOnClick(event) {
// Do not select the row if its child button-row is the target of the event.
if (event.target.closest(".tree-list-item-action")) {
@ -1674,7 +1753,6 @@ function extraNetworksSetupEventDelegators() {
const click_event_map = {
".tree-list-item--file": extraNetworksTreeFileOnClick,
".card": extraNetworksCardOnClick,
".copy-path-button": extraNetworksBtnCopyPathOnClick,
".edit-button": extraNetworksBtnEditMetadataOnClick,
".metadata-button": extraNetworksBtnShowMetadataOnClick,
@ -1717,6 +1795,19 @@ function extraNetworksSetupEventDelegators() {
selector: ".extra-network-dirs-view-button",
handler: extraNetworksBtnDirsViewItemOnClick,
},
{
selector: ".card",
negative: ".button-row",
handler: extraNetworksCardOnClick,
},
{
selector: ".model-info--close",
handler: extraNetworksDetsViewCloseOnClick,
},
{
selector: ".model-info--tag",
handler: extraNetworksDetsViewTagOnClick,
}
];
const short_ctrl_press_event_map = [
@ -1752,6 +1843,11 @@ function extraNetworksSetupEventDelegators() {
selector: ".extra-network-dirs-view-button",
handler: extraNetworksBtnDirsViewItemOnLongPress,
},
{
selector: ".card",
negative: ".button-row",
handler: extraNetworksCardOnLongPress,
},
];
const long_ctrl_press_event_map = [

View File

@ -842,6 +842,8 @@ class ResizeGrid extends ResizeGridAxis {
axis: axis,
callbacks: callbacks,
});
this.elem.id = id;
}
destroy() {
@ -1013,7 +1015,7 @@ class ResizeGrid extends ResizeGridAxis {
this.resize_observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target.id === this.elem.id) {
if (entry.target.id === this.id) {
clearTimeout(this.resize_observer_timer);
this.resize_observer_timer = setTimeout(() => {
this.onResize();
@ -1157,6 +1159,18 @@ class ResizeGrid extends ResizeGridAxis {
onResize() {
/** Resizes grid items on resize observer events. */
const curr_dims = this.elem.getBoundingClientRect();
// If height and width are 0, this indicates the element is not visible anymore.
// We don't want to do anything if the element isn't visible.
if (curr_dims.height === 0 && curr_dims.width === 0) {
return;
}
// Do nothing if the dimensions haven't changed.
if (JSON.stringify(curr_dims) === JSON.stringify(this.prev_dims)) {
return;
}
const d_w = curr_dims.width - this.prev_dims.width;
const d_h = curr_dims.height - this.prev_dims.height;

View File

@ -8,13 +8,14 @@ from base64 import b64decode
from io import BytesIO
from pathlib import Path
from typing import Callable, Optional
import datetime
import gradio as gr
from fastapi.exceptions import HTTPException
from PIL import Image
from starlette.responses import FileResponse, JSONResponse, Response
from modules import errors, extra_networks, shared, util
from modules import errors, extra_networks, shared, util, sysinfo
from modules.images import read_info_from_image, save_image_with_geninfo
from modules.infotext_utils import image_from_url_text
from modules.ui_common import OutputPanel
@ -24,6 +25,7 @@ extra_pages = []
allowed_dirs = set()
default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"]
class ListItem:
"""
Attributes:
@ -244,6 +246,7 @@ class ExtraNetworksPage:
self.btn_edit_metadata_tpl = shared.html("extra-networks-btn-edit-metadata.html")
self.btn_dirs_view_item_tpl = shared.html("extra-networks-btn-dirs-view-item.html")
self.btn_chevron_tpl = shared.html("extra-networks-btn-chevron.html")
self.model_details_tpl = shared.html("extra-networks-model-details.html")
def clear_data(self) -> None:
self.is_ready = False
@ -998,6 +1001,63 @@ class ExtraNetworksPage:
def create_user_metadata_editor(self, ui, tabname) -> UserMetadataEditor:
return UserMetadataEditor(ui, tabname, self)
def get_model_detail_metadata_table(self, model_name: str) -> str:
item = self.items.get(model_name, {})
def _relative_path(path):
for parent_path in self.allowed_directories_for_previews():
if path_is_parent(parent_path, path):
return os.path.relpath(path, parent_path)
return os.path.basename(path)
try:
filename = item["filename"]
shorthash = item.get("shorthash", None)
stats = os.stat(filename)
params = [
('Filename: ', _relative_path(filename)),
('File size: ', sysinfo.pretty_bytes(stats.st_size)),
('Hash: ', shorthash),
('Modified: ', datetime.datetime.fromtimestamp(stats.st_mtime).strftime('%Y-%m-%d %H:%M')),
]
except Exception as exc:
errors.display(exc, f"reading info for {model_name}")
params = []
return "".join([f"<tr><th>{tr[0]}</th><td>{tr[1]}</td>" for tr in params])
def get_model_detail_extra_html(self, _model_name: str) -> str:
"""Returns extra HTML to add to model details.
NOTE: This is intended to be subclassed to provide more model-specific info
in the model details. Thus the base class just returns an empty string.
"""
return ""
def gen_model_details_html(self, model_name):
tbl_metadata = self.get_model_detail_metadata_table(model_name)
item = self.items.get(model_name, {})
user_metadata = item.get("user_metadata", None)
if user_metadata:
description = user_metadata.get("description", "")
else:
description = item.get("description", "")
if not description:
description = ""
model_specific = self.get_model_detail_extra_html(model_name)
return self.model_details_tpl.format(
name=model_name,
description=description,
metadata_table=tbl_metadata,
model_specific=model_specific,
)
@functools.cache
def allowed_preview_extensions_with_extra(extra_extensions=None):
@ -1197,6 +1257,12 @@ def get_metadata(extra_networks_tabname: str = "", item: str = "") -> JSONRespon
return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)})
def get_model_details(extra_networks_tabname: str = "", model_name: str = "") -> JSONResponse:
page = get_page_by_name(extra_networks_tabname)
return JSONResponse({"html": page.gen_model_details_html(model_name)})
def get_single_card(
tabname: str = "",
extra_networks_tabname: str = "",
@ -1234,6 +1300,7 @@ def add_pages_to_demo(app):
app.add_api_route("/sd_extra_networks/fetch-card-data", fetch_card_data, methods=["GET"])
app.add_api_route("/sd_extra_networks/page-is-ready", page_is_ready, methods=["GET"])
app.add_api_route("/sd_extra_networks/clear-page-data", clear_page_data, methods=["GET"])
app.add_api_route("/sd_extra_networks/get-model-details", get_model_details, methods=["GET"])
def quote_js(s):

100
style.css
View File

@ -1711,7 +1711,7 @@ body.resizing.resize-grid-row {
/* Custom scrollbar style. Only works on chromium based browsers. */
.styled-scrollbar::-webkit-scrollbar {
background: transparent;
width: var(--text-lg);
width: var(--spacing-xxl);
}
.styled-scrollbar::-webkit-scrollbar-track {
@ -1722,13 +1722,17 @@ body.resizing.resize-grid-row {
.styled-scrollbar::-webkit-scrollbar-thumb {
background: var(--border-color-primary);
border-radius: var(--radius-xl);
border: calc(var(--button-border-width) * 4) solid var(--background-fill-primary);
border: var(--spacing-sm) solid var(--background-fill-primary);
}
.styled-scrollbar::-webkit-scrollbar-button {
display: none;
}
.styled-scrollbar::-webkit-scrollbar-corner {
background: var(--background-fill-primary);
}
/* Long-press buttons. Wipe L->R effect when button is held, then toggles color. */
.extra-network-dirs-view-button {
position: relative;
@ -1869,3 +1873,95 @@ body.resizing.resize-grid-row {
padding: var(--block-label-padding);
text-align: center;
}
.extra-network-content--dets-view {
padding: var(--block-padding);
overflow: clip auto;
}
.extra-network-content--dets-view-model-info {
display: flex;
flex-direction: column;
text-align: start;
}
.extra-network-content--dets-view-model-info p {
overflow-wrap: break-word;
}
.extra-network-content--dets-view-model-info table {
font-size: var(--text-sm);
font-weight: var(--prose-text-weight);
margin-left: var(--spacing-lg) !important;
}
.extra-network-content--dets-view-model-info table :is(th, td) {
padding-top: var(--spacing-sm) !important;
padding-bottom: var(--spacing-sm) !important;
}
.extra-network-content--dets-view-model-info table th {
white-space: nowrap;
}
.extra-network-content--dets-view-model-info table td {
width: 99%;
}
.model-info--tags {
display: inline-flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--spacing-sm);
}
.model-info--header {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.model-info--header h1 {
flex: 1;
margin: 0 !important;
justify-items: center;
word-break: break-all;
}
.model-info--close {
position: relative;
}
.model-info--tag {
display: inline-block;
cursor: pointer;
box-shadow: var(--button-shadow);
border-width: 0;
border-radius: var(--radius-sm);
text-shadow: var(--shadow-drop);
font-size: var(--button-large-text-size);
font-weight: var(--body-text-weight);
transition: var(--button-transition);
padding: var(--spacing-xs) calc(2 * var(--spacing-xs));
}
.model-info--tag:active {
box-shadow: var(--button-shadow-active);
}
.model-info--tag::hover {
box-shadow: var(--button-shadow-hover);
}
.model-info--tag-name {
padding: var(--spacing-xs) calc(2 * var(--spacing-xs));
word-break: break-all;
}
.model-info--tag-count {
border-radius: var(--radius-sm);
padding: var(--spacing-xs) calc(2 * var(--spacing-xs));
font-weight: var(--button-large-text-weight);
}