From 4d3be1844b75db7afc76ec70ec46356ce5f03cdb Mon Sep 17 00:00:00 2001 From: Christian Bastian Date: Wed, 7 Feb 2024 17:00:07 -0500 Subject: [PATCH] Model tab sort dropdown --- README.md | 10 ++---- __init__.py | 22 ++++++++---- web/model-manager.css | 2 +- web/model-manager.js | 82 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 2e31d91..3c370dc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Browse models in ComfyUI. (Downloading and deleting are WIP.) ## About this fork -I made this fork because the original repo was inactive and missing many things I needed to make the ComfyUI usable. Also, many other custom nodes bundle unrelated features together or search the internet in the background. +I made this fork because the original repo was inactive and missing many things I needed to make ComfyUI more usable. Also, many other custom nodes bundle unrelated features together or search the internet in the background. Currently it is still missing some features it should have. @@ -17,6 +17,7 @@ Currently it is still missing some features it should have. - 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). - 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. @@ -65,12 +66,7 @@ Currently it is still missing some features it should have. - ☐ Directory dropdown - ☐ Use always filter to filter directory content auto-suggest dropdown - ☐ Generalize model list filtering code to reuse approach - - ☐ Highlight selection - - ☐ Key shortcuts - - ☐ Down & Up arrows to select dropdown option - - ☐ Right arrow or enter to add selection - - ☐ Escape to loose focus on dropdown - - ☐ Generalize search dropdown for download location selection + - ☐ Generalize search dropdown for download location selection - ☐ Filters dropdown - ☐ Stable Diffusion model version/Clip/Upscale/? - ☐ Favorites diff --git a/__init__.py b/__init__.py index d58dd68..c20bb85 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ import os +import pathlib import sys import copy import hashlib @@ -268,7 +269,7 @@ async def load_download_models(request): models = {} for model_type in model_types: model_extensions = tuple(folder_paths_get_supported_pt_extensions(model_type)) - file_names = [] + file_infos = [] for base_path_index, model_base_path in enumerate(folder_paths_get_folder_paths(model_type)): if not os.path.exists(model_base_path): # Bug in main code? continue @@ -290,17 +291,26 @@ async def load_download_models(request): if model_name == image_name: image = end_swap_and_pop(dir_images, iImage) break + abs_path = os.path.join(cwd, model) + stats = pathlib.Path(abs_path).stat() + date_modified = stats.st_mtime_ns + date_created = stats.st_ctime_ns rel_path = "" if cwd == model_base_path else os.path.relpath(cwd, model_base_path) - file_names.append((model, image, base_path_index, rel_path)) - file_names.sort(key=lambda tup: tup[0].lower()) + info = (model, image, base_path_index, rel_path, date_modified, date_created) + file_infos.append(info) + file_infos.sort(key=lambda tup: tup[4], reverse=True) # TODO: remove sort; sorted on client model_items = [] - for model, image, base_path_index, rel_path in file_names: - # TODO: Stop sending redundant information + for model, image, base_path_index, rel_path, date_modified, date_created in file_infos: + # TODO: Stop sending redundant path information item = { "name": model, - "search-path": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack + "searchPath": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack "path": os.path.join(rel_path, model), + "dateModified": date_modified, + "dateCreated": date_created, + #"dateLastUsed": "", # TODO: track server-side, send increment client-side + #"countUsed": 0, # TODO: track server-side, send increment client-side } if image is not None: raw_post = os.path.join(model_type, str(base_path_index), rel_path, image) diff --git a/web/model-manager.css b/web/model-manager.css index a9bd543..22eb423 100644 --- a/web/model-manager.css +++ b/web/model-manager.css @@ -342,7 +342,7 @@ .model-manager .search-text-area, .model-manager .source-text-area, -.model-manager .model-type-dropdown { +.model-manager .model-select-dropdown { flex: 1; } diff --git a/web/model-manager.js b/web/model-manager.js index a373498..8f6f714 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -51,6 +51,10 @@ const modelNodeType = { const DROPDOWN_DIRECTORY_SELECTION_CLASS = "search-dropdown-selected"; +const MODEL_SORT_DATE_CREATED = "dateCreated"; +const MODEL_SORT_DATE_MODIFIED = "dateModified"; +const MODEL_SORT_DATE_NAME = "name"; + /** * @typedef {Object} DirectoryItem * @param {string} name @@ -594,7 +598,7 @@ class ModelGrid { static #filter(list, searchString) { /** @type {string[]} */ const keywords = searchString - .replace("*", " ") + //.replace("*", " ") // TODO: this is wrong for wildcards .split(/(-?".*?"|[^\s"]+)+/g) .map((item) => item .trim() @@ -603,7 +607,7 @@ class ModelGrid { .filter(Boolean); const regexSHA256 = /^[a-f0-9]{64}$/gi; - const fields = ["name", "search-path"]; // TODO: Remove "search-path" hack. + const fields = ["name", "searchPath"]; // TODO: Remove "searchPath" hack. return list.filter((element) => { const text = fields .reduce((memo, field) => memo + " " + element[field], "") @@ -621,6 +625,33 @@ class ModelGrid { }, true); }); } + + /** + * In-place sort. Returns an arrat alias. + * @param {Array} list + * @param {string} sortBy + * @param {bool} [reverse=false] + * @returns {Array} + */ + static #sort(list, sortBy, reverse = false) { + let compareFn = undefined; + switch (sortBy) { + case MODEL_SORT_DATE_NAME: + compareFn = (a, b) => { return a[MODEL_SORT_DATE_NAME].localeCompare(b[MODEL_SORT_DATE_NAME]); }; + break; + case MODEL_SORT_DATE_MODIFIED: + compareFn = (a, b) => { return b[MODEL_SORT_DATE_MODIFIED] - a[MODEL_SORT_DATE_MODIFIED]; }; + break; + case MODEL_SORT_DATE_CREATED: + compareFn = (a, b) => { return b[MODEL_SORT_DATE_CREATED] - a[MODEL_SORT_DATE_CREATED]; }; + break; + default: + console.warn("Invalid filter sort value: '" + sortBy + "'"); + return list; + } + const sorted = list.sort(compareFn); + return reverse ? sorted.reverse() : sorted; + } /** * @param {Event} event @@ -836,10 +867,12 @@ class ModelGrid { * @param {HTMLSelectElement} modelSelect * @param {Object.<{value: string}>} previousModelType * @param {Object} settings + * @param {string} sortBy + * @param {boolean} reverseSort * @param {Array} previousModelFilters * @param {HTMLInputElement} modelFilter */ - static update(modelGrid, models, modelSelect, previousModelType, settings, previousModelFilters, modelFilter) { + static update(modelGrid, models, modelSelect, previousModelType, settings, sortBy, reverseSort, previousModelFilters, modelFilter) { let modelType = modelSelect.value; if (models[modelType] === undefined) { modelType = "checkpoints"; // TODO: magic value @@ -870,6 +903,7 @@ class ModelGrid { const searchAppend = settings["model-search-always-append"].value; const searchText = modelFilter.value + " " + searchAppend; const modelList = ModelGrid.#filter(models[modelType], searchText); + ModelGrid.#sort(modelList, sortBy, reverseSort); modelGrid.innerHTML = ""; const modelGridModels = ModelGrid.#generateInnerHtml(modelList, modelType, settings); @@ -927,6 +961,7 @@ class ModelManager extends ComfyDialog { /** @type {HTMLDivElement} */ modelGrid: null, /** @type {HTMLSelectElement} */ modelTypeSelect: null, + /** @type {HTMLSelectElement} */ modelSortSelect: null, /** @type {HTMLDivElement} */ searchDirectoryDropdown: null, /** @type {HTMLInputElement} */ modelContentFilter: null, @@ -1181,11 +1216,25 @@ class ModelManager extends ComfyDialog { textContent: "⟳", onclick: () => this.#modelTab_updateModels(), }), - $el("select.model-type-dropdown", { + $el("select.model-select-dropdown", { $: (el) => (this.#el.modelTypeSelect = el), name: "model-type", onchange: () => this.#modelTab_updateModelGrid(), }), + $el("select.model-select-dropdown", + { + $: (el) => (this.#el.modelSortSelect = el), + 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_NAME }, ["Name (A-Z)"]), + $el("option", { value: "-" + MODEL_SORT_DATE_NAME }, ["Name (Z-A)"]), + ], + ), ]), $el("div.row.tab-header-flex-block", [ $el("div.search-models", [ @@ -1203,15 +1252,22 @@ class ModelManager extends ComfyDialog { ]; } - #modelTab_updateModelGrid = () => ModelGrid.update( - this.#el.modelGrid, - this.#data.models, - this.#el.modelTypeSelect, - this.#data.previousModelType, - this.#el.settings, - this.#data.previousModelFilters, - this.#el.modelContentFilter - ); + #modelTab_updateModelGrid = () => { + const sortValue = this.#el.modelSortSelect.value; + const reverseSort = sortValue[0] === "-"; + const sortBy = reverseSort ? sortValue.substring(1) : sortValue; + ModelGrid.update( + this.#el.modelGrid, + this.#data.models, + this.#el.modelTypeSelect, + this.#data.previousModelType, + this.#el.settings, + sortBy, + reverseSort, + this.#data.previousModelFilters, + this.#el.modelContentFilter + ); + } async #modelTab_updateModels() { this.#data.models = await request("/model-manager/models");