import { app } from "../../scripts/app.js"; import { ComfyDialog, $el } from "../../scripts/ui.js"; import { api } from "../../scripts/api.js"; import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js"; import { manager_instance, rebootAPI, install_via_git_url, fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt, sanitizeHTML, infoToast, showTerminal, setNeedRestart, storeColumnWidth, restoreColumnWidth, getTimeAgo, copyText, loadCss, showPopover, hidePopover, handle403Response } from "./common.js"; // https://cenfun.github.io/turbogrid/api.html import TG from "./turbogrid.esm.js"; loadCss("./custom-nodes-manager.css"); const gridId = "node"; const pageHtml = `
`; const ShowMode = { NORMAL: "Normal", UPDATE: "Update", MISSING: "Missing", FAVORITES: "Favorites", ALTERNATIVES: "Alternatives", IN_WORKFLOW: "In Workflow", }; 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.analyzeDependenciesBeforeInstall = false; // Default: false this.init(); api.addEventListener("cm-queue-status", this.onQueueStatus); api.getNodeDefs().then(objs => { this.nodeMap = objs; }) } init() { // Create checkbox for dependency analysis const analyzeDepsCheckbox = $el("input", { type: "checkbox", id: "cn-analyze-deps-checkbox", checked: this.analyzeDependenciesBeforeInstall, onchange: (e) => { this.analyzeDependenciesBeforeInstall = e.target.checked; }, style: { marginRight: "6px", cursor: "pointer" } }); const analyzeDepsLabel = $el("label", { for: "cn-analyze-deps-checkbox", style: { display: "flex", alignItems: "center", cursor: "pointer", color: "#fff", fontSize: "12px", marginRight: "10px", whiteSpace: "nowrap" } }, [ analyzeDepsCheckbox, $el("span", { textContent: "Analyse dependencies before node installation" }) ]); const header = $el("div.cn-manager-header.px-2", {}, [ // $el("label", {}, [ // $el("span", { textContent: "Filter" }), // $el("select.cn-manager-filter") // ]), createSettingsCombo("Filter", $el("select.cn-manager-filter")), $el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }), analyzeDepsLabel, $el("div.cn-manager-status"), $el("div.cn-flex-auto"), $el("div.cn-manager-channel") ]); const frame = buildGuiFrameCustomHeader( 'cn-manager-dialog', // dialog id header, // custom header element pageHtml, // dialog content element this ); // send this so we can attach close functions this.element = frame; this.element.setAttribute("tabindex", 0); this.element.focus(); this.initFilter(); this.bindEvents(); this.initGrid(); } showDependencySelectorDialog(dependencies, onSelect) { const dialog = new ComfyDialog(); dialog.element.style.zIndex = 1100; dialog.element.style.width = "900px"; dialog.element.style.maxHeight = "80vh"; 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: "100%", display: "flex", flexDirection: "column", padding: "20px", boxSizing: "border-box", gap: "15px" }; // Create scrollable table container with sticky header const tableContainer = $el("div", { style: { maxHeight: "500px", overflowY: "auto", border: "1px solid #4a4a4a", borderRadius: "4px", backgroundColor: "#1a1a1a", position: "relative" } }); // Create table const table = $el("table", { style: { width: "100%", borderCollapse: "separate", borderSpacing: "0", fontSize: "14px" } }); // Create table header with sticky positioning const thead = $el("thead", { style: { position: "sticky", top: "0", zIndex: "10", backgroundColor: "#2a2a2a", boxShadow: "0 2px 4px rgba(0,0,0,0.3)" } }, [ $el("tr", { style: { backgroundColor: "#2a2a2a", borderBottom: "2px solid #4a4a4a" } }, [ $el("th", { textContent: "", style: { padding: "10px", textAlign: "left", width: "40px", color: "#fff" } }), $el("th", { textContent: "Dependency Name", style: { padding: "10px", textAlign: "left", color: "#fff", fontWeight: "bold" } }), $el("th", { textContent: "Current Version", style: { padding: "10px", textAlign: "left", color: "#fff", fontWeight: "bold" } }), $el("th", { textContent: "Incoming Version", style: { padding: "10px", textAlign: "left", color: "#fff", fontWeight: "bold" } }) ]) ]); // Create table body const tbody = $el("tbody", {}); // Create table rows for each dependency and its subdependencies let rowIndex = 0; dependencies.forEach((dep) => { // Ensure name is not null/undefined and clean it let depName = dep.name; if (!depName || depName === 'null' || depName === 'None') { // Fallback: extract from line if (dep.line) { depName = dep.line.split(/[=<>!~]/)[0].trim(); } else { depName = "Unknown"; } } // Remove any "null" suffix that might have been appended depName = String(depName).replace(/null$/i, '').trim(); const isInstalled = dep.status === 'installed'; const incomingVersion = dep.version || "NA"; const currentVersion = dep.currentVersion || "NA"; // Main dependency row const row = $el("tr", { style: { backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222", borderBottom: "1px solid #3a3a3a" } }, [ $el("td", { style: { padding: "10px", textAlign: "center" } }, [ $el("input", { type: "checkbox", checked: dep.selected, onchange: (e) => { dep.selected = e.target.checked; }, style: { cursor: "pointer", width: "18px", height: "18px" } }) ]), $el("td", { style: { padding: "10px", color: isInstalled ? "#888" : "#fff" } }, [ $el("span", { textContent: depName, style: { fontWeight: "500", marginRight: isInstalled ? "8px" : "0" } }), isInstalled ? $el("span", { textContent: "Installed", style: { display: "inline-block", backgroundColor: "#2a4a2a", color: "#4a9", padding: "2px 6px", borderRadius: "3px", fontSize: "10px", fontWeight: "bold", border: "1px solid #4a9" } }) : '' ]), $el("td", { textContent: currentVersion, style: { padding: "10px", color: isInstalled ? "#4a9" : "#aaa", fontFamily: "monospace" } }), $el("td", { textContent: incomingVersion, style: { padding: "10px", color: "#fff", fontFamily: "monospace" } }) ]); tbody.appendChild(row); rowIndex++; // Add subdependencies as indented rows if(dep.subdependencies && dep.subdependencies.length > 0) { dep.subdependencies.forEach((subdep) => { // Ensure subdependency name is not null/undefined and clean it let subdepName = subdep.name; if (!subdepName || subdepName === 'null' || subdepName === 'None') { subdepName = "Unknown"; } // Remove any "null" suffix that might have been appended subdepName = String(subdepName).replace(/null$/i, '').trim(); const subIsInstalled = subdep.status === 'installed'; const subIncomingVersion = subdep.version || "NA"; const subCurrentVersion = subdep.currentVersion || "NA"; const subRow = $el("tr", { style: { backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222", borderBottom: "1px solid #3a3a3a" } }, [ $el("td", { style: { padding: "10px", textAlign: "center" } }, [ $el("input", { type: "checkbox", checked: subdep.selected, onchange: (e) => { subdep.selected = e.target.checked; }, style: { cursor: "pointer", width: "18px", height: "18px" } }) ]), $el("td", { style: { padding: "10px 10px 10px 30px", color: subIsInstalled ? "#888" : "#aaa", fontSize: "13px" } }, [ $el("span", { textContent: "└─ " + subdepName, style: { fontWeight: "400", marginRight: subIsInstalled ? "8px" : "0" } }), subIsInstalled ? $el("span", { textContent: "Installed", style: { display: "inline-block", backgroundColor: "#2a4a2a", color: "#4a9", padding: "2px 6px", borderRadius: "3px", fontSize: "10px", fontWeight: "bold", border: "1px solid #4a9" } }) : '' ]), $el("td", { textContent: subCurrentVersion, style: { padding: "10px", color: subIsInstalled ? "#4a9" : "#666", fontFamily: "monospace", fontSize: "13px" } }), $el("td", { textContent: subIncomingVersion, style: { padding: "10px", color: "#aaa", fontFamily: "monospace", fontSize: "13px" } }) ]); tbody.appendChild(subRow); rowIndex++; }); } }); table.appendChild(thead); table.appendChild(tbody); tableContainer.appendChild(table); const content = $el("div", { style: contentStyle }, [ $el("h3", { textContent: "Select Dependencies to Install", style: { color: "#ffffff", backgroundColor: "#1a1a1a", padding: "10px 15px", margin: "0 0 10px 0", width: "100%", textAlign: "center", borderRadius: "4px", boxSizing: "border-box" } }), $el("div", { textContent: `${dependencies.filter(d => d.status === 'installed').length} already installed, ${dependencies.filter(d => d.status !== 'installed').length} to install`, style: { color: "#aaa", fontSize: "12px", marginBottom: "5px" } }), tableContainer, $el("div", { style: { display: "flex", justifyContent: "space-between", width: "100%", gap: "10px", marginTop: "10px" } }, [ $el("button", { textContent: "Cancel", onclick: () => { onSelect(null); // Pass null to indicate cancellation dialog.close(); }, style: { flex: "1", padding: "8px", backgroundColor: "#4a4a4a", color: "#ffffff", border: "none", borderRadius: "4px", cursor: "pointer" } }), $el("button", { textContent: "Select All", onclick: () => { dependencies.forEach(dep => { if (dep.status !== 'installed') { dep.selected = true; } // Also select subdependencies if(dep.subdependencies) { dep.subdependencies.forEach(subdep => { if(subdep.status !== 'installed') { subdep.selected = true; } }); } }); // Update checkboxes in the table const checkboxes = tableContainer.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach((checkbox) => { if(!checkbox.disabled) { checkbox.checked = true; } }); }, style: { padding: "8px 15px", backgroundColor: "#4a6a4a", color: "#ffffff", border: "none", borderRadius: "4px", cursor: "pointer" } }), $el("button", { textContent: "Install Selected", onclick: () => { // Collect all selected dependencies (main + subdependencies) const selected = []; dependencies.forEach(d => { if(d.selected) { selected.push(d); } // Also include selected subdependencies if(d.subdependencies) { d.subdependencies.forEach(subdep => { if(subdep.selected) { selected.push(subdep); } }); } }); onSelect(selected); dialog.close(); }, style: { flex: "1", padding: "8px", backgroundColor: "#4CAF50", color: "#ffffff", border: "none", borderRadius: "4px", cursor: "pointer" } }), ]) ]); console.log('[Dependency Dialog] Showing dialog with', dependencies.length, 'dependencies'); dialog.show(content); console.log('[Dependency Dialog] Dialog shown'); } 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: "In Workflow", value: ShowMode.IN_WORKFLOW, 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 (!installGroups.updatable) { 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.flyover.hide(true); this.removeHighlight(); hidePopover(); this.close() manager_instance.show(); } }, ".cn-manager-restart": { click: () => { this.close(); this.manager_dialog.close(); rebootAPI(); } }, ".cn-manager-stop": { click: () => { api.fetchApi('/manager/queue/reset'); infoToast('Cancel', 'Remaining tasks will stop after completing the current task.'); } }, ".cn-manager-used-in-workflow": { click: (e) => { e.target.classList.add("cn-btn-loading"); this.setFilter(ShowMode.IN_WORKFLOW); this.loadData(ShowMode.IN_WORKFLOW); } }, ".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; this.flyover = this.createFlyover(container); 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("onColumnWidthChanged", (e, columnItem) => { storeColumnWidth(gridId, columnItem) }); grid.bind('onClick', (e, d) => { this.addHighlight(d.rowItem); if (d.columnItem.id === "nodes") { this.showNodes(d); return; } 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); } return; } }); // iteration events this.element.addEventListener("click", (e) => { if (container === e.target || container.contains(e.target)) { return; } this.removeHighlight(); }); // proxy keyboard events this.element.addEventListener("keydown", (e) => { if (e.target === this.element) { grid.containerKeyDownHandler(e); } }, true); 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 globalStyle = window.getComputedStyle(document.body); this.colorVars = { bgColor: globalStyle.getPropertyValue('--comfy-menu-bg'), borderColor: globalStyle.getPropertyValue('--border-color') } const colorPalette = this.app.ui.settings.settingsValues['Comfy.ColorPalette']; this.colorPalette = 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" }; 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-pack-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}`; link.title = rowItem.originalData.id; container.appendChild(link); return container; } }, { id: 'version', name: 'Version', width: 100, minWidth: 80, maxWidth: 300, classMap: 'cn-pack-version', formatter: (version, rowItem, columnItem) => { if(!version) { return; } if(rowItem.cnr_latest && version != rowItem.cnr_latest) { if(version == 'nightly') { return `
${version}
[${rowItem.cnr_latest}]
`; } return `
${version}
[↑${rowItem.cnr_latest}]
`; } 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: "nodes", name: "Nodes", width: 100, formatter: (v, rowItem, columnItem) => { if (!rowItem.nodes) { return ''; } const list = [`
`]; list.push(`
${rowItem.nodes} node${(rowItem.nodes>1?'s':'')}
`); if (rowItem.conflicts) { list.push(`
${rowItem.conflicts} conflict${(rowItem.conflicts>1?'s':'')}
`); } list.push('
'); return list.join(""); } }, { id: "alternatives", name: "Alternatives", width: 400, maxWidth: 5000, invisible: !this.hasAlternatives(), classMap: 'cn-pack-desc' }, { id: 'description', name: 'Description', width: 400, maxWidth: 5000, classMap: 'cn-pack-desc' }, { id: 'author', name: 'Author', width: 120, classMap: "cn-pack-author", formatter: (author, rowItem, columnItem) => { if (rowItem.trust) { return `✅ ${author}`; } return author; } }, { id: 'stars', name: '★', align: 'center', classMap: "cn-pack-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-pack-last-update", formatter: (last_update) => { if (last_update < 0) { return 'N/A'; } const ago = getTimeAgo(last_update); const short = `${last_update}`.split(' ')[0]; return `${short}`; } }]; restoreColumnWidth(gridId, columns); const rows_values = Object.values(this.custom_nodes); 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; }); rows_values.forEach((it, i) => { it.id = i + 1; }); this.grid.setData({ options: options, rows: rows_values, columns: columns }); this.grid.render(); } updateGrid() { if (this.grid) { this.grid.update(); if (this.hasAlternatives()) { this.grid.showColumn("alternatives"); } else { this.grid.hideColumn("alternatives"); } } } addHighlight(rowItem) { this.removeHighlight(); if (this.grid && rowItem) { this.grid.setRowState(rowItem, 'highlight', true); this.highlightRow = rowItem; } } removeHighlight() { if (this.grid && this.highlightRow) { this.grid.setRowState(this.highlightRow, 'highlight', false); this.highlightRow = null; } } // =========================================================================================== getWidgetType(type, inputName) { if (type === 'COMBO') { return 'COMBO' } const widgets = app.widgets; if (`${type}:${inputName}` in widgets) { return `${type}:${inputName}` } if (type in widgets) { return type } } createNodePreview(nodeItem) { // console.log(nodeItem); const list = [`
${nodeItem.name}
Preview
`]; // Node slot I/O const inputList = []; nodeItem.input_order.required?.map(name => { inputList.push({ name }); }) nodeItem.input_order.optional?.map(name => { inputList.push({ name, optional: true }); }); const slotInputList = []; const widgetInputList = []; const inputMap = Object.assign({}, nodeItem.input.optional, nodeItem.input.required); inputList.forEach(it => { const inputName = it.name; const _inputData = inputMap[inputName]; let type = _inputData[0]; let options = _inputData[1] || {}; if (Array.isArray(type)) { options.default = type[0]; type = 'COMBO'; } it.type = type; it.options = options; // convert force/default inputs if (options.forceInput || options.defaultInput) { slotInputList.push(it); return; } const widgetType = this.getWidgetType(type, inputName); if (widgetType) { it.default = options.default; widgetInputList.push(it); } else { slotInputList.push(it); } }); const outputList = nodeItem.output.map((type, i) => { return { type, name: nodeItem.output_name[i], list: nodeItem.output_is_list[i] } }); // dark const colorMap = { "CLIP": "#FFD500", "CLIP_VISION": "#A8DADC", "CLIP_VISION_OUTPUT": "#ad7452", "CONDITIONING": "#FFA931", "CONTROL_NET": "#6EE7B7", "IMAGE": "#64B5F6", "LATENT": "#FF9CF9", "MASK": "#81C784", "MODEL": "#B39DDB", "STYLE_MODEL": "#C2FFAE", "VAE": "#FF6E6E", "NOISE": "#B0B0B0", "GUIDER": "#66FFFF", "SAMPLER": "#ECB4B4", "SIGMAS": "#CDFFCD", "TAESD": "#DCC274" } const inputHtml = slotInputList.map(it => { const color = colorMap[it.type] || "gray"; const optional = it.optional ? " cn-preview-optional" : "" return `
${it.name}
`; }).join(""); const outputHtml = outputList.map(it => { const color = colorMap[it.type] || "gray"; const grid = it.list ? " cn-preview-grid" : ""; return `
${it.name}
`; }).join(""); list.push(`
${inputHtml}
${outputHtml}
`); // Node widget inputs if (widgetInputList.length) { list.push(`
`); // console.log(widgetInputList); widgetInputList.forEach(it => { let value = it.default; if (typeof value === "object" && value && Object.prototype.hasOwnProperty.call(value, "content")) { value = value.content; } if (typeof value === "undefined" || value === null) { value = ""; } else { value = `${value}`; } if ( (it.type === "STRING" && (value || it.options.multiline)) || it.type === "MARKDOWN" ) { if (value) { value = value.replace(/\r?\n/g, "
") } list.push(`
${value || it.name}
`); return; } list.push(`
${it.name}
${value}
`); }); list.push(`
`); } if (nodeItem.description) { list.push(`
${nodeItem.description}
`) } return list.join(""); } showNodePreview(target) { const nodeName = target.innerText; const nodeItem = this.nodeMap[nodeName]; if (!nodeItem) { this.hideNodePreview(); return; } const html = this.createNodePreview(nodeItem); showPopover(target, html, "cn-preview cn-preview-"+this.colorPalette, { positions: ['left'], bgColor: this.colorVars.bgColor, borderColor: this.colorVars.borderColor }) } hideNodePreview() { hidePopover(); } createFlyover(container) { const $flyover = document.createElement("div"); $flyover.className = "cn-flyover"; $flyover.innerHTML = `
${icons.arrowRight}
${icons.close}
` container.appendChild($flyover); const $flyoverTitle = $flyover.querySelector(".cn-flyover-title"); const $flyoverBody = $flyover.querySelector(".cn-flyover-body"); let width = '50%'; let visible = false; let timeHide; const closeHandler = (e) => { if ($flyover === e.target || $flyover.contains(e.target)) { return; } clearTimeout(timeHide); timeHide = setTimeout(() => { flyover.hide(); }, 100); } const hoverHandler = (e) => { if(e.type === "mouseenter") { if(e.target.classList.contains("cn-nodes-name")) { this.showNodePreview(e.target); } return; } this.hideNodePreview(); } const displayHandler = () => { if (visible) { $flyover.classList.remove("cn-slide-in-right"); } else { $flyover.classList.remove("cn-slide-out-right"); $flyover.style.width = '0px'; $flyover.style.display = "none"; } } const flyover = { show: (titleHtml, bodyHtml) => { clearTimeout(timeHide); this.element.removeEventListener("click", closeHandler); $flyoverTitle.innerHTML = titleHtml; $flyoverBody.innerHTML = bodyHtml; $flyover.style.display = "block"; $flyover.style.width = width; if(!visible) { $flyover.classList.add("cn-slide-in-right"); } visible = true; setTimeout(() => { this.element.addEventListener("click", closeHandler); }, 100); }, hide: (now) => { visible = false; this.element.removeEventListener("click", closeHandler); if(now) { displayHandler(); return; } $flyover.classList.add("cn-slide-out-right"); } } $flyover.addEventListener("animationend", (e) => { displayHandler(); }); $flyover.addEventListener("mouseenter", hoverHandler, true); $flyover.addEventListener("mouseleave", hoverHandler, true); $flyover.addEventListener("click", (e) => { if(e.target.classList.contains("cn-nodes-name")) { const nodeName = e.target.innerText; const nodeItem = this.nodeMap[nodeName]; if (!nodeItem) { copyText(nodeName).then((res) => { if (res) { e.target.setAttribute("action", "Copied"); e.target.classList.add("action"); setTimeout(() => { e.target.classList.remove("action"); e.target.removeAttribute("action"); }, 1000); } }); return; } const [x, y, w, h] = app.canvas.ds.visible_area; const dpi = Math.max(window.devicePixelRatio ?? 1, 1); const node = window.LiteGraph?.createNode( nodeItem.name, nodeItem.display_name, { pos: [x + (w-300) / dpi / 2, y] } ); if (node) { app.graph.add(node); e.target.setAttribute("action", "Added to Workflow"); e.target.classList.add("action"); setTimeout(() => { e.target.classList.remove("action"); e.target.removeAttribute("action"); }, 1000); } return; } if(e.target.classList.contains("cn-nodes-pack")) { const hash = e.target.getAttribute("hash"); const rowItem = this.grid.getRowItemBy("hash", hash); //console.log(rowItem); this.grid.scrollToRow(rowItem); this.addHighlight(rowItem); return; } if(e.target.classList.contains("cn-flyover-close")) { flyover.hide(); return; } }); return flyover; } showNodes(d) { const nodesList = d.rowItem.nodesList; if (!nodesList) { return; } const rowItem = d.rowItem; const isNotInstalled = rowItem.action == "not-installed"; let titleHtml = `
${rowItem.title}
`; if (isNotInstalled) { titleHtml += '
Not Installed
' } const list = []; list.push(`
`); nodesList.forEach((it, i) => { let rowClass = 'cn-nodes-row' if (it.conflicts) { rowClass += ' cn-nodes-conflict'; } list.push(`
`); list.push(`
${i+1}
`); list.push(`
${it.name}
`); if (it.conflicts) { list.push(`
${icons.conflicts}
Conflict with${it.conflicts.map(c => { return `
${c.title}
`; }).join(",")}
`); } list.push(`
`); }); list.push("
"); const bodyHtml = list.join(""); this.flyover.show(titleHtml, bodyHtml); } async loadNodes(node_packs) { const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading node mappings (${mode}) ...`); const res = await fetchData(`/customnode/getmappings?mode=${mode}`); if (res.error) { console.log(res.error); return; } const data = res.data; const findNode = (k, title) => { let item = node_packs[k]; if (item) { return item; } // git url if (k.includes("/")) { const gitName = k.split("/").pop(); item = node_packs[gitName]; if (item) { return item; } } return node_packs[title]; } const conflictsMap = {}; // add nodes data Object.keys(data).forEach(k => { const [nodes, metadata] = data[k]; if (nodes?.length) { const title = metadata?.title_aux; const nodeItem = findNode(k, title); if (nodeItem) { // deduped const eList = Array.from(new Set(nodes)); nodeItem.nodes = eList.length; const nodesMap = {}; eList.forEach(extName => { nodesMap[extName] = { name: extName }; let cList = conflictsMap[extName]; if(!cList) { cList = []; conflictsMap[extName] = cList; } cList.push(nodeItem.key); }); nodeItem.nodesMap = nodesMap; } else { // should be removed // console.log("not found", k, title, nodes) } } }); // calculate conflicts data Object.keys(conflictsMap).forEach(extName => { const cList = conflictsMap[extName]; if(cList.length <= 1) { return; } cList.forEach(key => { const nodeItem = node_packs[key]; const extItem = nodeItem.nodesMap[extName]; if(!extItem.conflicts) { extItem.conflicts = [] } const conflictsList = cList.filter(k => k !== key); conflictsList.forEach(k => { const nItem = node_packs[k]; extItem.conflicts.push({ key: k, title: nItem.title, hash: nItem.hash }) }) }) }) Object.values(node_packs).forEach(nodeItem => { if (nodeItem.nodesMap) { nodeItem.nodesList = Object.values(nodeItem.nodesMap); nodeItem.conflicts = nodeItem.nodesList.filter(it => it.conflicts).length; } }) } // =========================================================================================== renderSelected() { const selectedList = this.grid.getSelectedRows(); if (!selectedList.length) { this.showSelection(""); return; } const selectedMap = {}; selectedList.forEach(item => { 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, "action"); 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.cnr_latest != rowItem.originalData.active_version && obj.length > 0) { versions.push('latest'); } if(rowItem.originalData.active_version != 'nightly') { versions.push('nightly'); default_version = 'nightly'; version_cnt++; } } 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) { let stats = await api.fetchApi('/manager/queue/status'); stats = await stats.json(); if(stats.is_processing) { customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`); return; } 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; } } // For install mode, analyze dependencies BEFORE starting installation let selectedDependencies = []; let dependencyDialogShown = false; // Track if dialog was shown if(mode === "install" && this.analyzeDependenciesBeforeInstall) { // Analyze dependencies for all items first (only if checkbox is enabled) for (const hash of list) { const item = this.grid.getRowItemBy("hash", hash); if (!item) { console.log('[Dependency Analysis] Item not found for hash:', hash); continue; } const data = item.originalData; console.log('[Dependency Analysis] Item data:', { title: item.title, files: data.files, repository: data.repository, hasFiles: !!data.files, filesLength: data.files ? data.files.length : 0 }); // Try multiple ways to get the git URL let gitUrl = null; if(data.files && data.files.length > 0) { gitUrl = data.files[0]; } else if(data.repository) { gitUrl = data.repository; } if(gitUrl && (gitUrl.includes('github.com') || gitUrl.includes('.git'))) { try { this.showStatus(`Analyzing dependencies for ${item.title}...`); console.log('[Dependency Analysis] Fetching dependencies for:', gitUrl); const analyzeRes = await api.fetchApi('/customnode/analyze_dependencies', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: gitUrl, commitId: data.commit_id, branch: data.branch }) }); console.log('[Dependency Analysis] Response status:', analyzeRes.status); if(analyzeRes.status === 200) { const analyzeData = await analyzeRes.json(); console.log('[Dependency Analysis] Response data:', { success: analyzeData.success, hasDependencies: !!analyzeData.dependencies, dependenciesCount: analyzeData.dependencies ? analyzeData.dependencies.length : 0, noRequirementsFile: analyzeData.noRequirementsFile }); if(analyzeData.success && analyzeData.dependencies && analyzeData.dependencies.length > 0) { console.log('[Dependency Analysis] Showing dialog with', analyzeData.dependencies.length, 'dependencies'); dependencyDialogShown = true; // Show dependency selection dialog and wait for user const userSelection = await new Promise((resolve) => { this.showDependencySelectorDialog(analyzeData.dependencies, (selected) => { console.log('[Dependency Analysis] User selected:', selected); resolve(selected); }); }); // If user cancelled (null), stop installation if(userSelection === null) { console.log('[Dependency Analysis] User cancelled installation'); this.showStatus("Installation cancelled"); return; } selectedDependencies = userSelection || []; console.log('[Dependency Analysis] Selected dependencies:', selectedDependencies.length); } else if(analyzeData.noRequirementsFile) { console.log('[Dependency Analysis] No requirements.txt file found'); } else { console.log('[Dependency Analysis] No dependencies to show'); } } else { const errorText = await analyzeRes.text(); console.error('[Dependency Analysis] API error:', analyzeRes.status, errorText); } } catch(e) { console.error('[Dependency Analysis] Exception:', e); // Continue with installation even if dependency analysis fails } } else { console.log('[Dependency Analysis] Not a GitHub URL or no URL found:', gitUrl); } } } target.classList.add("cn-btn-loading"); this.showError(""); let needRestart = false; let errorMsg = ""; await api.fetchApi('/manager/queue/reset'); let target_items = []; for (const hash of list) { const item = this.grid.getRowItemBy("hash", hash); target_items.push(item); 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; data.ui_id = hash; // Add selected dependencies to data (including subdependencies) // Only install selected dependencies - respect user's selection const allSelected = []; if(selectedDependencies.length > 0) { selectedDependencies.forEach(d => { // Add main dependency if selected if(d.selected) { allSelected.push({ name: d.name, version: d.version, line: d.line }); } // Add selected subdependencies if(d.subdependencies) { d.subdependencies.forEach(subdep => { if(subdep.selected) { allSelected.push({ name: subdep.name, version: subdep.version, line: `${subdep.name}${subdep.version ? '==' + subdep.version : ''}` }); } }); } }); } // Set selectedDependencies: // - If dialog was shown: always set (even if empty) to respect user's selection // - If dialog was not shown: don't set (install all dependencies - original behavior) if(dependencyDialogShown) { // User saw the dialog, respect their selection (even if empty) data.selectedDependencies = allSelected; } else if(allSelected.length > 0) { // Dialog wasn't shown but we have selections (shouldn't happen, but just in case) data.selectedDependencies = allSelected; } // If dialog wasn't shown and no selections, don't set selectedDependencies // This means backend will install all dependencies (original behavior) 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(`/manager/queue/${api_mode}`, { method: 'POST', body: JSON.stringify(data) }); if (res.status != 200) { errorMsg = `'${item.title}': `; if(res.status == 403) { try { const data = await res.json(); if(data.error === 'comfyui_outdated') { errorMsg += `ComfyUI version is outdated. Please update ComfyUI to use Manager normally.\n`; } else { errorMsg += `This action is not allowed with this security level configuration.\n`; } } catch { errorMsg += `This action is not allowed with this security level configuration.\n`; } } else if(res.status == 404) { errorMsg += `With the current security level configuration, only custom nodes from the "default channel" can be installed.\n`; } else { errorMsg += await res.text() + '\n'; } break; } } this.install_context = {btn: btn, targets: target_items}; if(errorMsg) { this.showError(errorMsg); show_message("[Installation Errors]\n"+errorMsg); // reset for(let k in target_items) { const item = target_items[k]; this.grid.updateCell(item, "action"); } } else { await api.fetchApi('/manager/queue/start'); this.showStop(); showTerminal(); } } async onQueueStatus(event) { let self = CustomNodesManager.instance; if(event.detail.status == 'in_progress' && event.detail.ui_target == 'nodepack_manager') { const hash = event.detail.target; const item = self.grid.getRowItemBy("hash", hash); item.restart = true; self.restartMap[item.hash] = true; self.grid.updateCell(item, "action"); self.grid.setRowSelected(item, false); } else if(event.detail.status == 'done') { self.hideStop(); self.onQueueCompleted(event.detail); } } async onQueueCompleted(info) { let result = info.nodepack_result; if(result.length == 0) { return; } let self = CustomNodesManager.instance; if(!self.install_context) { return; } const { target, label, mode } = self.install_context.btn; target.classList.remove("cn-btn-loading"); let errorMsg = ""; for(let hash in result){ let v = result[hash]; if(v != 'success' && v != 'skip') errorMsg += v+'\n'; } for(let k in self.install_context.targets) { let item = self.install_context.targets[k]; self.grid.updateCell(item, "action"); } if (errorMsg) { self.showError(errorMsg); show_message("Installation Error:\n"+errorMsg); } else { self.showStatus(`${label} ${result.length} custom node(s) successfully`); } self.showRestart(); self.showMessage(`To apply the installed/updated/disabled/enabled custom node, please restart ComfyUI. And refresh browser.`, "red"); infoToast(`[ComfyUI-Manager] All node pack tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`); self.install_context = undefined; } // =========================================================================================== getNodesInWorkflow() { let usedGroupNodes = new Set(); let allUsedNodes = {}; const visitedGraphs = new Set(); const visitGraph = (graph) => { if (!graph || visitedGraphs.has(graph)) return; visitedGraphs.add(graph); const nodes = graph._nodes || graph.nodes || []; for(let k in nodes) { let node = nodes[k]; if (!node) continue; // If it's a SubgraphNode, recurse into its graph and continue searching if (node.isSubgraphNode?.() && node.subgraph) { visitGraph(node.subgraph); } if (!node.type) continue; // Group nodes / components if(typeof node.type === 'string' && node.type.startsWith('workflow>')) { usedGroupNodes.add(node.type.slice(9)); continue; } allUsedNodes[node.type] = node; } }; visitGraph(app.graph); for(let k of usedGroupNodes) { let subnodes = app.graph.extra.groupNodes[k]?.nodes; if(subnodes) { for(let k2 in subnodes) { let node = subnodes[k2]; allUsedNodes[node.type] = node; } } } return allUsedNodes; } async getMissingNodes() { let unresolved_missing_nodes = new Set(); let hashMap = {}; let allUsedNodes = this.getNodesInWorkflow(); const registered_nodes = new Set(); for (let i in LiteGraph.registered_node_types) { registered_nodes.add(LiteGraph.registered_node_types[i].type); } let unresolved_aux_ids = {}; let outdated_comfyui = false; let unresolved_cnr_list = []; for(let k in allUsedNodes) { let node = allUsedNodes[k]; if(!registered_nodes.has(node.type)) { // missing node if(node.properties.cnr_id) { if(node.properties.cnr_id == 'comfy-core') { outdated_comfyui = true; } let item = this.custom_nodes[node.properties.cnr_id]; if(item) { hashMap[item.hash] = true; } else { console.log(`CM: cannot find '${node.properties.cnr_id}' from cnr list.`); unresolved_aux_ids[node.properties.cnr_id] = node.type; unresolved_cnr_list.push(node.properties.cnr_id); } } else if(node.properties.aux_id) { unresolved_aux_ids[node.properties.aux_id] = node.type; } else { unresolved_missing_nodes.add(node.type); } } } if(unresolved_cnr_list.length > 0) { let error_msg = "Failed to find the following ComfyRegistry list.\nThe cache may be outdated, or the nodes may have been removed from ComfyRegistry.
"; for(let i in unresolved_cnr_list) { error_msg += '
  • '+unresolved_cnr_list[i]+'
  • '; } show_message(error_msg); } if(outdated_comfyui) { customAlert('ComfyUI is outdated, so some built-in nodes cannot be used.'); } if(Object.keys(unresolved_aux_ids).length > 0) { // building aux_id to nodepack map let aux_id_to_pack = {}; for(let k in this.custom_nodes) { let nodepack = this.custom_nodes[k]; let aux_id; if(nodepack.repository?.startsWith('https://github.com')) { aux_id = nodepack.repository.split('/').slice(-2).join('/'); aux_id_to_pack[aux_id] = nodepack; } else if(nodepack.repository) { aux_id = nodepack.repository.split('/').slice(-1); aux_id_to_pack[aux_id] = nodepack; } } // resolving aux_id for(let k in unresolved_aux_ids) { let nodepack = aux_id_to_pack[k]; if(nodepack) { hashMap[nodepack.hash] = true; } else { unresolved_missing_nodes.add(unresolved_aux_ids[k]); } } } if(unresolved_missing_nodes.size > 0) { await this.getMissingNodesLegacy(hashMap, unresolved_missing_nodes); } return hashMap; } async getMissingNodesLegacy(hashMap, missing_nodes) { 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); } } let unresolved_missing_nodes = new Set(); for (let node_type of missing_nodes) { const packs = name_to_packs[node_type.trim()]; if(packs) packs.forEach(url => { unresolved_missing_nodes.add(url); }); else { for(let j in regex_to_pack) { if(regex_to_pack[j].regex.test(node_type)) { unresolved_missing_nodes.add(regex_to_pack[j].url); } } } } for(let k in this.custom_nodes) { let item = this.custom_nodes[k]; if(unresolved_missing_nodes.has(item.id)) { hashMap[item.hash] = true; } else if (item.files?.some(file => unresolved_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 getNodepackInWorkflow() { let allUsedNodes = this.getNodesInWorkflow(); // building aux_id to nodepack map let aux_id_to_pack = {}; for(let k in this.custom_nodes) { let nodepack = this.custom_nodes[k]; let aux_id; if(nodepack.repository?.startsWith('https://github.com')) { aux_id = nodepack.repository.split('/').slice(-2).join('/'); aux_id_to_pack[aux_id] = nodepack; } else if(nodepack.repository) { aux_id = nodepack.repository.split('/').slice(-1); aux_id_to_pack[aux_id] = nodepack; } } const hashMap = {}; for(let k in allUsedNodes) { var item; if(allUsedNodes[k].properties.cnr_id) { item = this.custom_nodes[allUsedNodes[k].properties.cnr_id]; } else if(allUsedNodes[k].properties.aux_id) { item = aux_id_to_pack[allUsedNodes[k].properties.aux_id]; } if(item) 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) { const isElectron = 'electronAPI' in window; this.show_mode = show_mode; console.log("Show mode:", show_mode); this.showLoading(); const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading custom nodes (${mode}) ...`); const skip_update = this.show_mode === ShowMode.UPDATE ? "" : "&skip_update=true"; if(this.show_mode === ShowMode.UPDATE) { infoToast('Fetching updated information. This may take some time if many custom nodes are installed.'); } 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; if(isElectron) { delete node_packs['comfyui-manager']; } 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.key = k; item.hash = md5(k); } await this.loadNodes(node_packs); 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(); } else if(this.show_mode == ShowMode.IN_WORKFLOW) { hashMap = await this.getNodepackInWorkflow(); } filterItem.hashMap = hashMap; if(this.show_mode != ShowMode.IN_WORKFLOW) { 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 $stop = this.element.querySelector(".cn-manager-stop"); 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 && it !== $stop; }); 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"; setNeedRestart(true); } showStop() { this.element.querySelector(".cn-manager-stop").style.display = "block"; } hideStop() { this.element.querySelector(".cn-manager-stop").style.display = "none"; } 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.element.focus(); this.setFilter(show_mode); this.setKeywords(""); this.showSelection(""); this.showMessage(""); this.loadData(show_mode); } close() { this.element.style.display = "none"; } get isVisible() { return this.element?.style?.display !== "none"; } }