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
|
# 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
|
||||||
|
|||||||
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`.
|
- 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.)
|
||||||
|
|||||||
64
__init__.py
64
__init__.py
@@ -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
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;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user