From a4dd2f570b501c38e3bcf8bfe078c3271ceb1d47 Mon Sep 17 00:00:00 2001 From: Christian Bastian Date: Sun, 25 Feb 2024 23:02:02 -0500 Subject: [PATCH] Rename model in Model View added. - Generalized model/move to also support renaming. --- README.md | 13 ++++- __init__.py | 57 +++++++++++----------- web/model-manager.css | 5 +- web/model-manager.js | 108 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 135 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 17360f8..8fe4679 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ I made this fork because the original repo was inactive and missing many things - View model metadata, including training tags and bucket resolutions. - Read, edit and save notes in a `.txt` file beside the model. - Change or remove a model's preview image (add a different one using a url or local upload). -- Move or **permanently** remove models. +- Rename, move or **permanently** remove models. ### ComfyUI Node Graph @@ -55,8 +55,16 @@ I made this fork because the original repo was inactive and missing many things ## TODO +### Download Model + +- Checkbox to optionally save description in `.txt` file for Civitai. (what about "About Model"?) +- Server setting to enable creating new folders (on download, on move). + ### Download Model Info +- Auto-save notes? (requires debounce and save confirmation) +- Load workflow from preview (Should be easy to add with ComfyUI built-in clipboard.) +- Default weights on add/drag? (optional override on drag?) - Optional (re)download `📥︎` model info from the internet and cache the text file locally. (requires checksum?) - Radio buttons to swap between downloaded and server view. @@ -89,13 +97,14 @@ I made this fork because the original repo was inactive and missing many things - Filter directory dropdown - Filter directory content in auto-suggest dropdown (not clear how this should be implemented) - Filters dropdown - - Stable Diffusion model version, if applicable + - Stable Diffusion model version, if applicable (Maybe dropdown list of "Base Models" is more pratical to impliment?) - Favorites - Swap between `and` and `or` keyword search? (currently `and`) ### Code - Javascript cleanup. + - Stop abusing popup/modal. - Better abstraction and objectification. (After codebase settles down) - Separate into classes per tab? - HTML generation all inside main class? diff --git a/__init__.py b/__init__.py index 45d7e6e..f48a348 100644 --- a/__init__.py +++ b/__init__.py @@ -486,14 +486,9 @@ def download_file(url, filename, overwrite): def download_image(image_uri, model_path, overwrite): - extension = None # TODO: doesn't work for https://civitai.com/images/... - for image_extension in image_extensions: - if image_uri.endswith(image_extension): - extension = image_extension - break - if extension is None: + _, extension = os.path.splitext(image_uri) # TODO: doesn't work for https://civitai.com/images/... + if not extension in image_extensions: raise ValueError("Invalid image type!") - path_without_extension, _ = os.path.splitext(model_path) file = path_without_extension + extension download_file(image_uri, file, overwrite) @@ -607,12 +602,8 @@ async def download_model(request): return web.json_response(result) name = formdata.get("name") - model_extension = None - for ext in folder_paths_get_supported_pt_extensions(model_type): - if name.endswith(ext): - model_extension = ext - break - if model_extension is None: + _, model_extension = os.path.splitext(name) + if not model_extension in folder_paths_get_supported_pt_extensions(model_type): result["invalid"] = "name" return web.json_response(result) file_name = os.path.join(directory, name) @@ -646,21 +637,30 @@ async def move_model(request): old_file = body.get("oldFile", None) if old_file is None: return web.json_response({ "success": False }) - old_file, _ = search_path_to_system_path(old_file) + old_file, old_model_type = search_path_to_system_path(old_file) if not os.path.isfile(old_file): return web.json_response({ "success": False }) - _, filename = os.path.split(old_file) - - new_path = body.get("newDirectory", None) - if new_path is None: - return web.json_response({ "success": False }) - new_path, _ = search_path_to_system_path(new_path) - if new_path is None: - return web.json_response({ "success": False }) - if not os.path.isdir(new_path): + _, model_extension = os.path.splitext(old_file) + if not model_extension in folder_paths_get_supported_pt_extensions(old_model_type): + # cannot move arbitrary files + return web.json_response({ "success": False }) + + new_file = body.get("newFile", None) + if new_file is None or new_file == "": + # cannot have empty name + return web.json_response({ "success": False }) + new_file, new_model_type = search_path_to_system_path(new_file) + if not new_file.endswith(model_extension): + return web.json_response({ "success": False }) + if os.path.isfile(new_file): + # cannot overwrite existing file + return web.json_response({ "success": False }) + if not model_extension in folder_paths_get_supported_pt_extensions(new_model_type): + return web.json_response({ "success": False }) + new_file_dir, _ = os.path.split(new_file) + if not os.path.isdir(new_file_dir): return web.json_response({ "success": False }) - new_file = os.path.join(new_path, filename) if old_file == new_file: return web.json_response({ "success": False }) try: @@ -704,12 +704,9 @@ async def delete_model(request): if file is None: return web.json_response(result) - is_model = None - for ext in folder_paths_get_supported_pt_extensions(model_type): - if file.endswith(ext): - is_model = True - break - if not is_model: + _, extension = os.path.split(file) + if not extension in folder_paths_get_supported_pt_extensions(model_type): + # cannot move arbitrary files return web.json_response(result) if os.path.isfile(file): diff --git a/web/model-manager.css b/web/model-manager.css index a2ec050..3b5e392 100644 --- a/web/model-manager.css +++ b/web/model-manager.css @@ -225,6 +225,10 @@ } /* model manager common */ +.model-manager h1 { + min-width: 0; +} + .model-manager button, .model-manager select, .model-manager input { @@ -504,7 +508,6 @@ background-color: var(--bg-color); border-radius: 16px; color: var(--fg-color); - padding: 16px; width: auto; } diff --git a/web/model-manager.js b/web/model-manager.js index b90f5b1..ab1bb1f 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -750,11 +750,24 @@ function modelWidgetIndex(nodeType) { /** * @param {string} path - * @returns {string} + * @returns {[string, string]} */ -function pathToFileString(path) { +function searchPath_split(path) { const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1; - return path.slice(i); + return [path.slice(0, i), path.slice(i)]; +} + +/** + * @param {string} path + * @param {string[]} extensions + * @returns {[string, string]} + */ +function searchPath_splitExtension(path) { + const i = path.lastIndexOf("."); + if (i === -1) { + return [path, ""]; + } + return [path.slice(0, i), path.slice(i)]; } /** @@ -1016,7 +1029,7 @@ class ModelGrid { event.stopPropagation(); } else if (modelType === "embeddings") { - const embeddingFile = pathToFileString(path); + const [embeddingDirectory, embeddingFile] = searchPath_split(path); const selectedNodes = app.canvas.selected_nodes; for (var i in selectedNodes) { const selectedNode = selectedNodes[i]; @@ -1079,7 +1092,7 @@ class ModelGrid { const nodeAtPos = app.graph.getNodeOnPos(pos[0], pos[1], app.canvas.visible_nodes); if (nodeAtPos) { app.canvas.selectNode(nodeAtPos); - const embeddingFile = pathToFileString(path); + const [embeddingDirectory, embeddingFile] = searchPath_split(path); target.value = insertEmbeddingIntoText(target.value, embeddingFile, removeEmbeddingExtension); event.stopPropagation(); } @@ -1097,7 +1110,7 @@ class ModelGrid { let success = false; if (nodeType === "Embedding") { if (navigator.clipboard){ - const embeddingFile = pathToFileString(path); + const [embeddingDirectory, embeddingFile] = searchPath_split(path); const embeddingText = insertEmbeddingIntoText("", embeddingFile, removeEmbeddingExtension); navigator.clipboard.writeText(embeddingText); success = true; @@ -1669,12 +1682,12 @@ class ModelManager extends ComfyDialog { constructor() { super(); - const moveDestination = $el("input.search-text-area", { + const moveDestinationInput = $el("input.search-text-area", { placeholder: "/", }); let searchDropdown = null; searchDropdown = new DirectoryDropdown( - moveDestination, + moveDestinationInput, () => { searchDropdown.update( this.#data.modelDirectories, @@ -1811,7 +1824,7 @@ class ModelManager extends ComfyDialog { }, }), $el("div.search-models", [ - moveDestination, + moveDestinationInput, searchDropdown.element, ]), $el("button", { @@ -1821,21 +1834,30 @@ class ModelManager extends ComfyDialog { let moved = false; if (confirmation) { const container = this.#el.modelInfoContainer; + const oldFile = container.dataset.path; + const [oldFilePath, oldFileName] = searchPath_split(oldFile); + const [_, extension] = searchPath_splitExtension(oldFile); + const newFile = ( + moveDestinationInput.value + + this.#searchSeparator + + oldFileName + + extension + ); moved = await request( `/model-manager/model/move`, { method: "POST", body: JSON.stringify({ - "oldFile": container.dataset.path, - "newDirectory": moveDestination.value, + "oldFile": oldFile, + "newFile": newFile, }), } ) .then((result) => { const moved = result["success"]; - if (moved) + if (moved) { - moveDestination.value = ""; + moveDestinationInput.value = ""; container.innerHTML = ""; this.#el.modelInfoView.style.display = "none"; this.#modelTab_updateModels(); @@ -2043,7 +2065,63 @@ class ModelManager extends ComfyDialog { const innerHtml = []; const filename = info["File Name"]; if (filename !== undefined && filename !== null && filename !== "") { - innerHtml.push($el("h1", [filename])); + innerHtml.push( + $el("div.row", { + style: { margin: "8px 0 16px 0" }, + }, [ + $el("h1", { + style: { margin: "0" }, + }, [ + filename, + ]), + $el("div", [ + $el("button.icon-button", { + textContent: "✎", + onclick: async(e) => { + const name = window.prompt("New model name:"); + let renamed = false; + if (name !== null && name !== "") { + const container = this.#el.modelInfoContainer; + const oldFile = container.dataset.path; + const [oldFilePath, oldFileName] = searchPath_split(oldFile); + const [_, extension] = searchPath_splitExtension(oldFile); + const newFile = ( + oldFilePath + + this.#searchSeparator + + name + + extension + ); + renamed = await request( + `/model-manager/model/move`, + { + method: "POST", + body: JSON.stringify({ + "oldFile": oldFile, + "newFile": newFile, + }), + } + ) + .then((result) => { + const renamed = result["success"]; + if (renamed) + { + container.innerHTML = ""; + this.#el.modelInfoView.style.display = "none"; + this.#modelTab_updateModels(); + } + return renamed; + }) + .catch(err => { + console.log(err); + return false; + }); + } + buttonAlert(e.target, renamed); + }, + }), + ]), + ]), + ); } if (info["Preview"]) { @@ -2102,7 +2180,7 @@ class ModelManager extends ComfyDialog { elements.push($el("h2", [key + ":"])); const noteArea = $el("textarea.comfy-multiline-input", { value: value, - rows: 5, + rows: 10, }); elements.push(noteArea); elements.push($el("button", {