Better model manager UI (#802)

* fixed row height with lib API

* update grid lib

* UI adjustment

* move some of api to common utils

* added model manager

* update install

* replace model-downloader with model-manager

* replace model-downloader with model-manager

* fixed filter

* fixed status

* apply loading animation for install button

* sort type and base
This commit is contained in:
CenFun 2024-06-22 09:12:01 +08:00 committed by GitHub
parent f623e3fb01
commit 651496cae0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 842 additions and 566 deletions

View File

@ -12,8 +12,8 @@ import {
} from "./comfyui-share-common.js";
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js";
import { SnapshotManager } from "./snapshot.js";
import { ModelInstaller } from "./model-downloader.js";
import { manager_instance, setManagerInstance, install_via_git_url, install_pip, rebootAPI, free_models, show_message } from "./common.js";
import { ComponentBuilderDialog, load_components, set_component_policy, getPureName } from "./components-manager.js";
import { set_double_click_policy } from "./node_fixer.js";
@ -736,14 +736,16 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
type: "button",
textContent: "Install Models",
textContent: "Model Manager",
onclick:
() => {
if(!ModelInstaller.instance)
ModelInstaller.instance = new ModelInstaller(app, self);
ModelInstaller.instance.show();
if(!ModelManager.instance) {
ModelManager.instance = new ModelManager(app, self);
}
ModelManager.instance.show();
}
}),

View File

@ -119,3 +119,94 @@ export async function free_models() {
show_message('Unloading of models failed.<BR><BR>Installed ComfyUI may be an outdated version.')
}
}
export function md5(inputString) {
const hc = '0123456789abcdef';
const rh = n => {let j,s='';for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;}
const ad = (x,y) => {let l=(x&0xFFFF)+(y&0xFFFF);let m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);}
const rl = (n,c) => (n<<c)|(n>>>(32-c));
const cm = (q,a,b,x,s,t) => ad(rl(ad(ad(a,q),ad(x,t)),s),b);
const ff = (a,b,c,d,x,s,t) => cm((b&c)|((~b)&d),a,b,x,s,t);
const gg = (a,b,c,d,x,s,t) => cm((b&d)|(c&(~d)),a,b,x,s,t);
const hh = (a,b,c,d,x,s,t) => cm(b^c^d,a,b,x,s,t);
const ii = (a,b,c,d,x,s,t) => cm(c^(b|(~d)),a,b,x,s,t);
const sb = x => {
let i;const nblk=((x.length+8)>>6)+1;const blks=[];for(i=0;i<nblk*16;i++) { blks[i]=0 };
for(i=0;i<x.length;i++) {blks[i>>2]|=x.charCodeAt(i)<<((i%4)*8);}
blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks;
}
let i,x=sb(inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd;
for(i=0;i<x.length;i+=16) {olda=a;oldb=b;oldc=c;oldd=d;
a=ff(a,b,c,d,x[i+ 0], 7, -680876936);d=ff(d,a,b,c,x[i+ 1],12, -389564586);c=ff(c,d,a,b,x[i+ 2],17, 606105819);
b=ff(b,c,d,a,x[i+ 3],22,-1044525330);a=ff(a,b,c,d,x[i+ 4], 7, -176418897);d=ff(d,a,b,c,x[i+ 5],12, 1200080426);
c=ff(c,d,a,b,x[i+ 6],17,-1473231341);b=ff(b,c,d,a,x[i+ 7],22, -45705983);a=ff(a,b,c,d,x[i+ 8], 7, 1770035416);
d=ff(d,a,b,c,x[i+ 9],12,-1958414417);c=ff(c,d,a,b,x[i+10],17, -42063);b=ff(b,c,d,a,x[i+11],22,-1990404162);
a=ff(a,b,c,d,x[i+12], 7, 1804603682);d=ff(d,a,b,c,x[i+13],12, -40341101);c=ff(c,d,a,b,x[i+14],17,-1502002290);
b=ff(b,c,d,a,x[i+15],22, 1236535329);a=gg(a,b,c,d,x[i+ 1], 5, -165796510);d=gg(d,a,b,c,x[i+ 6], 9,-1069501632);
c=gg(c,d,a,b,x[i+11],14, 643717713);b=gg(b,c,d,a,x[i+ 0],20, -373897302);a=gg(a,b,c,d,x[i+ 5], 5, -701558691);
d=gg(d,a,b,c,x[i+10], 9, 38016083);c=gg(c,d,a,b,x[i+15],14, -660478335);b=gg(b,c,d,a,x[i+ 4],20, -405537848);
a=gg(a,b,c,d,x[i+ 9], 5, 568446438);d=gg(d,a,b,c,x[i+14], 9,-1019803690);c=gg(c,d,a,b,x[i+ 3],14, -187363961);
b=gg(b,c,d,a,x[i+ 8],20, 1163531501);a=gg(a,b,c,d,x[i+13], 5,-1444681467);d=gg(d,a,b,c,x[i+ 2], 9, -51403784);
c=gg(c,d,a,b,x[i+ 7],14, 1735328473);b=gg(b,c,d,a,x[i+12],20,-1926607734);a=hh(a,b,c,d,x[i+ 5], 4, -378558);
d=hh(d,a,b,c,x[i+ 8],11,-2022574463);c=hh(c,d,a,b,x[i+11],16, 1839030562);b=hh(b,c,d,a,x[i+14],23, -35309556);
a=hh(a,b,c,d,x[i+ 1], 4,-1530992060);d=hh(d,a,b,c,x[i+ 4],11, 1272893353);c=hh(c,d,a,b,x[i+ 7],16, -155497632);
b=hh(b,c,d,a,x[i+10],23,-1094730640);a=hh(a,b,c,d,x[i+13], 4, 681279174);d=hh(d,a,b,c,x[i+ 0],11, -358537222);
c=hh(c,d,a,b,x[i+ 3],16, -722521979);b=hh(b,c,d,a,x[i+ 6],23, 76029189);a=hh(a,b,c,d,x[i+ 9], 4, -640364487);
d=hh(d,a,b,c,x[i+12],11, -421815835);c=hh(c,d,a,b,x[i+15],16, 530742520);b=hh(b,c,d,a,x[i+ 2],23, -995338651);
a=ii(a,b,c,d,x[i+ 0], 6, -198630844);d=ii(d,a,b,c,x[i+ 7],10, 1126891415);c=ii(c,d,a,b,x[i+14],15,-1416354905);
b=ii(b,c,d,a,x[i+ 5],21, -57434055);a=ii(a,b,c,d,x[i+12], 6, 1700485571);d=ii(d,a,b,c,x[i+ 3],10,-1894986606);
c=ii(c,d,a,b,x[i+10],15, -1051523);b=ii(b,c,d,a,x[i+ 1],21,-2054922799);a=ii(a,b,c,d,x[i+ 8], 6, 1873313359);
d=ii(d,a,b,c,x[i+15],10, -30611744);c=ii(c,d,a,b,x[i+ 6],15,-1560198380);b=ii(b,c,d,a,x[i+13],21, 1309151649);
a=ii(a,b,c,d,x[i+ 4], 6, -145523070);d=ii(d,a,b,c,x[i+11],10,-1120210379);c=ii(c,d,a,b,x[i+ 2],15, 718787259);
b=ii(b,c,d,a,x[i+ 9],21, -343485551);a=ad(a,olda);b=ad(b,oldb);c=ad(c,oldc);d=ad(d,oldd);
}
return rh(a)+rh(b)+rh(c)+rh(d);
}
export async function fetchData(route, options) {
let err;
const res = await api.fetchApi(route, options).catch(e => {
err = e;
});
if (!res) {
return {
status: 400,
error: new Error("Unknown Error")
}
}
const { status, statusText } = res;
if (err) {
return {
status,
error: err
}
}
if (status !== 200) {
return {
status,
error: new Error(statusText || "Unknown Error")
}
}
const data = await res.json();
if (!data) {
return {
status,
error: new Error(`Failed to load data: ${route}`)
}
}
return {
status,
data
}
}
export const icons = {
search: '<svg viewBox="0 0 24 24" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.486-4.494M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0"/></svg>',
extensions: '<svg viewBox="64 64 896 896" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M843.5 737.4c-12.4-75.2-79.2-129.1-155.3-125.4S550.9 676 546 752c-153.5-4.8-208-40.7-199.1-113.7 3.3-27.3 19.8-41.9 50.1-49 18.4-4.3 38.8-4.9 57.3-3.2 1.7.2 3.5.3 5.2.5 11.3 2.7 22.8 5 34.3 6.8 34.1 5.6 68.8 8.4 101.8 6.6 92.8-5 156-45.9 159.2-132.7 3.1-84.1-54.7-143.7-147.9-183.6-29.9-12.8-61.6-22.7-93.3-30.2-14.3-3.4-26.3-5.7-35.2-7.2-7.9-75.9-71.5-133.8-147.8-134.4S189.7 168 180.5 243.8s40 146.3 114.2 163.9 149.9-23.3 175.7-95.1c9.4 1.7 18.7 3.6 28 5.8 28.2 6.6 56.4 15.4 82.4 26.6 70.7 30.2 109.3 70.1 107.5 119.9-1.6 44.6-33.6 65.2-96.2 68.6-27.5 1.5-57.6-.9-87.3-5.8-8.3-1.4-15.9-2.8-22.6-4.3-3.9-.8-6.6-1.5-7.8-1.8l-3.1-.6c-2.2-.3-5.9-.8-10.7-1.3-25-2.3-52.1-1.5-78.5 4.6-55.2 12.9-93.9 47.2-101.1 105.8-15.7 126.2 78.6 184.7 276 188.9 29.1 70.4 106.4 107.9 179.6 87 73.3-20.9 119.3-93.4 106.9-168.6M329.1 345.2a83.3 83.3 0 1 1 .01-166.61 83.3 83.3 0 0 1-.01 166.61M695.6 845a83.3 83.3 0 1 1 .01-166.61A83.3 83.3 0 0 1 695.6 845"/></svg>',
conflicts: '<svg viewBox="0 0 400 400" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m397.2 350.4.2-.2-180-320-.2.2C213.8 24.2 207.4 20 200 20s-13.8 4.2-17.2 10.4l-.2-.2-180 320 .2.2c-1.6 2.8-2.8 6-2.8 9.6 0 11 9 20 20 20h360c11 0 20-9 20-20 0-3.6-1.2-6.8-2.8-9.6M220 340h-40v-40h40zm0-60h-40V120h40z"/></svg>',
passed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.667 426.667"><path fill="#6AC259" d="M213.333,0C95.518,0,0,95.514,0,213.333s95.518,213.333,213.333,213.333c117.828,0,213.333-95.514,213.333-213.333S331.157,0,213.333,0z M174.199,322.918l-93.935-93.931l31.309-31.309l62.626,62.622l140.894-140.898l31.309,31.309L174.199,322.918z"/></svg>'
}

View File

@ -1,17 +1,13 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { $el } from "../../scripts/ui.js";
import { manager_instance, rebootAPI, install_via_git_url } from "./common.js";
import {
manager_instance, rebootAPI, install_via_git_url,
fetchData, md5, icons
} from "./common.js";
// https://cenfun.github.io/turbogrid/api.html
import TG from "./turbogrid.esm.js";
const icons = {
search: '<svg viewBox="0 0 24 24" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.486-4.494M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0"/></svg>',
extensions: '<svg viewBox="64 64 896 896" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M843.5 737.4c-12.4-75.2-79.2-129.1-155.3-125.4S550.9 676 546 752c-153.5-4.8-208-40.7-199.1-113.7 3.3-27.3 19.8-41.9 50.1-49 18.4-4.3 38.8-4.9 57.3-3.2 1.7.2 3.5.3 5.2.5 11.3 2.7 22.8 5 34.3 6.8 34.1 5.6 68.8 8.4 101.8 6.6 92.8-5 156-45.9 159.2-132.7 3.1-84.1-54.7-143.7-147.9-183.6-29.9-12.8-61.6-22.7-93.3-30.2-14.3-3.4-26.3-5.7-35.2-7.2-7.9-75.9-71.5-133.8-147.8-134.4S189.7 168 180.5 243.8s40 146.3 114.2 163.9 149.9-23.3 175.7-95.1c9.4 1.7 18.7 3.6 28 5.8 28.2 6.6 56.4 15.4 82.4 26.6 70.7 30.2 109.3 70.1 107.5 119.9-1.6 44.6-33.6 65.2-96.2 68.6-27.5 1.5-57.6-.9-87.3-5.8-8.3-1.4-15.9-2.8-22.6-4.3-3.9-.8-6.6-1.5-7.8-1.8l-3.1-.6c-2.2-.3-5.9-.8-10.7-1.3-25-2.3-52.1-1.5-78.5 4.6-55.2 12.9-93.9 47.2-101.1 105.8-15.7 126.2 78.6 184.7 276 188.9 29.1 70.4 106.4 107.9 179.6 87 73.3-20.9 119.3-93.4 106.9-168.6M329.1 345.2a83.3 83.3 0 1 1 .01-166.61 83.3 83.3 0 0 1-.01 166.61M695.6 845a83.3 83.3 0 1 1 .01-166.61A83.3 83.3 0 0 1 695.6 845"/></svg>',
conflicts: '<svg viewBox="0 0 400 400" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m397.2 350.4.2-.2-180-320-.2.2C213.8 24.2 207.4 20 200 20s-13.8 4.2-17.2 10.4l-.2-.2-180 320 .2.2c-1.6 2.8-2.8 6-2.8 9.6 0 11 9 20 20 20h360c11 0 20-9 20-20 0-3.6-1.2-6.8-2.8-9.6M220 340h-40v-40h40zm0-60h-40V120h40z"/></svg>'
}
const pageCss = `
.cn-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
@ -123,6 +119,7 @@ const pageCss = `
.cn-manager-grid .cn-node-name a {
color: skyblue;
text-decoration: none;
word-break: break-word;
}
.cn-manager-grid .cn-node-desc a {
@ -305,7 +302,7 @@ const pageHtml = `
<label>Filter
<select class="cn-manager-filter"></select>
</label>
<input class="cn-manager-keywords" type="search" placeholder="input search keyword" />
<input class="cn-manager-keywords" type="search" placeholder="Search" />
<div class="cn-manager-status"></div>
<div class="cn-flex-auto"></div>
<div class="cn-manager-channel"></div>
@ -614,58 +611,15 @@ export class CustomNodesManager {
const grid = new TG.Grid(container);
this.grid = grid;
const autoHeightColumns = ['description', "alternatives"];
let prevViewRowsLength = 0;
let prevViewRowsLength = -1;
grid.bind('onUpdated', (e, d) => {
const viewRows = grid.viewRows;
if (viewRows.length !== prevViewRowsLength) {
prevViewRowsLength = viewRows.length;
this.showStatus(`${prevViewRowsLength} custom nodes`);
this.showStatus(`${prevViewRowsLength.toLocaleString()} custom nodes`);
}
const visibleRowList = grid.viewport.rows;
const rows = [];
const heights = [];
visibleRowList.forEach(function(viewIndex) {
// display index after filter is no equal global index
const rowItem = grid.getViewRowItem(viewIndex);
if (rowItem.rowHeightFixed) {
return;
}
const list = autoHeightColumns.map(k => {
const cellNode = grid.getCellNode(rowItem, k);
if (cellNode) {
const div = cellNode.querySelector('.tg-multiline-fixing');
// 10px is padding top and bottom
const realHeight = Math.max(TG.$(div).height() + 10, grid.options.rowHeight);
return realHeight;
}
}).filter(n => n);
if (list.length) {
rowItem.rowHeightFixed = true;
rows.push(rowItem);
heights.push(Math.max.apply(null, list));
}
});
if (!rows.length) {
return;
}
grid.setRowHeight(rows, heights);
});
grid.bind('onColumnWidthChanged', (e, d) => {
if (autoHeightColumns.includes(d.id)) {
// reset when column width changed
grid.forEachRow(function(row) {
row.rowHeightFixed = false;
});
}
});
grid.bind('onSelectChanged', (e, changes) => {
@ -691,10 +645,15 @@ export class CustomNodesManager {
frozenColumn: 1,
rowNotFound: "No Results",
rowHeight: 30 * 3 + 3 * 4,
rowHeight: 40,
bindWindowResize: true,
bindContainerResize: true,
cellResizeObserver: (rowItem, columnItem) => {
const autoHeightColumns = ['title', 'installed', 'description', "alternatives"];
return autoHeightColumns.includes(columnItem.id)
},
// updateGrid handler for filter and keywords
rowFilter: (rowItem) => {
@ -765,12 +724,10 @@ export class CustomNodesManager {
width: 200,
minWidth: 100,
maxWidth: 500,
classMap: 'tg-multiline cn-node-name',
classMap: 'cn-node-name',
formatter: (title, rowItem, columnItem) => {
return `<div class="tg-multiline-wrapper">
${rowItem.installed === 'Fail' ? '<font color="red"><B>(IMPORT FAILED)</B></font>' : ''}
<a href=${rowItem.reference} target="_blank"><b>${title}</b></a>
</div>`;
return `${rowItem.installed === 'Fail' ? '<font color="red"><B>(IMPORT FAILED)</B></font>' : ''}
<a href=${rowItem.reference} target="_blank"><b>${title}</b></a>`;
}
}, {
id: 'installed',
@ -793,19 +750,13 @@ export class CustomNodesManager {
width: 400,
maxWidth: 5000,
invisible: !this.hasAlternatives(),
classMap: 'tg-multiline cn-node-desc',
formatter: (alternatives, rowItem, columnItem) => {
return `<div class="tg-multiline-fixing">${alternatives}</div>`;
}
classMap: 'cn-node-desc'
}, {
id: 'description',
name: 'Description',
width: 400,
maxWidth: 5000,
classMap: 'tg-multiline cn-node-desc',
formatter: (description, rowItem, columnItem) => {
return `<div class="tg-multiline-fixing">${description}</div>`;
}
classMap: 'cn-node-desc'
}, {
id: "extensions",
name: "Extensions",
@ -856,7 +807,7 @@ export class CustomNodesManager {
}, {
id: 'author',
name: 'Author',
width: 100,
width: 120,
classMap: "cn-node-author",
formatter: (author, rowItem, columnItem) => {
if (rowItem.trust) {
@ -907,9 +858,6 @@ export class CustomNodesManager {
if (this.grid) {
this.grid.update();
if (this.hasAlternatives()) {
this.grid.forEachRow(function(row) {
row.rowHeightFixed = false;
});
this.grid.showColumn("alternatives");
} else {
this.grid.hideColumn("alternatives");
@ -1000,7 +948,7 @@ export class CustomNodesManager {
this.showStatus(`${label} ${item.title} ...`);
const data = item.originalData;
const res = await this.fetchData(`/customnode/${mode}`, {
const res = await fetchData(`/customnode/${mode}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
@ -1049,53 +997,10 @@ export class CustomNodesManager {
// ===========================================================================================
async fetchData(route, options) {
let err;
const res = await api.fetchApi(route, options).catch(e => {
err = e;
});
if (!res) {
return {
status: 400,
error: new Error("Unknown Error")
}
}
const { status, statusText } = res;
if (err) {
return {
status,
error: err
}
}
if (status !== 200) {
return {
status,
error: new Error(statusText || "Unknown Error")
}
}
const data = await res.json();
if (!data) {
return {
status,
error: new Error(`Failed to load data: ${route}`)
}
}
return {
status,
data
}
}
// ===========================================================================================
async getExtensionMappings() {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading extension mappings (${mode}) ...`);
const res = await this.fetchData(`/customnode/getmappings?mode=${mode}`);
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
if (res.error) {
console.log(res.error);
return {}
@ -1149,7 +1054,7 @@ export class CustomNodesManager {
async getMissingNodes() {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading missing nodes (${mode}) ...`);
const res = await this.fetchData(`/customnode/getmappings?mode=${mode}`);
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
if (res.error) {
this.showError(`Failed to get custom node mappings: ${res.error}`);
return;
@ -1219,7 +1124,7 @@ export class CustomNodesManager {
}
}
const resUnresolved = await this.fetchData(`/component/get_unresolved`);
const resUnresolved = await fetchData(`/component/get_unresolved`);
const unresolved = resUnresolved.data;
if (unresolved && unresolved.nodes) {
unresolved.nodes.forEach(node_type => {
@ -1243,7 +1148,7 @@ export class CustomNodesManager {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading alternatives (${mode}) ...`);
const res = await this.fetchData(`/customnode/alternatives?mode=${mode}`);
const res = await fetchData(`/customnode/alternatives?mode=${mode}`);
if (res.error) {
this.showError(`Failed to get alternatives: ${res.error}`);
return [];
@ -1285,7 +1190,7 @@ export class CustomNodesManager {
this.showStatus(`Loading custom nodes (${mode}) ...`);
const skip_update = this.show_mode === ShowMode.UPDATE ? "" : "&skip_update=true";
const res = await this.fetchData(`/customnode/getlist?mode=${mode}${skip_update}`);
const res = await fetchData(`/customnode/getlist?mode=${mode}${skip_update}`);
if (res.error) {
this.showError("Failed to get custom node list.");
this.hideLoading();
@ -1302,7 +1207,8 @@ export class CustomNodesManager {
for (const item of custom_nodes) {
item.originalData = JSON.parse(JSON.stringify(item));
item.hash = await calculateHash(item);
const message = item.title + item.files[0];
item.hash = md5(message);
}
const filterItem = this.getFilterItem(this.show_mode);
@ -1472,53 +1378,3 @@ export class CustomNodesManager {
this.element.style.display = "none";
}
}
// ===========================================================================================
async function calculateHash(item) {
const message = item.title + item.files[0];
return md5(message);
}
function md5(inputString) {
const hc = '0123456789abcdef';
const rh = n => {let j,s='';for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;}
const ad = (x,y) => {let l=(x&0xFFFF)+(y&0xFFFF);let m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);}
const rl = (n,c) => (n<<c)|(n>>>(32-c));
const cm = (q,a,b,x,s,t) => ad(rl(ad(ad(a,q),ad(x,t)),s),b);
const ff = (a,b,c,d,x,s,t) => cm((b&c)|((~b)&d),a,b,x,s,t);
const gg = (a,b,c,d,x,s,t) => cm((b&d)|(c&(~d)),a,b,x,s,t);
const hh = (a,b,c,d,x,s,t) => cm(b^c^d,a,b,x,s,t);
const ii = (a,b,c,d,x,s,t) => cm(c^(b|(~d)),a,b,x,s,t);
const sb = x => {
let i;const nblk=((x.length+8)>>6)+1;const blks=[];for(i=0;i<nblk*16;i++) { blks[i]=0 };
for(i=0;i<x.length;i++) {blks[i>>2]|=x.charCodeAt(i)<<((i%4)*8);}
blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks;
}
let i,x=sb(inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd;
for(i=0;i<x.length;i+=16) {olda=a;oldb=b;oldc=c;oldd=d;
a=ff(a,b,c,d,x[i+ 0], 7, -680876936);d=ff(d,a,b,c,x[i+ 1],12, -389564586);c=ff(c,d,a,b,x[i+ 2],17, 606105819);
b=ff(b,c,d,a,x[i+ 3],22,-1044525330);a=ff(a,b,c,d,x[i+ 4], 7, -176418897);d=ff(d,a,b,c,x[i+ 5],12, 1200080426);
c=ff(c,d,a,b,x[i+ 6],17,-1473231341);b=ff(b,c,d,a,x[i+ 7],22, -45705983);a=ff(a,b,c,d,x[i+ 8], 7, 1770035416);
d=ff(d,a,b,c,x[i+ 9],12,-1958414417);c=ff(c,d,a,b,x[i+10],17, -42063);b=ff(b,c,d,a,x[i+11],22,-1990404162);
a=ff(a,b,c,d,x[i+12], 7, 1804603682);d=ff(d,a,b,c,x[i+13],12, -40341101);c=ff(c,d,a,b,x[i+14],17,-1502002290);
b=ff(b,c,d,a,x[i+15],22, 1236535329);a=gg(a,b,c,d,x[i+ 1], 5, -165796510);d=gg(d,a,b,c,x[i+ 6], 9,-1069501632);
c=gg(c,d,a,b,x[i+11],14, 643717713);b=gg(b,c,d,a,x[i+ 0],20, -373897302);a=gg(a,b,c,d,x[i+ 5], 5, -701558691);
d=gg(d,a,b,c,x[i+10], 9, 38016083);c=gg(c,d,a,b,x[i+15],14, -660478335);b=gg(b,c,d,a,x[i+ 4],20, -405537848);
a=gg(a,b,c,d,x[i+ 9], 5, 568446438);d=gg(d,a,b,c,x[i+14], 9,-1019803690);c=gg(c,d,a,b,x[i+ 3],14, -187363961);
b=gg(b,c,d,a,x[i+ 8],20, 1163531501);a=gg(a,b,c,d,x[i+13], 5,-1444681467);d=gg(d,a,b,c,x[i+ 2], 9, -51403784);
c=gg(c,d,a,b,x[i+ 7],14, 1735328473);b=gg(b,c,d,a,x[i+12],20,-1926607734);a=hh(a,b,c,d,x[i+ 5], 4, -378558);
d=hh(d,a,b,c,x[i+ 8],11,-2022574463);c=hh(c,d,a,b,x[i+11],16, 1839030562);b=hh(b,c,d,a,x[i+14],23, -35309556);
a=hh(a,b,c,d,x[i+ 1], 4,-1530992060);d=hh(d,a,b,c,x[i+ 4],11, 1272893353);c=hh(c,d,a,b,x[i+ 7],16, -155497632);
b=hh(b,c,d,a,x[i+10],23,-1094730640);a=hh(a,b,c,d,x[i+13], 4, 681279174);d=hh(d,a,b,c,x[i+ 0],11, -358537222);
c=hh(c,d,a,b,x[i+ 3],16, -722521979);b=hh(b,c,d,a,x[i+ 6],23, 76029189);a=hh(a,b,c,d,x[i+ 9], 4, -640364487);
d=hh(d,a,b,c,x[i+12],11, -421815835);c=hh(c,d,a,b,x[i+15],16, 530742520);b=hh(b,c,d,a,x[i+ 2],23, -995338651);
a=ii(a,b,c,d,x[i+ 0], 6, -198630844);d=ii(d,a,b,c,x[i+ 7],10, 1126891415);c=ii(c,d,a,b,x[i+14],15,-1416354905);
b=ii(b,c,d,a,x[i+ 5],21, -57434055);a=ii(a,b,c,d,x[i+12], 6, 1700485571);d=ii(d,a,b,c,x[i+ 3],10,-1894986606);
c=ii(c,d,a,b,x[i+10],15, -1051523);b=ii(b,c,d,a,x[i+ 1],21,-2054922799);a=ii(a,b,c,d,x[i+ 8], 6, 1873313359);
d=ii(d,a,b,c,x[i+15],10, -30611744);c=ii(c,d,a,b,x[i+ 6],15,-1560198380);b=ii(b,c,d,a,x[i+13],21, 1309151649);
a=ii(a,b,c,d,x[i+ 4], 6, -145523070);d=ii(d,a,b,c,x[i+11],10,-1120210379);c=ii(c,d,a,b,x[i+ 2],15, 718787259);
b=ii(b,c,d,a,x[i+ 9],21, -343485551);a=ad(a,olda);b=ad(b,oldb);c=ad(c,oldc);d=ad(d,oldd);
}
return rh(a)+rh(b)+rh(c)+rh(d);
}

View File

@ -1,388 +0,0 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { manager_instance, rebootAPI, show_message } from "./common.js";
async function install_model(target) {
if(ModelInstaller.instance) {
ModelInstaller.instance.startInstall(target);
try {
const response = await api.fetchApi('/model/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(target)
});
const status = await response.json();
app.ui.dialog.close();
target.installed = 'True';
return true;
}
catch(exception) {
show_message(`Install failed: ${target.title} / ${exception}`);
return false;
}
finally {
await ModelInstaller.instance.invalidateControl();
ModelInstaller.instance.updateMessage("<BR>To apply the installed model, please click the 'Refresh' button on the main menu.");
}
}
}
async function getModelList() {
var mode = manager_instance.datasrc_combo.value;
const response = await api.fetchApi(`/externalmodel/getlist?mode=${mode}`);
const data = await response.json();
return data;
}
export class ModelInstaller extends ComfyDialog {
static instance = null;
install_buttons = [];
message_box = null;
data = null;
clear() {
this.install_buttons = [];
this.message_box = null;
this.data = null;
}
constructor(app, manager_dialog) {
super();
this.manager_dialog = manager_dialog;
this.search_keyword = '';
this.element = $el("div.comfy-modal", { parent: document.body }, []);
}
createControls() {
return [
$el("button.cm-small-button", {
type: "button",
textContent: "Close",
onclick: () => { this.close(); }
})
];
}
startInstall(target) {
const self = ModelInstaller.instance;
self.updateMessage(`<BR><font color="green">Installing '${target.name}'</font>`);
for(let i in self.install_buttons) {
self.install_buttons[i].disabled = true;
self.install_buttons[i].style.backgroundColor = 'gray';
}
}
apply_searchbox(data) {
let keyword = this.search_box.value.toLowerCase();
for(let i in this.grid_rows) {
let data = this.grid_rows[i].data;
let content = data.name.toLowerCase() + data.type.toLowerCase() + data.base.toLowerCase() + data.filename.toLowerCase() + data.description.toLowerCase();
if(this.filter && this.filter != '*') {
if(this.filter != data.installed) {
this.grid_rows[i].control.style.display = 'none';
continue;
}
}
if(keyword == "")
this.grid_rows[i].control.style.display = null;
else if(content.includes(keyword)) {
this.grid_rows[i].control.style.display = null;
}
else {
this.grid_rows[i].control.style.display = 'none';
}
}
}
async invalidateControl() {
this.clear();
this.data = (await getModelList()).models;
while (this.element.children.length) {
this.element.removeChild(this.element.children[0]);
}
await this.createHeaderControls();
if(this.search_keyword) {
this.search_box.value = this.search_keyword;
}
await this.createGrid();
await this.createBottomControls();
this.apply_searchbox(this.data);
}
updateMessage(msg, btn_id) {
this.message_box.innerHTML = msg;
if(btn_id) {
const rebootButton = document.getElementById(btn_id);
const self = this;
rebootButton.addEventListener("click",
function() {
if(rebootAPI()) {
self.close();
self.manager_dialog.close();
}
});
}
}
async createGrid(models_json) {
var grid = document.createElement('table');
grid.setAttribute('id', 'external-models-grid');
var thead = document.createElement('thead');
var tbody = document.createElement('tbody');
var headerRow = document.createElement('tr');
thead.style.position = "sticky";
thead.style.top = "0px";
thead.style.borderCollapse = "collapse";
thead.style.tableLayout = "fixed";
var header1 = document.createElement('th');
header1.innerHTML = '&nbsp;&nbsp;ID&nbsp;&nbsp;';
header1.style.width = "20px";
var header2 = document.createElement('th');
header2.innerHTML = 'Type';
header2.style.width = "100px";
var header3 = document.createElement('th');
header3.innerHTML = 'Base';
header3.style.width = "100px";
var header4 = document.createElement('th');
header4.innerHTML = 'Name';
header4.style.width = "30%";
var header5 = document.createElement('th');
header5.innerHTML = 'Filename';
header5.style.width = "20%";
header5.style.tableLayout = "fixed";
var header6 = document.createElement('th');
header6.innerHTML = 'Description';
header6.style.width = "50%";
var header_down = document.createElement('th');
header_down.innerHTML = 'Download';
header_down.style.width = "50px";
thead.appendChild(headerRow);
headerRow.appendChild(header1);
headerRow.appendChild(header2);
headerRow.appendChild(header3);
headerRow.appendChild(header4);
headerRow.appendChild(header5);
headerRow.appendChild(header6);
headerRow.appendChild(header_down);
headerRow.style.backgroundColor = "Black";
headerRow.style.color = "White";
headerRow.style.textAlign = "center";
headerRow.style.width = "100%";
headerRow.style.padding = "0";
grid.appendChild(thead);
grid.appendChild(tbody);
this.grid_rows = {};
if(this.data)
for (var i = 0; i < this.data.length; i++) {
const data = this.data[i];
var dataRow = document.createElement('tr');
var data1 = document.createElement('td');
data1.style.textAlign = "center";
data1.innerHTML = i+1;
var data2 = document.createElement('td');
data2.innerHTML = `&nbsp;${data.type}`;
var data3 = document.createElement('td');
data3.innerHTML = `&nbsp;${data.base}`;
var data4 = document.createElement('td');
data4.className = "cm-node-name";
data4.innerHTML = `&nbsp;<a href=${data.reference} target="_blank"><font color="skyblue"><b>${data.name}</b></font></a>`;
var data5 = document.createElement('td');
data5.className = "cm-node-filename";
data5.innerHTML = `&nbsp;${data.filename}`;
data5.style.wordBreak = "break-all";
var data6 = document.createElement('td');
data6.className = "cm-node-desc";
data6.innerHTML = data.description;
data6.style.wordBreak = "break-all";
var data_install = document.createElement('td');
var installBtn = document.createElement('button');
data_install.style.textAlign = "center";
installBtn.innerHTML = 'Install';
this.install_buttons.push(installBtn);
switch(data.installed) {
case 'True':
installBtn.innerHTML = 'Installed';
installBtn.style.backgroundColor = 'green';
installBtn.style.color = 'white';
installBtn.disabled = true;
break;
default:
installBtn.innerHTML = 'Install';
installBtn.style.backgroundColor = 'black';
installBtn.style.color = 'white';
break;
}
installBtn.style.width = "100px";
installBtn.addEventListener('click', function() {
install_model(data);
});
data_install.appendChild(installBtn);
dataRow.style.backgroundColor = "var(--bg-color)";
dataRow.style.color = "var(--fg-color)";
dataRow.style.textAlign = "left";
dataRow.appendChild(data1);
dataRow.appendChild(data2);
dataRow.appendChild(data3);
dataRow.appendChild(data4);
dataRow.appendChild(data5);
dataRow.appendChild(data6);
dataRow.appendChild(data_install);
tbody.appendChild(dataRow);
this.grid_rows[i] = {data:data, control:dataRow};
}
let self = this;
const panel = document.createElement('div');
panel.style.width = "100%";
panel.appendChild(grid);
function handleResize() {
const parentHeight = self.element.clientHeight;
const gridHeight = parentHeight - 200;
grid.style.height = gridHeight + "px";
}
window.addEventListener("resize", handleResize);
grid.style.position = "relative";
grid.style.display = "inline-block";
grid.style.width = "100%";
grid.style.height = "100%";
grid.style.overflowY = "scroll";
this.element.style.height = "85%";
this.element.style.width = "80%";
this.element.appendChild(panel);
handleResize();
}
createFilterCombo() {
let combo = document.createElement("select");
combo.style.cssFloat = "left";
combo.style.fontSize = "14px";
combo.style.padding = "4px";
combo.style.background = "black";
combo.style.marginLeft = "2px";
combo.style.width = "199px";
combo.id = `combo-manger-filter`;
combo.style.borderRadius = "15px";
let items =
[
{ value:'*', text:'Filter: all' },
{ value:'True', text:'Filter: installed' },
{ value:'False', text:'Filter: not-installed' },
];
items.forEach(item => {
const option = document.createElement("option");
option.value = item.value;
option.text = item.text;
combo.appendChild(option);
});
let self = this;
combo.addEventListener('change', function(event) {
self.filter = event.target.value;
self.apply_searchbox();
});
return combo;
}
createHeaderControls() {
let self = this;
this.search_box = $el('input.cm-search-filter', {type:'text', id:'manager-model-search-box', placeholder:'input search keyword', value:this.search_keyword}, []);
this.search_box.style.height = "25px";
this.search_box.onkeydown = (event) => {
if (event.key === 'Enter') {
self.search_keyword = self.search_box.value;
self.apply_searchbox();
}
if (event.key === 'Escape') {
self.search_keyword = self.search_box.value;
self.apply_searchbox();
}
};
let search_button = document.createElement("button");
search_button.className = "cm-small-button";
search_button.innerHTML = "Search";
search_button.onclick = () => {
self.search_keyword = self.search_box.value;
self.apply_searchbox();
};
search_button.style.display = "inline-block";
let filter_control = this.createFilterCombo();
filter_control.style.display = "inline-block";
let cell = $el('td', {width:'100%'}, [filter_control, this.search_box, ' ', search_button]);
let search_control = $el('table', {width:'100%'},
[
$el('tr', {}, [cell])
]
);
cell.style.textAlign = "right";
this.element.appendChild(search_control);
}
async createBottomControls() {
var close_button = document.createElement("button");
close_button.className = "cm-small-button";
close_button.innerHTML = "Close";
close_button.onclick = () => { this.close(); }
close_button.style.display = "inline-block";
this.message_box = $el('div', {id:'custom-download-message'}, [$el('br'), '']);
this.message_box.style.height = '60px';
this.message_box.style.verticalAlign = 'middle';
this.element.appendChild(this.message_box);
this.element.appendChild(close_button);
}
async show() {
try {
this.invalidateControl();
this.element.style.display = "block";
this.element.style.zIndex = 10001;
}
catch(exception) {
app.ui.dialog.show(`Failed to get external model list. / ${exception}`);
}
}
}

715
js/model-manager.js Normal file
View File

@ -0,0 +1,715 @@
import { $el } from "../../scripts/ui.js";
import {
manager_instance, rebootAPI,
fetchData, md5, icons
} from "./common.js";
// https://cenfun.github.io/turbogrid/api.html
import TG from "./turbogrid.esm.js";
const pageCss = `
.cmm-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 10001;
width: 80%;
height: 80%;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
}
.cmm-manager .cmm-flex-auto {
flex: auto;
}
.cmm-manager button {
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
margin: 0;
padding: 4px 8px;
min-width: 100px;
}
.cmm-manager button:disabled,
.cmm-manager input:disabled,
.cmm-manager select:disabled {
color: gray;
}
.cmm-manager button:disabled {
background-color: var(--comfy-input-bg);
}
.cmm-manager-header {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cmm-manager-header label {
display: flex;
gap: 5px;
align-items: center;
}
.cmm-manager-type,
.cmm-manager-base,
.cmm-manager-filter {
height: 28px;
line-height: 28px;
}
.cmm-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,${encodeURIComponent(icons.search.replace("currentColor", "#888"))}");
}
.cmm-manager-status {
padding-left: 10px;
}
.cmm-manager-grid {
flex: auto;
border: 1px solid var(--border-color);
overflow: hidden;
}
.cmm-manager-message {
}
.cmm-manager-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.cmm-manager-grid .tg-turbogrid {
font-family: var(--grid-font);
font-size: 15px;
background: var(--bg-color);
}
.cmm-manager-grid .cmm-node-name a {
color: skyblue;
text-decoration: none;
word-break: break-word;
}
.cmm-manager-grid .cmm-node-desc a {
color: #5555FF;
font-weight: bold;
text-decoration: none;
}
.cmm-manager-grid .tg-cell a:hover {
text-decoration: underline;
}
.cmm-icon-passed {
width: 20px;
height: 20px;
position: absolute;
left: calc(50% - 10px);
top: calc(50% - 10px);
}
.cmm-manager .cmm-btn-enable {
background-color: blue;
color: white;
}
.cmm-manager .cmm-btn-disable {
background-color: MediumSlateBlue;
color: white;
}
.cmm-manager .cmm-btn-install {
background-color: black;
color: white;
}
@keyframes cmm-btn-loading-bg {
0% {
left: 0;
}
100% {
left: -100px;
}
}
.cmm-manager button.cmm-btn-loading {
position: relative;
overflow: hidden;
border-color: rgb(0 119 207 / 80%);
background-color: var(--comfy-input-bg);
}
.cmm-manager button.cmm-btn-loading::after {
position: absolute;
top: 0;
left: 0;
content: "";
width: 500px;
height: 100%;
background-image: repeating-linear-gradient(
-45deg,
rgb(0 119 207 / 30%),
rgb(0 119 207 / 30%) 10px,
transparent 10px,
transparent 15px
);
animation: cmm-btn-loading-bg 3s linear infinite;
}
.cmm-manager-light .cmm-node-name a {
color: blue;
}
.cmm-manager-light .cm-warn-note {
background-color: #ccc !important;
}
.cmm-manager-light .cmm-btn-install {
background-color: #333;
}
`;
const pageHtml = `
<div class="cmm-manager-header">
<label>Filter
<select class="cmm-manager-filter"></select>
</label>
<label>Type
<select class="cmm-manager-type"></select>
</label>
<label>Base
<select class="cmm-manager-base"></select>
</label>
<input class="cmm-manager-keywords" type="search" placeholder="Search" />
<div class="cmm-manager-status"></div>
<div class="cmm-flex-auto"></div>
</div>
<div class="cmm-manager-grid"></div>
<div class="cmm-manager-message"></div>
<div class="cmm-manager-footer">
<button class="cmm-manager-close">Close</button>
<div class="cmm-flex-auto"></div>
</div>
`;
export class ModelManager {
static instance = null;
constructor(app, manager_dialog) {
this.app = app;
this.manager_dialog = manager_dialog;
this.id = "cmm-manager";
this.filter = '';
this.type = '';
this.base = '';
this.keywords = '';
this.init();
}
init() {
if (!document.querySelector(`style[context="${this.id}"]`)) {
const $style = document.createElement("style");
$style.setAttribute("context", this.id);
$style.innerHTML = pageCss;
document.head.appendChild($style);
}
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cmm-manager"
});
this.element.innerHTML = pageHtml;
this.initFilter();
this.bindEvents();
this.initGrid();
}
initFilter() {
this.filterList = [{
label: "All",
value: ""
}, {
label: "Installed",
value: "True"
}, {
label: "Not Installed",
value: "False"
}];
this.typeList = [{
label: "All",
value: ""
}];
this.baseList = [{
label: "All",
value: ""
}];
this.updateFilter();
}
updateFilter() {
const $filter = this.element.querySelector(".cmm-manager-filter");
$filter.innerHTML = this.filterList.map(item => {
const selected = item.value === this.filter ? " selected" : "";
return `<option value="${item.value}"${selected}>${item.label}</option>`
}).join("");
const $type = this.element.querySelector(".cmm-manager-type");
$type.innerHTML = this.typeList.map(item => {
const selected = item.value === this.type ? " selected" : "";
return `<option value="${item.value}"${selected}>${item.label}</option>`
}).join("");
const $base = this.element.querySelector(".cmm-manager-base");
$base.innerHTML = this.baseList.map(item => {
const selected = item.value === this.base ? " selected" : "";
return `<option value="${item.value}"${selected}>${item.label}</option>`
}).join("");
}
bindEvents() {
const eventsMap = {
".cmm-manager-filter": {
change: (e) => {
this.filter = e.target.value;
this.updateGrid();
}
},
".cmm-manager-type": {
change: (e) => {
this.type = e.target.value;
this.updateGrid();
}
},
".cmm-manager-base": {
change: (e) => {
this.base = e.target.value;
this.updateGrid();
}
},
".cmm-manager-keywords": {
input: (e) => {
const keywords = `${e.target.value}`.trim();
if (keywords !== this.keywords) {
this.keywords = keywords;
this.updateGrid();
}
},
focus: (e) => e.target.select()
},
".cmm-manager-close": {
click: (e) => this.close()
},
};
Object.keys(eventsMap).forEach(selector => {
const target = this.element.querySelector(selector);
if (target) {
const events = eventsMap[selector];
if (events) {
Object.keys(events).forEach(type => {
target.addEventListener(type, events[type]);
});
}
}
});
}
// ===========================================================================================
initGrid() {
const container = this.element.querySelector(".cmm-manager-grid");
const grid = new TG.Grid(container);
this.grid = grid;
grid.bind('onUpdated', (e, d) => {
this.showStatus(`${grid.viewRows.length.toLocaleString()} external models`);
});
grid.bind('onClick', (e, d) => {
const { rowItem } = d;
const target = d.e.target;
const mode = target.getAttribute("mode");
if (mode === "install") {
this.installModel(rowItem, target);
}
});
grid.setOption({
theme: 'dark',
textSelectable: true,
scrollbarRound: true,
frozenColumn: 1,
rowNotFound: "No Results",
rowHeight: 40,
bindWindowResize: true,
bindContainerResize: true,
cellResizeObserver: (rowItem, columnItem) => {
const autoHeightColumns = ['name', 'description'];
return autoHeightColumns.includes(columnItem.id)
},
// updateGrid handler for filter and keywords
rowFilter: (rowItem) => {
const searchableColumns = ["name", "type", "base", "description", "filename", "save_path"];
let shouldShown = grid.highlightKeywordsFilter(rowItem, searchableColumns, this.keywords);
if (shouldShown) {
if(this.filter && rowItem.installed !== this.filter) {
return false;
}
if(this.type && rowItem.type !== this.type) {
return false;
}
if(this.base && rowItem.base !== this.base) {
return false;
}
}
return shouldShown;
}
});
}
renderGrid() {
// update theme
const colorPalette = this.app.ui.settings.settingsValues['Comfy.ColorPalette'];
Array.from(this.element.classList).forEach(cn => {
if (cn.startsWith("cmm-manager-")) {
this.element.classList.remove(cn);
}
});
this.element.classList.add(`cmm-manager-${colorPalette}`);
const options = {
theme: colorPalette === "light" ? "" : "dark"
};
const rows = this.modelList || [];
const columns = [{
id: 'id',
name: 'ID',
width: 50,
align: 'center'
}, {
id: 'name',
name: 'Name',
width: 200,
minWidth: 100,
maxWidth: 500,
classMap: 'cmm-node-name',
formatter: function(name, rowItem, columnItem, cellNode) {
return `<a href=${rowItem.reference} target="_blank"><b>${name}</b></a>`;
}
}, {
id: 'installed',
name: 'Download',
width: 130,
minWidth: 110,
maxWidth: 200,
sortable: false,
align: 'center',
formatter: (installed, rowItem, columnItem) => {
if (rowItem.refresh) {
return `<font color="red">Refresh Required</span>`;
}
if (installed === "True") {
return `<div class="cmm-icon-passed">${icons.passed}</div>`;
}
return `<button class="cmm-btn-install" mode="install">Install</button>`;
}
}, {
id: 'type',
name: 'Type',
width: 100
}, {
id: 'base',
name: 'Base'
}, {
id: 'description',
name: 'Description',
width: 400,
maxWidth: 5000,
classMap: 'cmm-node-desc'
}, {
id: 'filename',
name: 'Filename',
width: 200
}, {
id: "save_path",
name: 'Save Path',
width: 200
}];
this.grid.setData({
options,
rows,
columns
});
this.grid.render();
}
updateGrid() {
if (this.grid) {
this.grid.update();
}
}
// ===========================================================================================
async installModel(item, btn) {
this.showLoading();
this.showError("");
this.showStatus(`Install ${item.name} ...`);
btn.classList.add("cmm-btn-loading");
const data = item.originalData;
const res = await fetchData('/model/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.error) {
const errorMsg = `Install failed: ${item.name} ${res.error.message}`;
this.showError(errorMsg);
this.hideLoading();
return;
}
item.refresh = true;
this.grid.updateCell(item, "installed");
this.hideLoading();
this.showStatus(`Install ${item.name} successfully`);
this.showMessage(`To apply the installed model, please click the 'Refresh' button on the main menu.`, "red")
}
getModelList(models) {
const typeMap = new Map();
const baseMap = new Map();
models.forEach((item, i) => {
const { type, base, name, reference } = item;
item.originalData = JSON.parse(JSON.stringify(item));
item.hash = md5(name + reference);
item.id = i + 1;
typeMap.set(type, type);
baseMap.set(base, base);
});
const typeList = [];
typeMap.forEach(type => {
typeList.push({
label: type,
value: type
});
});
typeList.sort((a,b)=> {
const au = a.label.toUpperCase();
const bu = b.label.toUpperCase();
if (au !== bu) {
return au > bu ? 1 : -1;
}
return 0;
});
this.typeList = [{
label: "All",
value: ""
}].concat(typeList);
const baseList = [];
baseMap.forEach(base => {
baseList.push({
label: base,
value: base
});
});
baseList.sort((a,b)=> {
const au = a.label.toUpperCase();
const bu = b.label.toUpperCase();
if (au !== bu) {
return au > bu ? 1 : -1;
}
return 0;
});
this.baseList = [{
label: "All",
value: ""
}].concat(baseList);
return models;
}
// ===========================================================================================
async loadData() {
this.showLoading();
this.showStatus(`Loading external model list ...`);
const mode = manager_instance.datasrc_combo.value;
const res = await fetchData(`/externalmodel/getlist?mode=${mode}`);
if (res.error) {
this.showError("Failed to get external model list.");
this.hideLoading();
return
}
const { models } = res.data;
this.modelList = this.getModelList(models);
// console.log("models", this.modelList);
this.updateFilter();
this.renderGrid();
this.hideLoading();
}
// ===========================================================================================
showError(err) {
this.showMessage(err, "red");
}
showMessage(msg, color) {
if (color) {
msg = `<font color="${color}">${msg}</font>`;
}
this.element.querySelector(".cmm-manager-message").innerHTML = msg;
}
showStatus(msg, color) {
if (color) {
msg = `<font color="${color}">${msg}</font>`;
}
this.element.querySelector(".cmm-manager-status").innerHTML = msg;
}
showLoading() {
this.setDisabled(true);
if (this.grid) {
this.grid.showLoading();
this.grid.showMask({
opacity: 0.05
});
}
}
hideLoading() {
this.setDisabled(false);
if (this.grid) {
this.grid.hideLoading();
this.grid.hideMask();
}
}
setDisabled(disabled) {
const $close = this.element.querySelector(".cmm-manager-close");
const list = [
".cmm-manager-header input",
".cmm-manager-header select",
".cmm-manager-footer button"
].map(s => {
return Array.from(this.element.querySelectorAll(s));
})
.flat()
.filter(it => {
return it !== $close;
});
list.forEach($elem => {
if (disabled) {
$elem.setAttribute("disabled", "disabled");
} else {
$elem.removeAttribute("disabled");
}
});
Array.from(this.element.querySelectorAll(".cmm-btn-loading")).forEach($elem => {
$elem.classList.remove("cmm-btn-loading");
});
}
setKeywords(keywords = "") {
this.keywords = keywords;
this.element.querySelector(".cmm-manager-keywords").value = keywords;
}
show() {
this.element.style.display = "flex";
this.setKeywords("");
this.showMessage("");
this.loadData();
}
close() {
this.element.style.display = "none";
}
}

File diff suppressed because one or more lines are too long