From f67cac9887a75ce1d01f2c86b45292bfd4ddb256 Mon Sep 17 00:00:00 2001
From: Christian Bastian <80225746+cdb-boop@users.noreply.github.com>
Date: Thu, 25 Jul 2024 05:29:26 -0400
Subject: [PATCH] Add optional autosave notes. - By default it is disabled
because once the model info is closed, the changes are gone.
---
README.md | 2 +-
__init__.py | 20 +++++++++-
web/model-manager.js | 88 ++++++++++++++++++++++++++++++--------------
3 files changed, 79 insertions(+), 31 deletions(-)
diff --git a/README.md b/README.md
index 3e1e65d..05b27d2 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ Download, browse and delete models in ComfyUI.
- View model metadata and file info.
-- Read, edit and save notes in a `.txt` file beside the model.
+- Read, edit and save notes in a `.txt` file beside the model. (Autosave optional.)
- Change or remove a model's preview image (add a different one using a url or local upload).
- Rename, move or **permanently** remove model files.
diff --git a/__init__.py b/__init__.py
index 34bd6ee..3aca269 100644
--- a/__init__.py
+++ b/__init__.py
@@ -182,6 +182,7 @@ def ui_rules():
Rule("model-real-time-search", True, bool),
Rule("model-persistent-search", True, bool),
Rule("model-show-label-extensions", False, bool),
+ Rule("model-info-autosave-notes", False, bool),
Rule("model-preview-fallback-search-safetensors-thumbnail", False, bool),
Rule("model-show-add-button", True, bool),
Rule("model-show-copy-button", True, bool),
@@ -228,6 +229,11 @@ def get_def_headers(url=""):
return def_headers
+@server.PromptServer.instance.routes.get("/model-manager/timestamp")
+async def get_timestamp(request):
+ return web.json_response({ "timestamp": datetime.now().timestamp() })
+
+
@server.PromptServer.instance.routes.get("/model-manager/settings/load")
async def load_ui_settings(request):
rules = ui_rules()
@@ -1166,6 +1172,8 @@ async def set_notes(request):
body = await request.json()
result = { "success": False }
+ dt_epoch = body.get("timestamp", None)
+
text = body.get("notes", None)
if type(text) is not str:
result["alert"] = "Invalid note!"
@@ -1179,15 +1187,23 @@ async def set_notes(request):
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
file_path_without_extension, _ = split_valid_ext(model_path, model_extensions)
filename = os.path.normpath(file_path_without_extension + model_info_extension)
+
+ if dt_epoch is not None and os.path.exists(filename) and os.path.getmtime(filename) > dt_epoch:
+ # discard late save
+ result["success"] = True
+ return web.json_response(result)
+
if text.isspace() or text == "":
if os.path.exists(filename):
os.remove(filename)
- print("Deleted file: " + filename)
+ #print("Deleted file: " + filename) # autosave -> too verbose
else:
try:
with open(filename, "w", encoding="utf-8") as f:
f.write(text)
- print("Saved file: " + filename)
+ if dt_epoch is not None:
+ os.utime(filename, (dt_epoch, dt_epoch))
+ #print("Saved file: " + filename) # autosave -> too verbose
except ValueError as e:
print(e, file=sys.stderr, flush=True)
result["alert"] = "Failed to save notes!\n\n" + str(e)
diff --git a/web/model-manager.js b/web/model-manager.js
index 4976220..7147508 100644
--- a/web/model-manager.js
+++ b/web/model-manager.js
@@ -17,6 +17,21 @@ function request(url, options = undefined) {
});
}
+/**
+ * @param {(...args) => Promise} callback
+ * @param {number | undefined} delay
+ * @returns {(...args) => void}
+ */
+function debounce(callback, delay) {
+ let timeoutId = null;
+ return (...args) => {
+ window.clearTimeout(timeoutId);
+ timeoutId = window.setTimeout(() => {
+ callback(...args);
+ }, delay);
+ };
+}
+
/**
* @param {string} url
*/
@@ -153,21 +168,6 @@ const PREVIEW_NONE_URI = imageUri();
const PREVIEW_THUMBNAIL_WIDTH = 320;
const PREVIEW_THUMBNAIL_HEIGHT = 480;
-/**
- * @param {(...args) => void} callback
- * @param {number | undefined} delay
- * @returns {(...args) => void}
- */
-function debounce(callback, delay) {
- let timeoutId = null;
- return (...args) => {
- window.clearTimeout(timeoutId);
- timeoutId = window.setTimeout(() => {
- callback(...args);
- }, delay);
- };
-}
-
/**
*
* @param {HTMLButtonElement} element
@@ -248,6 +248,11 @@ function comfyButtonAlert(element, success, successClassName = undefined, failur
* @returns {Promise}
*/
async function saveNotes(modelPath, newValue) {
+ const timestamp = await request("/model-manager/timestamp")
+ .catch((err) => {
+ console.warn(err);
+ return false;
+ });
return await request(
"/model-manager/notes/save",
{
@@ -256,6 +261,7 @@ async function saveNotes(modelPath, newValue) {
"path": modelPath,
"notes": newValue,
}),
+ timestamp: timestamp,
}
).then((result) => {
const saved = result["success"];
@@ -2201,7 +2207,7 @@ class ModelInfo {
}
/**
- * @param {boolean}
+ * @param {boolean} promptUser
* @returns {Promise}
*/
async trySave(promptUser) {
@@ -2567,6 +2573,33 @@ class ModelInfo {
const tagButton = this.elements.tabButtons[2]; // TODO: remove magic value
tagButton.style.display = isTags ? "" : "none";
+ const saveIcon = "content-save";
+ const savingIcon = "cloud-upload-outline";
+
+ const saveNotesButton = new ComfyButton({
+ icon: saveIcon,
+ tooltip: "Save note",
+ classList: "comfyui-button icon-button",
+ action: async (e) => {
+ const [button, icon, span] = comfyButtonDisambiguate(e.target);
+ button.disabled = true;
+ const saved = await this.trySave(false);
+ comfyButtonAlert(e.target, saved);
+ button.disabled = false;
+ },
+ }).element;
+
+ const saveDebounce = debounce(async() => {
+ const saveIconClass = "mdi-" + saveIcon;
+ const savingIconClass = "mdi-" + savingIcon;
+ const iconElement = saveNotesButton.getElementsByTagName("i")[0];
+ iconElement.classList.remove(saveIconClass);
+ iconElement.classList.add(savingIconClass);
+ const saved = await this.trySave(false);
+ iconElement.classList.remove(savingIconClass);
+ iconElement.classList.add(saveIconClass);
+ }, 2000);
+
/** @type {HTMLDivElement} */
const notesElement = this.elements.tabContents[3]; // TODO: remove magic value
notesElement.innerHTML = "";
@@ -2575,6 +2608,11 @@ class ModelInfo {
const notes = $el("textarea.comfy-multiline-input", {
name: "model notes",
value: noteText,
+ oninput: (e) => {
+ if (this.#settingsElements["model-info-autosave-notes"].checked) {
+ saveDebounce();
+ }
+ },
});
this.elements.notes = notes;
this.#savedNotesValue = noteText;
@@ -2583,18 +2621,7 @@ class ModelInfo {
style: { "align-items": "center" },
}, [
$el("h1", ["Notes"]),
- new ComfyButton({
- icon: "content-save",
- tooltip: "Save note",
- classList: "comfyui-button icon-button",
- action: async (e) => {
- const [button, icon, span] = comfyButtonDisambiguate(e.target);
- button.disabled = true;
- const saved = await this.trySave(false);
- comfyButtonAlert(e.target, saved);
- button.disabled = false;
- },
- }).element,
+ saveNotesButton,
]),
$el("div", {
style: { "display": "flex", "height": "100%", "min-height": "60px" },
@@ -3614,6 +3641,7 @@ class SettingsView {
/** @type {HTMLInputElement} */ "model-real-time-search": null,
/** @type {HTMLInputElement} */ "model-persistent-search": null,
/** @type {HTMLInputElement} */ "model-show-label-extensions": null,
+ /** @type {HTMLInputElement} */ "model-info-autosave-notes": null,
/** @type {HTMLInputElement} */ "model-preview-fallback-search-safetensors-thumbnail": null,
/** @type {HTMLInputElement} */ "model-show-add-button": null,
@@ -3819,6 +3847,10 @@ class SettingsView {
$: (el) => (settings["model-show-label-extensions"] = el),
textContent: "Show model file extension in labels",
}),
+ $checkbox({
+ $: (el) => (settings["model-info-autosave-notes"] = el),
+ textContent: "Autosave notes",
+ }),
$checkbox({
$: (el) => (settings["model-preview-fallback-search-safetensors-thumbnail"] = el),
textContent: "Fallback on embedded thumbnail in safetensors (refresh slow)",