import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; const prwPath = "/extensions/ComfyUI_Primere_Nodes"; const validClasses = ['PrimereVisualCKPT', 'PrimereVisualLORA', 'PrimereVisualEmbedding', 'PrimereVisualHypernetwork', 'PrimereVisualStyle', 'PrimereVisualLYCORIS', 'PrimereVisualPromptOrganizerCSV']; const stackedClasses = ['PrimereVisualLORA', 'PrimereVisualEmbedding', 'PrimereVisualHypernetwork', 'PrimereVisualLYCORIS']; const state = { eventListenerInit: false, callbackfunct: null, widget_name: "", widget_object: {}, node_object: {}, lastDirObject: {}, currentClass: false, hiddenWidgets: {}, ShowHidden: false, FilterType: 'Subdir', SelectedModel: 'SelectedModel', sortType: 'name', operator: 'ASC', PreviewPath: true, aeScoreMin: 550, aeScoreMax: 800, nodeHelper: {}, cache_key: '', source_subdirname: '', AutoFilter: false, StackVersion: 'Auto', ModelVersion: 'SD1', SetupCacheData: {}, VersionCacheData: {}, supportedModels: [], ModelsByVersion: {}, AllPath: [], ModelList: {}, AscoreDataResponse: {}, STimeDataResponse: {}, FileDateResponse: {}, FileLinkResponse: {}, RawImageDataResponse: {}, }; function apiPost(endpoint, eventName, params = {}) { return new Promise((resolve) => { const timer = setTimeout(() => resolve(null), 10000); api.addEventListener(eventName, (event) => { clearTimeout(timer); resolve(event.detail); }, { once: true }); const body = new FormData(); for (const [key, value] of Object.entries(params)) body.append(key, value); api.fetchApi(endpoint, { method: "POST", body }); }); } const BADGE_OVERRIDES = { 'SD1': 'SD 1.x', 'SD2': 'SD 2.x', 'SD3': 'SD 3.x', 'StableCascade': 'Cascade', 'KwaiKolors': 'Kolors', 'StableAudio': 'S.Audio', 'Playground': 'PG', 'PixartSigma': 'Σ', 'Illustrious': 'Ill', 'Unknown': '?', }; function typeHash(str) { let h = 5381; for (let i = 0; i < str.length; i++) h = (h * 33 ^ str.charCodeAt(i)) >>> 0; return h; } function badgeLabel(type) { if (BADGE_OVERRIDES[type]) return BADGE_OVERRIDES[type]; return type.length < 10 ? type : type.substring(0, 6); } function injectTypeStyles(types) { const existing = document.getElementById('primere-type-styles'); if (existing) existing.remove(); const rules = types.map(type => { const h = typeHash(type); const hue = h % 360; const satBg = 30 + (h >> 4) % 20; const satBadge = 45 + (h >> 8) % 20; const label = badgeLabel(type).replace(/'/g, "\\'"); return `#primere_visual_modal .background-${type}{background:hsl(${hue},${satBg}%,80%);}` + `#primere_visual_modal .ckpt-version.${type}-ckpt::before{content:'${label}';background:hsl(${hue},${satBadge}%,22%);color:#ffffff;}`; }).join('\n'); const style = document.createElement('style'); style.id = 'primere-type-styles'; style.textContent = rules; document.head.appendChild(style); } const categoryHandler = (setupValue, method, setupKey) => apiPost('/primere_category_handler', 'LastCategoryResponse', { setupValue, setupMethod: method, setupKey }); const getSupportedModels = () => apiPost('/primere_supported_models', 'SupportedModelsResponse', { models: 'get' }); const getAllPath = (sourceType) => apiPost('/primere_modelpaths', 'AllPathResponse', { sourceType }); const getModelData = (message) => apiPost('/primere_get_category', 'CategoryListResponse', { cache_key: message }); const getModelDatabyPath = (subdir, type) => apiPost('/primere_get_subdir', 'SourceListResponse', { subdir, type }); const getModelDatabyVersion= (subdir, cachekey, version) => apiPost('/primere_get_version', 'VersionListResponse', { subdir, cachekey, version }); const getCacheByKey = (chachekey) => apiPost('/primere_get_cache', 'CacheByKey', { chachekey }); const ReadAScores = (type) => apiPost('/primere_get_ascores', 'AscoreData', { type }); const ReadSTimes = (type) => apiPost('/primere_get_stime', 'STimeData', { type }); const modelImageData = (SubdirName, PreviewPath) => apiPost('/primere_get_images', 'CollectedImageData', { SubdirName, PreviewPath }); const ReadFileDate = (sourcetype) => apiPost('/primere_get_filedates', 'FileDateData', { type: sourcetype }); const ReadFileSymlink = (sourcetype) => apiPost('/primere_get_filelinks', 'FileLinkData', { type: sourcetype }); function sendPOSTModelName(modelName) { const body = new FormData(); body.append('modelName', modelName); api.fetchApi("/primere_keyword_parser", { method: "POST", body }); } function setText(selector, value) { const el = document.querySelector(selector); if (el) el.textContent = value; } function getFilterInput() { return document.querySelector('body div.subdirtab input'); } function clearModalCardsKeepingSelected() { const cards = document.querySelectorAll('div.primere-modal-content div.visual-ckpt'); for (const card of cards) { if (!card.classList.contains('visual-ckpt-selected')) card.remove(); } } function clearSelectedPathButtons() { const buttons = document.querySelectorAll('div.subdirtab button'); for (const button of buttons) { if (!button.classList.contains('preview_sort') && !button.classList.contains('preview_sort_direction')) { button.classList.remove('selected_path'); } } } app.registerExtension({ name: "Primere.VisualMenu", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (validClasses.includes(nodeData.name)) { if (nodeData.input.hasOwnProperty('hidden') === true) { state.hiddenWidgets[nodeData.name] = nodeData.input.hidden; } const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { onNodeCreated ? onNodeCreated.apply(this, []) : undefined; new ModalControl(this); } } }, loadedGraphNode(node) { if (validClasses.includes(node.type)) { new ModalControl(node); } }, }); function ModalHandler() { state.eventListenerInit = true; document.body.addEventListener("click", async function (event) { let modal = null; const closeButton = event.target.closest('button.modal-closer'); if (closeButton) { modal = document.getElementById("primere_visual_modal"); if (modal) modal.setAttribute('style', 'display: none; width: 80%; height: 70%;'); var lastDirValue = 'All'; if (state.lastDirObject.hasOwnProperty(state.currentClass) === true) lastDirValue = state.lastDirObject[state.currentClass]; if (typeof state.nodeHelper['sortbuttons'] !== "object" || typeof state.nodeHelper['sortbuttons'][0] !== "object" || state.nodeHelper['sortbuttons'][0].indexOf("Path") > -1) { if (state.AutoFilter !== true) { await categoryHandler(lastDirValue, 'add', 'last_visual_category' + '_' + state.cache_key); await categoryHandler(state.FilterType, 'add', 'last_visual_category_type' + '_' + state.cache_key); const filter = getFilterInput()?.value ?? ''; await categoryHandler(filter, 'add', 'last_visual_filter' + '_' + state.cache_key); await categoryHandler(state.sortType, 'add', 'last_visual_sort_type' + '_' + state.cache_key); await categoryHandler(state.operator, 'add', 'last_visual_sort_operator' + '_' + state.cache_key); } } return; } const selectedImage = event.target.closest('div.primere-modal-content div.visual-ckpt img'); if (selectedImage) { var ckptName = selectedImage.dataset.ckptname; modal = document.getElementById("primere_visual_modal"); if (modal) modal.setAttribute('style', 'display: none; width: 80%; height: 70%;'); var lastDirValue = 'All'; if (state.lastDirObject.hasOwnProperty(state.currentClass) === true) lastDirValue = state.lastDirObject[state.currentClass]; if (state.source_subdirname == 'styles') { let pathLastIndex = ckptName.lastIndexOf('\\'); ckptName = ckptName.substring(pathLastIndex + 1); } if (ckptName && typeof state.callbackfunct == 'function') { state.callbackfunct(ckptName); sendPOSTModelName(ckptName); } if (typeof state.nodeHelper['sortbuttons'] !== "object" || typeof state.nodeHelper['sortbuttons'][0] !== "object" || state.nodeHelper['sortbuttons'][0].indexOf("Path") > -1) { if (state.AutoFilter !== true) { await categoryHandler(lastDirValue, 'add', 'last_visual_category' + '_' + state.cache_key); await categoryHandler(state.FilterType, 'add', 'last_visual_category_type' + '_' + state.cache_key); const filter = getFilterInput()?.value ?? ''; await categoryHandler(filter, 'add', 'last_visual_filter' + '_' + state.cache_key); await categoryHandler(state.sortType, 'add', 'last_visual_sort_type' + '_' + state.cache_key); await categoryHandler(state.operator, 'add', 'last_visual_sort_operator' + '_' + state.cache_key); } } return; } var subdirName = 'All'; var filteredCheckpoints = 0; const subdirButton = event.target.closest('div.subdirtab button.subdirfilter'); if (subdirButton) { const filterInput = getFilterInput(); if (filterInput) filterInput.value = ''; subdirName = subdirButton.dataset.ckptsubdir; if (state.currentClass !== false) state.lastDirObject[state.currentClass] = subdirName; filteredCheckpoints = 0; clearModalCardsKeepingSelected(); if (state.source_subdirname != 'styles') { state.ModelsByVersion = await getModelData(state.cache_key + '_version'); } state.ModelList = await getModelDatabyPath(state.source_subdirname, subdirName); if (state.source_subdirname == 'styles') { if (state.ModelList.includes(state.SelectedModel) || state.ModelList.includes(subdirName + '\\' + state.SelectedModel)) { var index_pre = (state.ModelList.indexOf(state.SelectedModel) + state.ModelList.indexOf(subdirName + '\\' + state.SelectedModel)) + 1; if (index_pre !== -1) await state.ModelList.splice(index_pre, 1); } } for (var checkpoint of state.ModelList) { let firstletter = checkpoint.charAt(0); var filterpass = true; if (state.SelectedModel == checkpoint) filterpass = false; if (((firstletter === '.' && state.ShowHidden === true) || firstletter !== '.') && ((checkpoint.match('^NSFW') && state.ShowHidden === true) || !checkpoint.match('^NSFW')) && filterpass == true) { let pathLastIndex = checkpoint.lastIndexOf('\\'); let ckptName_full = checkpoint.substring(pathLastIndex + 1); let dotLastIndex = ckptName_full.lastIndexOf('.'); var ckptName = ckptName_full.substring(0, dotLastIndex); var CategoryName = 'Unknown'; for (const [ver_index, ver_value] of Object.entries(state.ModelsByVersion)) { if (ver_value.includes(ckptName)) CategoryName = ver_index; } var container = document.querySelector('div.primere-modal-content.ckpt-container'); if (subdirName === 'Root') { let isSubdirExist = checkpoint.lastIndexOf('\\'); if (isSubdirExist < 0) { filteredCheckpoints++; await createCardElement(checkpoint, container, state.SelectedModel, state.source_subdirname, CategoryName); } } else { filteredCheckpoints++; await createCardElement(checkpoint, container, state.SelectedModel, state.source_subdirname, CategoryName); } } setText('div#primere_visual_modal div.modal_header label.ckpt-counter', String(filteredCheckpoints)); } previewSorter(state.operator, state.sortType); setText('div#primere_visual_modal div.modal_header label.ckpt-name', subdirName); setText('div#primere_visual_modal div.modal_header label.ckpt-ver', 'Subdir'); clearSelectedPathButtons(); subdirButton.classList.add('selected_path'); state.FilterType = 'Subdir'; return; } const versionButton = event.target.closest('div.subdirtab button.verfilter'); if (versionButton) { const filterInput = getFilterInput(); if (filterInput) filterInput.value = ''; var versionName = versionButton.dataset.ckptver; if (state.currentClass !== false) state.lastDirObject[state.currentClass] = versionName; filteredCheckpoints = 0; clearModalCardsKeepingSelected(); state.ModelList = await getModelDatabyVersion(state.source_subdirname, state.cache_key + '_version', versionName); for (var checkpoint of state.ModelList) { let firstletter = checkpoint.charAt(0); var filterpass = true; if (state.SelectedModel == checkpoint) filterpass = false; if (((firstletter === '.' && state.ShowHidden === true) || firstletter !== '.') && ((checkpoint.match('^NSFW') && state.ShowHidden === true) || !checkpoint.match('^NSFW')) && filterpass == true) { filteredCheckpoints++; var container = document.querySelector('div.primere-modal-content.ckpt-container'); await createCardElement(checkpoint, container, state.SelectedModel, state.source_subdirname, versionName); } } setText('div#primere_visual_modal div.modal_header label.ckpt-counter', String(filteredCheckpoints)); previewSorter(state.operator, state.sortType); setText('div#primere_visual_modal div.modal_header label.ckpt-name', versionName); setText('div#primere_visual_modal div.modal_header label.ckpt-ver', 'Version'); clearSelectedPathButtons(); versionButton.classList.add('selected_path'); state.FilterType = 'Version'; return; } const clearButton = event.target.closest('div.subdirtab button.filter_clear'); if (clearButton) { const filterInput = getFilterInput(); if (filterInput) filterInput.value = ''; const images = document.querySelectorAll('div.primere-modal-content div.visual-ckpt img'); filteredCheckpoints = 0; for (const image of images) { const parent = image.parentElement; if (parent) { parent.style.display = ''; filteredCheckpoints++; } } setText('div#primere_visual_modal div.modal_header label.ckpt-counter', String(filteredCheckpoints - 1)); return; } const sortButton = event.target.closest('div.subdirtab button.preview_sort'); if (sortButton) { state.sortType = sortButton.dataset.sortsource; state.operator = document.querySelector('button.preview_sort_direction')?.textContent ?? 'ASC'; previewSorter(state.operator, state.sortType); const sortButtons = document.querySelectorAll('div.subdirtab button.preview_sort'); for (const button of sortButtons) button.classList.remove('selected_path'); sortButton.classList.add('selected_path'); return; } const sortDirectionButton = event.target.closest('div.subdirtab button.preview_sort_direction'); if (sortDirectionButton) { state.operator = sortDirectionButton.textContent; if (state.operator == 'ASC') { sortDirectionButton.textContent = 'DESC'; state.operator = 'DESC'; sortDirectionButton.classList.add('selected_path'); } else { sortDirectionButton.textContent = 'ASC'; state.operator = 'ASC'; sortDirectionButton.classList.remove('selected_path'); } previewSorter(state.operator, state.sortType); } }); document.body.addEventListener("keyup", function (event) { const filterInput = event.target.closest('div.subdirtab input'); if (filterInput) previewFilter(filterInput.value); }); } class ModalControl { constructor(node) { const modaltitle = ''; const nodematch = ''; for (var i = 0; i < node.widgets.length; ++i) { var w = node.widgets[i]; if (!w || w.disabled) continue; if (w.type != "combo") continue; let nx = i; node.widgets[i].mouse = async function (event, pos, node) { if (event.type == 'pointerup' && (pos[0] < 35 || pos[0] > node.size[0] - 35)) return false; if (event.type == 'pointermove' && validClasses.includes(node.type)) return false; if (event.type == 'pointerdown' && (pos[0] < 35 || pos[0] > node.size[0] - 35)) return false; const isnumeric_end = stackedClasses.includes(node.type); state.currentClass = node.type; var ShowModal = false; state.AutoFilter = null; state.StackVersion = null; state.ModelVersion = null; for (var p = 0; p < node.widgets.length; ++p) { if (node.widgets[p].name == 'show_hidden') state.ShowHidden = node.widgets[p].value; if (node.widgets[p].name == 'show_modal') ShowModal = node.widgets[p].value; if (node.widgets[p].name == 'preview_path') state.PreviewPath = node.widgets[p].value; if (node.widgets[p].name == 'aescore_percent_min' && node.widgets[p].value >= 0) state.aeScoreMin = node.widgets[p].value; if (node.widgets[p].name == 'aescore_percent_max' && node.widgets[p].value > 0) state.aeScoreMax = node.widgets[p].value; if (node.widgets[p].name == 'auto_filter') state.AutoFilter = node.widgets[p].value; if (node.widgets[p].name == 'stack_version') state.StackVersion = node.widgets[p].value; if (node.widgets[p].name == 'model_version') state.ModelVersion = node.widgets[p].value; } state.widget_name = node.widgets[nx].name; state.widget_object = node.widgets[nx]; state.node_object = node; if (ShowModal === true) { const contextMenu = document.querySelector('div.litegraph.litecontextmenu.litemenubar-panel'); if (contextMenu) contextMenu.style.display = 'none'; state.nodeHelper = state.hiddenWidgets[state.currentClass]; if (!state.nodeHelper) return; state.source_subdirname = state.nodeHelper['subdir']; state.cache_key = state.nodeHelper['cache_key']; if (state.eventListenerInit == false) ModalHandler(); if (document.querySelector('div.primere-modal-content div.visual-ckpt')) { const ckptContainer = document.querySelector('div.primere-modal-content.ckpt-container'); if (ckptContainer) ckptContainer.innerHTML = ''; } const widgetNumericEnd = /\d$/.test(String(state.widget_object?.name ?? '')); if (state.widget_object.name.match(nodematch) && widgetNumericEnd === isnumeric_end) { await sleep(200); state.SelectedModel = state.node_object.widgets[nx].value; await new Promise((resolve) => requestAnimationFrame(resolve)); setup_visual_modal(modaltitle, 'AllModels', state.ShowHidden, state.SelectedModel, state.source_subdirname, state.node_object, state.PreviewPath); state.callbackfunct = inner_clicked.bind(state.widget_object); function inner_clicked(value, option, event) { inner_value_change(state.widget_object, value); app.canvas.setDirty(true); return false; } function inner_value_change(widget_object, value) { if (typeof state.nodeHelper['sortbuttons'] === "object" && typeof state.nodeHelper['sortbuttons'][0] === "object" && state.nodeHelper['sortbuttons'][0].indexOf("Path") == -1) { for (var ic = 0; ic < state.node_object.widgets.length; ++ic) { if (state.node_object.widgets[ic].type == 'combo' && state.node_object.widgets[ic].value != 'None') { state.node_object.widgets[ic].value = 'None'; } } } if (widget_object.type == "number") value = Number(value); widget_object.value = value; if (widget_object.options && widget_object.options.property && state.node_object.properties[widget_object.options.property] !== undefined) { state.node_object.setProperty(widget_object.options.property, value); } if (widget_object.callback) { widget_object.callback(widget_object.value, this, state.node_object, pos, event); } } return null; } } } } } } async function setup_visual_modal(combo_name, AllModels, ShowHidden, SelectedModel, ModelType, node, PreviewPath) { var container = null; var modal = null; modal = document.getElementById("primere_visual_modal"); if (!modal) { modal = document.createElement("div"); modal.classList.add("comfy-modal"); modal.setAttribute("id","primere_visual_modal"); modal.innerHTML = '