diff --git a/__init__.py b/__init__.py index e6301a2..6f4ed60 100644 --- a/__init__.py +++ b/__init__.py @@ -185,6 +185,7 @@ def ui_rules(): Rule("model-add-embedding-extension", False, bool), Rule("model-add-drag-strict-on-field", False, bool), Rule("model-add-offset", 25, int), + Rule("download-save-description-as-text-file", False, bool), ] @@ -232,6 +233,7 @@ async def save_ui_settings(request): rules = ui_rules() validated_settings = config_loader.validated(rules, settings) success = config_loader.yaml_save(ui_settings_uri, rules, validated_settings) + print("Saved file: " + ui_settings_uri) return web.json_response({ "success": success, "settings": validated_settings if success else "", @@ -920,6 +922,7 @@ async def move_model(request): return web.json_response({ "success": False }) try: shutil.move(old_file, new_file) + print("Moved file: " + new_file) except ValueError as e: print(e, file=sys.stderr, flush=True) return web.json_response({ "success": False }) @@ -928,8 +931,10 @@ async def move_model(request): for extension in preview_extensions + (model_info_extension,): old_file = old_file_without_extension + extension if os.path.isfile(old_file): + new_file = new_file_without_extension + extension try: - shutil.move(old_file, new_file_without_extension + extension) + shutil.move(old_file, new_file) + print("Moved file: " + new_file) except ValueError as e: print(e, file=sys.stderr, flush=True) @@ -942,6 +947,7 @@ def delete_same_name_files(path_without_extension, extensions, keep_extension=No file = path_without_extension + extension if os.path.isfile(file): os.remove(file) + print("Deleted file: " + file) @server.PromptServer.instance.routes.post("/model-manager/model/delete") @@ -965,6 +971,7 @@ async def delete_model(request): if os.path.isfile(model_path): os.remove(model_path) result["success"] = True + print("Deleted file: " + model_path) delete_same_name_files(path_and_name, preview_extensions) delete_same_name_files(path_and_name, (model_info_extension,)) @@ -994,6 +1001,7 @@ async def set_notes(request): try: with open(filename, "w", encoding="utf-8") as f: f.write(text) + print("Saved file: " + filename) except ValueError as e: print(e, file=sys.stderr, flush=True) web.json_response({ "success": False }) diff --git a/web/model-manager.css b/web/model-manager.css index 66aeb63..a460585 100644 --- a/web/model-manager.css +++ b/web/model-manager.css @@ -239,10 +239,16 @@ word-wrap: break-word; } -.model-manager [data-name="Download"] .download-settings { +.model-manager [data-name="Download"] .download-settings-wrapper { flex: 1; } +.model-manager [data-name="Download"] .download-settings { + display: flex; + flex-direction: column; + row-gap: 8px; +} + .model-manager .download-model-infos { padding: 16px 0; } diff --git a/web/model-manager.js b/web/model-manager.js index 89bb090..42760a5 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -175,6 +175,31 @@ function buttonAlert(element, success, successText = "", failureText = "", reset }, 1000, element, name, resetText); } +/** + * + * @param {string} modelPath + * @param {string} newValue + * @returns {Promise} + */ +async function saveNotes(modelPath, newValue) { + return request( + "/model-manager/notes/save", + { + method: "POST", + body: JSON.stringify({ + "path": modelPath, + "notes": newValue, + }), + } + ).then((result) => { + return result["success"]; + }) + .catch((err) => { + console.warn(err); + return false; + }); +} + class Tabs { /** @type {Record} */ #head = {}; @@ -1938,7 +1963,8 @@ class ModelInfoView { if (noteValue.trim() !== savedNotesValue.trim()) { const saveChanges = window.confirm("Save notes?"); if (saveChanges) { - const saved = await this.#saveNotes(noteValue); + const path = this.elements.info.dataset.path; + const saved = await saveNotes(path, noteValue); if (!saved) { window.alert("Failed to save notes!"); return; @@ -1957,32 +1983,6 @@ class ModelInfoView { this.element.style.display = "none"; } - /** - * @param {string} newValue - * @returns {Promise} - */ - async #saveNotes(newValue) { - return request( - "/model-manager/notes/save", - { - method: "POST", - body: JSON.stringify({ - "path": this.elements.info.dataset.path, - "notes": newValue, - }), - } - ).then((result) => { - const success = result["success"]; - if (success) { - this.#savedNotesValue = newValue; - } - return success; - }) - .catch((err) => { - return false; - }); - } - /** * @param {string} searchPath * @param {() => Promise} updateModels @@ -2140,7 +2140,12 @@ class ModelInfoView { elements.push($el("button", { textContent: "Save Notes", onclick: async (e) => { - const saved = await this.#saveNotes(notes.value); + const path = this.elements.info.dataset.path; + const newValue = notes.value; + const saved = await saveNotes(path, newValue); + if (saved) { + this.#savedNotesValue = newValue; + } buttonAlert(e.target, saved); }, })); @@ -2240,6 +2245,7 @@ class Civitai { return image["url"]; }), "name": modelVersionInfo["name"], + "description": modelVersionInfo["description"] ?? "", }; } @@ -2275,6 +2281,7 @@ class Civitai { return { "name": modelVersionInfo["model"]["name"], "type": modelVersionInfo["model"]["type"], + "description": modelVersionInfo["description"] ?? "", "versions": [filesInfo] } } @@ -2306,7 +2313,8 @@ class Civitai { return { "name": modelInfo["name"], "type": modelInfo["type"], - "versions": modelVersions + "description": modelInfo["description"] ?? "", + "versions": modelVersions, } } else { @@ -2506,9 +2514,51 @@ class DownloadTab { /** @type {HTMLInputElement} */ overwrite: null, }; + /** @type {DOMParser} */ + #domParser = null; + /** @type {() => Promise} */ #updateModels = () => {}; + /** + * @param {ModelData} modelData + * @param {any} settings + * @param {() => Promise} updateModels + */ + constructor(modelData, settings, updateModels) { + this.#domParser = new DOMParser(); + this.#updateModels = updateModels; + const search = async() => this.search(modelData, settings); + $el("div.tab-header", { + $: (el) => (this.element = el), + }, [ + $el("div.row.tab-header-flex-block", [ + $el("input.search-text-area", { + $: (el) => (this.elements.url = el), + type: "text", + name: "model download url", + autocomplete: "off", + placeholder: "example: https://civitai.com/models/207992/stable-video-diffusion-svd", + onkeydown: (e) => { + if (e.key === "Enter") { + e.stopPropagation(); + search(); + } + }, + }), + $el("button.icon-button", { + onclick: () => search(), + textContent: "🔍︎", + }), + ]), + $el("div.download-model-infos", { + $: (el) => (this.elements.infos = el), + }, [ + $el("div", ["Input a URL to select a model to download."]), + ]), + ]); + } + /** * Tries to return the related ComfyUI model directory if unambiguous. * @@ -2554,9 +2604,10 @@ class DownloadTab { * @param {Object} info * @param {ModelData} modelData * @param {int} id + * @param {any} settings * @returns {HTMLDivElement} */ - #modelInfo(info, modelData, id) { + #modelInfo(info, modelData, id, settings) { const downloadPreviewSelect = new ImageSelect( "model-download-info-preview-model" + "-" + id, info["images"], @@ -2604,17 +2655,13 @@ class DownloadTab { style: { display: "flex", "flex-wrap": "wrap", gap: "16px" }, }, [ downloadPreviewSelect.elements.previews, - $el("div.download-settings", [ - $el("div", { - style: { "margin-top": "8px" } - }, [ + $el("div.download-settings-wrapper", [ + $el("div.download-settings", [ $el("button.icon-button", { textContent: "📥︎", onclick: async (e) => { - const formData = new FormData(); - formData.append("download", info["downloadUrl"]); - formData.append("path", el_saveDirectoryPath.value); - formData.append("name", (() => { + const pathDirectory = el_saveDirectoryPath.value; + const modelName = (() => { const filename = info["fileName"]; const name = el_filename.value; if (name === "") { @@ -2624,7 +2671,11 @@ class DownloadTab { return filename.endsWith(ext); }) ?? ""; return name + ext; - })()); + })(); + const formData = new FormData(); + formData.append("download", info["downloadUrl"]); + formData.append("path", pathDirectory); + formData.append("name", modelName); const image = await downloadPreviewSelect.getImage(); formData.append("image", image === PREVIEW_NONE_URI ? "" : image); formData.append("overwrite", this.elements.overwrite.checked); @@ -2645,6 +2696,14 @@ class DownloadTab { return [false, "📥︎"]; }); if (success) { + const description = info["description"]; + if (settings["download-save-description-as-text-file"].checked && description !== "") { + const modelPath = pathDirectory + searchSeparator + modelName; + const saved = await saveNotes(modelPath, description); + if (!saved) { + console.warn("Description was note saved as notes!"); + } + } this.#updateModels(); } buttonAlert(e.target, success, "✔", "✖", resultText); @@ -2669,8 +2728,9 @@ class DownloadTab { /** * @param {ModelData} modelData + * @param {any} settings */ - async search(modelData) { + async search(modelData, settings) { const infosHtml = this.elements.infos; infosHtml.innerHTML = ""; @@ -2683,8 +2743,11 @@ class DownloadTab { } const infos = []; const type = civitaiInfo["type"]; + const modelInfo = civitaiInfo["description"]?? ""; civitaiInfo["versions"].forEach((version) => { const images = version["images"]; + const versionDescription = version["description"]??""; + const description = (versionDescription + "\n\n" + modelInfo).trim().replace(/<[^>]+>/g, ""); // quick hack version["files"].forEach((file) => { infos.push({ "images": images, @@ -2692,6 +2755,7 @@ class DownloadTab { "modelType": type, "downloadUrl": file["downloadUrl"], "downloadFilePath": "", + "description": description, "details": { "fileSizeKB": file["sizeKB"], "fileType": file["type"], @@ -2724,6 +2788,7 @@ class DownloadTab { "modelType": "", "downloadUrl": baseDownloadUrl + "/" + file + "?download=true", "downloadFilePath": file.substring(0, indexSep + 1), + "description": "", "details": { "fileSizeKB": undefined, // TODO: too hard? }, @@ -2739,6 +2804,7 @@ class DownloadTab { "modelType": DownloadTab.modelTypeToComfyUiDirectory(file["type"], "") ?? "", "downloadUrl": file["download"], "downloadFilePath": "", + "description": file["description"], "details": {}, }; }); @@ -2756,6 +2822,7 @@ class DownloadTab { modelInfo, modelData, id, + settings, ); }); if (modelInfosHtml.length === 0) { @@ -2765,51 +2832,18 @@ class DownloadTab { if (modelInfosHtml.length === 1) { modelInfosHtml[0].open = true; } - const label = $checkbox({ - $: (el) => { this.elements.overwrite = el; }, - textContent: "Overwrite Existing Files", - }); - modelInfosHtml.unshift(label); + + const downloadSettings = $el("div", [ + $checkbox({ + $: (el) => { this.elements.overwrite = el; }, + textContent: "Overwrite Existing Files.", + checked: false, + }), + ]); + modelInfosHtml.unshift(downloadSettings); } infosHtml.append.apply(infosHtml, modelInfosHtml); } - - /** - * @param {ModelData} modelData - * @param {() => Promise} updateModels - */ - constructor(modelData, updateModels) { - this.#updateModels = updateModels; - const search = async() => this.search(modelData); - $el("div.tab-header", { - $: (el) => (this.element = el), - }, [ - $el("div.row.tab-header-flex-block", [ - $el("input.search-text-area", { - $: (el) => (this.elements.url = el), - type: "text", - name: "model download url", - autocomplete: "off", - placeholder: "example: https://civitai.com/models/207992/stable-video-diffusion-svd", - onkeydown: (e) => { - if (e.key === "Enter") { - e.stopPropagation(); - search(); - } - }, - }), - $el("button.icon-button", { - onclick: () => search(), - textContent: "🔍︎", - }), - ]), - $el("div.download-model-infos", { - $: (el) => (this.elements.infos = el), - }, [ - $el("div", ["Input a URL to select a model to download."]), - ]), - ]); - } } class ModelTab { @@ -2986,6 +3020,8 @@ class SettingsTab { /** @type {HTMLInputElement} */ "model-add-embedding-extension": null, /** @type {HTMLInputElement} */ "model-add-drag-strict-on-field": null, /** @type {HTMLInputElement} */ "model-add-offset": null, + + /** @type {HTMLInputElement} */ "download-save-description-as-text-file": null, }, }; @@ -3164,6 +3200,11 @@ class SettingsTab { }), $el("p", ["Add model offset"]), ]), + $el("h2", ["Download"]), + $checkbox({ + $: (el) => (settings["download-save-description-as-text-file"] = el), + textContent: "Save descriptions as notes (in .txt file).", + }), ]); } } @@ -3284,6 +3325,7 @@ class ModelManager extends ComfyDialog { const downloadTab = new DownloadTab( this.#modelData, + this.#settingsTab.elements.settings, this.#refreshModels, ); this.#downloadTab = DownloadTab;