WIP: Download tab rewrite

- Slight improvements to directory auto-suggest dropdown
- Slight refactoring (code needs to be split into files)
This commit is contained in:
Christian Bastian
2024-02-12 13:59:02 -05:00
parent ec62b22a63
commit c94a85c3fa
2 changed files with 776 additions and 54 deletions

View File

@@ -139,10 +139,10 @@
.comfy-radio {
display: flex;
gap: 4px;
padding: 4px 8px;
padding: 4px 16px;
color: var(--input-text);
border: 2px solid var(--comfy-input-bg);
border-radius: 8px;
border: 2px solid var(--border-color);
border-radius: 16px;
background-color: var(--comfy-input-bg);
font-size: 18px;
}
@@ -356,8 +356,6 @@
position: absolute;
background-color: var(--bg-color);
border: 2px var(--border-color) solid;
top: 45px;
width: 94.7%;
max-height: 30vh;
overflow: auto;
border-radius: 10px;
@@ -415,3 +413,13 @@
border: solid 2px var(--border-color);
border-radius: 8px;
}
.model-preview-select-radio-container img {
position: relative;
width: 230px;
height: 345px;
text-align: center;
overflow: hidden;
border-radius: 8px;
object-fit: cover;
}

View File

@@ -55,6 +55,294 @@ const MODEL_SORT_DATE_CREATED = "dateCreated";
const MODEL_SORT_DATE_MODIFIED = "dateModified";
const MODEL_SORT_DATE_NAME = "name";
/**
* Tries to return the related ComfyUI model directory if unambigious.
*
* @param {string} modelType - Civitai model type.
* @param {string} [fileType] - Civitai file type. Relevant for "Diffusers".
*
* @returns {(string|null)} Logical base directory name for model type. May be null if the directory is ambiguous or not a model type.
*/
function civitai_comfyUiDirectory(modelType, fileType) {
if (fileType == "Diffusers") { return "diffusers"; } // TODO: is this correct?
// TODO: somehow allow for SERVER to set dir?
// TODO: allow user to choose EXISTING folder override/null? (style_models, HuggingFace) (use an object/map instead so settings can be dynamically set)
if (modelType == "AestheticGradient") { return null; }
else if (modelType == "Checkpoint") { return "checkpoints"; } // TODO: what about VAE?
//else if (modelType == "") { return "clip"; }
//else if (modelType == "") { return "clip_vision"; }
else if (modelType == "Controlnet") { return "controlnet"; }
//else if (modelType == "Controlnet") { return "style_models"; } // are these controlnets? (TI-Adapter)
//else if (modelType == "") { return "gligen"; }
else if (modelType == "Hypernetwork") { return "hypernetworks"; }
else if (modelType == "LORA") { return "loras"; }
else if (modelType == "LoCon") { return "loras"; }
else if (modelType == "MotionModule") { return null; }
else if (modelType == "Other") { return null; }
else if (modelType == "Pose") { return null; }
else if (modelType == "TextualInversion") { return "embeddings"; }
//else if (modelType == "") { return "unet"; }
else if (modelType == "Upscaler") { return "upscale_models"; }
else if (modelType == "VAE") { return "vae"; }
else if (modelType == "Wildcards") { return null; }
else if (modelType == "Workflows") { return null; }
return null;
}
/**
* Get model info from Civitai.
*
* @param {string} id - Model ID.
* @param {string} apiPath - Civitai request subdirectory. "models" for 'model' urls. "model-version" for 'api' urls.
*
* @returns {Object} Dictionary containing recieved model info. Returns an empty if fails.
*/
async function civitai_requestInfo(id, apiPath) {
const url = "https://civitai.com/api/v1/" + apiPath + "/" + id;
return await request(url);
}
/**
* Extract file information from the given model version infomation.
*
* @param {Object} modelVersionInfo - Model version infomation.
* @param {(string|null)} [type=null] - Optional select by model type.
* @param {(string|null)} [fp=null] - Optional select by floating point quantization.
* @param {(string|null)} [size=null] - Optional select by sizing.
* @param {(string|null)} [format=null] - Optional select by file format.
*
* @returns {Object} - Extracted list of infomation on each file of the given model version.
*/
function civitai_getModelFilesInfo(modelVersionInfo, type = null, fp = null, size = null, format = null) {
const files = [];
const modelVersionFiles = modelVersionInfo["files"];
for (let i = 0; i < modelVersionFiles.length; i++) {
const modelVersionFile = modelVersionFiles[i];
const fileType = modelVersionFile["type"];
if (type instanceof String && type != fileType) { continue; }
const fileMeta = modelVersionFile["metadata"];
const fileFp = fileMeta["fp"];
if (fp instanceof String && fp != fileFp) { continue; }
const fileSize = fileMeta["size"];
if (size instanceof String && size != fileSize) { continue; }
const fileFormat = fileMeta["format"];
if (format instanceof String && format != fileFormat) { continue; }
files.push({
"downloadUrl": modelVersionFile["downloadUrl"],
"format": fileFormat,
"fp": fileFp,
"hashes": modelVersionFile["hashes"],
"name": modelVersionFile["name"],
"size": fileSize,
"sizeKB": modelVersionFile["sizeKB"],
"type": fileType,
});
}
return {
"files": files,
"id": modelVersionInfo["id"],
"images": modelVersionInfo["images"].map((image) => {
// TODO: do I need to double-check image matches resource?
return image["url"];
}),
"name": modelVersionInfo["name"],
};
}
/**
*
*
* @param {string} stringUrl - Model url.
*
* @returns {Object} - Download information for the given url.
*/
async function civitai_getFilteredInfo(stringUrl) {
const url = new URL(stringUrl);
if (url.hostname != 'civitai.com') { return {}; }
if (url.pathname == '/') { return {} }
const urlPath = url.pathname;
if (urlPath.startsWith('/api')) {
const idEnd = urlPath.length - (urlPath.at(-1) == "/" ? 1 : 0);
const idStart = urlPath.lastIndexOf("/", idEnd - 1) + 1;
const modelVersionId = urlPath.substring(idStart, idEnd);
if (parseInt(modelVersionId, 10) == NaN) {
return {};
}
const modelVersionInfo = await civitai_requestInfo(modelVersionId, "model-versions");
if (Object.keys(modelVersionInfo).length == 0) {
return {};
}
const searchParams = url.searchParams;
const filesInfo = civitai_getModelFilesInfo(
modelVersionInfo,
searchParams.get("type"),
searchParams.get("fp"),
searchParams.get("size"),
searchParams.get("format"),
);
return {
"name": modelVersionInfo["model"]["name"],
"type": modelVersionInfo["model"]["type"],
"versions": [filesInfo]
}
}
else if (urlPath.startsWith('/models')) {
const idStart = urlPath.indexOf("/", 1) + 1;
const idEnd = urlPath.indexOf("/", idStart);
const modelId = urlPath.substring(idStart, idEnd);
if (parseInt(modelId, 10) == NaN) {
return {};
}
const modelInfo = await civitai_requestInfo(modelId, "models");
if (Object.keys(modelInfo).length == 0) {
return {};
}
const modelVersionId = parseInt(url.searchParams.get("modelVersionId"));
const modelVersions = [];
const modelVersionInfos = modelInfo["modelVersions"];
for (let i = 0; i < modelVersionInfos.length; i++) {
const versionInfo = modelVersionInfos[i];
if (modelVersionId instanceof String && modelVersionId != versionInfo["id"]) { continue; }
const filesInfo = civitai_getModelFilesInfo(versionInfo);
modelVersions.push(filesInfo);
}
return {
"name": modelInfo["name"],
"type": modelInfo["type"],
"versions": modelVersions
}
}
else {
return {};
}
}
/**
* Get model info from Huggingface.
*
* @param {string} id - Model ID.
* @param {string} apiPath - API path.
*
* @returns {Promise<Object>} Dictionary containing recieved model info. Returns an empty if fails.
*/
async function huggingFace_requestInfo(id, apiPath = "models") {
const url = "https://huggingface.co/api/" + apiPath + "/" + id;
return await request(url);
}
/**
*
*
* @param {string} stringUrl - Model url.
*
* @returns {Promise<Object>}
*/
async function huggingFace_getFilteredInfo(stringUrl) {
const url = new URL(stringUrl);
if (url.hostname != 'huggingface.co') { return {}; }
if (url.pathname == '/') { return {} }
const urlPath = url.pathname;
const i0 = 1;
const i1 = urlPath.indexOf("/", i0);
if (i1 == -1 || urlPath.length - 1 == i1) {
// user-name only
return {};
}
let i2 = urlPath.indexOf("/", i1 + 1);
if (i2 == -1) {
// model id only
i2 = urlPath.length;
}
const modelId = urlPath.substring(i0, i2);
const urlPathEnd = urlPath.substring(i2);
let branch = null;
if (urlPathEnd.startsWith("/resolve")) {
branch = "/resolve";
}
else if (urlPathEnd.startsWith("/blob")) {
branch = "/blob";
}
else if (urlPathEnd.startsWith("/tree")) {
branch = "/tree";
}
let filePath = "";
if (branch == null) {
branch = "/tree/main";
}
else {
const i0 = branch.length;
const i1 = urlPathEnd.indexOf("/", i0 + 1);
if (i1 == -1) {
if (i0 == urlPathEnd.length) {
// ends with '/tree' (invalid?)
branch = "/tree/main";
}
else {
// ends with branch
branch = "/tree" + urlPathEnd.substring(i0);
}
}
else {
branch = "/tree" + urlPathEnd.substring(i0, i1);
if (urlPathEnd.length - 1 > i1) {
filePath = urlPathEnd.substring(i1);
}
}
}
const modelInfo = await huggingFace_requestInfo(modelId);
//const modelInfo = await requestInfo(modelId + branch); // this only gives you the files at the given branch path...
// oid: SHA-1?, lfs.oid: SHA-256
const validModelExtensions = [".ckpt", ".pt", ".bin", ".pth", ".safetensors"]; // TODO: ask server for?
const clippedFilePath = filePath.substring(filePath[0] === "/" ? 1 : 0);
const modelFiles = modelInfo["siblings"].filter((sib) => {
const filename = sib["rfilename"];
for (let i = 0; i < validModelExtensions.length; i++) {
if (filename.endsWith(validModelExtensions[i])) {
return filename.startsWith(clippedFilePath);
}
}
return false;
}).map((sib) => {
const filename = sib["rfilename"];
return filename;
});
if (modelFiles.length === 0) {
return {};
}
const validImageExtensions = [".png", ".webp", ".gif"]; // TODO: ask server for?
const imageFiles = modelInfo["siblings"].filter((sib) => {
const filename = sib["rfilename"];
for (let i = 0; i < validImageExtensions.length; i++) {
if (filename.endsWith(validImageExtensions[i])) {
return filename.startsWith(filePath);
}
}
return false;
}).map((sib) => {
const filename = sib["rfilename"];
return filename;
});
const baseDownloadUrl = url.origin + urlPath.substring(0, i2) + "/resolve" + branch;
return {
"baseDownloadUrl": baseDownloadUrl,
"modelFiles": modelFiles,
"imageFiles": imageFiles,
};
}
/**
* @typedef {Object} DirectoryItem
* @param {string} name
@@ -68,23 +356,36 @@ class DirectoryDropdown {
/** @type {HTMLInputElement} */
#input = undefined;
// TODO: remove this
/** @type {Function} */
#submitSearch = null;
#updateDropdown = null;
/** @type {Function} */
#updateCallback = null;
/** @type {Function} */
#submitCallback = null;
/**
* @param {HTMLInputElement} input
* @param {Function} updateDropdown
* @param {Function} submitSearch
* @param {Function} [updateCallback= () => {}]
* @param {Function} [submitCallback= () => {}]
* @param {String} [sep="/"]
*/
constructor(input, updateDropdown, submitSearch) {
constructor(input, updateDropdown, updateCallback = () => {}, submitCallback = () => {}, sep = "/") {
/** @type {HTMLDivElement} */
const dropdown = $el("div.search-dropdown", { // TODO: change to `search-directory-dropdown`
style: { display: "none" },
style: {
display: "none",
},
});
this.element = dropdown;
this.#input = input;
this.#submitSearch = submitSearch;
this.#updateDropdown = updateDropdown;
this.#updateCallback = updateCallback;
this.#submitCallback = submitCallback;
input.addEventListener("input", () => updateDropdown());
input.addEventListener("focus", () => updateDropdown());
@@ -110,12 +411,35 @@ class DirectoryDropdown {
e.target.blur();
}
}
else if (e.key === "ArrowRight") {
const selection = options[iSelection];
if (selection !== undefined && selection !== null) {
e.stopPropagation();
e.preventDefault(); // prevent cursor move
DirectoryDropdown.submitSearch(
e.target,
selection,
updateDropdown,
updateCallback,
submitCallback,
sep,
);
}
}
else if (e.key === "Enter") {
e.stopPropagation();
submitSearch(e.target, options[iSelection]);
DirectoryDropdown.submitSearch(
e.target,
options[iSelection],
updateDropdown,
updateCallback,
submitCallback,
sep,
);
}
else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.stopPropagation();
e.preventDefault(); // prevent cursor move
let iNext = options.length;
if (iSelection < options.length) {
const selection = options[iSelection];
@@ -157,6 +481,34 @@ class DirectoryDropdown {
);
}
/**
* @param {HTMLInputElement} input
* @param {HTMLParagraphElement | undefined | null} selection
* @param {Function} updateDropdown
* @param {Fucntion} [updateCallback=() => {}]
* @param {Function} [submitCallback=() => {}]
* @param {String} [sep="/"]
*/
static submitSearch(input, selection, updateDropdown, updateCallback = () => {}, submitCallback = () => {}, sep = "/") {
let blur = true;
if (selection !== undefined && selection !== null) {
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
const selectedText = selection.innerText;
blur = !selectedText.endsWith(sep); // is directory
const oldFilterText = input.value;
const iSep = oldFilterText.lastIndexOf(sep);
const previousPath = oldFilterText.substring(0, iSep + 1);
input.value = previousPath + selectedText;
updateDropdown();
updateCallback();
}
if (blur) {
input.blur();
}
submitCallback();
}
/**
* @param {DirectoryItem[]} directories
* @param {string} sep
@@ -165,7 +517,9 @@ class DirectoryDropdown {
update(directories, sep, modelType = "") {
const dropdown = this.element;
const input = this.#input;
const submitSearch = this.#submitSearch;
const updateDropdown = this.#updateDropdown;
const updateCallback = this.#updateCallback;
const submitCallback = this.#submitCallback;
const filter = input.value;
if (filter[0] !== sep) {
@@ -239,9 +593,12 @@ class DirectoryDropdown {
const childCount = item["childCount"];
const items = directories.slice(childIndex, childIndex + childCount);
for (let i = 0; i < items.length; i++) {
const itemName = items[i]["name"];
const child = items[i];
const grandChildCount = child["childCount"];
const isDir = grandChildCount !== undefined && grandChildCount !== null && grandChildCount > 0;
const itemName = child["name"];
if (itemName.startsWith(lastWord)) {
options.push(itemName);
options.push(itemName + (isDir ? "/" : ""));
}
}
}
@@ -277,7 +634,14 @@ class DirectoryDropdown {
};
const selection_submit = (e) => {
e.stopPropagation();
submitSearch(input, e.target);
DirectoryDropdown.submitSearch(
input,
e.target,
updateDropdown,
updateCallback,
submitCallback,
sep
);
};
const innerHtml = options.map((text) => {
/** @type {HTMLParagraphElement} */
@@ -297,22 +661,13 @@ class DirectoryDropdown {
});
dropdown.innerHTML = "";
dropdown.append.apply(dropdown, innerHtml);
// TODO: handle when dropdown is near the bottom of the window
const inputRect = input.getBoundingClientRect();
dropdown.style.minWidth = inputRect.width + "px";
dropdown.style.top = (input.offsetTop + inputRect.height) + "px";
dropdown.style.left = input.offsetLeft + "px";
dropdown.style.display = "block";
}
/**
* @param {HTMLParagraphElement} selection
* @param {HTMLInputElement} input
* @param {string} sep
*/
static appendSelectionToInput(selection, input, sep) {
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
const selectedText = selection.innerText;
const oldFilterText = input.value;
const iSep = oldFilterText.lastIndexOf(sep);
const previousPath = oldFilterText.substring(0, iSep + 1);
input.value = previousPath + selectedText;
}
}
/**
@@ -958,6 +1313,9 @@ class ModelManager extends ComfyDialog {
/** @type {HTMLInputElement} */ loadSourceFromInput: null,
/** @type {HTMLSelectElement} */ sourceInstalledFilter: null,
/** @type {HTMLInputElement} */ sourceContentFilter: null,
/** @type {HTMLDivElement} */ modelInfoUrl: null,
/** @type {HTMLDivElement} */ modelInfos: null,
/** @type {HTMLDivElement} */ modelGrid: null,
/** @type {HTMLSelectElement} */ modelTypeSelect: null,
@@ -1041,6 +1399,7 @@ class ModelManager extends ComfyDialog {
$tab("Install", this.#createSourceInstall()),
$tab("Models", this.#modelTab_new()),
$tab("Settings", [this.#settingsTab_new()]),
//$tab("Download2", [this.#downloadTab_new()]),
]),
]),
]
@@ -1204,7 +1563,9 @@ class ModelManager extends ComfyDialog {
const searchDropdown = new DirectoryDropdown(
searchInput,
this.#modelTab_updateDirectoryDropdown,
this.#modelTab_submitSearch
this.#modelTab_updatePreviousModelFilter,
this.#modelTab_updateModelGrid,
this.#sep,
);
this.#modelContentFilterDirectoryDropdown = searchDropdown;
@@ -1227,10 +1588,10 @@ class ModelManager extends ComfyDialog {
onchange: () => this.#modelTab_updateModelGrid(),
},
[
$el("option", { value: MODEL_SORT_DATE_CREATED }, ["Date Created (newest to oldest)"]),
$el("option", { value: "-" + MODEL_SORT_DATE_CREATED }, ["Date Created (oldest to newest)"]),
$el("option", { value: MODEL_SORT_DATE_MODIFIED }, ["Date Modified (newest to oldest)"]),
$el("option", { value: "-" + MODEL_SORT_DATE_MODIFIED }, ["Date Modified (oldest to newest)"]),
$el("option", { value: MODEL_SORT_DATE_CREATED }, ["Created (newest to oldest)"]),
$el("option", { value: "-" + MODEL_SORT_DATE_CREATED }, ["Created (oldest to newest)"]),
$el("option", { value: MODEL_SORT_DATE_MODIFIED }, ["Modified (newest to oldest)"]),
$el("option", { value: "-" + MODEL_SORT_DATE_MODIFIED }, ["Modified (oldest to newest)"]),
$el("option", { value: MODEL_SORT_DATE_NAME }, ["Name (A-Z)"]),
$el("option", { value: "-" + MODEL_SORT_DATE_NAME }, ["Name (Z-A)"]),
],
@@ -1271,32 +1632,24 @@ class ModelManager extends ComfyDialog {
async #modelTab_updateModels() {
this.#data.models = await request("/model-manager/models");
this.#data.modelDirectories = await request("/model-manager/model-directory-list");
const newModelDirectories = await request("/model-manager/model-directory-list");
this.#data.modelDirectories.splice(0, Infinity, ...newModelDirectories); // note: do NOT create a new array
this.#modelTab_updateModelGrid();
}
#modelTab_updateDirectoryDropdown = () => {
#modelTab_updatePreviousModelFilter = () => {
const modelType = this.#el.modelTypeSelect.value;
const value = this.#el.modelContentFilter.value;
this.#data.previousModelFilters[modelType] = value;
};
#modelTab_updateDirectoryDropdown = () => {
this.#modelContentFilterDirectoryDropdown.update(
this.#data.modelDirectories,
this.#sep,
modelType,
this.#el.modelTypeSelect.value,
);
const value = this.#el.modelContentFilter.value;
this.#data.previousModelFilters[modelType] = value;
}
/**
* @param {HTMLInputElement} input
* @param {HTMLParagraphElement | undefined | null} selection
*/
#modelTab_submitSearch = (input, selection) => {
if (selection !== undefined && selection !== null) {
DirectoryDropdown.appendSelectionToInput(selection, input, this.#sep);
this.#modelTab_updateDirectoryDropdown();
}
input.blur();
this.#modelTab_updateModelGrid();
this.#modelTab_updatePreviousModelFilter();
}
/**
@@ -1512,6 +1865,367 @@ class ModelManager extends ComfyDialog {
modelManager.classList.add(newSidebarState);
}
}
/**
* @param {HTMLDivElement} previewImageContainer
* @param {Event} e
* @param {1 | -1} step
*/
static #downloadTab_updatePreview(previewImageContainer, step) {
const children = previewImageContainer.children;
if (children.length === 0) {
return;
}
let currentIndex = -step;
for (let i = 0; i < children.length; i++) {
const previewImage = children[i];
const display = previewImage.style.display;
if (display !== "none") {
currentIndex = i;
}
previewImage.style.display = "none";
}
currentIndex = currentIndex + step;
if (currentIndex >= children.length) { currentIndex = 0; }
else if (currentIndex < 0) { currentIndex = children.length - 1; }
children[currentIndex].style.display = "block";
}
/**
* @param {Object} info
* @param {String[]} modelTypes
* @param {DirectoryItem[]} modelDirectories
* @param {String} sep
* @returns {HTMLDivElement}
*/
#downloadTab_modelInfo(info, modelTypes, modelDirectories, sep) {
// TODO: use passed in info
const RADIO_MODEL_PREVIEW_GROUP_NAME = "model-download-info-preview-model";
const RADIO_MODEL_PREVIEW_DEFAULT = "Default Preview";
const RADIO_MODEL_PREVIEW_CUSTOM = "Custom Preview Url";
const els = {
modelPreviewContainer: null,
previewImgs: null,
buttonLeft: null,
buttonRight: null,
customPreviewContainer: null,
customPreviewUrl: null,
modelTypeSelect: null,
saveDirectoryPath: null,
filename: null,
};
const datas = {
cachedUrl: "",
};
$el("input", {
$: (el) => (els.saveDirectoryPath = el),
type: "text",
placeholder: "/0",
value: "/0",
});
$el("select", {
$: (el) => (els.modelTypeSelect = el),
}, (() => {
const options = [$el("option", { value: "" }, ["-- Model Type --"])];
modelTypes.forEach((modelType) => {
options.push($el("option", { value: modelType }, [modelType]));
});
return options;
})());
let searchDropdown = null;
searchDropdown = new DirectoryDropdown(
els.saveDirectoryPath,
() => {
const modelType = els.modelTypeSelect.value;
if (modelType === "") { return; }
searchDropdown.update(
modelDirectories,
sep,
modelType,
);
},
() => {},
() => {},
sep,
);
const filepath = info["downloadFilePath"];
const modelInfo = $el("details", [
$el("summary", [filepath + info["fileName"]]),
$el("div", [
$el("div", [
$el("button", {
onclick: (e) => {
const url = datas.cachedUrl;
const modelType = els.modelTypeSelect.value; // TODO: cannot be empty string or invalid selection
const path = els.saveDirectoryPath.value; // TODO: server: root must be valid
const filename = els.filename.value; // note: does not include file extension
const imgUrl = (() => {
const value = document.querySelector(`input[name="${RADIO_MODEL_PREVIEW_GROUP_NAME}"]:checked`).value;
switch (value) {
case RADIO_MODEL_PREVIEW_DEFAULT:
const children = els.previewImgs.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.style.display !== "none") {
return child.src;
}
}
return "";
case RADIO_MODEL_PREVIEW_CUSTOM:
return els.customPreviewUrl.value;
}
return "";
})();
// TODO: lock downloading
// TODO: send download info to server
// TODO: unlock downloading
},
}, ["Download"]),
els.modelTypeSelect,
$el("div", [
els.saveDirectoryPath,
searchDropdown.element,
]),
$el("input", {
$: (el) => (els.filename = el),
type: "text",
placeholder: (() => {
const filename = info["fileName"];
// TODO: only remove valid model file extensions
const i = filename.lastIndexOf(".");
return i === - 1 ? filename : filename.substring(0, i);
})(),
}),
]),
/*
$el("div", (() => {
return Object.entries(info["details"]).filter(([, value]) => {
return value !== undefined && value !== null;
}).map(([key, value]) => {
const el = document.createElement("p");
el.innerText = key + ": " + value;
return el;
});
})()),
*/
$el("div.model-preview-select-radio-container", [
$radioGroup({
name: RADIO_MODEL_PREVIEW_GROUP_NAME,
onchange: (value) => {
switch (value) {
case RADIO_MODEL_PREVIEW_DEFAULT:
const bottonStyleDisplay = els.previewImgs.children.length > 1 ? "block" : "none";
els.buttonLeft.style.display = bottonStyleDisplay;
els.buttonRight.style.display = bottonStyleDisplay;
els.modelPreviewContainer.style.display = "block";
els.customPreviewContainer.style.display = "none";
break;
case RADIO_MODEL_PREVIEW_CUSTOM:
els.modelPreviewContainer.style.display = "none";
els.customPreviewContainer.style.display = "block";
break;
default:
els.modelPreviewContainer.style.display = "none";
els.customPreviewContainer.style.display = "none";
break;
}
},
options: (() => {
const radios = [];
radios.push({ value: "No Preview" });
if (info["images"].length > 0) {
radios.push({ value: RADIO_MODEL_PREVIEW_DEFAULT });
}
radios.push({ value: RADIO_MODEL_PREVIEW_CUSTOM });
return radios;
})(),
}),
$el("div", [
$el("div", {
$: (el) => (els.modelPreviewContainer = el),
style: { display: "none" },
}, [
$el("div", {
$: (el) => (els.previewImgs = el),
}, (() => {
const imgs = info["images"].map((url) => {
return $el("img", {
src: url,
style: { display: "none" },
loading: "lazy",
});
});
if (imgs.length > 0) {
imgs[0].style.display = "block";
}
return imgs;
})()),
$el("div", [
$el("button", {
$: (el) => (els.buttonLeft = el),
onclick: () => ModelManager.#downloadTab_updatePreview(els.previewImgs, -1),
}, ["LEFT"]),
$el("button", {
$: (el) => (els.buttonRight = el),
onclick: () => ModelManager.#downloadTab_updatePreview(els.previewImgs, 1),
}, ["RIGHT"]),
]),
]),
$el("div", {
$: (el) => (els.customPreviewContainer = el),
style: { display: "none" },
}, [
$el("input.search-text-area", {
$: (el) => (els.customPreviewUrl = el),
type: "text",
placeholder: "(preview image url)"
}),
]),
]),
]),
]),
]);
const modelTypeSelect = els.modelTypeSelect;
modelTypeSelect.selectedIndex = 0; // reset
const comfyUIModelType = (
civitai_comfyUiDirectory(info["details"]["fileType"]) ??
civitai_comfyUiDirectory(info["modelType"]) ??
null
);
if (comfyUIModelType !== undefined && comfyUIModelType !== null) {
const modelTypeOptions = modelTypeSelect.children;
for (let i = 0; i < modelTypeOptions.length; i++) {
const option = modelTypeOptions[i];
if (option.value === comfyUIModelType) {
modelTypeSelect.selectedIndex = i;
break;
}
}
}
return modelInfo;
}
async #downloadTab_search() {
const infosHtml = this.#el.modelInfos;
infosHtml.innerHTML = "";
const urlText = this.#el.modelInfoUrl.value;
const modelInfos = await (async () => {
if (urlText.startsWith("https://civitai.com")) {
const civitaiInfo = await civitai_getFilteredInfo(urlText);
if (Object.keys(civitaiInfo).length === 0) {
return [];
}
const infos = [];
const type = civitaiInfo["type"];
civitaiInfo["versions"].forEach((version) => {
const images = version["images"];
version["files"].forEach((file) => {
infos.push({
"images": images,
"fileName": file["name"],
"modelType": type,
"downloadUrl": file["downloadUrl"],
"downloadFilePath": "",
"details": {
"fileSizeKB": file["sizeKB"],
"fileType": file["type"],
"fp": file["fp"],
"quant": file["size"],
"fileFormat": file["format"],
},
});
});
});
return infos;
}
if (urlText.startsWith("https://huggingface.co")) {
const hfInfo = await huggingFace_getFilteredInfo(urlText);
if (Object.keys(hfInfo).length === 0) {
return [];
}
const files = hfInfo["modelFiles"];
if (files.length === 0) {
return [];
}
const baseDownloadUrl = hfInfo["baseDownloadUrl"];
return hfInfo["modelFiles"].map((file) => {
const indexSep = file.lastIndexOf("/");
const filename = file.substring(indexSep + 1);
return {
"images": [], // TODO: ambiguous?
"fileName": filename,
"modelType": "",
"downloadUrl": baseDownloadUrl + "/" + file,
"downloadFilePath": file.substring(0, indexSep + 1),
"details": {
"fileSizeKB": undefined, // TODO: too hard?
},
};
});
}
if (urlText.endsWith(".json")) {
// TODO: support old index model files
return [];
}
return [];
})();
const modelTypes = Object.keys(this.#data.models);
const modelInfosHtml = modelInfos.map((modelInfo) => {
return this.#downloadTab_modelInfo(
modelInfo,
modelTypes,
this.#data.modelDirectories,
this.#sep,
);
});
if (modelInfos.length === 0) {
modelInfosHtml.push($el("div", ["No results found."]));
}
else if (modelInfos.length === 1) {
modelInfosHtml[0].open = true;
}
infosHtml.append.apply(infosHtml, modelInfosHtml);
}
/**
* @returns {HTMLElement}
*/
#downloadTab_new() {
return $el("div", [
$el("div", [
$el("input.search-text-area", {
$: (el) => (this.#el.modelInfoUrl = el),
type: "text",
placeholder: "Civitai or HuggingFace model",
onkeydown: (e) => {
if (e.key === "Enter") {
e.stopPropagation();
this.#downloadTab_search();
}
},
}),
$el("button", {
onclick: () => this.#downloadTab_search(),
}, ["Search"]),
]),
$el("div", {
$: (el) => (this.#el.modelInfos = el),
}),
]);
}
}
let instance;