Add optional autosave notes.

- By default it is disabled because once the model info is closed, the changes are gone.
This commit is contained in:
Christian Bastian
2024-07-25 05:29:26 -04:00
parent 6e43b2fb4c
commit f67cac9887
3 changed files with 79 additions and 31 deletions

View File

@@ -32,7 +32,7 @@ Download, browse and delete models in ComfyUI.
<img src="demo-tab-model-info-overview.png" alt="Model Manager Demo Screenshot" width="65%"/>
- 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.

View File

@@ -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)

View File

@@ -17,6 +17,21 @@ function request(url, options = undefined) {
});
}
/**
* @param {(...args) => Promise<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 {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<boolean>}
*/
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<boolean>}
*/
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)",