copy to clipboard, drag to add, sidebar modes
This commit is contained in:
18
README.md
18
README.md
@@ -16,23 +16,21 @@ Currently it is still missing some features it should have.
|
|||||||
- Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`.
|
- Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`.
|
||||||
- Search `/`subdirectories of model directories based on your file structure (for example, `/styles/clothing`).
|
- Search `/`subdirectories of model directories based on your file structure (for example, `/styles/clothing`).
|
||||||
- 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. (Embedding cannot be copied to the system clipboard with an http connection.)
|
||||||
|
- Right, left and bottom toggleable sidebar modes.
|
||||||
|
- Drag model to graph to add or existing node to set model.
|
||||||
|
- Drag embedding to textarea to append.
|
||||||
- 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.
|
||||||
|
|
||||||
## TODO:
|
## TODO:
|
||||||
|
|
||||||
### One-click to add a model/node to workspace
|
|
||||||
|
|
||||||
- ☐ Add icon `+`? (Copy icon `⎘`?)
|
|
||||||
- ☐ Sidebar mode
|
|
||||||
- ☐ Drag to add to workspace?
|
|
||||||
|
|
||||||
### Downloading tab
|
### Downloading tab
|
||||||
|
|
||||||
- ☐ Replace Install tab with Downloading tab (more practical IMO).
|
- ☐ Replace Install tab with Downloading tab (more practical IMO).
|
||||||
- ☐ Download a model from a url.
|
- ☐ Download a model from a url.
|
||||||
- ☐ Choose save path to download within browser.
|
- ☐ Choose save path/directory to download within vaild model directories. (Alert Yes/No if need to create new dirs?)
|
||||||
|
|
||||||
### Search filtering and sort
|
### Search filtering and sort
|
||||||
|
|
||||||
@@ -49,10 +47,14 @@ Currently it is still missing some features it should have.
|
|||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
|
- ☐ Add `settings.yaml` and add file to `.gitignore`.
|
||||||
- ☐ Exclude hidden folders with a `.` prefix.
|
- ☐ Exclude hidden folders with a `.` prefix.
|
||||||
- ☐ Include a optional string to always add to searches.
|
- ☐ 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.
|
||||||
- ☐ Add `settings.yaml` and add file to `.gitignore`.
|
- ☐ Change copy icon to an add icon `✚`.
|
||||||
|
- ☐ Allow user to drag width of sidebar or height of bottom bar and remember it.
|
||||||
|
- ☐ Hide/Show model extension.
|
||||||
|
- ☐ Optionally remove embedding extension.
|
||||||
|
|
||||||
### Model info window/panel (server load/send on demand)
|
### Model info window/panel (server load/send on demand)
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,8 @@ async def load_download_models(request):
|
|||||||
name, _ = os.path.splitext(model)
|
name, _ = os.path.splitext(model)
|
||||||
item = {
|
item = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"path": os.path.join(model_type, rel_path, model).replace(os.path.sep, "/"),
|
"search-path": os.path.join(model_type, rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack
|
||||||
|
"path": os.path.join(rel_path, model),
|
||||||
}
|
}
|
||||||
if image is not None:
|
if image is not None:
|
||||||
raw_post = os.path.join(model_type, str(base_path_index), rel_path, image)
|
raw_post = os.path.join(model_type, str(base_path_index), rel_path, image)
|
||||||
|
|||||||
@@ -55,8 +55,10 @@
|
|||||||
|
|
||||||
.comfy-grid .item {
|
.comfy-grid .item {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 230px;
|
aspect-ratio: 2/3;
|
||||||
height: 345px;
|
max-width: 230px;
|
||||||
|
/*width: 230px;*/
|
||||||
|
/*height: 345px;*/
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -97,6 +99,39 @@
|
|||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comfy-grid .item .model-preview-overlay {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-grid .copy-model-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-grid .copy-model-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.icon-button.copy-model-button.copy-alert-success {
|
||||||
|
color: green;
|
||||||
|
border-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.icon-button.copy-model-button.copy-alert-fail {
|
||||||
|
color: darkred;
|
||||||
|
border-color: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-grid .model-label {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
/* comfy radio group */
|
/* comfy radio group */
|
||||||
.comfy-radio-group {
|
.comfy-radio-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -141,6 +176,21 @@
|
|||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-manager.sidebar-left {
|
||||||
|
width: 50%;
|
||||||
|
left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager.sidebar-bottom {
|
||||||
|
height: 50%;
|
||||||
|
top: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager.sidebar-right {
|
||||||
|
width: 50%;
|
||||||
|
left: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
.model-manager .comfy-modal-content {
|
.model-manager .comfy-modal-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -201,7 +251,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
aspect-ratio: 1;
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* model manager row */
|
/* model manager row */
|
||||||
@@ -231,13 +283,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* model manager special */
|
/* model manager special */
|
||||||
.model-manager .close {
|
.model-manager .topbar-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 1px 6px;
|
display: flex;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-manager .topbar-buttons button {
|
||||||
|
width: 33px;
|
||||||
|
height: 33px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-manager .sidebar-buttons {
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 10px;
|
||||||
|
color: var(--input-text);
|
||||||
|
}
|
||||||
|
|
||||||
.model-manager .row {
|
.model-manager .row {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
|||||||
@@ -2,16 +2,55 @@ import { app } from "../../scripts/app.js";
|
|||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||||
|
|
||||||
function debounce(func, delay) {
|
function debounce(callback, delay) {
|
||||||
let timer;
|
let timeoutId = null;
|
||||||
return function () {
|
return (...args) => {
|
||||||
clearTimeout(timer);
|
window.clearTimeout(timeoutId);
|
||||||
timer = setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
func.apply(this, arguments);
|
callback(...args);
|
||||||
}, delay);
|
}, delay);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function request(url, options) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.fetchApi(url, options)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelNodeType(modelType) {
|
||||||
|
if (modelType === "checkpoints") return "CheckpointLoaderSimple";
|
||||||
|
else if (modelType === "clip") return "CLIPLoader";
|
||||||
|
else if (modelType === "clip_vision") return "CLIPVisionLoader";
|
||||||
|
else if (modelType === "controlnet") return "ControlNetLoader";
|
||||||
|
else if (modelType === "diffusers") return "DiffusersLoader";
|
||||||
|
else if (modelType === "embeddings") return "Embedding";
|
||||||
|
else if (modelType === "gligen") return "GLIGENLoader";
|
||||||
|
else if (modelType === "hypernetworks") return "HypernetworkLoader";
|
||||||
|
else if (modelType === "loras") return "LoraLoader";
|
||||||
|
else if (modelType === "style_models") return "StyleModelLoader";
|
||||||
|
else if (modelType === "unet") return "UNETLoader";
|
||||||
|
else if (modelType === "upscale_models") return "UpscaleModelLoader";
|
||||||
|
else if (modelType === "vae") return "VAELoader";
|
||||||
|
else if (modelType === "vae_approx") return undefined;
|
||||||
|
else { console.warn(`ModelType ${modelType} unrecognized.`); return undefined; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelWidgetIndex(nodeType) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathToEmbeddingString(path, removeExtension = false) {
|
||||||
|
const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1;
|
||||||
|
if (removeExtension) {
|
||||||
|
// TODO: setting.remove_extension_embedding
|
||||||
|
}
|
||||||
|
return "(embedding:" + path.slice(i) + ":1.0)";
|
||||||
|
}
|
||||||
|
|
||||||
class Tabs {
|
class Tabs {
|
||||||
/** @type {Record<string, HTMLDivElement>} */
|
/** @type {Record<string, HTMLDivElement>} */
|
||||||
#head = {};
|
#head = {};
|
||||||
@@ -87,7 +126,7 @@ function $tab(name, el) {
|
|||||||
return $el("div", { dataset: { name } }, el);
|
return $el("div", { dataset: { name } }, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
class List {
|
class SourceList {
|
||||||
/**
|
/**
|
||||||
* @typedef Column
|
* @typedef Column
|
||||||
* @prop {string} title
|
* @prop {string} title
|
||||||
@@ -163,39 +202,180 @@ class List {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterList(list, searchString, installedType) {
|
||||||
|
/** @type {Array<string>} */
|
||||||
|
const keywords = searchString
|
||||||
|
.replace("*", " ")
|
||||||
|
.split(/(-?".*?"|[^\s"]+)+/g)
|
||||||
|
.map((item) => item
|
||||||
|
.trim()
|
||||||
|
.replace(/(?:'|")+/g, "")
|
||||||
|
.toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
let fields = ["type", "name", "base", "description"];
|
||||||
|
const regexSHA256 = /^[a-f0-9]{64}$/gi;
|
||||||
|
const newList = list.filter((element) => {
|
||||||
|
if (installedType !== "Filter: All") {
|
||||||
|
if ((installedType === "Downloaded" && !element["installed"]) ||
|
||||||
|
(installedType === "Not Downloaded" && element["installed"])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const text = fields
|
||||||
|
.reduce((memo, field) => memo + " " + element[field], "")
|
||||||
|
.toLowerCase();
|
||||||
|
return keywords.reduce((memo, target) => {
|
||||||
|
const excludeTarget = target[0] === "-";
|
||||||
|
if (excludeTarget && target.length === 1) { return memo; }
|
||||||
|
const filteredTarget = excludeTarget ? target.slice(1) : target;
|
||||||
|
if (element["SHA256"] !== undefined && regexSHA256.test(filteredTarget)) {
|
||||||
|
return memo && excludeTarget !== (filteredTarget === element["SHA256"]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return memo && excludeTarget !== text.includes(filteredTarget);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setData(newList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Grid {
|
class ModelGrid {
|
||||||
constructor() {
|
static filter(list, searchString) {
|
||||||
this.element = $el("div.comfy-grid");
|
/** @type {Array<string>} */
|
||||||
|
const keywords = searchString
|
||||||
|
.replace("*", " ")
|
||||||
|
.split(/(-?".*?"|[^\s"]+)+/g)
|
||||||
|
.map((item) => item
|
||||||
|
.trim()
|
||||||
|
.replace(/(?:'|")+/g, "")
|
||||||
|
.toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const regexSHA256 = /^[a-f0-9]{64}$/gi;
|
||||||
|
const fields = ["name", "search-path"]; // TODO: Remove "search-path" hack.
|
||||||
|
return list.filter((element) => {
|
||||||
|
const text = fields
|
||||||
|
.reduce((memo, field) => memo + " " + element[field], "")
|
||||||
|
.toLowerCase();
|
||||||
|
return keywords.reduce((memo, target) => {
|
||||||
|
const excludeTarget = target[0] === "-";
|
||||||
|
if (excludeTarget && target.length === 1) { return memo; }
|
||||||
|
const filteredTarget = excludeTarget ? target.slice(1) : target;
|
||||||
|
if (element["SHA256"] !== undefined && regexSHA256.test(filteredTarget)) {
|
||||||
|
return memo && excludeTarget !== (filteredTarget === element["SHA256"]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return memo && excludeTarget !== text.includes(filteredTarget);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#dataSource = [];
|
static #addModel(event, modelType, path) {
|
||||||
|
const target = document.elementFromPoint(event.x, event.y);
|
||||||
setData(dataSource) {
|
if (modelType !== "embeddings" && target.id === "graph-canvas") {
|
||||||
this.#dataSource = dataSource;
|
const nodeType = modelNodeType(modelType);
|
||||||
this.element.innerHTML = [];
|
const widgetIndex = modelWidgetIndex(nodeType);
|
||||||
this.#updateList();
|
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]) {
|
||||||
|
if (nodeAtPos && nodeAtPos.type === nodeType) {
|
||||||
|
let node = nodeAtPos;
|
||||||
|
node.widgets[widgetIndex].value = path;
|
||||||
|
app.canvas.selectNode(node);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let node = LiteGraph.createNode(nodeType, null, []);
|
||||||
|
if (node) {
|
||||||
|
node.pos[0] = pos[0];
|
||||||
|
node.pos[1] = pos[1];
|
||||||
|
node.widgets[widgetIndex].value = path;
|
||||||
|
app.graph.add(node, {doProcessChange: true});
|
||||||
|
app.canvas.selectNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
else if (modelType === "embeddings" && target.type === "textarea") {
|
||||||
|
const text = pathToEmbeddingString(path);
|
||||||
|
const currentText = target.value;
|
||||||
|
const sep = currentText.length === 0 || currentText.slice(-1).match(/\s/) ? "" : " ";
|
||||||
|
target.value = currentText + sep + text;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateList() {
|
static #copyModelToClipboard(event, modelType, path) {
|
||||||
this.element.innerHTML = null;
|
const nodeType = modelNodeType(modelType);
|
||||||
if (this.#dataSource.length > 0) {
|
let successful = false;
|
||||||
this.element.append.apply(
|
if (nodeType === "Embedding") {
|
||||||
this.element,
|
if (navigator.clipboard){
|
||||||
this.#dataSource.map((item) => {
|
const text = pathToEmbeddingString(path);
|
||||||
const uri = item.post ?? "no-post";
|
navigator.clipboard.writeText(text);
|
||||||
const imgUrl = `/model-manager/image-preview?uri=${uri}`;
|
successful = true;
|
||||||
return $el("div.item", {}, [
|
}
|
||||||
$el("img", { src: imgUrl }),
|
else {
|
||||||
$el("div", {}, [
|
console.warn("Cannot copy embedding to the system clipboard; Try dragging the element instead.");
|
||||||
$el("p", [item.name])
|
}
|
||||||
]),
|
}
|
||||||
]);
|
else if (nodeType) {
|
||||||
})
|
const node = LiteGraph.createNode(nodeType, null, []);
|
||||||
);
|
const widgetIndex = modelWidgetIndex(nodeType);
|
||||||
|
node.widgets[widgetIndex].value = path;
|
||||||
|
app.canvas.copyToClipboard([node]);
|
||||||
|
successful = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn(`Unable to copy unknown model type '${modelType}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = event.target;
|
||||||
|
const name = successful ? "copy-alert-success" : "copy-alert-fail";
|
||||||
|
element.classList.add(name);
|
||||||
|
element.innerHTML = successful ? "✔" : "✖";
|
||||||
|
window.setTimeout((element, name) => {
|
||||||
|
element.classList.remove(name);
|
||||||
|
element.innerHTML = "⧉︎";
|
||||||
|
}, 500, element, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateInnerHtml(models, modelType) {
|
||||||
|
if (models.length > 0) {
|
||||||
|
return models.map((item) => {
|
||||||
|
const uri = item.post ?? "no-post";
|
||||||
|
const imgUrl = `/model-manager/image-preview?uri=${uri}`;
|
||||||
|
const addModel = (e) => ModelGrid.#addModel(e, modelType, item.path);
|
||||||
|
const copy = (e) => ModelGrid.#copyModelToClipboard(e, modelType, item.path);
|
||||||
|
return $el("div.item", {}, [
|
||||||
|
$el("img.model-preview", {
|
||||||
|
src: imgUrl,
|
||||||
|
draggable: false,
|
||||||
|
}),
|
||||||
|
$el("div.model-preview-overlay", {
|
||||||
|
src: imgUrl,
|
||||||
|
ondragend: (e) => addModel(e),
|
||||||
|
draggable: true,
|
||||||
|
}),
|
||||||
|
$el("button.icon-button.copy-model-button", {
|
||||||
|
type: "button",
|
||||||
|
textContent: "⧉︎",
|
||||||
|
onclick: (e) => copy(e),
|
||||||
|
draggable: false,
|
||||||
|
}),
|
||||||
|
$el("div.model-label", {
|
||||||
|
ondragend: (e) => addModel(e),
|
||||||
|
draggable: true,
|
||||||
|
}, [
|
||||||
|
$el("p", [item.name])
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.element.innerHTML = "<h2>No Models</h2>";
|
return [$el("h2", ["No Models"])];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,23 +418,16 @@ function $radioGroup(attr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ModelManager extends ComfyDialog {
|
class ModelManager extends ComfyDialog {
|
||||||
#request(url, options) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.fetchApi(url, options)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then(resolve)
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#el = {
|
#el = {
|
||||||
loadSourceBtn: null,
|
loadSourceBtn: null,
|
||||||
loadSourceFromInput: null,
|
loadSourceFromInput: null,
|
||||||
sourceInstalledFilter: null,
|
sourceInstalledFilter: null,
|
||||||
sourceContentFilter: null,
|
sourceContentFilter: null,
|
||||||
sourceFilterBtn: null,
|
sourceFilterBtn: null,
|
||||||
|
modelGrid: null,
|
||||||
modelTypeSelect: null,
|
modelTypeSelect: null,
|
||||||
modelContentFilter: null,
|
modelContentFilter: null,
|
||||||
|
sidebarButtons: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
#data = {
|
#data = {
|
||||||
@@ -262,23 +435,47 @@ class ModelManager extends ComfyDialog {
|
|||||||
models: {},
|
models: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {List} */
|
/** @type {SourceList} */
|
||||||
#sourceList = null;
|
#sourceList = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.element = $el(
|
this.element = $el(
|
||||||
"div.comfy-modal.model-manager",
|
"div.comfy-modal.model-manager",
|
||||||
{ parent: document.body },
|
{
|
||||||
|
parent: document.body,
|
||||||
|
},
|
||||||
[
|
[
|
||||||
$el("div.comfy-modal-content", [
|
$el("div.comfy-modal-content", [
|
||||||
$el("button.close.icon-button", {
|
$el("div.topbar-buttons",
|
||||||
textContent: "✕",
|
[
|
||||||
onclick: () => this.close(),
|
$el("div.sidebar-buttons",
|
||||||
}),
|
{
|
||||||
|
$: (el) => (this.#el.sidebarButtons = el),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
$el("button.icon-button", {
|
||||||
|
textContent: "◧",
|
||||||
|
onclick: (event) => this.#setSidebar(event),
|
||||||
|
}),
|
||||||
|
$el("button.icon-button", {
|
||||||
|
textContent: "⬓",
|
||||||
|
onclick: (event) => this.#setSidebar(event),
|
||||||
|
}),
|
||||||
|
$el("button.icon-button", {
|
||||||
|
textContent: "◨",
|
||||||
|
onclick: (event) => this.#setSidebar(event),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
$el("button.icon-button", {
|
||||||
|
textContent: "✖",
|
||||||
|
onclick: () => this.close(),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
),
|
||||||
$tabs([
|
$tabs([
|
||||||
$tab("Install", this.#createSourceInstall()),
|
$tab("Install", this.#createSourceInstall()),
|
||||||
$tab("Models", this.#createModelList()),
|
$tab("Models", this.#createModelTabHtml()),
|
||||||
$tab("Settings", []),
|
$tab("Settings", []),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
@@ -290,12 +487,11 @@ class ModelManager extends ComfyDialog {
|
|||||||
|
|
||||||
#init() {
|
#init() {
|
||||||
this.#refreshSourceList();
|
this.#refreshSourceList();
|
||||||
this.#refreshModelList();
|
this.#modelGridRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
#createSourceInstall() {
|
#createSourceInstall() {
|
||||||
this.#createSourceList();
|
this.#createSourceList();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$el("div.row.tab-header", [
|
$el("div.row.tab-header", [
|
||||||
$el("div.row.tab-header-flex-block", [
|
$el("div.row.tab-header-flex-block", [
|
||||||
@@ -316,8 +512,7 @@ class ModelManager extends ComfyDialog {
|
|||||||
placeholder: "example: \"sd_xl\" -vae",
|
placeholder: "example: \"sd_xl\" -vae",
|
||||||
onkeyup: (e) => e.key === "Enter" && this.#filterSourceList(),
|
onkeyup: (e) => e.key === "Enter" && this.#filterSourceList(),
|
||||||
}),
|
}),
|
||||||
$el(
|
$el("select",
|
||||||
"select",
|
|
||||||
{
|
{
|
||||||
$: (el) => (this.#el.sourceInstalledFilter = el),
|
$: (el) => (this.#el.sourceInstalledFilter = el),
|
||||||
style: { width: 0 },
|
style: { width: 0 },
|
||||||
@@ -341,7 +536,7 @@ class ModelManager extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#createSourceList() {
|
#createSourceList() {
|
||||||
const sourceList = new List([
|
const sourceList = new SourceList([
|
||||||
{
|
{
|
||||||
title: "Type",
|
title: "Type",
|
||||||
dataIndex: "type",
|
dataIndex: "type",
|
||||||
@@ -378,7 +573,7 @@ class ModelManager extends ComfyDialog {
|
|||||||
textContent: installed ? "✓︎" : "📥︎",
|
textContent: installed ? "✓︎" : "📥︎",
|
||||||
onclick: async (e) => {
|
onclick: async (e) => {
|
||||||
e.disabled = true;
|
e.disabled = true;
|
||||||
const response = await this.#request(
|
const response = await request(
|
||||||
"/model-manager/download",
|
"/model-manager/download",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -400,7 +595,7 @@ class ModelManager extends ComfyDialog {
|
|||||||
|
|
||||||
const source = this.#el.loadSourceFromInput.value;
|
const source = this.#el.loadSourceFromInput.value;
|
||||||
const uri = (source === "https://ComfyUI-Model-Manager/index.json") || (source === "") ? "local" : source;
|
const uri = (source === "https://ComfyUI-Model-Manager/index.json") || (source === "") ? "local" : source;
|
||||||
const dataSource = await this.#request(
|
const dataSource = await request(
|
||||||
`/model-manager/source?uri=${uri}`
|
`/model-manager/source?uri=${uri}`
|
||||||
).catch(() => []);
|
).catch(() => []);
|
||||||
this.#data.sources = dataSource;
|
this.#data.sources = dataSource;
|
||||||
@@ -411,69 +606,30 @@ class ModelManager extends ComfyDialog {
|
|||||||
this.#el.loadSourceBtn.disabled = false;
|
this.#el.loadSourceBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filterSourceList() {
|
#filterSourceList() {
|
||||||
/** @type {Array<string>} */
|
this.#sourceList.filterList(
|
||||||
const content = this.#el.sourceContentFilter.value
|
this.#data.sources,
|
||||||
.replace("*", " ")
|
this.#el.sourceContentFilter.value,
|
||||||
.split(/(-?".*?"|[^\s"]+)+/g)
|
this.#el.sourceInstalledFilter.value
|
||||||
.map((item) => item
|
);
|
||||||
.trim()
|
|
||||||
.replace(/(?:'|")+/g, "")
|
|
||||||
.toLowerCase() // TODO: Quotes should be exact?
|
|
||||||
)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const installedType = this.#el.sourceInstalledFilter.value;
|
|
||||||
const newDataSource = this.#data.sources.filter((row) => {
|
|
||||||
if (installedType !== "Filter: All") {
|
|
||||||
if ((installedType === "Downloaded" && !row["installed"]) ||
|
|
||||||
(installedType === "Not Downloaded" && row["installed"])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let filterField = ["type", "name", "base", "description"];
|
|
||||||
const rowText = filterField
|
|
||||||
.reduce((memo, field) => memo + " " + row[field], "")
|
|
||||||
.toLowerCase();
|
|
||||||
return content.reduce((memo, target) => {
|
|
||||||
const excludeTarget = target[0] === "-";
|
|
||||||
if (excludeTarget && target.length === 1) { return memo; }
|
|
||||||
const filteredTarget = excludeTarget ? target.slice(1) : target;
|
|
||||||
const regexSHA256 = /^[a-f0-9]{64}$/gi;
|
|
||||||
if (row["SHA256"] !== undefined && regexSHA256.test(filteredTarget)) {
|
|
||||||
return memo && excludeTarget !== (filteredTarget === row["SHA256"]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return memo && excludeTarget !== rowText.includes(filteredTarget);
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#sourceList.setData(newDataSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Grid} */
|
#createModelTabHtml() {
|
||||||
#modelList = null;
|
const modelGrid = $el("div.comfy-grid");
|
||||||
|
this.#el.modelGrid = modelGrid;
|
||||||
#createModelList() {
|
|
||||||
const gridInstance = new Grid();
|
|
||||||
this.#modelList = gridInstance;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$el("div.row.tab-header", [
|
$el("div.row.tab-header", [
|
||||||
$el("div.row.tab-header-flex-block",
|
$el("div.row.tab-header-flex-block", [
|
||||||
[
|
|
||||||
$el("button.icon-button", {
|
$el("button.icon-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "⟳",
|
textContent: "⟳",
|
||||||
onclick: () => this.#refreshModelList(),
|
onclick: () => this.#modelGridRefresh(),
|
||||||
}),
|
}),
|
||||||
$el("select.model-type-dropdown",
|
$el("select.model-type-dropdown",
|
||||||
{
|
{
|
||||||
$: (el) => (this.#el.modelTypeSelect = el),
|
$: (el) => (this.#el.modelTypeSelect = el),
|
||||||
name: "model-type",
|
name: "model-type",
|
||||||
onchange: () => this.#filterModelList(),
|
onchange: () => this.#modelGridUpdate(),
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
$el("option", ["checkpoints"]),
|
$el("option", ["checkpoints"]),
|
||||||
@@ -492,64 +648,69 @@ class ModelManager extends ComfyDialog {
|
|||||||
$el("option", ["vae_approx"]),
|
$el("option", ["vae_approx"]),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]
|
]),
|
||||||
),
|
|
||||||
$el("div.row.tab-header-flex-block", [
|
$el("div.row.tab-header-flex-block", [
|
||||||
$el("input.search-text-area", {
|
$el("input.search-text-area", {
|
||||||
$: (el) => (this.#el.modelContentFilter = el),
|
$: (el) => (this.#el.modelContentFilter = el),
|
||||||
placeholder: "example: styles/clothing -.pt",
|
placeholder: "example: styles/clothing -.pt",
|
||||||
onkeyup: (e) => e.key === "Enter" && this.#filterModelList(),
|
onkeyup: (e) => e.key === "Enter" && this.#modelGridUpdate(),
|
||||||
}),
|
}),
|
||||||
$el("button.icon-button", {
|
$el("button.icon-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "🔍︎",
|
textContent: "🔍︎",
|
||||||
onclick: () => this.#filterModelList(),
|
onclick: () => this.#modelGridUpdate(),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
gridInstance.element,
|
modelGrid,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async #refreshModelList() {
|
#modelGridUpdate() {
|
||||||
const dataSource = await this.#request("/model-manager/models");
|
const searchText = this.#el.modelContentFilter.value;
|
||||||
this.#data.models = dataSource;
|
|
||||||
this.#filterModelList();
|
|
||||||
}
|
|
||||||
|
|
||||||
#filterModelList() {
|
|
||||||
/** @type {Array<string>} */
|
|
||||||
const content = this.#el.modelContentFilter.value
|
|
||||||
.replace("*", " ")
|
|
||||||
.split(/(-?".*?"|[^\s"]+)+/g)
|
|
||||||
.map((item) => item
|
|
||||||
.trim()
|
|
||||||
.replace(/(?:'|")+/g, "")
|
|
||||||
.toLowerCase() // TODO: Quotes should be exact?
|
|
||||||
)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const modelType = this.#el.modelTypeSelect.value;
|
const modelType = this.#el.modelTypeSelect.value;
|
||||||
|
const models = this.#data.models;
|
||||||
|
const modelList = ModelGrid.filter(models[modelType], searchText);
|
||||||
|
|
||||||
const newDataSource = this.#data.models[modelType].filter((modelInfo) => {
|
const modelGrid = this.#el.modelGrid;
|
||||||
const filterField = ["name", "path"];
|
modelGrid.innerHTML = [];
|
||||||
const modelText = filterField
|
const innerHTML = ModelGrid.generateInnerHtml(modelList, modelType);
|
||||||
.reduce((memo, field) => memo + " " + modelInfo[field], "")
|
modelGrid.append.apply(modelGrid, innerHTML);
|
||||||
.toLowerCase();
|
};
|
||||||
return content.reduce((memo, target) => {
|
|
||||||
const excludeTarget = target[0] === "-";
|
async #modelGridRefresh() {
|
||||||
if (excludeTarget && target.length === 1) { return memo; }
|
this.#data.models = await request("/model-manager/models");
|
||||||
const filteredTarget = excludeTarget ? target.slice(1) : target;
|
this.#modelGridUpdate();
|
||||||
const regexSHA256 = /^[a-f0-9]{64}$/gi;
|
};
|
||||||
if (modelInfo["SHA256"] !== undefined && regexSHA256.test(filteredTarget)) {
|
|
||||||
return memo && excludeTarget !== (filteredTarget === modelInfo["SHA256"]);
|
#setSidebar(event) {
|
||||||
}
|
// TODO: settings.sidebar_side_width
|
||||||
else {
|
// TODO: settings.sidebar_bottom_height
|
||||||
return memo && excludeTarget !== modelText.includes(filteredTarget);
|
// TODO: draggable resize?
|
||||||
}
|
const button = event.target;
|
||||||
}, true);
|
const sidebarButtons = this.#el.sidebarButtons.children;
|
||||||
});
|
let buttonIndex;
|
||||||
this.#modelList.setData(newDataSource);
|
for (buttonIndex = 0; buttonIndex < sidebarButtons.length; buttonIndex++) {
|
||||||
|
if (sidebarButtons[buttonIndex] === button) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelManager = this.element;
|
||||||
|
const sidebarStates = ["sidebar-left", "sidebar-bottom", "sidebar-right"];
|
||||||
|
let stateIndex;
|
||||||
|
for (stateIndex = 0; stateIndex < sidebarStates.length; stateIndex++) {
|
||||||
|
const state = sidebarStates[stateIndex];
|
||||||
|
if (modelManager.classList.contains(state)) {
|
||||||
|
modelManager.classList.remove(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateIndex != buttonIndex) {
|
||||||
|
const newSidebarState = sidebarStates[buttonIndex];
|
||||||
|
modelManager.classList.add(newSidebarState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,3 +747,5 @@ app.registerExtension({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ◧ ◨ ⬒ ⬓ ⛶ ✚
|
||||||
|
|||||||
Reference in New Issue
Block a user