import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { $el, ComfyDialog } from "../../scripts/ui.js"; import { getBestPosition, getPositionStyle, getRect } from './popover-helper.js'; function internalCustomConfirm(message, confirmMessage, cancelMessage) { return new Promise((resolve) => { // transparent bg const modalOverlay = document.createElement('div'); modalOverlay.style.position = 'fixed'; modalOverlay.style.top = 0; modalOverlay.style.left = 0; modalOverlay.style.width = '100%'; modalOverlay.style.height = '100%'; modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; modalOverlay.style.display = 'flex'; modalOverlay.style.alignItems = 'center'; modalOverlay.style.justifyContent = 'center'; modalOverlay.style.zIndex = '1101'; // Modal window container (dark bg) const modalDialog = document.createElement('div'); modalDialog.style.backgroundColor = '#333'; modalDialog.style.padding = '20px'; modalDialog.style.borderRadius = '4px'; modalDialog.style.maxWidth = '400px'; modalDialog.style.width = '80%'; modalDialog.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.5)'; modalDialog.style.color = '#fff'; // Display message const modalMessage = document.createElement('p'); modalMessage.textContent = message; modalMessage.style.margin = '0'; modalMessage.style.padding = '0 0 20px'; modalMessage.style.wordBreak = 'keep-all'; // Button container const modalButtons = document.createElement('div'); modalButtons.style.display = 'flex'; modalButtons.style.justifyContent = 'flex-end'; // Confirm button (green) const confirmButton = document.createElement('button'); if(confirmMessage) confirmButton.textContent = confirmMessage; else confirmButton.textContent = 'Confirm'; confirmButton.style.marginLeft = '10px'; confirmButton.style.backgroundColor = '#28a745'; // green confirmButton.style.color = '#fff'; confirmButton.style.border = 'none'; confirmButton.style.padding = '6px 12px'; confirmButton.style.borderRadius = '4px'; confirmButton.style.cursor = 'pointer'; confirmButton.style.fontWeight = 'bold'; // Cancel button (red) const cancelButton = document.createElement('button'); if(cancelMessage) cancelButton.textContent = cancelMessage; else cancelButton.textContent = 'Cancel'; cancelButton.style.marginLeft = '10px'; cancelButton.style.backgroundColor = '#dc3545'; // red cancelButton.style.color = '#fff'; cancelButton.style.border = 'none'; cancelButton.style.padding = '6px 12px'; cancelButton.style.borderRadius = '4px'; cancelButton.style.cursor = 'pointer'; cancelButton.style.fontWeight = 'bold'; const closeModal = () => { document.body.removeChild(modalOverlay); }; confirmButton.addEventListener('click', () => { closeModal(); resolve(true); }); cancelButton.addEventListener('click', () => { closeModal(); resolve(false); }); modalButtons.appendChild(confirmButton); modalButtons.appendChild(cancelButton); modalDialog.appendChild(modalMessage); modalDialog.appendChild(modalButtons); modalOverlay.appendChild(modalDialog); document.body.appendChild(modalOverlay); }); } export function show_message(msg) { app.ui.dialog.show(msg); app.ui.dialog.element.style.zIndex = 1100; } export async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } export async function customConfirm(message) { try { let res = await window['app'].extensionManager.dialog .confirm({ title: 'Confirm', message: message }); return res; } catch { let res = await internalCustomConfirm(message); return res; } } export function customAlert(message) { try { window['app'].extensionManager.toast.addAlert(message); } catch { alert(message); } } export function infoToast(summary, message) { try { app.extensionManager.toast.add({ severity: 'info', summary: summary, detail: message, life: 3000 }) } catch { // do nothing } } export async function customPrompt(title, message) { try { let res = await window['app'].extensionManager.dialog .prompt({ title: title, message: message }); return res; } catch { return prompt(title, message) } } export function rebootAPI() { if ('electronAPI' in window) { window.electronAPI.restartApp(); return true; } customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => { if (isConfirmed) { try { api.fetchApi("/manager/reboot"); } catch(exception) {} } }); return false; } export var manager_instance = null; export function setManagerInstance(obj) { manager_instance = obj; } export function showToast(message, duration = 3000) { const toast = $el("div.comfy-toast", {textContent: message}); document.body.appendChild(toast); setTimeout(() => { toast.classList.add("comfy-toast-fadeout"); setTimeout(() => toast.remove(), 500); }, duration); } function isValidURL(url) { if(url.includes('&')) return false; const http_pattern = /^(https?|ftp):\/\/[^\s$?#]+$/; const ssh_pattern = /^(.+@|ssh:\/\/).+:.+$/; return http_pattern.test(url) || ssh_pattern.test(url); } export async function install_pip(packages) { if(packages.includes('&')) app.ui.dialog.show(`Invalid PIP package enumeration: '${packages}'`); const res = await api.fetchApi("/customnode/install/pip", { method: "POST", body: packages, }); if(res.status == 403) { show_message('This action is not allowed with this security level configuration.'); return; } if(res.status == 200) { show_message(`PIP package installation is processed.
To apply the pip packages, please click the button in ComfyUI.`); const rebootButton = document.getElementById('cm-reboot-button3'); const self = this; rebootButton.addEventListener("click", rebootAPI); } else { show_message(`Failed to install '${packages}'
See terminal log.`); } } export async function install_via_git_url(url, manager_dialog) { if(!url) { return; } if(!isValidURL(url)) { show_message(`Invalid Git url '${url}'`); return; } show_message(`Wait...

Installing '${url}'`); const res = await api.fetchApi("/customnode/install/git_url", { method: "POST", body: url, }); if(res.status == 403) { show_message('This action is not allowed with this security level configuration.'); return; } if(res.status == 200) { show_message(`'${url}' is installed
To apply the installed custom node, please ComfyUI.`); const rebootButton = document.getElementById('cm-reboot-button4'); const self = this; rebootButton.addEventListener("click", function() { if(rebootAPI()) { manager_dialog.close(); } }); } else { show_message(`Failed to install '${url}'
See terminal log.`); } } export async function free_models(free_execution_cache) { try { let mode = ""; if(free_execution_cache) { mode = '{"unload_models": true, "free_memory": true}'; } else { mode = '{"unload_models": true}'; } let res = await api.fetchApi(`/free`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: mode }); if (res.status == 200) { if(free_execution_cache) { showToast("'Models' and 'Execution Cache' have been cleared.", 3000); } else { showToast("Models' have been unloaded.", 3000); } } else { showToast('Unloading of models failed. Installed ComfyUI may be an outdated version.', 5000); } } catch (error) { showToast('An error occurred while trying to unload models.', 5000); } } export function md5(inputString) { const hc = '0123456789abcdef'; const rh = n => {let j,s='';for(j=0;j<=3;j++) s+=hc.charAt((n>>(j*8+4))&0x0F)+hc.charAt((n>>(j*8))&0x0F);return s;} const ad = (x,y) => {let l=(x&0xFFFF)+(y&0xFFFF);let m=(x>>16)+(y>>16)+(l>>16);return (m<<16)|(l&0xFFFF);} const rl = (n,c) => (n<>>(32-c)); const cm = (q,a,b,x,s,t) => ad(rl(ad(ad(a,q),ad(x,t)),s),b); const ff = (a,b,c,d,x,s,t) => cm((b&c)|((~b)&d),a,b,x,s,t); const gg = (a,b,c,d,x,s,t) => cm((b&d)|(c&(~d)),a,b,x,s,t); const hh = (a,b,c,d,x,s,t) => cm(b^c^d,a,b,x,s,t); const ii = (a,b,c,d,x,s,t) => cm(c^(b|(~d)),a,b,x,s,t); const sb = x => { let i;const nblk=((x.length+8)>>6)+1;const blks=[];for(i=0;i>2]|=x.charCodeAt(i)<<((i%4)*8);} blks[i>>2]|=0x80<<((i%4)*8);blks[nblk*16-2]=x.length*8;return blks; } let i,x=sb(inputString),a=1732584193,b=-271733879,c=-1732584194,d=271733878,olda,oldb,oldc,oldd; for(i=0;i { err = e; }); if (!res) { return { status: 400, error: new Error("Unknown Error") } } const { status, statusText } = res; if (err) { return { status, error: err } } if (status !== 200) { return { status, error: new Error(statusText || "Unknown Error") } } const data = await res.json(); if (!data) { return { status, error: new Error(`Failed to load data: ${route}`) } } return { status, data } } // https://cenfun.github.io/open-icons/ export const icons = { search: '', conflicts: '', passed: '', download: '', close: '', arrowRight: '' } export function sanitizeHTML(str) { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export function showTerminal() { try { const panel = app.extensionManager.bottomPanel; const isTerminalVisible = panel.bottomPanelVisible && panel.activeBottomPanelTab.id === 'logs-terminal'; if (!isTerminalVisible) panel.toggleBottomPanelTab('logs-terminal'); } catch(exception) { // do nothing } } let need_restart = false; export function setNeedRestart(value) { need_restart = value; } async function onReconnected(event) { if(need_restart) { setNeedRestart(false); const confirmed = await customConfirm("To apply the changes to the node pack's installation status, you need to refresh the browser. Would you like to refresh?"); if (!confirmed) { return; } window.location.reload(true); } } api.addEventListener('reconnected', onReconnected); const storeId = "comfyui-manager-grid"; let timeId; export function storeColumnWidth(gridId, columnItem) { clearTimeout(timeId); timeId = setTimeout(() => { let data = {}; const dataStr = localStorage.getItem(storeId); if (dataStr) { try { data = JSON.parse(dataStr); } catch (e) {} } if (!data[gridId]) { data[gridId] = {}; } data[gridId][columnItem.id] = columnItem.width; localStorage.setItem(storeId, JSON.stringify(data)); }, 200) } export function restoreColumnWidth(gridId, columns) { const dataStr = localStorage.getItem(storeId); if (!dataStr) { return; } let data; try { data = JSON.parse(dataStr); } catch (e) {} if(!data) { return; } const widthMap = data[gridId]; if (!widthMap) { return; } columns.forEach(columnItem => { const w = widthMap[columnItem.id]; if (w) { columnItem.width = w; } }); } export function getTimeAgo(dateStr) { const date = new Date(dateStr); if (!date || !(date instanceof Date) || isNaN(date.getTime())) { return ""; } const units = [ { max: 2760000, value: 60000, name: 'minute', past: 'a minute ago', future: 'in a minute' }, { max: 72000000, value: 3600000, name: 'hour', past: 'an hour ago', future: 'in an hour' }, { max: 518400000, value: 86400000, name: 'day', past: 'yesterday', future: 'tomorrow' }, { max: 2419200000, value: 604800000, name: 'week', past: 'last week', future: 'in a week' }, { max: 28512000000, value: 2592000000, name: 'month', past: 'last month', future: 'in a month' } ]; const diff = Date.now() - date.getTime(); // less than a minute if (Math.abs(diff) < 60000) return 'just now'; for (let i = 0; i < units.length; i++) { if (Math.abs(diff) < units[i].max) { return format(diff, units[i].value, units[i].name, units[i].past, units[i].future, diff < 0); } } function format(diff, divisor, unit, past, future, isInTheFuture) { const val = Math.round(Math.abs(diff) / divisor); if (isInTheFuture) return val <= 1 ? future : 'in ' + val + ' ' + unit + 's'; return val <= 1 ? past : val + ' ' + unit + 's ago'; } return format(diff, 31536000000, 'year', 'last year', 'in a year', diff < 0); }; export const loadCss = (cssFile) => { const cssPath = import.meta.resolve(cssFile); //console.log(cssPath); const $link = document.createElement("link"); $link.setAttribute("rel", 'stylesheet'); $link.setAttribute("href", cssPath); document.head.appendChild($link); }; export const copyText = (text) => { return new Promise((resolve) => { let err; try { navigator.clipboard.writeText(text); } catch (e) { err = e; } if (err) { resolve(false); } else { resolve(true); } }); }; function renderPopover($elem, target, options = {}) { // async microtask queueMicrotask(() => { const containerRect = getRect(window); const targetRect = getRect(target); const elemRect = getRect($elem); const positionInfo = getBestPosition( containerRect, targetRect, elemRect, options.positions ); const style = getPositionStyle(positionInfo, { bgColor: options.bgColor, borderColor: options.borderColor, borderRadius: options.borderRadius }); $elem.style.top = positionInfo.top + "px"; $elem.style.left = positionInfo.left + "px"; $elem.style.background = style.background; }); } let $popover; export function hidePopover() { if ($popover) { $popover.remove(); $popover = null; } } export function showPopover(target, text, className, options) { hidePopover(); $popover = document.createElement("div"); $popover.className = ['cn-popover', className].filter(it => it).join(" "); document.body.appendChild($popover); $popover.innerHTML = text; $popover.style.display = "block"; renderPopover($popover, target, { borderRadius: 10, ... options }); } let $tooltip; export function hideTooltip(target) { if ($tooltip) { $tooltip.style.display = "none"; $tooltip.innerHTML = ""; $tooltip.style.top = "0px"; $tooltip.style.left = "0px"; } } export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {}) { if (!$tooltip) { $tooltip = document.createElement("div"); $tooltip.className = className; $tooltip.style.cssText = ` pointer-events: none; position: fixed; z-index: 10001; padding: 20px; color: #1e1e1e; max-width: 350px; filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%)); ${Object.keys(styleMap).map(k=>k+":"+styleMap[k]+";").join("")} `; document.body.appendChild($tooltip); } $tooltip.innerHTML = text; $tooltip.style.display = "block"; renderPopover($tooltip, target, { positions: ['top', 'bottom', 'right', 'center'], bgColor: "#ffffff", borderColor: "#cccccc", borderRadius: 5 }); } function initTooltip () { const mouseenterHandler = (e) => { const target = e.target; const text = target.getAttribute('tooltip'); if (text) { showTooltip(target, text); } }; const mouseleaveHandler = (e) => { const target = e.target; const text = target.getAttribute('tooltip'); if (text) { hideTooltip(target); } }; document.body.removeEventListener('mouseenter', mouseenterHandler, true); document.body.removeEventListener('mouseleave', mouseleaveHandler, true); document.body.addEventListener('mouseenter', mouseenterHandler, true); document.body.addEventListener('mouseleave', mouseleaveHandler, true); } initTooltip();