From d6b197d9c7dca217bf7ce042c041c7b41099c859 Mon Sep 17 00:00:00 2001 From: Christian Bastian Date: Tue, 13 Feb 2024 00:48:09 -0500 Subject: [PATCH] Remade download tab. --- README.md | 26 +-- web/model-manager.css | 47 +++++- web/model-manager.js | 370 +++++++++++++++++++++++------------------- 3 files changed, 262 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 6e180b0..977b30a 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,34 @@ Currently it is still missing some features it should have. ## Fork Improvements +### Download Tab + +- Remade download tab. + - View multiple models connected to url. + - Download preview images. +- Civitai and HuggingFace API token configurable in `server_settings.yaml`. + +### Models Tab + - Search bar in models tab. - Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`. - Search `/`subdirectories of model directories based on your file structure (for example, `/0/1.5/styles/clothing`). - Add `/` at the start of the search bar to see auto-complete suggestions. - Include models listed in ComfyUI's `extra_model_paths.yaml` or added in `ComfyUI/models`. - Sort for models (Date Created, Date Modified, Name). + +### ComfyUI Node Graph + - Button to copy a model to the ComfyUI clipboard or embedding to system clipboard. (Embedding copying requires secure http connection.) - Button to add model to ComfyUI graph or embedding to selected nodes. (For small screens/low resolution.) - Right, left, top and bottom toggleable sidebar modes. - Drag a model onto the graph to add a new node. - Drag a model onto an existing node to set the model field. - Drag an embedding onto a text area to add it to the end. -- Increased supported preview image types. + +### Settings Tab + - Correctly change colors using ComfyUI's theme colors. -- Simplified UI. -- Civitai and HuggingFace API token configurable in `server_settings.yaml`. - Settings tab saved in `ui_settings.yaml`. - Hide/Show 'add' and 'copy-to-clipboard' buttons. - Text to always search. @@ -89,11 +101,3 @@ Currently it is still missing some features it should have. ### Sidebar - ☐ Drag sidebar width/height dynamically. - -### Directory Browser and Downloading tab - -(NOTE: It is a impossible to put a model automatically in the correct folder if model type information is not given or ambigious. To fully solve this requires making a file browser where files can be moved around.) - -- ☐ Replace Install tab with Downloading tab (more practical IMO). -- ☐ Download a model from a url. -- ☐ Choose save path/directory to download within vaild model directories. (Alert Yes/No if need to create new dirs?) diff --git a/web/model-manager.css b/web/model-manager.css index a01a1dd..f5829b7 100644 --- a/web/model-manager.css +++ b/web/model-manager.css @@ -55,7 +55,7 @@ gap: 16px; } -.comfy-grid .item { +.model-manager .item { position: relative; width: 230px; height: 345px; @@ -64,12 +64,29 @@ border-radius: 8px; } -.comfy-grid .item img { +.model-manager .item img { width: 100%; height: 100%; object-fit: cover; } +.model-manager .model-preview-button-left, +.model-manager .model-preview-button-right { + position: absolute; + top: 0; + bottom: 0; + margin: auto; + border-radius: 20px; +} + +.model-manager .model-preview-button-right { + right: 4px; +} + +.model-manager .model-preview-button-left { + left: 4px; +} + .comfy-grid .model-label { background-color: #000a; width: 100%; @@ -99,7 +116,7 @@ height: 0; } -.comfy-grid .item .model-preview-overlay { +.model-manager .item .model-preview-overlay { position: absolute; top: 0; left: 0; @@ -306,13 +323,18 @@ } .model-manager .row { - position: sticky; + position: relative; padding-top: 2px; margin-top: -2px; padding-bottom: 18px; margin-bottom: 1px; top: -1px; background-color: var(--comfy-input-bg); +} + +.model-manager [data-name="Install"] .row, +.model-manager [data-name="Models"] .row { + position: sticky; z-index: 1; } @@ -347,9 +369,15 @@ } .model-manager .search-text-area, -.model-manager .source-text-area, +.model-manager .plain-text-area, .model-manager .model-select-dropdown { flex: 1; + min-height: 36px; + padding-block: 0; +} + +.model-manager .model-select-dropdown { + min-height: 40px; } .model-manager .search-dropdown { @@ -424,3 +452,12 @@ border-radius: 8px; object-fit: cover; } + +.model-manager [data-name="Download"] summary { + padding: 16px; +} + +.model-manager .download-details { + border-radius: 16px; + padding: 0px 16px 0px 16px; +} \ No newline at end of file diff --git a/web/model-manager.js b/web/model-manager.js index 8b8a553..b42261e 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -1439,10 +1439,10 @@ class ModelManager extends ComfyDialog { ] ), $tabs([ - $tab("Install", this.#createSourceInstall()), + $tab("Download", [this.#downloadTab_new()]), + //$tab("Install", this.#createSourceInstall()), $tab("Models", this.#modelTab_new()), $tab("Settings", [this.#settingsTab_new()]), - //$tab("Download2", [this.#downloadTab_new()]), ]), ]), ] @@ -1453,7 +1453,7 @@ class ModelManager extends ComfyDialog { #init() { this.#settingsTab_reload(false); - this.#refreshSourceList(); + //this.#refreshSourceList(); this.#modelTab_updateModels(); } @@ -1471,7 +1471,7 @@ class ModelManager extends ComfyDialog { $: (el) => (this.#el.loadSourceBtn = el), onclick: () => this.#refreshSourceList(), }), - $el("input.source-text-area", { + $el("input.plain-text-area", { $: (el) => (this.#el.loadSourceFromInput = el), placeholder: "https://ComfyUI-Model-Manager/index.json", }), @@ -1718,7 +1718,7 @@ class ModelManager extends ComfyDialog { if (reloadData) { // Is this slow? - this.#refreshSourceList(); + //this.#refreshSourceList(); this.#modelTab_updateModels(); } } @@ -1940,11 +1940,11 @@ class ModelManager extends ComfyDialog { * @param {String[]} modelTypes * @param {DirectoryItem[]} modelDirectories * @param {String} sep + * @param {int} id * @returns {HTMLDivElement} */ - #downloadTab_modelInfo(info, modelTypes, modelDirectories, sep) { + #downloadTab_modelInfo(info, modelTypes, modelDirectories, sep, id) { // 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"; @@ -1962,14 +1962,14 @@ class ModelManager extends ComfyDialog { filename: null, }; - $el("input", { + $el("input.search-text-area", { $: (el) => (els.saveDirectoryPath = el), type: "text", placeholder: "/0", value: "/0", }); - $el("select", { + $el("select.model-select-dropdown", { $: (el) => (els.modelTypeSelect = el), }, (() => { const options = [$el("option", { value: "" }, ["-- Model Type --"])]; @@ -1997,172 +1997,208 @@ class ModelManager extends ComfyDialog { true, ); + const radioGroupName = "model-download-info-preview-model" + "-" + id; + const radioGroup = $radioGroup({ + name: radioGroupName, + 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 = "flex"; + 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; + })(), + }); + const filepath = info["downloadFilePath"]; - const modelInfo = $el("details", [ + const modelInfo = $el("details.download-details", [ $el("summary", [filepath + info["fileName"]]), - $el("div", [ - $el("div", [ - $el("button", { - onclick: async (e) => { - const record = {}; - record["download"] = info["downloadUrl"]; - record["type"] = els.modelTypeSelect.value; - if (record["type"] === "") { return; } // TODO: notify user in app - record["path"] = els.saveDirectoryPath.value; - if (record["path"] === "/") { return; } // TODO: notify user in app - record["name"] = (() => { - const filename = info["fileName"]; - const name = els.filename.value; - if (name === "") { - return filename; - } - const ext = MODEL_EXTENSIONS.find((ext) => { - return filename.endsWith(ext); - }) ?? ""; - return name + ext; - })(); - record["image"] = (() => { - 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 ""; - })(); - record["overwrite"] = true; // TODO: add to UI - e.disabled = true; - await request( - "/model-manager/download", - { - method: "POST", - body: JSON.stringify(record), - } - ).then(data => { - if (data["success"] !== true) { - // TODO: notify user in app - console.error('Failed to download model:', data); - } - }).catch(err => { - // TODO: notify user in app - console.error('Failed to download model:', err); + $el("div", { + style: { display: "flex", gap: "16px" }, + }, [ + $el("div.item", { + $: (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", }); - e.disabled = false; - }, - }, ["Download"]), - els.modelTypeSelect, - $el("div", [ - els.saveDirectoryPath, - searchDropdown.element, + }); + if (imgs.length > 0) { + imgs[0].style.display = "block"; + } + return imgs; + })()), + $el("div.model-preview-overlay", [ + $el("button.icon-button.model-preview-button-left", { + $: (el) => (els.buttonLeft = el), + onclick: () => ModelManager.#downloadTab_updatePreview(els.previewImgs, -1), + textContent: "←", + }), + $el("button.icon-button.model-preview-button-right", { + $: (el) => (els.buttonRight = el), + onclick: () => ModelManager.#downloadTab_updatePreview(els.previewImgs, 1), + textContent: "→", + }), ]), - $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("div", { + style: { "margin-top": "8px" } + }, [ + $el("div.model-preview-select-radio-container", [ + $el("div.row.tab-header-flex-block", [radioGroup]), $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.row.tab-header-flex-block", { + $: (el) => (els.customPreviewContainer = el), + style: { display: "none" }, + }, [ + $el("input.search-text-area", { + $: (el) => (els.customPreviewUrl = el), + type: "text", + placeholder: "https://custom-image-preview.png" + }), + ]), ]), ]), - $el("div", { - $: (el) => (els.customPreviewContainer = el), - style: { display: "none" }, - }, [ - $el("input.search-text-area", { - $: (el) => (els.customPreviewUrl = el), + $el("div.row.tab-header-flex-block", [ + els.modelTypeSelect, + ]), + $el("div.row.tab-header-flex-block", [ + els.saveDirectoryPath, + searchDropdown.element, + ]), + $el("div.row.tab-header-flex-block", [ + $el("button.icon-button", { + textContent: "📥︎", + onclick: async (e) => { + const record = {}; + record["download"] = info["downloadUrl"]; + record["type"] = els.modelTypeSelect.value; + if (record["type"] === "") { return; } // TODO: notify user in app + record["path"] = els.saveDirectoryPath.value; + if (record["path"] === "/") { return; } // TODO: notify user in app + record["name"] = (() => { + const filename = info["fileName"]; + const name = els.filename.value; + if (name === "") { + return filename; + } + const ext = MODEL_EXTENSIONS.find((ext) => { + return filename.endsWith(ext); + }) ?? ""; + return name + ext; + })(); + record["image"] = (() => { + const value = document.querySelector(`input[name="${radioGroupName}"]: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 ""; + })(); + record["overwrite"] = false; // TODO: add to UI + e.target.disabled = true; + let success = true; + let resultText = "✔"; + await request( + "/model-manager/download", + { + method: "POST", + body: JSON.stringify(record), + } + ).then(data => { + if (data["success"] !== true) { + // TODO: notify user in app + console.error('Failed to download model:', data); + success = false; + resultText = "📥︎"; + } + }).catch(err => { + // TODO: notify user in app + console.error('Failed to download model:', err); + success = false; + resultText = "📥︎"; + }); + buttonAlert(e.target, success, "✔", "✖", resultText); + e.target.disabled = success; + }, + }), + $el("input.plain-text-area", { + $: (el) => (els.filename = el), type: "text", - placeholder: "(preview image url)" + 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; + }); + })()), + */ ]), ]), ]); + if (info["images"].length > 0) { + const children = radioGroup.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const radioButton = child.children[0]; + if (radioButton.value === RADIO_MODEL_PREVIEW_DEFAULT) { + els.modelPreviewContainer.style.display = "block"; + radioButton.checked = true; + break; + } + }; + } + const modelTypeSelect = els.modelTypeSelect; modelTypeSelect.selectedIndex = 0; // reset const comfyUIModelType = ( @@ -2257,12 +2293,13 @@ class ModelManager extends ComfyDialog { return MODEL_EXTENSIONS.find((ext) => { return filename.endsWith(ext); }) ?? false; - }).map((modelInfo) => { + }).map((modelInfo, id) => { return this.#downloadTab_modelInfo( modelInfo, modelTypes, this.#data.modelDirectories, this.#sep, + id, ); }); if (modelInfos.length === 0) { @@ -2279,11 +2316,11 @@ class ModelManager extends ComfyDialog { */ #downloadTab_new() { return $el("div", [ - $el("div", [ + $el("div.row.tab-header-flex-block", [ $el("input.search-text-area", { $: (el) => (this.#el.modelInfoUrl = el), type: "text", - placeholder: "Civitai or HuggingFace model", + placeholder: "example: https://civitai.com/models/207992/stable-video-diffusion-svd", onkeydown: (e) => { if (e.key === "Enter") { e.stopPropagation(); @@ -2291,13 +2328,16 @@ class ModelManager extends ComfyDialog { } }, }), - $el("button", { + $el("button.icon-button", { onclick: () => this.#downloadTab_search(), - }, ["Search"]), + textContent: "🔍︎", + }), ]), $el("div", { $: (el) => (this.#el.modelInfos = el), - }), + }, [ + $el("div", ["Input a URL to view download settings."]), + ]), ]); } }