Reorganized model info into tabs.

- Changed save button to floppy disk emoji.
- Fixed search bug where scroll was not resetting to the top.
This commit is contained in:
Christian Bastian
2024-07-19 01:45:19 -04:00
parent 1d83d52bc1
commit 4e37259168
3 changed files with 368 additions and 226 deletions

View File

@@ -755,8 +755,8 @@ async def get_model_info(request):
stats = pathlib.Path(abs_path).stat()
date_format = "%Y-%m-%d %H:%M:%S"
date_modified = datetime.fromtimestamp(stats.st_mtime).strftime(date_format)
info["Date Modified"] = date_modified
info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
#info["Date Modified"] = date_modified
#info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
abs_name , _ = split_valid_ext(abs_path, model_extensions)
@@ -774,8 +774,6 @@ async def get_model_info(request):
header = get_safetensor_header(abs_path)
metadata = header.get("__metadata__", None)
#json.dump(metadata, sys.stdout, indent=4)
#print()
if metadata is not None and info.get("Preview", None) is None:
thumbnail = metadata.get("modelspec.thumbnail")
@@ -790,41 +788,10 @@ async def get_model_info(request):
}
if metadata is not None:
train_end = metadata.get("modelspec.date", "").replace("T", " ")
train_start = metadata.get("ss_training_started_at", "")
if train_start != "":
try:
train_start = float(train_start)
train_start = datetime.fromtimestamp(train_start).strftime(date_format)
except:
train_start = ""
info["Date Trained"] = (
train_start +
(" ... " if train_start != "" and train_end != "" else "") +
train_end
)
info["Base Training Model"] = metadata.get("ss_sd_model_name", "")
info["Base Model"] = metadata.get("ss_base_model_version", "")
info["Architecture"] = metadata.get("modelspec.architecture", "")
info["Network Dimension"] = metadata.get("ss_network_dim", "") # features trained
info["Network Alpha"] = metadata.get("ss_network_alpha", "") # trained features applied
info["Model Sampling Type"] = metadata.get("modelspec.prediction_type", "")
clip_skip = metadata.get("ss_clip_skip", "")
if clip_skip == "None" or clip_skip == "1": # assume 1 means no clip skip
clip_skip = ""
info["Clip Skip"] = clip_skip
# it is unclear what these are
#info["Hash SHA256"] = metadata.get("modelspec.hash_sha256", "")
#info["SSHS Model Hash"] = metadata.get("sshs_model_hash", "")
#info["SSHS Legacy Hash"] = metadata.get("sshs_legacy_hash", "")
#info["New SD Model Hash"] = metadata.get("ss_new_sd_model_hash", "")
#info["Output Name"] = metadata.get("ss_output_name", "")
#info["Title"] = metadata.get("modelspec.title", "")
info["Author"] = metadata.get("modelspec.author", "")
info["License"] = metadata.get("modelspec.license", "")
info["Base Model Version"] = metadata.get("ss_base_model_version", "")
info["Network Dimension"] = metadata.get("ss_network_dim", "")
info["Network Alpha"] = metadata.get("ss_network_alpha", "")
if metadata is not None:
training_comment = metadata.get("ss_training_comment", "")
@@ -841,7 +808,6 @@ async def get_model_info(request):
if os.path.isfile(info_text_file):
with open(info_text_file, 'r', encoding="utf-8") as f:
notes = f.read()
info["Notes"] = notes
if metadata is not None:
img_buckets = metadata.get("ss_bucket_info", "{}")
@@ -859,6 +825,8 @@ async def get_model_info(request):
resolutions.sort(key=lambda x: x[1], reverse=True)
info["Bucket Resolutions"] = resolutions
tags = None
if metadata is not None:
dir_tags = metadata.get("ss_tag_frequency", "{}")
if type(dir_tags) is str:
dir_tags = json.loads(dir_tags)
@@ -868,10 +836,14 @@ async def get_model_info(request):
tags[tag] = tags.get(tag, 0) + count
tags = list(tags.items())
tags.sort(key=lambda x: x[1], reverse=True)
info["Tags"] = tags
result["success"] = True
result["info"] = info
if metadata is not None:
result["metadata"] = metadata
if tags is not None:
result["tags"] = tags
result["notes"] = notes
return web.json_response(result)

View File

@@ -72,7 +72,12 @@
width: 100%;
}
.model-manager button,
.model-manager button {
margin: 0;
border: 2px solid var(--border-color);
}
.model-manager button:not(.icon-button),
.model-manager select,
.model-manager input {
padding: 4px 8px;
@@ -121,6 +126,7 @@
}
.model-manager .icon-button {
padding: 0;
height: 40px;
width: 40px;
line-height: 1.15;
@@ -174,13 +180,13 @@
color: var(--fg-color);
}
.model-manager .model-manager-tabs {
.model-manager .model-tab-group {
display: flex;
gap: 4px;
height: 40px;
}
.model-manager .model-manager-tabs .head-item {
.model-manager .model-tab-group .tab-button {
background-color: var(--comfy-menu-bg);
border: 2px solid var(--border-color);
border-bottom: none;
@@ -192,7 +198,7 @@
z-index: 1;
}
.model-manager .model-manager-tabs .head-item.active {
.model-manager .model-tab-group .tab-button.active {
background-color: var(--bg-color);
cursor: default;
position: relative;
@@ -233,7 +239,6 @@
height: 100%;
overflow-wrap: break-word;
overflow-y: auto;
padding: 20px;
position: relative;
}
@@ -244,6 +249,31 @@
width: auto;
}
.model-manager .model-metadata {
table-layout: fixed;
text-align: left;
width: 100%;
}
.model-manager .model-metadata-key {
overflow-wrap: break-word;
width: 20%;
}
.model-manager .model-metadata-value {
overflow-wrap: anywhere;
width: 80%;
}
.model-manager table {
border-collapse: collapse;
}
.model-manager th {
border: 1px solid;
padding: 4px 8px;
}
/* download tab */
.model-manager .download-model-infos {

View File

@@ -269,6 +269,98 @@ function $radioGroup(attr) {
return $el("div.comfy-radio-group", radioGroup);
}
/**
* @param {{name: string, icon: string, tabContent: HTMLDivElement}[]} tabData
* @returns {[Record<string, HTMLDivElement>[], Record<string, HTMLDivElement>[]]}
*/
function GenerateTabGroup(tabData) {
/** @type {HTMLDivElement[]} */
const tabContents = tabData.map((data) => {
return $el("div", {
dataset: {
name: data.name,
icon: data.icon, // TODO: remove this; not needed
}
}, [
data.tabContent
]);
});
/** @type {Record<string, HTMLDivElement>} */
const tabButton = {};
/** @type {Record<string, HTMLDivElement>} */
const tabContent = {};
const ACTIVE_TAB_CLASS = "active";
/** @type {HTMLDivElement[]} */
const tabButtons = tabContents.map((content) => {
const name = content.getAttribute("data-name");
const icon = content.getAttribute("data-icon");
/** @type {HTMLDivElement} */
const tab = $el("div.tab-button", {
dataset: { name: name, icon: icon },
onclick: () => {
Object.keys(tabButton).forEach((key) => {
if (name === key) {
tabButton[key].classList.add(ACTIVE_TAB_CLASS);
tabContent[key].style.display = "";
} else {
tabButton[key].classList.remove(ACTIVE_TAB_CLASS);
tabContent[key].style.display = "none";
}
});
},
},
[name],
);
tabButton[name] = tab;
tabContent[name] = content;
return tab;
});
return [tabButtons, tabContents];
}
/**
* @param {HTMLDivElement} element
* @param {Record<string, HTMLDivElement>[]} tabButtons
*/
function GenerateDynamicTabTextCallback(element, tabButtons, minWidth) {
return () => {
if (element.style.display === "none") {
return;
}
const managerRect = element.getBoundingClientRect();
const isNarrow = managerRect.width < minWidth; // TODO: `minWidth` is a magic value
tabButtons.forEach((tabButton) => {
const attribute = isNarrow ? "data-icon" : "data-name";
tabButton.innerText = tabButton.getAttribute(attribute);
});
};
}
/**
*
* @param {[String, int][]} map
* @returns {String}
*/
function TagCountMapToParagraph(map) {
let text = "<p>";
for (let i = 0; i < map.length; i++) {
const v = map[i];
const tag = v[0];
const count = v[1];
text += tag + "<span class=\"no-select\"> (" + count + ")</span>";
if (i !== map.length - 1) {
text += ", ";
}
}
text += "</p>";
return text;
}
class ImageSelect {
/** @constant {string} */ #PREVIEW_DEFAULT = "Default";
/** @constant {string} */ #PREVIEW_UPLOAD = "Upload";
@@ -1703,11 +1795,13 @@ class ModelGrid {
}
}
class ModelInfoView {
class ModelInfo {
/** @type {HTMLDivElement} */
element = null;
elements = {
/** @type {Record<string, HTMLDivElement>[]} */ tabButtons: null,
/** @type {Record<string, HTMLDivElement>[]} */ tabContents: null,
/** @type {HTMLDivElement} */ info: null,
/** @type {HTMLTextAreaElement} */ notes: null,
/** @type {HTMLButtonElement} */ setPreviewButton: null,
@@ -1916,6 +2010,13 @@ class ModelInfoView {
"data-path": "",
}),
]);
[this.elements.tabButtons, this.elements.tabContents] = GenerateTabGroup([
{ name: "Overview", icon: "ⓘ", tabContent: this.element },
{ name: "Metadata", icon: "📄", tabContent: $el("div", ["Metadata"]) },
{ name: "Tags", icon: "🏷️", tabContent: $el("div", ["Tags"]) },
{ name: "Notes", icon: "✏️", tabContent: $el("div", ["Notes"]) },
]);
}
/** @returns {void} */
@@ -1985,22 +2086,28 @@ class ModelInfoView {
*/
async update(searchPath, updateModels, searchSeparator) {
const path = encodeURIComponent(searchPath);
const info = await request(`/model-manager/model/info?path=${path}`)
.then((result) => {
const success = result["success"];
const message = result["alert"];
if (message !== undefined) {
window.alert(message);
}
if (!success) {
const [info, metadata, tags, noteText] = await request(`/model-manager/model/info?path=${path}`)
.then((result) => {
const success = result["success"];
const message = result["alert"];
if (message !== undefined) {
window.alert(message);
}
if (!success) {
return undefined;
}
return [
result["info"],
result["metadata"],
result["tags"],
result["notes"]
];
})
.catch((err) => {
console.log(err);
return undefined;
}
return result["info"];
})
.catch((err) => {
console.log(err);
return undefined;
});
);
if (info === undefined || info === null) {
return;
}
@@ -2107,7 +2214,7 @@ class ModelInfoView {
setPreviewButton,
]),
]),
$el("h2", ["Details:"]),
$el("h2", ["File Info:"]),
$el("div",
(() => {
const elements = [];
@@ -2117,45 +2224,17 @@ class ModelInfoView {
}
if (Array.isArray(value)) {
// currently only used for "Bucket Resolutions"
if (value.length > 0) {
elements.push($el("h2", [key + ":"]));
let text = "<p>";
for (let i = 0; i < value.length; i++) {
const v = value[i];
const tag = v[0];
const count = v[1];
text += tag + "<span class=\"no-select\"> (" + count + ")</span>";
if (i !== value.length - 1) {
text += ", ";
}
}
text += "</p>";
const text = TagCountMapToParagraph(value);
const div = $el("div");
div.innerHTML = text;
elements.push(div);
}
}
else {
if (key === "Notes") {
elements.push($el("h2", [key + ":"]));
const notes = $el("textarea.comfy-multiline-input", {
name: "model notes",
value: value,
rows: 12,
});
this.elements.notes = notes;
this.#savedNotesValue = value;
elements.push($el("button", {
textContent: "Save Notes",
onclick: async (e) => {
const saved = await this.trySave(false);
buttonAlert(e.target, saved);
},
}));
elements.push(notes);
}
else if (key === "Description") {
if (key === "Description") {
if (value !== "") {
elements.push($el("h2", [key + ":"]));
elements.push($el("p", [value]));
@@ -2177,6 +2256,86 @@ class ModelInfoView {
]));
infoHtml.append.apply(infoHtml, innerHtml);
// TODO: set default value of dropdown and value to model type?
/** @type {HTMLDivElement} */
const metadataElement = this.elements.tabContents[1]; // TODO: remove magic value
const isMetadata = typeof metadata === 'object' && metadata !== null && Object.keys(metadata).length > 0;
metadataElement.innerHTML = "";
metadataElement.append.apply(metadataElement, [
$el("h1", ["Metadata"]),
$el("div", (() => {
const tableRows = [];
if (isMetadata) {
for (const [key, value] of Object.entries(metadata)) {
if (value === undefined || value === null) {
continue;
}
if (value !== "") {
tableRows.push($el("tr", [
$el("th.model-metadata-key", [key]),
$el("th.model-metadata-value", [value]),
]));
}
}
}
return $el("table.model-metadata", tableRows);
})(),
),
]);
const metadataButton = this.elements.tabButtons[1]; // TODO: remove magic value
metadataButton.style.display = isMetadata ? "" : "none";
/** @type {HTMLDivElement} */
const tagsElement = this.elements.tabContents[2]; // TODO: remove magic value
const isTags = Array.isArray(tags) && tags.length > 0;
tagsElement.innerHTML = "";
tagsElement.append.apply(tagsElement, [
$el("h1", ["Tags"]),
$el("div", (() => {
const elements = [];
if (isTags) {
let text = TagCountMapToParagraph(tags);
const div = $el("div");
div.innerHTML = text;
elements.push(div);
}
return elements;
})(),
),
]);
const tagButton = this.elements.tabButtons[2]; // TODO: remove magic value
tagButton.style.display = isTags ? "" : "none";
/** @type {HTMLDivElement} */
const notesElement = this.elements.tabContents[3]; // TODO: remove magic value
notesElement.innerHTML = "";
notesElement.append.apply(notesElement, [
$el("div", (() => {
const notes = $el("textarea.comfy-multiline-input", {
name: "model notes",
value: noteText,
rows: 14,
});
this.elements.notes = notes;
this.#savedNotesValue = noteText;
return [
$el("div.row", {
style: { margin: "8px 0px 16px" },
}, [
$el("h1", { style: { margin: "0px" } }, ["Notes"]),
$el("button.icon-button", {
textContent: "💾",
onclick: async (e) => {
const saved = await this.trySave(false);
buttonAlert(e.target, saved);
},
}),
]),
notes,
];
})(),
),
]);
}
}
@@ -2585,7 +2744,7 @@ async function getModelInfos(urlText) {
return {
"images": [],
"fileName": file["name"],
"modelType": DownloadTab.modelTypeToComfyUiDirectory(file["type"], "") ?? "",
"modelType": DownloadView.modelTypeToComfyUiDirectory(file["type"], "") ?? "",
"downloadUrl": file["download"],
"downloadFilePath": "",
"description": file["description"],
@@ -2598,7 +2757,7 @@ async function getModelInfos(urlText) {
})();
}
class DownloadTab {
class DownloadView {
/** @type {HTMLDivElement} */
element = null;
@@ -2709,8 +2868,8 @@ class DownloadTab {
);
const comfyUIModelType = (
DownloadTab.modelTypeToComfyUiDirectory(info["details"]["fileType"]) ??
DownloadTab.modelTypeToComfyUiDirectory(info["modelType"]) ??
DownloadView.modelTypeToComfyUiDirectory(info["details"]["fileType"]) ??
DownloadView.modelTypeToComfyUiDirectory(info["modelType"]) ??
""
);
const searchSeparator = modelData.searchSeparator;
@@ -2870,7 +3029,7 @@ class DownloadTab {
}
}
class ModelTab {
class BrowseView {
/** @type {HTMLDivElement} */
element = null;
@@ -2906,9 +3065,10 @@ class ModelTab {
* @param {() => Promise<void>} updateModels
* @param {ModelData} modelData
* @param {(searchPath: string) => Promise<void>} showModelInfo
* @param {() => void} updateModelGridCallback
* @param {any} settingsElements
*/
constructor(updateModels, modelData, showModelInfo, settingsElements) {
constructor(updateModels, modelData, showModelInfo, updateModelGridCallback, settingsElements) {
/** @type {HTMLDivElement} */
const modelGrid = $el("div.comfy-grid");
this.elements.modelGrid = modelGrid;
@@ -2947,7 +3107,7 @@ class ModelTab {
this.elements.modelContentFilter,
showModelInfo,
);
this.element.parentElement.scrollTop = 0;
updateModelGridCallback();
}
this.updateModelGrid = updateModelGrid;
@@ -3009,7 +3169,7 @@ class ModelTab {
}
}
class SettingsTab {
class SettingsView {
/** @type {HTMLDivElement} */
element = null;
@@ -3285,7 +3445,6 @@ class SidebarButtons {
$: (el) => (this.element = el),
},
[
$el("button.icon-button", {
textContent: "◨",
onclick: (event) => this.#setSidebar(event),
@@ -3313,23 +3472,29 @@ class ModelManager extends ComfyDialog {
/** @type {ModelData} */
#modelData = null;
/** @type {ModelInfoView} */
#modelInfoView = null;
/** @type {ModelInfo} */
#modelInfo = null;
/** @type {DownloadTab} */
#downloadTab = null;
/** @type {DownloadView} */
#downloadView = null;
/** @type {ModelTab} */
#modelTab = null;
/** @type {BrowseView} */
#browseView = null;
/** @type {SettingsTab} */
#settingsTab = null;
/** @type {SettingsView} */
#settingsView = null;
/** @type {HTMLDivElement} */
#tabs = null;
#tabManagerButtons = null;
/** @type {HTMLDivElement} */
#tabContents = null;
#tabManagerContents = null;
/** @type {HTMLDivElement} */
#tabInfoButtons = null;
/** @type {HTMLDivElement} */
#tabInfoContents = null;
/** @type {HTMLButtonElement} */
#closeModelInfoButton = null;
@@ -3339,106 +3504,38 @@ class ModelManager extends ComfyDialog {
this.#modelData = new ModelData();
const modelInfoView = new ModelInfoView(
this.#settingsView = new SettingsView(
this.#refreshModels,
);
this.#modelInfo = new ModelInfo(
this.#modelData,
this.#refreshModels,
);
this.#modelInfoView = modelInfoView;
const settingsTab = new SettingsTab(
this.#refreshModels,
);
this.#settingsTab = settingsTab;
const ACTIVE_TAB_CLASS = "active";
/**
* @param {searchPath: string}
* @return {Promise<void>}
*/
const showModelInfo = async(searchPath) => {
await this.#modelInfoView.update(
searchPath,
this.#refreshModels,
this.#modelData.searchSeparator
).then(() => {
this.#tabs.style.display = "none";
this.#tabContents.style.display = "none";
this.#closeModelInfoButton.style.display = "";
this.#modelInfoView.show();
});
}
const modelTab = new ModelTab(
this.#browseView = new BrowseView(
this.#refreshModels,
this.#modelData,
showModelInfo,
this.#settingsTab.elements.settings, // TODO: decouple settingsData from elements?
this.#showModelInfo,
this.#resetManagerContentsScroll,
this.#settingsView.elements.settings, // TODO: decouple settingsData from elements?
);
this.#modelTab = modelTab;
const downloadTab = new DownloadTab(
this.#downloadView = new DownloadView(
this.#modelData,
this.#settingsTab.elements.settings,
this.#settingsView.elements.settings,
this.#refreshModels,
);
this.#downloadTab = downloadTab;
const sidebarButtons = new SidebarButtons(this);
const [tabManagerButtons, tabManagerContents] = GenerateTabGroup([
{ name: "Download", icon: "⬇️", tabContent: this.#downloadView.element },
{ name: "Models", icon: "📁", tabContent: this.#browseView.element },
{ name: "Settings", icon: "⚙️", tabContent: this.#settingsView.element },
]);
tabManagerButtons[0]?.click();
/** @type {Record<string, HTMLDivElement>} */
const head = {};
/** @type {Record<string, HTMLDivElement>} */
const body = {};
/** @type {HTMLDivElement[]} */
const contents = [
$el("div", { dataset: { name: "Download" } }, [downloadTab.element]),
$el("div", { dataset: { name: "Models" } }, [modelTab.element]),
$el("div", { dataset: { name: "Settings" } }, [settingsTab.element]),
];
const tabs = contents.map((content) => {
const name = content.getAttribute("data-name");
/** @type {HTMLDivElement} */
const tab = $el("div.head-item", {
onclick: () => {
Object.keys(head).forEach((key) => {
if (name === key) {
head[key].classList.add(ACTIVE_TAB_CLASS);
body[key].style.display = "";
} else {
head[key].classList.remove(ACTIVE_TAB_CLASS);
body[key].style.display = "none";
}
});
},
},
[name],
);
head[name] = tab;
body[name] = content;
return tab;
});
tabs[0]?.click();
const closeManagerButton = $el("button.icon-button", {
textContent: "✖",
onclick: async() => {
const saved = await modelInfoView.trySave(true);
if (saved) {
this.close();
}
}
});
const closeModelInfoButton = $el("button.icon-button", {
$: (el) => (this.#closeModelInfoButton = el),
style: { display: "none" },
textContent: "⬅",
onclick: async() => { await this.#tryHideModelInfo(true); },
});
const tabInfoButtons = this.#modelInfo.elements.tabButtons;
const tabInfoContents = this.#modelInfo.elements.tabContents;
const modelManager = $el(
"div.comfy-modal.model-manager",
@@ -3451,48 +3548,64 @@ class ModelManager extends ComfyDialog {
$el("div.model-manager-panel", [
$el("div.model-manager-head", [
$el("div.topbar-right", [
closeManagerButton,
closeModelInfoButton,
sidebarButtons.element,
$el("button.icon-button", {
textContent: "✖",
onclick: async() => {
const saved = await this.#modelInfo.trySave(true);
if (saved) {
this.close();
}
}
}),
$el("button.icon-button", {
$: (el) => (this.#closeModelInfoButton = el),
style: { display: "none" },
textContent: "⬅",
onclick: async() => { await this.#tryHideModelInfo(true); },
}),
(new SidebarButtons(this)).element,
]),
$el("div.topbar-left", [
$el("div.model-manager-tabs", {
$: (el) => (this.#tabs = el),
}, tabs),
$el("div", [
$el("div.model-tab-group", {
$: (el) => (this.#tabManagerButtons = el),
}, tabManagerButtons),
$el("div.model-tab-group", {
$: (el) => (this.#tabInfoButtons = el),
style: { display: "none"},
}, tabInfoButtons),
]),
]),
]),
$el("div.model-manager-body", [
$el("div.model-manager-tab-contents", {
$: (el) => (this.#tabContents = el),
}, contents),
modelInfoView.element,
$el("div", {
$: (el) => (this.#tabManagerContents = el)
}, tabManagerContents),
$el("div", {
$: (el) => (this.#tabInfoContents = el),
style: { display: "none"},
}, tabInfoContents),
]),
]),
]),
]
);
new ResizeObserver(() => {
if (modelManager.style.display === "none") {
return;
}
const minWidth = 768; // magic value (could easily break)
const managerRect = modelManager.getBoundingClientRect();
const isNarrow = managerRect.width < minWidth;
let texts = isNarrow ? ["⬇️", "📁", "⚙️"] : ["Download", "Models", "Settings"]; // magic values
texts.forEach((text, i) => {
tabs[i].innerText = text;
});
}).observe(modelManager);
new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabManagerButtons, 768)).observe(modelManager);
new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabInfoButtons, 768)).observe(modelManager);
this.#init();
}
#init() {
this.#settingsTab.reload(false);
this.#settingsView.reload(false);
this.#refreshModels();
}
#resetManagerContentsScroll = () => {
this.#tabManagerContents.scrollTop = 0;
}
#refreshModels = async() => {
const modelData = this.#modelData;
modelData.systemSeparator = await request("/model-manager/system-separator");
@@ -3501,24 +3614,51 @@ class ModelManager extends ComfyDialog {
const newModelDirectories = await request("/model-manager/models/directory-list");
modelData.directories.data.splice(0, Infinity, ...newModelDirectories); // NOTE: do NOT create a new array
this.#modelTab.updateModelGrid();
this.#browseView.updateModelGrid();
await this.#tryHideModelInfo(false);
document.getElementById("comfy-refresh-button")?.click();
}
/**
* @param {searchPath: string}
* @return {Promise<void>}
*/
#showModelInfo = async(searchPath) => {
await this.#modelInfo.update(
searchPath,
this.#refreshModels,
this.#modelData.searchSeparator,
).then(() => {
this.#tabManagerButtons.style.display = "none";
this.#tabManagerContents.style.display = "none";
this.#closeModelInfoButton.style.display = "";
this.#tabInfoButtons.style.display = "";
this.#tabInfoContents.style.display = "";
this.#tabInfoButtons.children[0]?.click();
this.#modelInfo.show();
this.#tabInfoContents.scrollTop = 0;
});
}
/**
* @param {boolean} promptSave
* @returns {Promise<boolean>}
*/
#tryHideModelInfo = async(promptSave) => {
if (this.#tabContents.style.display === "none") {
if (!await this.#modelInfoView.tryHide(promptSave)) {
if (this.#tabInfoContents.style.display !== "none") {
if (!await this.#modelInfo.tryHide(promptSave)) {
return false;
}
this.#closeModelInfoButton.style.display = "none";
this.#tabs.style.display = "";
this.#tabContents.style.display = "";
this.#tabInfoButtons.style.display = "none";
this.#tabInfoContents.style.display = "none";
this.#tabManagerButtons.style.display = "";
this.#tabManagerContents.style.display = "";
}
return true;
}