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:
@@ -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.
|
||||
|
||||
|
||||
20
__init__.py
20
__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)
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user