Added settings & config yaml.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -158,3 +158,4 @@ cython_debug/
|
||||
# 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.
|
||||
#.idea/
|
||||
ui_settings.yaml
|
||||
|
||||
25
README.md
25
README.md
@@ -18,16 +18,27 @@ Currently it is still missing some features it should have.
|
||||
- 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 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 an existing node to set the model field.
|
||||
- Drag an embedding onto a text area to add it to the end.
|
||||
- Increased supported preview image types.
|
||||
- Correctly change colors using ComfyUI's theme colors.
|
||||
- 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:
|
||||
|
||||
### 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
|
||||
|
||||
- ☐ Copy image?
|
||||
@@ -41,15 +52,9 @@ Currently it is still missing some features it should have.
|
||||
|
||||
### Settings
|
||||
|
||||
- ☐ Add `settings.yaml` and add file to `.gitignore`. (Generate if not there.)
|
||||
- ☐ 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/Disable add and copy buttons.
|
||||
- ☐ 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.
|
||||
- ☐ Sidebar width.
|
||||
|
||||
### Search filtering and sort
|
||||
|
||||
@@ -76,6 +81,10 @@ Currently it is still missing some features it should have.
|
||||
- ☐ Proper naming and labeling.
|
||||
- ☐ Tool tips?
|
||||
|
||||
### Sidebar
|
||||
|
||||
- ☐ Drag sidebar width/height dynamically.
|
||||
|
||||
### 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.)
|
||||
|
||||
64
__init__.py
64
__init__.py
@@ -1,21 +1,29 @@
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import importlib
|
||||
|
||||
from aiohttp import web
|
||||
import server
|
||||
import urllib.parse
|
||||
import struct
|
||||
import json
|
||||
import requests
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
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")
|
||||
extension_uri = os.path.join(os.getcwd(), "custom_nodes" + os.path.sep + "ComfyUI-Model-Manager")
|
||||
index_uri = os.path.join(extension_uri, "index.json")
|
||||
#checksum_cache_uri = os.path.join(extension_uri, "checksum_cache.txt")
|
||||
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")
|
||||
#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
|
||||
if folder_name in paths:
|
||||
return paths[folder_name][1]
|
||||
|
||||
return set(['.ckpt', '.pt', '.bin', '.pth', '.safetensors'])
|
||||
return set([".ckpt", ".pt", ".bin", ".pth", ".safetensors"])
|
||||
|
||||
|
||||
def get_safetensor_header(path):
|
||||
@@ -76,6 +83,49 @@ def model_type_to_dir_name(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")
|
||||
async def img_preview(request):
|
||||
uri = request.query.get("uri")
|
||||
@@ -83,7 +133,7 @@ async def img_preview(request):
|
||||
image_path = no_preview_image
|
||||
image_extension = "png"
|
||||
|
||||
if (uri != "no-post"):
|
||||
if uri != "no-post":
|
||||
rel_image_path = os.path.dirname(uri)
|
||||
|
||||
i = uri.find(os.path.sep)
|
||||
@@ -225,9 +275,9 @@ async def load_download_models(request):
|
||||
|
||||
model_items = []
|
||||
for model, image, base_path_index, rel_path in file_names:
|
||||
name, _ = os.path.splitext(model)
|
||||
# TODO: Stop sending redundant information
|
||||
item = {
|
||||
"name": name,
|
||||
"name": model,
|
||||
"search-path": os.path.join(model_type, rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack
|
||||
"path": os.path.join(rel_path, model),
|
||||
}
|
||||
@@ -269,7 +319,7 @@ def download_model_file(url, filename):
|
||||
|
||||
with open(dl_filename, "ab") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
if chunk is not None:
|
||||
downloaded_size += len(chunk)
|
||||
f.write(chunk)
|
||||
f.flush()
|
||||
|
||||
65
config_loader.py
Normal file
65
config_loader.py
Normal 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
|
||||
@@ -123,16 +123,6 @@
|
||||
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 {
|
||||
user-select: text;
|
||||
}
|
||||
@@ -347,3 +337,41 @@
|
||||
.model-manager .model-type-dropdown {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -48,13 +48,36 @@ function pathToFileString(path) {
|
||||
return path.slice(i);
|
||||
}
|
||||
|
||||
function insertEmbeddingIntoText(currentText, embeddingFile, extensionRegex = null) {
|
||||
if (extensionRegex) {
|
||||
// TODO: setting.remove_extension_embedding
|
||||
function removeModelExtension(file) {
|
||||
// This is a bit sloppy (can assume server sends without)
|
||||
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 {
|
||||
@@ -281,20 +304,8 @@ class ModelGrid {
|
||||
});
|
||||
}
|
||||
|
||||
static #buttonAlert(event, successful, innerHTML) {
|
||||
const element = event.target;
|
||||
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;
|
||||
static #addModel(event, modelType, path, removeEmbeddingExtension, addOffset) {
|
||||
let success = false;
|
||||
if (modelType !== "embeddings") {
|
||||
const nodeType = modelNodeType(modelType);
|
||||
const widgetIndex = modelWidgetIndex(nodeType);
|
||||
@@ -305,9 +316,8 @@ class ModelGrid {
|
||||
let isSelectedNode = false;
|
||||
for (var i in selectedNodes) {
|
||||
const selectedNode = selectedNodes[i];
|
||||
// TODO: settings.model_add_offset
|
||||
node.pos[0] = selectedNode.pos[0] + 25;
|
||||
node.pos[1] = selectedNode.pos[1] + 25;
|
||||
node.pos[0] = selectedNode.pos[0] + addOffset;
|
||||
node.pos[1] = selectedNode.pos[1] + addOffset;
|
||||
isSelectedNode = true;
|
||||
break;
|
||||
}
|
||||
@@ -318,7 +328,7 @@ class ModelGrid {
|
||||
}
|
||||
app.graph.add(node, {doProcessChange: true});
|
||||
app.canvas.selectNode(node);
|
||||
successful = true;
|
||||
success = true;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
@@ -331,27 +341,33 @@ class ModelGrid {
|
||||
const widgetIndex = modelWidgetIndex(nodeType);
|
||||
const target = selectedNode.widgets[widgetIndex].element;
|
||||
if (target && target.type === "textarea") {
|
||||
target.value = insertEmbeddingIntoText(target.value, embeddingFile);
|
||||
successful = true;
|
||||
target.value = insertEmbeddingIntoText(target.value, embeddingFile, removeEmbeddingExtension);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
if (!successful) {
|
||||
if (!success) {
|
||||
console.warn("Try selecting a node before adding the embedding.");
|
||||
}
|
||||
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);
|
||||
if (modelType !== "embeddings" && target.id === "graph-canvas") {
|
||||
const nodeType = modelNodeType(modelType);
|
||||
const widgetIndex = modelWidgetIndex(nodeType);
|
||||
const pos = app.canvas.convertEventToCanvasOffset(event);
|
||||
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;
|
||||
node.widgets[widgetIndex].value = path;
|
||||
app.canvas.selectNode(node);
|
||||
@@ -374,20 +390,21 @@ class ModelGrid {
|
||||
if (nodeAtPos) {
|
||||
app.canvas.selectNode(nodeAtPos);
|
||||
const embeddingFile = pathToFileString(path);
|
||||
target.value = insertEmbeddingIntoText(target.value, embeddingFile);
|
||||
target.value = insertEmbeddingIntoText(target.value, embeddingFile, removeEmbeddingExtension);
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static #copyModelToClipboard(event, modelType, path) {
|
||||
static #copyModelToClipboard(event, modelType, path, removeEmbeddingExtension) {
|
||||
const nodeType = modelNodeType(modelType);
|
||||
let successful = false;
|
||||
let success = false;
|
||||
if (nodeType === "Embedding") {
|
||||
if (navigator.clipboard){
|
||||
const embeddingText = pathToFileString(path);
|
||||
const embeddingFile = pathToFileString(path);
|
||||
const embeddingText = insertEmbeddingIntoText("", embeddingFile, removeEmbeddingExtension);
|
||||
navigator.clipboard.writeText(embeddingText);
|
||||
successful = true;
|
||||
success = true;
|
||||
}
|
||||
else {
|
||||
console.warn("Cannot copy the embedding to the system clipboard; Try dragging it instead.");
|
||||
@@ -398,24 +415,47 @@ class ModelGrid {
|
||||
const widgetIndex = modelWidgetIndex(nodeType);
|
||||
node.widgets[widgetIndex].value = path;
|
||||
app.canvas.copyToClipboard([node]);
|
||||
successful = true;
|
||||
success = true;
|
||||
}
|
||||
else {
|
||||
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) {
|
||||
// TODO: settings.show_model_add_button
|
||||
// TODO: settings.show_model_copy_button
|
||||
return models.map((item) => {
|
||||
const uri = item.post ?? "no-post";
|
||||
const imgUrl = `/model-manager/image-preview?uri=${uri}`;
|
||||
const dragAdd = (e) => ModelGrid.#dragAddModel(e, modelType, item.path);
|
||||
const clickCopy = (e) => ModelGrid.#copyModelToClipboard(e, modelType, item.path);
|
||||
const clickAdd = (e) => ModelGrid.#addModel(e, modelType, item.path);
|
||||
let buttons = [];
|
||||
if (showAddButton) {
|
||||
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", {}, [
|
||||
$el("img.model-preview", {
|
||||
src: imgUrl,
|
||||
@@ -429,25 +469,13 @@ class ModelGrid {
|
||||
$el("div.model-preview-top-right", {
|
||||
draggable: false,
|
||||
},
|
||||
[
|
||||
$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,
|
||||
}),
|
||||
]),
|
||||
buttons
|
||||
),
|
||||
$el("div.model-label", {
|
||||
ondragend: (e) => dragAdd(e),
|
||||
draggable: true,
|
||||
}, [
|
||||
$el("p", [item.name])
|
||||
$el("p", [showModelExtension ? item.name : removeModelExtension(item.name)])
|
||||
]),
|
||||
]);
|
||||
});
|
||||
@@ -501,10 +529,27 @@ class ModelManager extends ComfyDialog {
|
||||
sourceInstalledFilter: null,
|
||||
sourceContentFilter: null,
|
||||
sourceFilterBtn: null,
|
||||
|
||||
modelGrid: null,
|
||||
modelTypeSelect: null,
|
||||
modelContentFilter: 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 = {
|
||||
@@ -557,7 +602,7 @@ class ModelManager extends ComfyDialog {
|
||||
$tabs([
|
||||
$tab("Install", this.#createSourceInstall()),
|
||||
$tab("Models", this.#createModelTabHtml()),
|
||||
$tab("Settings", []),
|
||||
$tab("Settings", this.#createSettingsTabHtml()),
|
||||
]),
|
||||
]),
|
||||
]
|
||||
@@ -567,6 +612,7 @@ class ModelManager extends ComfyDialog {
|
||||
}
|
||||
|
||||
#init() {
|
||||
this.#reloadSettings(false);
|
||||
this.#refreshSourceList();
|
||||
this.#modelGridRefresh();
|
||||
}
|
||||
@@ -748,15 +794,15 @@ class ModelManager extends ComfyDialog {
|
||||
}
|
||||
|
||||
#modelGridUpdate() {
|
||||
const searchText = this.#el.modelContentFilter.value;
|
||||
// TODO: settings.always_append_to_search
|
||||
const searchAppend = this.#el.settings["model-search-always-append"].value;
|
||||
const searchText = this.#el.modelContentFilter.value + " " + searchAppend;
|
||||
const modelType = this.#el.modelTypeSelect.value;
|
||||
const models = this.#data.models;
|
||||
const modelList = ModelGrid.filter(models[modelType], searchText);
|
||||
|
||||
const modelGrid = this.#el.modelGrid;
|
||||
modelGrid.innerHTML = [];
|
||||
const innerHTML = ModelGrid.generateInnerHtml(modelList, modelType);
|
||||
const innerHTML = ModelGrid.generateInnerHtml(modelList, modelType, this.#el.settings);
|
||||
modelGrid.append.apply(modelGrid, innerHTML);
|
||||
};
|
||||
|
||||
@@ -766,9 +812,8 @@ class ModelManager extends ComfyDialog {
|
||||
};
|
||||
|
||||
#setSidebar(event) {
|
||||
// TODO: use checkboxes with 0 or 1 values set at once?
|
||||
// TODO: settings.sidebar_side_width
|
||||
// TODO: settings.sidebar_bottom_height
|
||||
// TODO: settings["sidebar-default-width"]
|
||||
// TODO: settings["sidebar-default-height"]
|
||||
// TODO: draggable resize?
|
||||
const button = event.target;
|
||||
const sidebarButtons = this.#el.sidebarButtons.children;
|
||||
@@ -795,6 +840,169 @@ class ModelManager extends ComfyDialog {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user