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:
52
__init__.py
52
__init__.py
@@ -755,8 +755,8 @@ async def get_model_info(request):
|
|||||||
stats = pathlib.Path(abs_path).stat()
|
stats = pathlib.Path(abs_path).stat()
|
||||||
date_format = "%Y-%m-%d %H:%M:%S"
|
date_format = "%Y-%m-%d %H:%M:%S"
|
||||||
date_modified = datetime.fromtimestamp(stats.st_mtime).strftime(date_format)
|
date_modified = datetime.fromtimestamp(stats.st_mtime).strftime(date_format)
|
||||||
info["Date Modified"] = date_modified
|
#info["Date Modified"] = date_modified
|
||||||
info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
|
#info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
|
||||||
|
|
||||||
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
|
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
|
||||||
abs_name , _ = split_valid_ext(abs_path, model_extensions)
|
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)
|
header = get_safetensor_header(abs_path)
|
||||||
metadata = header.get("__metadata__", None)
|
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:
|
if metadata is not None and info.get("Preview", None) is None:
|
||||||
thumbnail = metadata.get("modelspec.thumbnail")
|
thumbnail = metadata.get("modelspec.thumbnail")
|
||||||
@@ -790,41 +788,10 @@ async def get_model_info(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if metadata is not None:
|
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 Training Model"] = metadata.get("ss_sd_model_name", "")
|
||||||
info["Base Model"] = metadata.get("ss_base_model_version", "")
|
info["Base Model Version"] = metadata.get("ss_base_model_version", "")
|
||||||
info["Architecture"] = metadata.get("modelspec.architecture", "")
|
info["Network Dimension"] = metadata.get("ss_network_dim", "")
|
||||||
info["Network Dimension"] = metadata.get("ss_network_dim", "") # features trained
|
info["Network Alpha"] = metadata.get("ss_network_alpha", "")
|
||||||
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", "")
|
|
||||||
|
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
training_comment = metadata.get("ss_training_comment", "")
|
training_comment = metadata.get("ss_training_comment", "")
|
||||||
@@ -841,7 +808,6 @@ async def get_model_info(request):
|
|||||||
if os.path.isfile(info_text_file):
|
if os.path.isfile(info_text_file):
|
||||||
with open(info_text_file, 'r', encoding="utf-8") as f:
|
with open(info_text_file, 'r', encoding="utf-8") as f:
|
||||||
notes = f.read()
|
notes = f.read()
|
||||||
info["Notes"] = notes
|
|
||||||
|
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
img_buckets = metadata.get("ss_bucket_info", "{}")
|
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)
|
resolutions.sort(key=lambda x: x[1], reverse=True)
|
||||||
info["Bucket Resolutions"] = resolutions
|
info["Bucket Resolutions"] = resolutions
|
||||||
|
|
||||||
|
tags = None
|
||||||
|
if metadata is not None:
|
||||||
dir_tags = metadata.get("ss_tag_frequency", "{}")
|
dir_tags = metadata.get("ss_tag_frequency", "{}")
|
||||||
if type(dir_tags) is str:
|
if type(dir_tags) is str:
|
||||||
dir_tags = json.loads(dir_tags)
|
dir_tags = json.loads(dir_tags)
|
||||||
@@ -868,10 +836,14 @@ async def get_model_info(request):
|
|||||||
tags[tag] = tags.get(tag, 0) + count
|
tags[tag] = tags.get(tag, 0) + count
|
||||||
tags = list(tags.items())
|
tags = list(tags.items())
|
||||||
tags.sort(key=lambda x: x[1], reverse=True)
|
tags.sort(key=lambda x: x[1], reverse=True)
|
||||||
info["Tags"] = tags
|
|
||||||
|
|
||||||
result["success"] = True
|
result["success"] = True
|
||||||
result["info"] = info
|
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)
|
return web.json_response(result)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,12 @@
|
|||||||
width: 100%;
|
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 select,
|
||||||
.model-manager input {
|
.model-manager input {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
@@ -121,6 +126,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.model-manager .icon-button {
|
.model-manager .icon-button {
|
||||||
|
padding: 0;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
@@ -174,13 +180,13 @@
|
|||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-manager .model-manager-tabs {
|
.model-manager .model-tab-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-manager .model-manager-tabs .head-item {
|
.model-manager .model-tab-group .tab-button {
|
||||||
background-color: var(--comfy-menu-bg);
|
background-color: var(--comfy-menu-bg);
|
||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@@ -192,7 +198,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-manager .model-manager-tabs .head-item.active {
|
.model-manager .model-tab-group .tab-button.active {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -233,7 +239,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +249,31 @@
|
|||||||
width: auto;
|
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 */
|
/* download tab */
|
||||||
|
|
||||||
.model-manager .download-model-infos {
|
.model-manager .download-model-infos {
|
||||||
|
|||||||
@@ -269,6 +269,98 @@ function $radioGroup(attr) {
|
|||||||
return $el("div.comfy-radio-group", radioGroup);
|
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 {
|
class ImageSelect {
|
||||||
/** @constant {string} */ #PREVIEW_DEFAULT = "Default";
|
/** @constant {string} */ #PREVIEW_DEFAULT = "Default";
|
||||||
/** @constant {string} */ #PREVIEW_UPLOAD = "Upload";
|
/** @constant {string} */ #PREVIEW_UPLOAD = "Upload";
|
||||||
@@ -1703,11 +1795,13 @@ class ModelGrid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModelInfoView {
|
class ModelInfo {
|
||||||
/** @type {HTMLDivElement} */
|
/** @type {HTMLDivElement} */
|
||||||
element = null;
|
element = null;
|
||||||
|
|
||||||
elements = {
|
elements = {
|
||||||
|
/** @type {Record<string, HTMLDivElement>[]} */ tabButtons: null,
|
||||||
|
/** @type {Record<string, HTMLDivElement>[]} */ tabContents: null,
|
||||||
/** @type {HTMLDivElement} */ info: null,
|
/** @type {HTMLDivElement} */ info: null,
|
||||||
/** @type {HTMLTextAreaElement} */ notes: null,
|
/** @type {HTMLTextAreaElement} */ notes: null,
|
||||||
/** @type {HTMLButtonElement} */ setPreviewButton: null,
|
/** @type {HTMLButtonElement} */ setPreviewButton: null,
|
||||||
@@ -1916,6 +2010,13 @@ class ModelInfoView {
|
|||||||
"data-path": "",
|
"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} */
|
/** @returns {void} */
|
||||||
@@ -1985,22 +2086,28 @@ class ModelInfoView {
|
|||||||
*/
|
*/
|
||||||
async update(searchPath, updateModels, searchSeparator) {
|
async update(searchPath, updateModels, searchSeparator) {
|
||||||
const path = encodeURIComponent(searchPath);
|
const path = encodeURIComponent(searchPath);
|
||||||
const info = await request(`/model-manager/model/info?path=${path}`)
|
const [info, metadata, tags, noteText] = await request(`/model-manager/model/info?path=${path}`)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const success = result["success"];
|
const success = result["success"];
|
||||||
const message = result["alert"];
|
const message = result["alert"];
|
||||||
if (message !== undefined) {
|
if (message !== undefined) {
|
||||||
window.alert(message);
|
window.alert(message);
|
||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
result["info"],
|
||||||
|
result["metadata"],
|
||||||
|
result["tags"],
|
||||||
|
result["notes"]
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return result["info"];
|
);
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
if (info === undefined || info === null) {
|
if (info === undefined || info === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2107,7 +2214,7 @@ class ModelInfoView {
|
|||||||
setPreviewButton,
|
setPreviewButton,
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
$el("h2", ["Details:"]),
|
$el("h2", ["File Info:"]),
|
||||||
$el("div",
|
$el("div",
|
||||||
(() => {
|
(() => {
|
||||||
const elements = [];
|
const elements = [];
|
||||||
@@ -2117,45 +2224,17 @@ class ModelInfoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
// currently only used for "Bucket Resolutions"
|
||||||
if (value.length > 0) {
|
if (value.length > 0) {
|
||||||
elements.push($el("h2", [key + ":"]));
|
elements.push($el("h2", [key + ":"]));
|
||||||
|
const text = TagCountMapToParagraph(value);
|
||||||
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 div = $el("div");
|
const div = $el("div");
|
||||||
div.innerHTML = text;
|
div.innerHTML = text;
|
||||||
elements.push(div);
|
elements.push(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (key === "Notes") {
|
if (key === "Description") {
|
||||||
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 (value !== "") {
|
if (value !== "") {
|
||||||
elements.push($el("h2", [key + ":"]));
|
elements.push($el("h2", [key + ":"]));
|
||||||
elements.push($el("p", [value]));
|
elements.push($el("p", [value]));
|
||||||
@@ -2177,6 +2256,86 @@ class ModelInfoView {
|
|||||||
]));
|
]));
|
||||||
infoHtml.append.apply(infoHtml, innerHtml);
|
infoHtml.append.apply(infoHtml, innerHtml);
|
||||||
// TODO: set default value of dropdown and value to model type?
|
// 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 {
|
return {
|
||||||
"images": [],
|
"images": [],
|
||||||
"fileName": file["name"],
|
"fileName": file["name"],
|
||||||
"modelType": DownloadTab.modelTypeToComfyUiDirectory(file["type"], "") ?? "",
|
"modelType": DownloadView.modelTypeToComfyUiDirectory(file["type"], "") ?? "",
|
||||||
"downloadUrl": file["download"],
|
"downloadUrl": file["download"],
|
||||||
"downloadFilePath": "",
|
"downloadFilePath": "",
|
||||||
"description": file["description"],
|
"description": file["description"],
|
||||||
@@ -2598,7 +2757,7 @@ async function getModelInfos(urlText) {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadTab {
|
class DownloadView {
|
||||||
/** @type {HTMLDivElement} */
|
/** @type {HTMLDivElement} */
|
||||||
element = null;
|
element = null;
|
||||||
|
|
||||||
@@ -2709,8 +2868,8 @@ class DownloadTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const comfyUIModelType = (
|
const comfyUIModelType = (
|
||||||
DownloadTab.modelTypeToComfyUiDirectory(info["details"]["fileType"]) ??
|
DownloadView.modelTypeToComfyUiDirectory(info["details"]["fileType"]) ??
|
||||||
DownloadTab.modelTypeToComfyUiDirectory(info["modelType"]) ??
|
DownloadView.modelTypeToComfyUiDirectory(info["modelType"]) ??
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
const searchSeparator = modelData.searchSeparator;
|
const searchSeparator = modelData.searchSeparator;
|
||||||
@@ -2870,7 +3029,7 @@ class DownloadTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModelTab {
|
class BrowseView {
|
||||||
/** @type {HTMLDivElement} */
|
/** @type {HTMLDivElement} */
|
||||||
element = null;
|
element = null;
|
||||||
|
|
||||||
@@ -2906,9 +3065,10 @@ class ModelTab {
|
|||||||
* @param {() => Promise<void>} updateModels
|
* @param {() => Promise<void>} updateModels
|
||||||
* @param {ModelData} modelData
|
* @param {ModelData} modelData
|
||||||
* @param {(searchPath: string) => Promise<void>} showModelInfo
|
* @param {(searchPath: string) => Promise<void>} showModelInfo
|
||||||
|
* @param {() => void} updateModelGridCallback
|
||||||
* @param {any} settingsElements
|
* @param {any} settingsElements
|
||||||
*/
|
*/
|
||||||
constructor(updateModels, modelData, showModelInfo, settingsElements) {
|
constructor(updateModels, modelData, showModelInfo, updateModelGridCallback, settingsElements) {
|
||||||
/** @type {HTMLDivElement} */
|
/** @type {HTMLDivElement} */
|
||||||
const modelGrid = $el("div.comfy-grid");
|
const modelGrid = $el("div.comfy-grid");
|
||||||
this.elements.modelGrid = modelGrid;
|
this.elements.modelGrid = modelGrid;
|
||||||
@@ -2947,7 +3107,7 @@ class ModelTab {
|
|||||||
this.elements.modelContentFilter,
|
this.elements.modelContentFilter,
|
||||||
showModelInfo,
|
showModelInfo,
|
||||||
);
|
);
|
||||||
this.element.parentElement.scrollTop = 0;
|
updateModelGridCallback();
|
||||||
}
|
}
|
||||||
this.updateModelGrid = updateModelGrid;
|
this.updateModelGrid = updateModelGrid;
|
||||||
|
|
||||||
@@ -3009,7 +3169,7 @@ class ModelTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsTab {
|
class SettingsView {
|
||||||
/** @type {HTMLDivElement} */
|
/** @type {HTMLDivElement} */
|
||||||
element = null;
|
element = null;
|
||||||
|
|
||||||
@@ -3285,7 +3445,6 @@ class SidebarButtons {
|
|||||||
$: (el) => (this.element = el),
|
$: (el) => (this.element = el),
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|
||||||
$el("button.icon-button", {
|
$el("button.icon-button", {
|
||||||
textContent: "◨",
|
textContent: "◨",
|
||||||
onclick: (event) => this.#setSidebar(event),
|
onclick: (event) => this.#setSidebar(event),
|
||||||
@@ -3313,23 +3472,29 @@ class ModelManager extends ComfyDialog {
|
|||||||
/** @type {ModelData} */
|
/** @type {ModelData} */
|
||||||
#modelData = null;
|
#modelData = null;
|
||||||
|
|
||||||
/** @type {ModelInfoView} */
|
/** @type {ModelInfo} */
|
||||||
#modelInfoView = null;
|
#modelInfo = null;
|
||||||
|
|
||||||
/** @type {DownloadTab} */
|
/** @type {DownloadView} */
|
||||||
#downloadTab = null;
|
#downloadView = null;
|
||||||
|
|
||||||
/** @type {ModelTab} */
|
/** @type {BrowseView} */
|
||||||
#modelTab = null;
|
#browseView = null;
|
||||||
|
|
||||||
/** @type {SettingsTab} */
|
/** @type {SettingsView} */
|
||||||
#settingsTab = null;
|
#settingsView = null;
|
||||||
|
|
||||||
/** @type {HTMLDivElement} */
|
/** @type {HTMLDivElement} */
|
||||||
#tabs = null;
|
#tabManagerButtons = null;
|
||||||
|
|
||||||
/** @type {HTMLDivElement} */
|
/** @type {HTMLDivElement} */
|
||||||
#tabContents = null;
|
#tabManagerContents = null;
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
#tabInfoButtons = null;
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
#tabInfoContents = null;
|
||||||
|
|
||||||
/** @type {HTMLButtonElement} */
|
/** @type {HTMLButtonElement} */
|
||||||
#closeModelInfoButton = null;
|
#closeModelInfoButton = null;
|
||||||
@@ -3339,106 +3504,38 @@ class ModelManager extends ComfyDialog {
|
|||||||
|
|
||||||
this.#modelData = new ModelData();
|
this.#modelData = new ModelData();
|
||||||
|
|
||||||
const modelInfoView = new ModelInfoView(
|
this.#settingsView = new SettingsView(
|
||||||
|
this.#refreshModels,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#modelInfo = new ModelInfo(
|
||||||
this.#modelData,
|
this.#modelData,
|
||||||
this.#refreshModels,
|
this.#refreshModels,
|
||||||
);
|
);
|
||||||
this.#modelInfoView = modelInfoView;
|
|
||||||
|
|
||||||
const settingsTab = new SettingsTab(
|
this.#browseView = new BrowseView(
|
||||||
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.#refreshModels,
|
this.#refreshModels,
|
||||||
this.#modelData,
|
this.#modelData,
|
||||||
showModelInfo,
|
this.#showModelInfo,
|
||||||
this.#settingsTab.elements.settings, // TODO: decouple settingsData from elements?
|
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.#modelData,
|
||||||
this.#settingsTab.elements.settings,
|
this.#settingsView.elements.settings,
|
||||||
this.#refreshModels,
|
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 tabInfoButtons = this.#modelInfo.elements.tabButtons;
|
||||||
const head = {};
|
const tabInfoContents = this.#modelInfo.elements.tabContents;
|
||||||
|
|
||||||
/** @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 modelManager = $el(
|
const modelManager = $el(
|
||||||
"div.comfy-modal.model-manager",
|
"div.comfy-modal.model-manager",
|
||||||
@@ -3451,48 +3548,64 @@ class ModelManager extends ComfyDialog {
|
|||||||
$el("div.model-manager-panel", [
|
$el("div.model-manager-panel", [
|
||||||
$el("div.model-manager-head", [
|
$el("div.model-manager-head", [
|
||||||
$el("div.topbar-right", [
|
$el("div.topbar-right", [
|
||||||
closeManagerButton,
|
$el("button.icon-button", {
|
||||||
closeModelInfoButton,
|
textContent: "✖",
|
||||||
sidebarButtons.element,
|
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.topbar-left", [
|
||||||
$el("div.model-manager-tabs", {
|
$el("div", [
|
||||||
$: (el) => (this.#tabs = el),
|
$el("div.model-tab-group", {
|
||||||
}, tabs),
|
$: (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-body", [
|
||||||
$el("div.model-manager-tab-contents", {
|
$el("div", {
|
||||||
$: (el) => (this.#tabContents = el),
|
$: (el) => (this.#tabManagerContents = el)
|
||||||
}, contents),
|
}, tabManagerContents),
|
||||||
modelInfoView.element,
|
$el("div", {
|
||||||
|
$: (el) => (this.#tabInfoContents = el),
|
||||||
|
style: { display: "none"},
|
||||||
|
}, tabInfoContents),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
new ResizeObserver(() => {
|
new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabManagerButtons, 768)).observe(modelManager);
|
||||||
if (modelManager.style.display === "none") {
|
new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabInfoButtons, 768)).observe(modelManager);
|
||||||
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);
|
|
||||||
|
|
||||||
this.#init();
|
this.#init();
|
||||||
}
|
}
|
||||||
|
|
||||||
#init() {
|
#init() {
|
||||||
this.#settingsTab.reload(false);
|
this.#settingsView.reload(false);
|
||||||
this.#refreshModels();
|
this.#refreshModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#resetManagerContentsScroll = () => {
|
||||||
|
this.#tabManagerContents.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
#refreshModels = async() => {
|
#refreshModels = async() => {
|
||||||
const modelData = this.#modelData;
|
const modelData = this.#modelData;
|
||||||
modelData.systemSeparator = await request("/model-manager/system-separator");
|
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");
|
const newModelDirectories = await request("/model-manager/models/directory-list");
|
||||||
modelData.directories.data.splice(0, Infinity, ...newModelDirectories); // NOTE: do NOT create a new array
|
modelData.directories.data.splice(0, Infinity, ...newModelDirectories); // NOTE: do NOT create a new array
|
||||||
|
|
||||||
this.#modelTab.updateModelGrid();
|
this.#browseView.updateModelGrid();
|
||||||
await this.#tryHideModelInfo(false);
|
await this.#tryHideModelInfo(false);
|
||||||
|
|
||||||
document.getElementById("comfy-refresh-button")?.click();
|
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
|
* @param {boolean} promptSave
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
#tryHideModelInfo = async(promptSave) => {
|
#tryHideModelInfo = async(promptSave) => {
|
||||||
if (this.#tabContents.style.display === "none") {
|
if (this.#tabInfoContents.style.display !== "none") {
|
||||||
if (!await this.#modelInfoView.tryHide(promptSave)) {
|
if (!await this.#modelInfo.tryHide(promptSave)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#closeModelInfoButton.style.display = "none";
|
this.#closeModelInfoButton.style.display = "none";
|
||||||
this.#tabs.style.display = "";
|
this.#tabInfoButtons.style.display = "none";
|
||||||
this.#tabContents.style.display = "";
|
this.#tabInfoContents.style.display = "none";
|
||||||
|
|
||||||
|
this.#tabManagerButtons.style.display = "";
|
||||||
|
this.#tabManagerContents.style.display = "";
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user