import { app } from "../../scripts/app.js"; import { ComfyDialog, $el } from "../../scripts/ui.js"; import { api } from "../../scripts/api.js"; import { manager_instance, rebootAPI, install_via_git_url, fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt, sanitizeHTML } from "./common.js"; // https://cenfun.github.io/turbogrid/api.html import TG from "./turbogrid.esm.js"; const pageCss = ` .cn-manager { --grid-font: -apple-system, BlinkMacSystemFont, "Segue UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; z-index: 1099; width: 80%; height: 80%; display: flex; flex-direction: column; gap: 10px; color: var(--fg-color); font-family: arial, sans-serif; } .cn-manager .cn-flex-auto { flex: auto; } .cn-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; } .cn-manager button:disabled, .cn-manager input:disabled, .cn-manager select:disabled { color: gray; } .cn-manager button:disabled { background-color: var(--comfy-input-bg); } .cn-manager .cn-manager-restart { display: none; background-color: #500000; color: white; } .cn-manager .cn-manager-back { align-items: center; justify-content: center; } .arrow-icon { height: 1em; width: 1em; margin-right: 5px; transform: translateY(2px); } .cn-manager-header { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; padding: 0 5px; } .cn-manager-header label { display: flex; gap: 5px; align-items: center; } .cn-manager-filter { height: 28px; line-height: 28px; } .cn-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"))}"); } .cn-manager-status { padding-left: 10px; } .cn-manager-grid { flex: auto; border: 1px solid var(--border-color); overflow: hidden; } .cn-manager-selection { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .cn-manager-message { } .cn-manager-footer { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .cn-manager-grid .tg-turbogrid { font-family: var(--grid-font); font-size: 15px; background: var(--bg-color); } .cn-manager-grid .cn-node-name a { color: skyblue; text-decoration: none; word-break: break-word; } .cn-manager-grid .cn-node-desc a { color: #5555FF; font-weight: bold; text-decoration: none; } .cn-manager-grid .tg-cell a:hover { text-decoration: underline; } .cn-manager-grid .cn-extensions-button, .cn-manager-grid .cn-conflicts-button { display: inline-block; width: 20px; height: 20px; color: green; border: none; padding: 0; margin: 0; background: none; min-width: 20px; } .cn-manager-grid .cn-conflicts-button { color: orange; } .cn-manager-grid .cn-extensions-list, .cn-manager-grid .cn-conflicts-list { line-height: normal; text-align: left; max-height: 80%; min-height: 200px; min-width: 300px; overflow-y: auto; font-size: 12px; border-radius: 5px; padding: 10px; filter: drop-shadow(2px 5px 5px rgb(0 0 0 / 30%)); white-space: normal; } .cn-manager-grid .cn-extensions-list { border-color: var(--bg-color); } .cn-manager-grid .cn-conflicts-list { background-color: #CCCC55; color: #AA3333; } .cn-manager-grid .cn-extensions-list h3, .cn-manager-grid .cn-conflicts-list h3 { margin: 0; padding: 5px 0; color: #000; } .cn-tag-list { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; margin-bottom: 5px; } .cn-tag-list > div { background-color: var(--border-color); border-radius: 5px; padding: 0 5px; } .cn-install-buttons { display: flex; flex-direction: column; gap: 3px; padding: 3px; align-items: center; justify-content: center; height: 100%; } .cn-selected-buttons { display: flex; gap: 5px; align-items: center; padding-right: 20px; } .cn-manager .cn-btn-enable { background-color: #333399; color: white; } .cn-manager .cn-btn-disable { background-color: #442277; color: white; } .cn-manager .cn-btn-update { background-color: #1155AA; color: white; } .cn-manager .cn-btn-try-update { background-color: Gray; color: white; } .cn-manager .cn-btn-try-fix { background-color: #6495ED; color: white; } .cn-manager .cn-btn-import-failed { background-color: #AA1111; font-size: 10px; font-weight: bold; color: white; } .cn-manager .cn-btn-install { background-color: black; color: white; } .cn-manager .cn-btn-try-install { background-color: Gray; color: white; } .cn-manager .cn-btn-uninstall { background-color: #993333; color: white; } .cn-manager .cn-btn-reinstall { background-color: #993333; color: white; } .cn-manager .cn-btn-switch { background-color: #448833; color: white; } @keyframes cn-btn-loading-bg { 0% { left: 0; } 100% { left: -105px; } } .cn-manager button.cn-btn-loading { position: relative; overflow: hidden; border-color: rgb(0 119 207 / 80%); background-color: var(--comfy-input-bg); } .cn-manager button.cn-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: cn-btn-loading-bg 2s linear infinite; } .cn-manager-light .cn-node-name a { color: blue; } .cn-manager-light .cm-warn-note { background-color: #ccc !important; } .cn-manager-light .cn-btn-install { background-color: #333; } `; const pageHtml = `
`; const ShowMode = { NORMAL: "Normal", UPDATE: "Update", MISSING: "Missing", FAVORITES: "Favorites", ALTERNATIVES: "Alternatives" }; export class CustomNodesManager { static instance = null; static ShowMode = ShowMode; constructor(app, manager_dialog) { this.app = app; this.manager_dialog = manager_dialog; this.id = "cn-manager"; app.registerExtension({ name: "Comfy.CustomNodesManager", afterConfigureGraph: (missingNodeTypes) => { const item = this.getFilterItem(ShowMode.MISSING); if (item) { item.hasData = false; item.hashMap = null; } } }); this.filter = ''; this.keywords = ''; this.restartMap = {}; 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 cn-manager" }); this.element.innerHTML = pageHtml; this.initFilter(); this.bindEvents(); this.initGrid(); } showVersionSelectorDialog(versions, onSelect) { const dialog = new ComfyDialog(); dialog.element.style.zIndex = 1100; dialog.element.style.width = "300px"; dialog.element.style.padding = "0"; dialog.element.style.backgroundColor = "#2a2a2a"; dialog.element.style.border = "1px solid #3a3a3a"; dialog.element.style.borderRadius = "8px"; dialog.element.style.boxSizing = "border-box"; dialog.element.style.overflow = "hidden"; const contentStyle = { width: "300px", display: "flex", flexDirection: "column", alignItems: "center", padding: "20px", boxSizing: "border-box", gap: "15px" }; let selectedVersion = versions[0]; const versionList = $el("select", { multiple: true, size: Math.min(10, versions.length), style: { width: "260px", height: "auto", backgroundColor: "#383838", color: "#ffffff", border: "1px solid #4a4a4a", borderRadius: "4px", padding: "5px", boxSizing: "border-box" } }, versions.map((v, index) => $el("option", { value: v, textContent: v, selected: index === 0 })) ); versionList.addEventListener('change', (e) => { selectedVersion = e.target.value; Array.from(e.target.options).forEach(opt => { opt.selected = opt.value === selectedVersion; }); }); const content = $el("div", { style: contentStyle }, [ $el("h3", { textContent: "Select Version", style: { color: "#ffffff", backgroundColor: "#1a1a1a", padding: "10px 15px", margin: "0 0 10px 0", width: "260px", textAlign: "center", borderRadius: "4px", boxSizing: "border-box", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }), versionList, $el("div", { style: { display: "flex", justifyContent: "space-between", width: "260px", gap: "10px" } }, [ $el("button", { textContent: "Cancel", onclick: () => dialog.close(), style: { flex: "1", padding: "8px", backgroundColor: "#4a4a4a", color: "#ffffff", border: "none", borderRadius: "4px", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }), $el("button", { textContent: "Select", onclick: () => { if (selectedVersion) { onSelect(selectedVersion); dialog.close(); } else { customAlert("Please select a version."); } }, style: { flex: "1", padding: "8px", backgroundColor: "#4CAF50", color: "#ffffff", border: "none", borderRadius: "4px", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }), ]) ]); dialog.show(content); } initFilter() { const $filter = this.element.querySelector(".cn-manager-filter"); const filterList = [{ label: "All", value: "", hasData: true }, { label: "Installed", value: "installed", hasData: true }, { label: "Enabled", value: "enabled", hasData: true }, { label: "Disabled", value: "disabled", hasData: true }, { label: "Import Failed", value: "import-fail", hasData: true }, { label: "Not Installed", value: "not-installed", hasData: true }, { label: "ComfyRegistry", value: "cnr", hasData: true }, { label: "Non-ComfyRegistry", value: "unknown", hasData: true }, { label: "Update", value: ShowMode.UPDATE, hasData: false }, { label: "Missing", value: ShowMode.MISSING, hasData: false }, { label: "Favorites", value: ShowMode.FAVORITES, hasData: false }, { label: "Alternatives of A1111", value: ShowMode.ALTERNATIVES, hasData: false }]; this.filterList = filterList; $filter.innerHTML = filterList.map(item => { return `` }).join(""); } getFilterItem(filter) { return this.filterList.find(it => it.value === filter) } getActionButtons(action, rowItem, is_selected_button) { const buttons = { "enable": { label: "Enable", mode: "enable" }, "disable": { label: "Disable", mode: "disable" }, "update": { label: "Update", mode: "update" }, "try-update": { label: "Try update", mode: "update" }, "try-fix": { label: "Try fix", mode: "fix" }, "reinstall": { label: "Reinstall", mode: "reinstall" }, "install": { label: "Install", mode: "install" }, "try-install": { label: "Try install", mode: "install" }, "uninstall": { label: "Uninstall", mode: "uninstall" }, "switch": { label: "Switch Ver", mode: "switch" } } const installGroups = { "disabled": ["enable", "switch", "uninstall"], "updatable": ["update", "switch", "disable", "uninstall"], "import-fail": ["try-fix", "switch", "disable", "uninstall"], "enabled": ["try-update", "switch", "disable", "uninstall"], "not-installed": ["install"], 'unknown': ["try-install"], "invalid-installation": ["reinstall"], } if (!manager_instance.update_check_checkbox.checked) { installGroups.enabled = installGroups.enabled.filter(it => it !== "try-update"); } if (rowItem?.title === "ComfyUI-Manager") { installGroups.enabled = installGroups.enabled.filter(it => it !== "disable" && it !== "uninstall" && it !== "switch"); } let list = installGroups[action]; if(is_selected_button || rowItem?.version === "unknown") { list = list.filter(it => it !== "switch"); } if (!list) { return ""; } return list.map(id => { const bt = buttons[id]; return ``; }).join(""); } getButton(target) { if(!target) { return; } const mode = target.getAttribute("mode"); if (!mode) { return; } const group = target.getAttribute("group"); if (!group) { return; } return { group, mode, target, label: target.innerText } } bindEvents() { const eventsMap = { ".cn-manager-filter": { change: (e) => { if (this.grid) { this.grid.selectAll(false); } const value = e.target.value this.filter = value; const item = this.getFilterItem(value); if (item && !item.hasData) { this.loadData(value); return; } this.updateGrid(); } }, ".cn-manager-keywords": { input: (e) => { const keywords = `${e.target.value}`.trim(); if (keywords !== this.keywords) { this.keywords = keywords; this.updateGrid(); } }, focus: (e) => e.target.select() }, ".cn-manager-selection": { click: (e) => { const btn = this.getButton(e.target); if (btn) { const nodes = this.selectedMap[btn.group]; if (nodes) { this.installNodes(nodes, btn); } } } }, ".cn-manager-back": { click: (e) => { this.close() manager_instance.show(); } }, ".cn-manager-restart": { click: () => { if(rebootAPI()) { this.close(); this.manager_dialog.close(); } } }, ".cn-manager-check-update": { click: (e) => { e.target.classList.add("cn-btn-loading"); this.setFilter(ShowMode.UPDATE); this.loadData(ShowMode.UPDATE); } }, ".cn-manager-check-missing": { click: (e) => { e.target.classList.add("cn-btn-loading"); this.setFilter(ShowMode.MISSING); this.loadData(ShowMode.MISSING); } }, ".cn-manager-install-url": { click: async (e) => { const url = await customPrompt("Please enter the URL of the Git repository to install", ""); if (url !== null) { install_via_git_url(url, this.manager_dialog); } } } }; 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(".cn-manager-grid"); const grid = new TG.Grid(container); this.grid = grid; let prevViewRowsLength = -1; grid.bind('onUpdated', (e, d) => { const viewRows = grid.viewRows; prevViewRowsLength = viewRows.length; this.showStatus(`${prevViewRowsLength.toLocaleString()} custom nodes`); }); grid.bind('onSelectChanged', (e, changes) => { this.renderSelected(); }); grid.bind('onClick', (e, d) => { const btn = this.getButton(d.e.target); if (btn) { const item = this.grid.getRowItemBy("hash", d.rowItem.hash); const { target, label, mode} = btn; if((mode === "install" || mode === "switch" || mode == "enable") && item.originalData.version != 'unknown') { // install after select version via dialog if item is cnr node this.installNodeWithVersion(d.rowItem, btn, mode == 'enable'); } else { this.installNodes([d.rowItem.hash], btn, d.rowItem.title); } } }); grid.setOption({ theme: 'dark', selectVisible: true, selectMultiple: true, selectAllVisible: true, textSelectable: true, scrollbarRound: true, frozenColumn: 1, rowNotFound: "No Results", rowHeight: 40, bindWindowResize: true, bindContainerResize: true, cellResizeObserver: (rowItem, columnItem) => { const autoHeightColumns = ['title', 'action', 'description', "alternatives"]; return autoHeightColumns.includes(columnItem.id) }, // updateGrid handler for filter and keywords rowFilter: (rowItem) => { const searchableColumns = ["title", "author", "description"]; if (this.hasAlternatives()) { searchableColumns.push("alternatives"); } let shouldShown = grid.highlightKeywordsFilter(rowItem, searchableColumns, this.keywords); if (shouldShown) { if(this.filter && rowItem.filterTypes) { shouldShown = rowItem.filterTypes.includes(this.filter); } } return shouldShown; } }); } hasAlternatives() { return this.filter === ShowMode.ALTERNATIVES } async handleImportFail(rowItem) { var info; if(rowItem.version == 'unknown'){ info = { 'url': rowItem.originalData.files[0] }; } else{ info = { 'cnr_id': rowItem.originalData.id }; } const response = await api.fetchApi(`/customnode/import_fail_info`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(info) }); let res = await response.json(); let title = `Error message occurred while importing the '${rowItem.title}' module.


` if(res.code == 400) { show_message(title+'The information is not available.') } else { show_message(title+sanitizeHTML(res['msg']).replace(/ /g, ' ').replace(/\n/g, '
')); } } renderGrid() { // update theme const colorPalette = this.app.ui.settings.settingsValues['Comfy.ColorPalette']; Array.from(this.element.classList).forEach(cn => { if (cn.startsWith("cn-manager-")) { this.element.classList.remove(cn); } }); this.element.classList.add(`cn-manager-${colorPalette}`); const options = { theme: colorPalette === "light" ? "" : "dark" }; const rows = this.custom_nodes || {}; for(let nodeKey in rows) { let item = rows[nodeKey]; const extensionInfo = this.extension_mappings[nodeKey]; if(extensionInfo) { const { extensions, conflicts } = extensionInfo; if (extensions.length) { item.extensions = extensions.length; item.extensionsList = extensions; } if (conflicts) { item.conflicts = conflicts.length; item.conflictsList = conflicts; } } } let self = this; const columns = [{ id: 'id', name: 'ID', width: 50, align: 'center' }, { id: 'title', name: 'Title', width: 200, minWidth: 100, maxWidth: 500, classMap: 'cn-node-name', formatter: (title, rowItem, columnItem) => { const container = document.createElement('div'); if (rowItem.action === 'invalid-installation') { const invalidTag = document.createElement('span'); invalidTag.style.color = 'red'; invalidTag.innerHTML = '(INVALID)'; container.appendChild(invalidTag); } else if (rowItem.action === 'import-fail') { const button = document.createElement('button'); button.className = 'cn-btn-import-failed'; button.innerText = 'IMPORT FAILED ↗'; button.onclick = () => self.handleImportFail(rowItem); container.appendChild(button); container.appendChild(document.createElement('br')); } const link = document.createElement('a'); if(rowItem.originalData.repository) link.href = rowItem.originalData.repository; else link.href = rowItem.reference; link.target = '_blank'; link.innerHTML = `${title}`; container.appendChild(link); return container; } }, { id: 'version', name: 'Version', width: 200, minWidth: 100, maxWidth: 500, classMap: 'cn-node-desc', formatter: (version, rowItem, columnItem) => { if(version == undefined) { return `undef`; } else { if(rowItem.cnr_latest && version != rowItem.cnr_latest) { if(version == 'nightly') { return `${version} [${rowItem.cnr_latest}]`; } else { return `${version} [↑${rowItem.cnr_latest}]`; } } else { return `${version}`; } } } }, { id: 'action', name: 'Action', width: 130, minWidth: 110, maxWidth: 200, sortable: false, align: 'center', formatter: (action, rowItem, columnItem) => { if (rowItem.restart) { return `Restart Required`; } const buttons = this.getActionButtons(action, rowItem); return `
${buttons}
`; } }, { id: "alternatives", name: "Alternatives", width: 400, maxWidth: 5000, invisible: !this.hasAlternatives(), classMap: 'cn-node-desc' }, { id: 'description', name: 'Description', width: 400, maxWidth: 5000, classMap: 'cn-node-desc' }, { id: "extensions", name: "Extensions", width: 80, align: 'center', formatter: (extensions, rowItem, columnItem) => { const extensionsList = rowItem.extensionsList; if (!extensionsList) { return; } const list = []; const eId = `popover_extensions_${columnItem.id}_${rowItem.tg_index}`; list.push(``) list.push(`
`) list.push(`

【${rowItem.title}】Extension Nodes (${extensionsList.length})

`); extensionsList.forEach(en => { list.push(`
  • ${en}
  • `); }) list.push("
    "); return list.join(""); } }, { id: "conflicts", name: "Conflicts", width: 80, align: 'center', formatter: (conflicts, rowItem, columnItem) => { const conflictsList = rowItem.conflictsList; if (!conflictsList) { return; } const list = []; const cId = `popover_conflicts_${columnItem.id}_${rowItem.tg_index}`; list.push(``) list.push(`
    `) list.push(`

    【${rowItem.title}】Conflicted Nodes (${conflictsList.length})

    `); conflictsList.forEach(en => { let [node_name, extension_name] = en; extension_name = extension_name.split('/').filter(it => it).pop(); if(extension_name.endsWith('.git')) { extension_name = extension_name.slice(0, -4); } list.push(`
  • ${node_name} [${extension_name}]
  • `); }) list.push("
    "); return list.join(""); } }, { id: 'author', name: 'Author', width: 120, classMap: "cn-node-author", formatter: (author, rowItem, columnItem) => { if (rowItem.trust) { return `✅ ${author}`; } return author; } }, { id: 'stars', name: '★', align: 'center', classMap: "cn-node-stars", formatter: (stars) => { if (stars < 0) { return 'N/A'; } if (typeof stars === 'number') { return stars.toLocaleString(); } return stars; } }, { id: 'last_update', name: 'Last Update', align: 'center', type: 'date', width: 100, classMap: "cn-node-last-update", formatter: (last_update) => { if (last_update < 0) { return 'N/A'; } return `${last_update}`.split(' ')[0]; } }]; let rows_values = Object.keys(rows).map(key => rows[key]); rows_values = rows_values.sort((a, b) => { if (a.version == 'unknown' && b.version != 'unknown') return 1; if (a.version != 'unknown' && b.version == 'unknown') return -1; if (a.stars !== b.stars) { return b.stars - a.stars; } if (a.last_update !== b.last_update) { return new Date(b.last_update) - new Date(a.last_update); } return 0; }); this.grid.setData({ options: options, rows: rows_values, columns: columns }); for(let i=0; i { let type = item.action; if (item.restart) { type = "Restart Required"; } if (selectedMap[type]) { selectedMap[type].push(item.hash); } else { selectedMap[type] = [item.hash]; } }); this.selectedMap = selectedMap; const list = []; Object.keys(selectedMap).forEach(v => { const filterItem = this.getFilterItem(v); list.push(`
    Selected ${selectedMap[v].length} ${filterItem ? filterItem.label : v} ${this.grid.hasMask ? "" : this.getActionButtons(v, null, true)}
    `); }); this.showSelection(list.join("")); } focusInstall(item, mode) { const cellNode = this.grid.getCellNode(item, "installed"); if (cellNode) { const cellBtn = cellNode.querySelector(`button[mode="${mode}"]`); if (cellBtn) { cellBtn.classList.add("cn-btn-loading"); return true } } } async installNodeWithVersion(rowItem, btn, is_enable) { let hash = rowItem.hash; let title = rowItem.title; const item = this.grid.getRowItemBy("hash", hash); let node_id = item.originalData.id; this.showLoading(); let res; if(is_enable) { res = await api.fetchApi(`/customnode/disabled_versions/${node_id}`, { cache: "no-store" }); } else { res = await api.fetchApi(`/customnode/versions/${node_id}`, { cache: "no-store" }); } this.hideLoading(); if(res.status == 200) { let obj = await res.json(); let versions = []; let default_version; let version_cnt = 0; if(!is_enable) { if(rowItem.originalData.active_version != 'nightly') { versions.push('nightly'); default_version = 'nightly'; version_cnt++; } if(rowItem.cnr_latest != rowItem.originalData.active_version && obj.length > 0) { versions.push('latest'); } } for(let v of obj) { if(rowItem.originalData.active_version != v.version) { default_version = v.version; versions.push(v.version); version_cnt++; } } this.showVersionSelectorDialog(versions, (selected_version) => { this.installNodes([hash], btn, title, selected_version); }); } else { show_message('Failed to fetch versions from ComfyRegistry.'); } } async installNodes(list, btn, title, selected_version) { const { target, label, mode} = btn; if(mode === "uninstall") { title = title || `${list.length} custom nodes`; const confirmed = await customConfirm(`Are you sure uninstall ${title}?`); if (!confirmed) { return; } } if(mode === "reinstall") { title = title || `${list.length} custom nodes`; const confirmed = await customConfirm(`Are you sure reinstall ${title}?`); if (!confirmed) { return; } } target.classList.add("cn-btn-loading"); this.showError(""); let needRestart = false; let errorMsg = ""; for (const hash of list) { const item = this.grid.getRowItemBy("hash", hash); if (!item) { errorMsg = `Not found custom node: ${hash}`; break; } this.grid.scrollRowIntoView(item); if (!this.focusInstall(item, mode)) { this.grid.onNextUpdated(() => { this.focusInstall(item, mode); }); } this.showStatus(`${label} ${item.title} ...`); const data = item.originalData; data.selected_version = selected_version; data.channel = this.channel; data.mode = this.mode; let install_mode = mode; if(mode == 'switch') { install_mode = 'install'; } // don't post install if install_mode == 'enable' data.skip_post_install = install_mode == 'enable'; let api_mode = install_mode; if(install_mode == 'enable') { api_mode = 'install'; } if(install_mode == 'reinstall') { api_mode = 'reinstall'; } const res = await api.fetchApi(`/customnode/${api_mode}`, { method: 'POST', body: JSON.stringify(data) }); if (res.status != 200) { errorMsg = `${item.title} ${mode} failed: `; if(res.status == 403) { errorMsg += `This action is not allowed with this security level configuration.`; } else if(res.status == 404) { errorMsg += `With the current security level configuration, only custom nodes from the "default channel" can be installed.`; } else { errorMsg += await res.text(); } break; } needRestart = true; this.grid.setRowSelected(item, false); item.restart = true; this.restartMap[item.hash] = true; this.grid.updateCell(item, "action"); //console.log(res.data); } target.classList.remove("cn-btn-loading"); if (errorMsg) { this.showError(errorMsg); show_message("Installation Error:\n"+errorMsg); } else { this.showStatus(`${label} ${list.length} custom node(s) successfully`); } if (needRestart) { this.showRestart(); this.showMessage(`To apply the installed/updated/disabled/enabled custom node, please restart ComfyUI. And refresh browser.`, "red") } } // =========================================================================================== async getExtensionMappings() { const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading extension mappings (${mode}) ...`); const res = await fetchData(`/customnode/getmappings?mode=${mode}`); if (res.error) { console.log(res.error); return {} } const data = res.data; const extension_mappings = {}; const conflicts_map = {}; Object.keys(data).forEach(k => { const [extensions, metadata] = data[k]; extension_mappings[k] = { extensions, metadata } extensions.forEach(node => { let l = conflicts_map[node]; if(!l) { l = []; conflicts_map[node] = l; } l.push(k); }) }) Object.keys(conflicts_map).forEach(node => { const list = conflicts_map[node]; if(list.length > 1) { list.forEach(k => { const item = extension_mappings[k]; if(!item) { console.log(`not found ${k}`) return; } if (!item.conflicts) { item.conflicts = []; } list.forEach(key => { if(k !== key) { item.conflicts.push([node, key]) } }) }) } }) return extension_mappings; } async getMissingNodes() { const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading missing nodes (${mode}) ...`); const res = await fetchData(`/customnode/getmappings?mode=${mode}`); if (res.error) { this.showError(`Failed to get custom node mappings: ${res.error}`); return; } const mappings = res.data; // build regex->url map const regex_to_pack = []; for(let k in this.custom_nodes) { let node = this.custom_nodes[k]; if(node.nodename_pattern) { regex_to_pack.push({ regex: new RegExp(node.nodename_pattern), url: node.files[0] }); } } // build name->url map const name_to_packs = {}; for (const url in mappings) { const names = mappings[url]; for(const name in names[0]) { let v = name_to_packs[names[0][name]]; if(v == undefined) { v = []; name_to_packs[names[0][name]] = v; } v.push(url); } } const registered_nodes = new Set(); for (let i in LiteGraph.registered_node_types) { registered_nodes.add(LiteGraph.registered_node_types[i].type); } const missing_nodes = new Set(); const workflow = app.graph.serialize(); const group_nodes = workflow.extra && workflow.extra.groupNodes ? workflow.extra.groupNodes : []; let nodes = workflow.nodes; for (let i in group_nodes) { let group_node = group_nodes[i]; nodes = nodes.concat(group_node.nodes); } for (let i in nodes) { const node_type = nodes[i].type; if(node_type.startsWith('workflow/') || node_type.startsWith('workflow>')) continue; if (!registered_nodes.has(node_type)) { const packs = name_to_packs[node_type.trim()]; if(packs) packs.forEach(url => { missing_nodes.add(url); }); else { for(let j in regex_to_pack) { if(regex_to_pack[j].regex.test(node_type)) { missing_nodes.add(regex_to_pack[j].url); } } } } } const hashMap = {}; for(let k in this.custom_nodes) { let item = this.custom_nodes[k]; if(missing_nodes.has(item.id)) { hashMap[item.hash] = true; } else if (item.files?.some(file => missing_nodes.has(file))) { hashMap[item.hash] = true; } } return hashMap; } async getFavorites() { const hashMap = {}; for(let k in this.custom_nodes) { let item = this.custom_nodes[k]; if(item.is_favorite) hashMap[item.hash] = true; } return hashMap; } async getAlternatives() { const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading alternatives (${mode}) ...`); const res = await fetchData(`/customnode/alternatives?mode=${mode}`); if (res.error) { this.showError(`Failed to get alternatives: ${res.error}`); return []; } const hashMap = {}; const items = res.data; for(let i in items) { let item = items[i]; let custom_node = this.custom_nodes[i]; if (!custom_node) { console.log(`Not found custom node: ${item.id}`); continue; } const tags = `${item.tags}`.split(",").map(tag => { return `
    ${tag.trim()}
    `; }).join(""); hashMap[custom_node.hash] = { alternatives: `
    ${tags}
    ${item.description}` } } return hashMap; } async loadData(show_mode = ShowMode.NORMAL) { this.show_mode = show_mode; console.log("Show mode:", show_mode); this.showLoading(); this.extension_mappings = await this.getExtensionMappings(); const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading custom nodes (${mode}) ...`); const skip_update = this.show_mode === ShowMode.UPDATE ? "" : "&skip_update=true"; const res = await fetchData(`/customnode/getlist?mode=${mode}${skip_update}`); if (res.error) { this.showError("Failed to get custom node list."); this.hideLoading(); return } const { channel, node_packs } = res.data; this.channel = channel; this.mode = mode; this.custom_nodes = node_packs; if(this.channel !== 'default') { this.element.querySelector(".cn-manager-channel").innerHTML = `Channel: ${this.channel} (Incomplete list)`; } for (const k in node_packs) { let item = node_packs[k]; item.originalData = JSON.parse(JSON.stringify(item)); if(item.originalData.id == undefined) { item.originalData.id = k; } item.hash = md5(k); } const filterItem = this.getFilterItem(this.show_mode); if(filterItem) { let hashMap; if(this.show_mode == ShowMode.UPDATE) { hashMap = {}; for (const k in node_packs) { let it = node_packs[k]; if (it['update-state'] === "true") { hashMap[it.hash] = true; } } } else if(this.show_mode == ShowMode.MISSING) { hashMap = await this.getMissingNodes(); } else if(this.show_mode == ShowMode.ALTERNATIVES) { hashMap = await this.getAlternatives(); } else if(this.show_mode == ShowMode.FAVORITES) { hashMap = await this.getFavorites(); } filterItem.hashMap = hashMap; filterItem.hasData = true; } for(let k in node_packs) { let nodeItem = node_packs[k]; if (this.restartMap[nodeItem.hash]) { nodeItem.restart = true; } if(nodeItem['update-state'] == "true") { nodeItem.action = 'updatable'; } else if(nodeItem['import-fail']) { nodeItem.action = 'import-fail'; } else { nodeItem.action = nodeItem.state; } if(nodeItem['invalid-installation']) { nodeItem.action = 'invalid-installation'; } const filterTypes = new Set(); this.filterList.forEach(filterItem => { const { value, hashMap } = filterItem; if (hashMap) { const hashData = hashMap[nodeItem.hash] if (hashData) { filterTypes.add(value); if (value === ShowMode.UPDATE) { nodeItem['update-state'] = "true"; } if (value === ShowMode.MISSING) { nodeItem['missing-node'] = "true"; } if (typeof hashData === "object") { Object.assign(nodeItem, hashData); } } } else { if (nodeItem.state === value) { filterTypes.add(value); } switch(nodeItem.state) { case "enabled": filterTypes.add("enabled"); case "disabled": filterTypes.add("installed"); break; case "not-installed": filterTypes.add("not-installed"); break; } if(nodeItem.version != 'unknown') { filterTypes.add("cnr"); } else { filterTypes.add("unknown"); } if(nodeItem['update-state'] == 'true') { filterTypes.add("updatable"); } if(nodeItem['import-fail']) { filterTypes.add("import-fail"); } if(nodeItem['invalid-installation']) { filterTypes.add("invalid-installation"); } } }); nodeItem.filterTypes = Array.from(filterTypes); } this.renderGrid(); this.hideLoading(); } // =========================================================================================== showSelection(msg) { this.element.querySelector(".cn-manager-selection").innerHTML = msg; } showError(err) { this.showMessage(err, "red"); } showMessage(msg, color) { if (color) { msg = `${msg}`; } this.element.querySelector(".cn-manager-message").innerHTML = msg; } showStatus(msg, color) { if (color) { msg = `${msg}`; } this.element.querySelector(".cn-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(".cn-manager-close"); const $restart = this.element.querySelector(".cn-manager-restart"); const list = [ ".cn-manager-header input", ".cn-manager-header select", ".cn-manager-footer button", ".cn-manager-selection button" ].map(s => { return Array.from(this.element.querySelectorAll(s)); }) .flat() .filter(it => { return it !== $close && it !== $restart; }); list.forEach($elem => { if (disabled) { $elem.setAttribute("disabled", "disabled"); } else { $elem.removeAttribute("disabled"); } }); Array.from(this.element.querySelectorAll(".cn-btn-loading")).forEach($elem => { $elem.classList.remove("cn-btn-loading"); }); } showRestart() { this.element.querySelector(".cn-manager-restart").style.display = "block"; } setFilter(filterValue) { let filter = ""; const filterItem = this.getFilterItem(filterValue); if(filterItem) { filter = filterItem.value; } this.filter = filter; this.element.querySelector(".cn-manager-filter").value = filter; } setKeywords(keywords = "") { this.keywords = keywords; this.element.querySelector(".cn-manager-keywords").value = keywords; } show(show_mode) { this.element.style.display = "flex"; this.setFilter(show_mode); this.setKeywords(""); this.showSelection(""); this.showMessage(""); this.loadData(show_mode); } close() { this.element.style.display = "none"; } }