copy to clipboard, drag to add, sidebar modes

This commit is contained in:
Christian Bastian
2024-01-03 06:58:25 -05:00
parent cc7f463f91
commit 3983873591
4 changed files with 393 additions and 163 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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({
); );
}, },
}); });
// ◧ ◨ ⬒ ⬓ ⛶ ✚