Added settings & config yaml.

This commit is contained in:
Christian Bastian
2024-01-05 07:35:24 -05:00
parent 724a9425c4
commit a8fa7c6c15
6 changed files with 452 additions and 91 deletions

1
.gitignore vendored
View File

@@ -158,3 +158,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
ui_settings.yaml

View File

@@ -18,16 +18,27 @@ Currently it is still missing some features it should have.
- Include models listed in ComfyUI's `extra_model_paths.yaml`. - Include models listed in ComfyUI's `extra_model_paths.yaml`.
- Button to copy a model to the ComfyUI clipboard or embedding to system clipboard. (Embedding copying requires secure http connection.) - 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.) - Button to add model to ComfyUI graph or embedding to selected nodes. (For small screens/low resolution.)
- Right, left and bottom toggleable sidebar modes. - Right, left, top and bottom toggleable sidebar modes.
- Drag a model onto the graph to add a new node. - Drag a model onto the graph to add a new node.
- Drag a model onto an existing node to set the model field. - Drag a model onto an existing node to set the model field.
- Drag an embedding onto a text area to add it to the end. - Drag an embedding onto a text area to add it to the end.
- Increased supported preview image types. - Increased supported preview image types.
- Correctly change colors using ComfyUI's theme colors. - Correctly change colors using ComfyUI's theme colors.
- Simplified UI. - Simplified UI.
- Settings tab and config file.
- Hide/Show 'add' and 'copy-to-clipboard' buttons.
- Text to always search.
- Show/Hide add embedding extension.
## TODO: ## TODO:
### Code
- ☐ Javascript cleanup.
- ☐ Seperate into classes per tab?
- ☐ HTML generation all inside main class?
- ☐ More server driven, HTMX-like HTML generation? (Avoid x2 states)
### Model Copying ### Model Copying
- ☐ Copy image? - ☐ Copy image?
@@ -41,15 +52,9 @@ Currently it is still missing some features it should have.
### Settings ### Settings
- ☐ Add `settings.yaml` and add file to `.gitignore`. (Generate if not there.)
- ☐ Exclude hidden folders with a `.` prefix. - ☐ Exclude hidden folders with a `.` prefix.
- ☐ Include a optional string to always add to searches.
- ☐ Enable optional checksum to detect if a model is already downloaded. - ☐ Enable optional checksum to detect if a model is already downloaded.
- ☐ Enable/Disable add and copy buttons. - ☐ Sidebar width.
- ☐ Allow user to drag width of sidebar or height of bottom bar and remember it.
- ☐ Hide/Show model extension.
- ☐ Optionally remove embedding extension.
- ☐ Strict model drag on node widget textbox.
### Search filtering and sort ### Search filtering and sort
@@ -76,6 +81,10 @@ Currently it is still missing some features it should have.
- ☐ Proper naming and labeling. - ☐ Proper naming and labeling.
- ☐ Tool tips? - ☐ Tool tips?
### Sidebar
- ☐ Drag sidebar width/height dynamically.
### Directory Browser and Downloading tab ### Directory Browser and Downloading tab
(NOTE: It is a impossible to put a model automatically in the correct folder if model type information is not given or ambigious. To fully solve this requires making a file browser where files can be moved around.) (NOTE: It is a impossible to put a model automatically in the correct folder if model type information is not given or ambigious. To fully solve this requires making a file browser where files can be moved around.)

View File

@@ -1,21 +1,29 @@
import os import os
import sys import sys
import hashlib import hashlib
import importlib
from aiohttp import web from aiohttp import web
import server import server
import urllib.parse import urllib.parse
import struct import struct
import json import json
import requests import requests
requests.packages.urllib3.disable_warnings()
import folder_paths import folder_paths
requests.packages.urllib3.disable_warnings() config_loader_path = os.path.join(os.path.dirname(__file__), 'config_loader.py')
config_loader_spec = importlib.util.spec_from_file_location('config_loader', config_loader_path)
config_loader = importlib.util.module_from_spec(config_loader_spec)
config_loader_spec.loader.exec_module(config_loader)
comfyui_model_uri = os.path.join(os.getcwd(), "models") comfyui_model_uri = os.path.join(os.getcwd(), "models")
extension_uri = os.path.join(os.getcwd(), "custom_nodes" + os.path.sep + "ComfyUI-Model-Manager") extension_uri = os.path.join(os.getcwd(), "custom_nodes" + os.path.sep + "ComfyUI-Model-Manager")
index_uri = os.path.join(extension_uri, "index.json") index_uri = os.path.join(extension_uri, "index.json")
#checksum_cache_uri = os.path.join(extension_uri, "checksum_cache.txt") #checksum_cache_uri = os.path.join(extension_uri, "checksum_cache.txt")
no_preview_image = os.path.join(extension_uri, "no-preview.png") no_preview_image = os.path.join(extension_uri, "no-preview.png")
ui_settings_uri = os.path.join(extension_uri, "ui_settings.yaml")
image_extensions = (".apng", ".gif", ".jpeg", ".jpg", ".png", ".webp") image_extensions = (".apng", ".gif", ".jpeg", ".jpg", ".png", ".webp")
#video_extensions = (".avi", ".mp4", ".webm") # TODO: Requires ffmpeg or cv2. Cache preview frame? #video_extensions = (".avi", ".mp4", ".webm") # TODO: Requires ffmpeg or cv2. Cache preview frame?
@@ -38,8 +46,7 @@ def folder_paths_get_supported_pt_extensions(folder_name): # Missing API functio
paths = folder_paths.folder_names_and_paths paths = folder_paths.folder_names_and_paths
if folder_name in paths: if folder_name in paths:
return paths[folder_name][1] return paths[folder_name][1]
return set([".ckpt", ".pt", ".bin", ".pth", ".safetensors"])
return set(['.ckpt', '.pt', '.bin', '.pth', '.safetensors'])
def get_safetensor_header(path): def get_safetensor_header(path):
@@ -76,6 +83,49 @@ def model_type_to_dir_name(model_type):
else: return model_type else: return model_type
def ui_rules():
Rule = config_loader.Rule
return [
Rule("sidebar-default-height", 0.5, float, 0.0, 1.0),
Rule("sidebar-default-width", 0.5, float, 0.0, 1.0),
Rule("model-search-always-append", "", str),
Rule("model-show-label-extensions", False, bool),
Rule("model-show-add-button", True, bool),
Rule("model-show-copy-button", True, bool),
Rule("model-add-embedding-extension", False, bool),
Rule("model-add-drag-strict-on-field", False, bool),
Rule("model-add-offset", 25, int),
]
#def server_rules():
# Rule = config_loader.Rule
# return [
# Rule("model_extension_download_whitelist", [".safetensors"], list),
# Rule("civitai_api_key", "", str),
# ]
@server.PromptServer.instance.routes.get("/model-manager/settings/load")
async def load_ui_settings(request):
rules = ui_rules()
settings = config_loader.yaml_load(ui_settings_uri, rules)
return web.json_response({ "settings": settings })
@server.PromptServer.instance.routes.post("/model-manager/settings/save")
async def save_ui_settings(request):
body = await request.json()
settings = body.get("settings")
rules = ui_rules()
validated_settings = config_loader.validated(rules, settings)
success = config_loader.yaml_save(ui_settings_uri, rules, validated_settings)
return web.json_response({
"success": success,
"settings": validated_settings if success else "",
})
@server.PromptServer.instance.routes.get("/model-manager/image-preview") @server.PromptServer.instance.routes.get("/model-manager/image-preview")
async def img_preview(request): async def img_preview(request):
uri = request.query.get("uri") uri = request.query.get("uri")
@@ -83,7 +133,7 @@ async def img_preview(request):
image_path = no_preview_image image_path = no_preview_image
image_extension = "png" image_extension = "png"
if (uri != "no-post"): if uri != "no-post":
rel_image_path = os.path.dirname(uri) rel_image_path = os.path.dirname(uri)
i = uri.find(os.path.sep) i = uri.find(os.path.sep)
@@ -225,9 +275,9 @@ async def load_download_models(request):
model_items = [] model_items = []
for model, image, base_path_index, rel_path in file_names: for model, image, base_path_index, rel_path in file_names:
name, _ = os.path.splitext(model) # TODO: Stop sending redundant information
item = { item = {
"name": name, "name": model,
"search-path": os.path.join(model_type, rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack "search-path": os.path.join(model_type, rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack
"path": os.path.join(rel_path, model), "path": os.path.join(rel_path, model),
} }
@@ -269,7 +319,7 @@ def download_model_file(url, filename):
with open(dl_filename, "ab") as f: with open(dl_filename, "ab") as f:
for chunk in r.iter_content(chunk_size=1024): for chunk in r.iter_content(chunk_size=1024):
if chunk: if chunk is not None:
downloaded_size += len(chunk) downloaded_size += len(chunk)
f.write(chunk) f.write(chunk)
f.flush() f.flush()

65
config_loader.py Normal file
View File

@@ -0,0 +1,65 @@
import yaml
from dataclasses import dataclass
@dataclass
class Rule:
key: any
value_default: any
value_type: type
value_min: int | float | None
value_max: int | float | None
def __init__(self, key, value_default, value_type: type, value_min: int | float | None = None, value_max: int | float | None = None):
self.key = key
self.value_default = value_default
self.value_type = value_type
self.value_min = value_min
self.value_max = value_max
def _get_valid_value(data: dict, r: Rule):
if r.value_type != type(r.value_default):
raise Exception(f"'value_type' does not match type of 'value_default'!")
value = data.get(r.key)
if value is None:
value = r.value_default
else:
try:
value = r.value_type(value)
except:
value = r.value_default
value_is_numeric = r.value_type == int or r.value_type == float
if value_is_numeric and r.value_min:
if r.value_type != type(r.value_min):
raise Exception(f"Type of 'value_type' does not match the type of 'value_min'!")
value = max(r.value_min, value)
if value_is_numeric and r.value_max:
if r.value_type != type(r.value_max):
raise Exception(f"Type of 'value_type' does not match the type of 'value_max'!")
value = min(r.value_max, value)
return value
def validated(rules: list[Rule], data: dict = {}):
valid = {}
for r in rules:
valid[r.key] = _get_valid_value(data, r)
return valid
def yaml_load(path, rules: list[Rule]):
data = {}
try:
with open(path, 'r') as file:
data = yaml.safe_load(file)
except:
pass
return validated(rules, data)
def yaml_save(path, rules: list[Rule], data: dict) -> bool:
data = validated(rules, data)
try:
with open(path, 'w') as file:
yaml.dump(data, file)
return True
except:
return False

View File

@@ -123,16 +123,6 @@
opacity: 1; opacity: 1;
} }
.comfy-grid .model-button.model-button-success {
color: green;
border-color: green;
}
.comfy-grid .model-button.model-button-failure {
color: darkred;
border-color: darkred;
}
.comfy-grid .model-label { .comfy-grid .model-label {
user-select: text; user-select: text;
} }
@@ -347,3 +337,41 @@
.model-manager .model-type-dropdown { .model-manager .model-type-dropdown {
flex: 1; flex: 1;
} }
.model-manager .button-success {
color: green;
border-color: green;
}
.model-manager .button-failure {
color: darkred;
border-color: darkred;
}
/* model manager settings */
.model-manager .model-manager-settings > div {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.model-manager .model-manager-settings button {
height: 40px;
width: 120px;
}
.model-manager .model-manager-settings input[type="number"] {
width: 50px;
}
.search-settings-text {
width: 100%;
}
.model-manager .model-manager-settings textarea {
width: 100%;
font-size: 1.2em;
border: solid 2px var(--border-color);
border-radius: 8px;
}

View File

@@ -48,13 +48,36 @@ function pathToFileString(path) {
return path.slice(i); return path.slice(i);
} }
function insertEmbeddingIntoText(currentText, embeddingFile, extensionRegex = null) { function removeModelExtension(file) {
if (extensionRegex) { // This is a bit sloppy (can assume server sends without)
// TODO: setting.remove_extension_embedding const i = file.lastIndexOf(".");
if (i != -1) {
return file.substring(0, i);
} }
// TODO: don't add if it is already in the text? }
const sep = currentText.length === 0 || currentText.slice(-1).match(/\s/) ? "" : " ";
return currentText + sep + "(embedding:" + embeddingFile + ":1.0)"; function insertEmbeddingIntoText(text, file, removeExtension) {
let name = file;
if (removeExtension) {
name = removeModelExtension(name)
}
const sep = text.length === 0 || text.slice(-1).match(/\s/) ? "" : " ";
return text + sep + "(embedding:" + name + ":1.0)";
}
function buttonAlert(element, success, successText = "", failureText = "", resetText = "") {
const name = success ? "button-success" : "button-failure";
element.classList.add(name);
if (successText != "" && failureText != "") {
element.innerHTML = success ? successText : failureText;
}
// TODO: debounce would be nice to get working...
window.setTimeout((element, name, innerHTML) => {
element.classList.remove(name);
if (innerHTML != "") {
element.innerHTML = innerHTML;
}
}, 500, element, name, resetText);
} }
class Tabs { class Tabs {
@@ -281,20 +304,8 @@ class ModelGrid {
}); });
} }
static #buttonAlert(event, successful, innerHTML) { static #addModel(event, modelType, path, removeEmbeddingExtension, addOffset) {
const element = event.target; let success = false;
const name = successful ? "model-button-success" : "model-button-failure";
element.classList.add(name);
element.innerHTML = successful ? "✔" : "✖";
// TODO: debounce would be nice to get working...
window.setTimeout((element, name) => {
element.classList.remove(name);
element.innerHTML = innerHTML;
}, 500, element, name);
}
static #addModel(event, modelType, path) {
let successful = false;
if (modelType !== "embeddings") { if (modelType !== "embeddings") {
const nodeType = modelNodeType(modelType); const nodeType = modelNodeType(modelType);
const widgetIndex = modelWidgetIndex(nodeType); const widgetIndex = modelWidgetIndex(nodeType);
@@ -305,9 +316,8 @@ class ModelGrid {
let isSelectedNode = false; let isSelectedNode = false;
for (var i in selectedNodes) { for (var i in selectedNodes) {
const selectedNode = selectedNodes[i]; const selectedNode = selectedNodes[i];
// TODO: settings.model_add_offset node.pos[0] = selectedNode.pos[0] + addOffset;
node.pos[0] = selectedNode.pos[0] + 25; node.pos[1] = selectedNode.pos[1] + addOffset;
node.pos[1] = selectedNode.pos[1] + 25;
isSelectedNode = true; isSelectedNode = true;
break; break;
} }
@@ -318,7 +328,7 @@ class ModelGrid {
} }
app.graph.add(node, {doProcessChange: true}); app.graph.add(node, {doProcessChange: true});
app.canvas.selectNode(node); app.canvas.selectNode(node);
successful = true; success = true;
} }
event.stopPropagation(); event.stopPropagation();
} }
@@ -331,27 +341,33 @@ class ModelGrid {
const widgetIndex = modelWidgetIndex(nodeType); const widgetIndex = modelWidgetIndex(nodeType);
const target = selectedNode.widgets[widgetIndex].element; const target = selectedNode.widgets[widgetIndex].element;
if (target && target.type === "textarea") { if (target && target.type === "textarea") {
target.value = insertEmbeddingIntoText(target.value, embeddingFile); target.value = insertEmbeddingIntoText(target.value, embeddingFile, removeEmbeddingExtension);
successful = true; success = true;
} }
} }
if (!successful) { if (!success) {
console.warn("Try selecting a node before adding the embedding."); console.warn("Try selecting a node before adding the embedding.");
} }
event.stopPropagation(); event.stopPropagation();
} }
this.#buttonAlert(event, successful, "✚"); buttonAlert(event.target, success, "✔", "✖", "✚");
} }
static #dragAddModel(event, modelType, path) { static #dragAddModel(event, modelType, path, removeEmbeddingExtension, strictDragToAdd) {
const target = document.elementFromPoint(event.x, event.y); const target = document.elementFromPoint(event.x, event.y);
if (modelType !== "embeddings" && target.id === "graph-canvas") { if (modelType !== "embeddings" && target.id === "graph-canvas") {
const nodeType = modelNodeType(modelType); const nodeType = modelNodeType(modelType);
const widgetIndex = modelWidgetIndex(nodeType); const widgetIndex = modelWidgetIndex(nodeType);
const pos = app.canvas.convertEventToCanvasOffset(event); const pos = app.canvas.convertEventToCanvasOffset(event);
const nodeAtPos = app.graph.getNodeOnPos(pos[0], pos[1], app.canvas.visible_nodes); const nodeAtPos = app.graph.getNodeOnPos(pos[0], pos[1], app.canvas.visible_nodes);
//if (nodeAtPos && nodeAtPos.type === nodeType && app.canvas.processNodeWidgets(nodeAtPos, pos, event) !== nodeAtPos.widgets[widgetIndex]) { // TODO: settings.strict_model_drag
if (nodeAtPos && nodeAtPos.type === nodeType) { let draggedOnNode = nodeAtPos && nodeAtPos.type === nodeType;
if (strictDragToAdd) {
const draggedOnWidget = app.canvas.processNodeWidgets(nodeAtPos, pos, event) === nodeAtPos.widgets[widgetIndex];
draggedOnNode = draggedOnNode && draggedOnWidget;
}
if (draggedOnNode) {
let node = nodeAtPos; let node = nodeAtPos;
node.widgets[widgetIndex].value = path; node.widgets[widgetIndex].value = path;
app.canvas.selectNode(node); app.canvas.selectNode(node);
@@ -374,20 +390,21 @@ class ModelGrid {
if (nodeAtPos) { if (nodeAtPos) {
app.canvas.selectNode(nodeAtPos); app.canvas.selectNode(nodeAtPos);
const embeddingFile = pathToFileString(path); const embeddingFile = pathToFileString(path);
target.value = insertEmbeddingIntoText(target.value, embeddingFile); target.value = insertEmbeddingIntoText(target.value, embeddingFile, removeEmbeddingExtension);
event.stopPropagation(); event.stopPropagation();
} }
} }
} }
static #copyModelToClipboard(event, modelType, path) { static #copyModelToClipboard(event, modelType, path, removeEmbeddingExtension) {
const nodeType = modelNodeType(modelType); const nodeType = modelNodeType(modelType);
let successful = false; let success = false;
if (nodeType === "Embedding") { if (nodeType === "Embedding") {
if (navigator.clipboard){ if (navigator.clipboard){
const embeddingText = pathToFileString(path); const embeddingFile = pathToFileString(path);
const embeddingText = insertEmbeddingIntoText("", embeddingFile, removeEmbeddingExtension);
navigator.clipboard.writeText(embeddingText); navigator.clipboard.writeText(embeddingText);
successful = true; success = true;
} }
else { else {
console.warn("Cannot copy the embedding to the system clipboard; Try dragging it instead."); console.warn("Cannot copy the embedding to the system clipboard; Try dragging it instead.");
@@ -398,24 +415,47 @@ class ModelGrid {
const widgetIndex = modelWidgetIndex(nodeType); const widgetIndex = modelWidgetIndex(nodeType);
node.widgets[widgetIndex].value = path; node.widgets[widgetIndex].value = path;
app.canvas.copyToClipboard([node]); app.canvas.copyToClipboard([node]);
successful = true; success = true;
} }
else { else {
console.warn(`Unable to copy unknown model type '${modelType}.`); console.warn(`Unable to copy unknown model type '${modelType}.`);
} }
this.#buttonAlert(event, successful, "⧉︎"); buttonAlert(event.target, success, "✔", "✖", "⧉︎");
} }
static generateInnerHtml(models, modelType) { static generateInnerHtml(models, modelType, settingsElements) {
const showAddButton = settingsElements["model-show-add-button"].checked;
const showCopyButton = settingsElements["model-show-copy-button"].checked;
const strictDragToAdd = settingsElements["model-add-drag-strict-on-field"].checked;
const addOffset = parseInt(settingsElements["model-add-offset"].value);
const showModelExtension = settingsElements["model-show-label-extensions"].checked;
const removeEmbeddingExtension = !settingsElements["model-add-embedding-extension"].checked;
if (models.length > 0) { if (models.length > 0) {
// TODO: settings.show_model_add_button
// TODO: settings.show_model_copy_button
return models.map((item) => { return models.map((item) => {
const uri = item.post ?? "no-post"; const uri = item.post ?? "no-post";
const imgUrl = `/model-manager/image-preview?uri=${uri}`; const imgUrl = `/model-manager/image-preview?uri=${uri}`;
const dragAdd = (e) => ModelGrid.#dragAddModel(e, modelType, item.path); let buttons = [];
const clickCopy = (e) => ModelGrid.#copyModelToClipboard(e, modelType, item.path); if (showAddButton) {
const clickAdd = (e) => ModelGrid.#addModel(e, modelType, item.path); buttons.push(
$el("button.icon-button.model-button", {
type: "button",
textContent: "⧉︎",
onclick: (e) => ModelGrid.#copyModelToClipboard(e, modelType, item.path, removeEmbeddingExtension),
draggable: false,
})
);
}
if (showCopyButton) {
buttons.push(
$el("button.icon-button.model-button", {
type: "button",
textContent: "✚",
onclick: (e) => ModelGrid.#addModel(e, modelType, item.path, removeEmbeddingExtension, addOffset),
draggable: false,
})
);
}
const dragAdd = (e) => ModelGrid.#dragAddModel(e, modelType, item.path, removeEmbeddingExtension, strictDragToAdd);
return $el("div.item", {}, [ return $el("div.item", {}, [
$el("img.model-preview", { $el("img.model-preview", {
src: imgUrl, src: imgUrl,
@@ -429,25 +469,13 @@ class ModelGrid {
$el("div.model-preview-top-right", { $el("div.model-preview-top-right", {
draggable: false, draggable: false,
}, },
[ buttons
$el("button.icon-button.model-button", { ),
type: "button",
textContent: "⧉︎",
onclick: (e) => clickCopy(e),
draggable: false,
}),
$el("button.icon-button.model-button", {
type: "button",
textContent: "✚",
onclick: (e) => clickAdd(e),
draggable: false,
}),
]),
$el("div.model-label", { $el("div.model-label", {
ondragend: (e) => dragAdd(e), ondragend: (e) => dragAdd(e),
draggable: true, draggable: true,
}, [ }, [
$el("p", [item.name]) $el("p", [showModelExtension ? item.name : removeModelExtension(item.name)])
]), ]),
]); ]);
}); });
@@ -501,10 +529,27 @@ class ModelManager extends ComfyDialog {
sourceInstalledFilter: null, sourceInstalledFilter: null,
sourceContentFilter: null, sourceContentFilter: null,
sourceFilterBtn: null, sourceFilterBtn: null,
modelGrid: null, modelGrid: null,
modelTypeSelect: null, modelTypeSelect: null,
modelContentFilter: null, modelContentFilter: null,
sidebarButtons: null, sidebarButtons: null,
settingsTab: null,
reloadSettingsBtn: null,
saveSettingsBtn: null,
settings: {
"sidebar-default-height": null,
"sidebar-default-width": null,
"model-search-always-append": null,
"model-show-label-extensions": null,
"model-show-add-button": null,
"model-show-copy-button": null,
"model-add-embedding-extension": null,
"model-add-drag-strict-on-field": null,
"model-add-offset": null,
}
}; };
#data = { #data = {
@@ -557,7 +602,7 @@ class ModelManager extends ComfyDialog {
$tabs([ $tabs([
$tab("Install", this.#createSourceInstall()), $tab("Install", this.#createSourceInstall()),
$tab("Models", this.#createModelTabHtml()), $tab("Models", this.#createModelTabHtml()),
$tab("Settings", []), $tab("Settings", this.#createSettingsTabHtml()),
]), ]),
]), ]),
] ]
@@ -567,6 +612,7 @@ class ModelManager extends ComfyDialog {
} }
#init() { #init() {
this.#reloadSettings(false);
this.#refreshSourceList(); this.#refreshSourceList();
this.#modelGridRefresh(); this.#modelGridRefresh();
} }
@@ -748,15 +794,15 @@ class ModelManager extends ComfyDialog {
} }
#modelGridUpdate() { #modelGridUpdate() {
const searchText = this.#el.modelContentFilter.value; const searchAppend = this.#el.settings["model-search-always-append"].value;
// TODO: settings.always_append_to_search const searchText = this.#el.modelContentFilter.value + " " + searchAppend;
const modelType = this.#el.modelTypeSelect.value; const modelType = this.#el.modelTypeSelect.value;
const models = this.#data.models; const models = this.#data.models;
const modelList = ModelGrid.filter(models[modelType], searchText); const modelList = ModelGrid.filter(models[modelType], searchText);
const modelGrid = this.#el.modelGrid; const modelGrid = this.#el.modelGrid;
modelGrid.innerHTML = []; modelGrid.innerHTML = [];
const innerHTML = ModelGrid.generateInnerHtml(modelList, modelType); const innerHTML = ModelGrid.generateInnerHtml(modelList, modelType, this.#el.settings);
modelGrid.append.apply(modelGrid, innerHTML); modelGrid.append.apply(modelGrid, innerHTML);
}; };
@@ -766,9 +812,8 @@ class ModelManager extends ComfyDialog {
}; };
#setSidebar(event) { #setSidebar(event) {
// TODO: use checkboxes with 0 or 1 values set at once? // TODO: settings["sidebar-default-width"]
// TODO: settings.sidebar_side_width // TODO: settings["sidebar-default-height"]
// TODO: settings.sidebar_bottom_height
// TODO: draggable resize? // TODO: draggable resize?
const button = event.target; const button = event.target;
const sidebarButtons = this.#el.sidebarButtons.children; const sidebarButtons = this.#el.sidebarButtons.children;
@@ -795,6 +840,169 @@ class ModelManager extends ComfyDialog {
modelManager.classList.add(newSidebarState); modelManager.classList.add(newSidebarState);
} }
} }
#setSettings(settings, reloadData) {
const el = this.#el.settings;
for (const [key, value] of Object.entries(settings)) {
const setting = el[key];
if (setting) {
const type = setting.type;
switch (type) {
case "checkbox": setting.checked = Boolean(value); break;
case "range": setting.value = parseFloat(value); break;
case "textarea": setting.value = value; break;
case "number": setting.value = parseInt(value); break;
default: console.warn("Unknown settings input type!");
}
}
}
if (reloadData) {
// Is this slow?
this.#refreshSourceList();
this.#modelGridRefresh();
}
}
async #reloadSettings(reloadData) {
const data = await request("/model-manager/settings/load");
const settings = data["settings"];
this.#setSettings(settings, reloadData);
buttonAlert(this.#el.reloadSettingsBtn, true);
};
async #saveSettings() {
let settings = {};
for (const [setting, el] of Object.entries(this.#el.settings)) {
if (!el) { continue; } // hack
const type = el.type;
let value = null;
switch (type) {
case "checkbox": value = el.checked; break;
case "range": value = el.value; break;
case "textarea": value = el.value; break;
case "number": value = el.value; break;
default: console.warn("Unknown settings input type!");
}
settings[setting] = value;
}
const data = await request(
"/model-manager/settings/save",
{
method: "POST",
body: JSON.stringify({ "settings": settings }),
}
);
const success = data["success"];
if (success) {
const settings = data["settings"];
this.#setSettings(settings, true);
}
buttonAlert(this.#el.saveSettingsBtn, success);
}
#createSettingsTabHtml() {
const settingsTab = $el("div.model-manager-settings", [
$el("h1", ["Settings"]),
$el("div", [
$el("button", {
$: (el) => (this.#el.reloadSettingsBtn = el),
type: "button",
textContent: "Reload", // ⟳
onclick: () => this.#reloadSettings(true),
}),
$el("button", {
$: (el) => (this.#el.saveSettingsBtn = el),
type: "button",
textContent: "Save", // 💾︎
onclick: () => this.#saveSettings(),
}),
]),
/*
$el("h2", ["Window"]),
$el("div", [
$el("p", ["Default sidebar width"]),
$el("input", {
$: (el) => (this.#el.settings["sidebar-default-width"] = el),
type: "number",
value: 0.5,
min: 0.0,
max: 1.0,
step: 0.05,
}),
]),
$el("div", [
$el("p", ["Default sidebar height"]),
$el("input", {
$: (el) => (this.#el.settings["sidebar-default-height"] = el),
type: "number",
textContent: "Default sidebar height",
value: 0.5,
min: 0.0,
max: 1.0,
step: 0.05,
}),
]),
*/
$el("h2", ["Model Search"]),
$el("div", [
$el("div.search-settings-text", [
$el("p", ["Always append to model search:"]),
$el("textarea.comfy-multiline-input", {
$: (el) => (this.#el.settings["model-search-always-append"] = el),
placeholder: "example: -nsfw",
}),
]),
]),
$el("div", [
$el("input", {
$: (el) => (this.#el.settings["model-show-label-extensions"] = el),
type: "checkbox",
}),
$el("p", ["Show extensions in models tab"]),
]),
$el("div", [
$el("input", {
$: (el) => (this.#el.settings["model-show-add-button"] = el),
type: "checkbox",
}),
$el("p", ["Show add button"]),
]),
$el("div", [
$el("input", {
$: (el) => (this.#el.settings["model-show-copy-button"] = el),
type: "checkbox",
}),
$el("p", ["Show copy button"]),
]),
$el("h2", ["Model Add"]),
$el("div", [
$el("input", {
$: (el) => (this.#el.settings["model-add-embedding-extension"] = el),
type: "checkbox",
}),
$el("p", ["Add extension to embedding"]),
]),
$el("div", [
$el("input", {
$: (el) => (this.#el.settings["model-add-drag-strict-on-field"] = el),
type: "checkbox",
}),
$el("p", ["Strict dragging model onto a node's model field to add"]),
]),
$el("div", [
$el("p", ["Add model offset"]),
$el("input", {
$: (el) => (this.#el.settings["model-add-offset"] = el),
type: "number",
step: 5,
}),
]),
]);
this.#el.settingsTab = settingsTab;
return [settingsTab];
}
} }
let instance; let instance;