Files

1261 lines
61 KiB
JavaScript

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', 'PrimereCharacterFactory'];
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: {},
SimilarityDataResponse: {},
FileLinkResponse: {},
RawImageDataResponse: {},
PreviewCacheBust: 0,
};
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 ReadSimilarity = (SubdirName, PreviewPath, SelectedModel) => apiPost('/primere_get_similarity', 'SimilarityData', { SubdirName, PreviewPath, SelectedModel });
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('#primere_visual_modal 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;
function getActiveModal() {
const v = document.getElementById("primere_visual_modal");
const c = document.getElementById("primere_charfactory_modal");
if (v && v.style.display !== 'none') return { modal: v, id: 'visual' };
if (c && c.style.display !== 'none') return { modal: c, id: 'cf' };
return null;
}
document.body.addEventListener("click", async function (event) {
// Close button
const closeButton = event.target.closest('button.modal-closer');
if (closeButton) {
const visModal = document.getElementById("primere_visual_modal");
const cfModal = document.getElementById("primere_charfactory_modal");
if (visModal && visModal.style.display !== 'none') {
visModal.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);
}
}
}
if (cfModal && cfModal.style.display !== 'none') {
cfModal.setAttribute('style', 'display: none; width: 80%; height: 70%;');
}
return;
}
// Image/option card click — check CF modal first (its cards match the same selector)
const active = getActiveModal();
const selectedImage = event.target.closest('div.primere-modal-content div.visual-ckpt img');
if (selectedImage && active) {
if (active.id === 'cf') {
// CF modal card
const cfValue = selectedImage.dataset.charfactoryValue ?? selectedImage.dataset.ckptname;
active.modal.setAttribute('style', 'display: none; width: 80%; height: 70%;');
if (cfValue && typeof state.callbackfunct == 'function') {
state.callbackfunct(cfValue);
}
} else {
// Visual modal card
var ckptName = selectedImage.dataset.ckptname;
active.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 active = getActiveModal();
if (!active || active.id !== 'visual') return;
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 active = getActiveModal();
if (!active || active.id !== 'visual') return;
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 visModal = document.getElementById("primere_visual_modal");
const cfModal = document.getElementById("primere_charfactory_modal");
if (cfModal && cfModal.style.display !== 'none') {
const filterInput = cfModal.querySelector('input.filter_input');
if (filterInput) filterInput.value = '';
const cards = cfModal.querySelectorAll('div.primere-modal-content div.visual-ckpt');
for (const card of cards) card.style.display = '';
const counter = cfModal.querySelector('label.ckpt-counter');
if (counter) counter.textContent = String(cards.length);
} else if (visModal && visModal.style.display !== 'none') {
const filterInput = getFilterInput();
if (filterInput) filterInput.value = '';
const images = document.querySelectorAll('#primere_visual_modal 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) {
const active = getActiveModal();
if (active && active.id === 'cf') {
// CF modal sort
state.sortType = sortButton.dataset.sortsource;
state.operator = active.modal.querySelector('button.preview_sort_direction')?.textContent ?? 'ASC';
const container = active.modal.querySelector('div.ckpt-container');
sortCharFactoryCards(container, state.operator, state.sortType);
const sortButtons = active.modal.querySelectorAll('div.subdirtab button.preview_sort');
for (const button of sortButtons) button.classList.remove('selected_path');
sortButton.classList.add('selected_path');
return;
}
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) {
const active = getActiveModal();
if (active && active.id === 'cf') {
// CF direction
const newDir = sortDirectionButton.textContent === 'ASC' ? 'DESC' : 'ASC';
sortDirectionButton.textContent = newDir;
const sortType = active.modal.querySelector('button.preview_sort.selected_path')?.dataset.sortsource || 'name';
const container = active.modal.querySelector('div.ckpt-container');
sortCharFactoryCards(container, newDir, sortType);
return;
}
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) {
const cfModal = document.getElementById("primere_charfactory_modal");
if (cfModal && cfModal.style.display !== 'none') {
cfPreviewFilter(filterInput.value);
} else {
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;
if (w.__visual_mouse_set) continue;
// For CF nodes, skip gender and content_rating — they're static dropdowns, not modal widgets
if (node.type === 'PrimereCharacterFactory' && (w.name === 'gender' || w.name === 'content_rating')) {
continue;
}
w.__visual_mouse_set = true;
let nx = i;
const origMouse = w.mouse;
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 = '';
}
if (state.currentClass === 'PrimereCharacterFactory') {
// gender and content_rating trigger rebuild, not the preview modal
if (state.widget_object.name === 'gender' || state.widget_object.name === 'content_rating') {
return origMouse?.apply(this, arguments);
}
await sleep(200);
state.SelectedModel = state.node_object.widgets[nx].value;
await new Promise((resolve) => requestAnimationFrame(resolve));
await openCharFactoryVisualModal(node, state.widget_object);
state.callbackfunct = inner_clicked.bind(state.widget_object);
return null;
}
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);
return null;
}
// Shared helpers used by both CF and visual modal callbacks
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 (state.currentClass !== 'PrimereCharacterFactory' && 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);
}
}
}
}
}
}
}
// ── Character Factory visual modal ──
function sanitizePreviewName(value) {
return String(value ?? "")
.trim()
.replace(/ /g, "_")
.replace(/^_+|_+$/g, "");
}
function sectionPreviewName(sectionKey, value) {
const valueName = sanitizePreviewName(String(value ?? "").substring(0, 20));
return valueName ? `${sectionKey}_${valueName}` : sectionKey;
}
function savedCharacterPreviewName(characterName) {
const savedName = sanitizePreviewName(characterName);
return savedName || "character";
}
function getCharFactoryModalItems(node, widget) {
if (!widget) return [];
if (widget.name === "saved_character") {
const values = Array.isArray(widget.options?.values) ? widget.options.values : [];
return values.map((value) => ({
value,
label: value,
previewName: value === "None" ? "none" : savedCharacterPreviewName(value),
}));
}
const values = Array.isArray(widget.options?.values) ? widget.options.values : [];
return values.map((value) => ({
value,
label: value,
previewName: sectionPreviewName(widget.name, value),
}));
}
let CharFactoryImageData = null;
async function fetchCharFactoryImages(node) {
try {
const hiddenData = state.hiddenWidgets["PrimereCharacterFactory"] || {};
const rawSubdir = hiddenData.subdir;
const subdir = Array.isArray(rawSubdir) ? rawSubdir[0] : (rawSubdir || "charfactory");
const body = new FormData();
body.append("subdir", subdir);
const response = await api.fetchApi("/primere_charfactory_images", { method: "POST", body });
CharFactoryImageData = await response.json();
} catch {
CharFactoryImageData = {};
}
return CharFactoryImageData;
}
async function fetchCharFactorySimilarity(node, referenceName) {
try {
const hiddenData = state.hiddenWidgets["PrimereCharacterFactory"] || {};
const rawSubdir = hiddenData.subdir;
const subdir = Array.isArray(rawSubdir) ? rawSubdir[0] : (rawSubdir || "charfactory");
const body = new FormData();
body.append("SubdirName", subdir);
body.append("PreviewPath", "true");
body.append("SelectedModel", referenceName || "");
const result = await new Promise((resolve) => {
const timer = setTimeout(() => resolve({}), 10000);
api.addEventListener("SimilarityData", (event) => {
clearTimeout(timer);
resolve(event.detail || {});
}, { once: true });
api.fetchApi("/primere_get_similarity", { method: "POST", body });
});
return result;
} catch {
return {};
}
}
async function openCharFactoryVisualModal(node, widget) {
const PREVIEW_MODAL_ID = "primere_charfactory_modal";
const MISSING_IMAGE = "/extensions/comfyui_primere_nodes/images/missing.jpg";
if (!widget) return;
const items = getCharFactoryModalItems(node, widget);
if (!items.length) return;
const hiddenData = state.hiddenWidgets["PrimereCharacterFactory"] || {};
const rawSort = hiddenData.sortbuttons;
const sortButtonsList = Array.isArray(rawSort?.[0]) ? rawSort[0] : (rawSort || ["Name"]);
const imageData = await fetchCharFactoryImages(node);
const similarityData = sortButtonsList.some(s => s.toLowerCase() === "similarity")
? await fetchCharFactorySimilarity(node, items.find(i => i.value === widget.value)?.previewName)
: {};
let modal = document.getElementById(PREVIEW_MODAL_ID);
if (!modal) {
modal = document.createElement("div");
modal.classList.add("comfy-modal", "primere-modal-base");
modal.setAttribute("id", PREVIEW_MODAL_ID);
modal.innerHTML = '<div class="modal_header"><button type="button" class="modal-closer">Close modal</button> <h3 class="visual_modal_title"></h3></div>';
const subdirContainer = document.createElement("div");
subdirContainer.classList.add("subdirtab");
modal.appendChild(subdirContainer);
const container = document.createElement("div");
container.classList.add("primere-modal-content", "ckpt-container", "ckpt-grid-layout");
modal.appendChild(container);
document.body.appendChild(modal);
}
const title = modal.querySelector("h3.visual_modal_title");
if (title) {
const sectionName = widget.name === "saved_character"
? "Saved characters"
: widget.name.split("_").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
title.innerHTML = `${sectionName} :: <label class="ckpt-name">${widget.value}</label> :: <label class="ckpt-counter">${items.length}</label>`;
}
const subdirContainer = modal.querySelector("div.subdirtab");
if (subdirContainer) {
let html = '<input type="text" name="ckptfilter" placeholder="filter" class="filter_input"> <button type="button" class="filter_clear">Clear filter</button><label class="sort_by_label"> | Sort by: </label>';
for (const btn of sortButtonsList) {
const isActive = btn.toLowerCase() === "name";
html += `<button type="button" class="preview_sort${isActive ? " selected_path" : ""}" data-sortsource="${btn.toLowerCase()}">${btn}</button><label> | </label>`;
}
html += '<button type="button" class="preview_sort_direction">ASC</button>';
subdirContainer.innerHTML = html;
}
const container = modal.querySelector("div.ckpt-container");
if (!container) return;
container.innerHTML = "";
for (const item of items) {
const card = document.createElement("div");
card.classList.add("visual-ckpt", "version-CharacterFactory");
if (item.value === widget.value) card.classList.add("visual-ckpt-selected");
card.dataset.name = item.label.toLowerCase();
card.dataset.value = item.value;
const similarityVal = similarityData && similarityData[item.previewName] != null ? Math.floor(similarityData[item.previewName] * 100) : 0;
card.dataset.similarity = String(similarityVal);
let imgSrc;
if (item.value === "None" && imageData && imageData["none"]) {
imgSrc = 'data:image/jpeg;charset=utf-8;base64,' + imageData["none"];
} else if (item.value === "Random" && imageData && imageData["random"]) {
imgSrc = 'data:image/jpeg;charset=utf-8;base64,' + imageData["random"];
} else if (imageData && imageData[item.previewName]) {
imgSrc = 'data:image/jpeg;charset=utf-8;base64,' + imageData[item.previewName];
} else {
imgSrc = MISSING_IMAGE;
}
card.innerHTML = `<div class="checkpoint-name background-CharacterFactory">${item.label}</div><img src="${imgSrc}" title="${item.label}" data-charfactory-value="${item.value}" data-ckptname="${item.value}">`;
const img = card.querySelector("img");
if (img) {
img.onerror = () => { img.src = MISSING_IMAGE; };
}
container.appendChild(card);
}
// Initial sort — keep None/Random first
sortCharFactoryCards(container, "ASC", "name");
modal.setAttribute("style", "display: block; width: 80%; height: 70%;");
}
function sortCharFactoryCards(container, direction, sortType) {
if (!container) return;
const fixedValues = ["None", "Random"];
const cards = Array.from(container.querySelectorAll("div.visual-ckpt"));
const fixed = [];
const normal = [];
for (const card of cards) {
if (fixedValues.includes(card.dataset.value)) {
fixed.push(card);
} else {
normal.push(card);
}
}
normal.sort(function (a, b) {
var aVal = a.dataset[sortType] ?? "";
var bVal = b.dataset[sortType] ?? "";
if (isNaN(parseInt(aVal)) && isNaN(parseInt(bVal))) {
aVal = String(aVal).toUpperCase();
bVal = String(bVal).toUpperCase();
if (direction === "ASC") return aVal < bVal ? -1 : (aVal > bVal ? 1 : 0);
return aVal > bVal ? -1 : (aVal < bVal ? 1 : 0);
}
if (direction === "ASC") return parseInt(aVal) - parseInt(bVal);
return parseInt(bVal) - parseInt(aVal);
});
container.innerHTML = "";
for (const card of fixed) container.appendChild(card);
for (const card of normal) container.appendChild(card);
// Update state for previewFilter
state.sortType = sortType;
state.operator = direction;
}
function cfPreviewFilter(filterString) {
const cfModal = document.getElementById("primere_charfactory_modal");
if (!cfModal || cfModal.style.display === "none") return;
const images = cfModal.querySelectorAll('div.primere-modal-content div.visual-ckpt img');
let count = 0;
for (const img of images) {
const card = img.closest(".visual-ckpt");
if (!card) continue;
const isFixed = card.dataset.value === "None" || card.dataset.value === "Random";
const visible = isFixed || (img.title || "").toLowerCase().includes(filterString.toLowerCase());
card.style.display = visible ? "" : "none";
if (visible) count++;
}
const counter = cfModal.querySelector("label.ckpt-counter");
if (counter) counter.textContent = String(count);
}
async function setup_visual_modal(combo_name, AllModels, ShowHidden, SelectedModel, ModelType, node, PreviewPath) {
var container = null;
var modal = null;
state.PreviewCacheBust = Date.now();
modal = document.getElementById("primere_visual_modal");
if (!modal) {
modal = document.createElement("div");
modal.classList.add("comfy-modal", "primere-modal-base");
modal.setAttribute("id","primere_visual_modal");
modal.innerHTML = '<div class="modal_header"><button type="button" class="modal-closer">Close modal</button> <h3 class="visual_modal_title">' + combo_name.replace("_"," ") + '<label class="ckpt-ver">Subdir</label> :: <label class="ckpt-name">All</label> :: <label class="ckpt-counter"></label></h3></div>';
let subdir_container = document.createElement("div");
subdir_container.classList.add("subdirtab");
let container = document.createElement("div");
container.classList.add("primere-modal-content", "ckpt-container", "ckpt-grid-layout");
modal.appendChild(subdir_container);
modal.appendChild(container);
document.body.appendChild(modal);
} else {
const title = document.querySelector('div#primere_visual_modal div.modal_header h3.visual_modal_title');
if (title) {
title.innerHTML = combo_name.replace("_", " ") + '<label class="ckpt-ver">Subdir</label> :: <label class="ckpt-name">All</label> :: <label class="ckpt-counter"></label>';
}
}
container = modal.getElementsByClassName("ckpt-container")[0];
container.innerHTML = "";
var subdirArray = ['All'];
for (var checkpoints of AllModels) {
let pathLastIndex = checkpoints.lastIndexOf('\\');
let ckptSubdir = checkpoints.substring(0, pathLastIndex);
if (ckptSubdir === '') ckptSubdir = 'Root';
if (subdirArray.indexOf(ckptSubdir) === -1) subdirArray.push(ckptSubdir);
}
var subdir_tabs = modal.getElementsByClassName("subdirtab")[0];
var menu_html = '';
var type_html = '';
var version_html = '';
if (typeof state.nodeHelper['sortbuttons'] !== "object" || typeof state.nodeHelper['sortbuttons'][0] !== "object" || state.nodeHelper['sortbuttons'][0].indexOf("Path") > -1 && state.AutoFilter !== true) {
for (var subdir of subdirArray) {
var addWhiteClass = '';
let firstletter = subdir.charAt(0);
var subdirName = subdir;
if (firstletter === '.') subdirName = subdir.substring(1);
if ((firstletter === '.' && ShowHidden === true) || firstletter !== '.') {
if (state.lastDirObject.hasOwnProperty(state.currentClass) === true && subdir === state.lastDirObject[state.currentClass]) {
addWhiteClass = ' selected_path';
}
menu_html += '<button type="button" data-ckptsubdir="' + subdir + '" class="subdirfilter' + addWhiteClass + '">' + subdirName + '</button>';
}
}
}
var LastCat = 'All';
var LastCatType = 'Subdir';
var savedfilter = "";
state.SetupCacheData = await getCacheByKey("setup");
if (state.SetupCacheData != null && typeof state.SetupCacheData === "object") {
if (typeof state.nodeHelper['sortbuttons'] !== "object" || typeof state.nodeHelper['sortbuttons'][0] !== "object" || state.nodeHelper['sortbuttons'][0].indexOf("Path") > -1) {
if (state.SetupCacheData.hasOwnProperty("last_visual_category" + '_' + state.cache_key)) {
LastCat = state.SetupCacheData["last_visual_category" + '_' + state.cache_key];
}
if (state.SetupCacheData.hasOwnProperty("last_visual_category_type" + '_' + state.cache_key)) {
LastCatType = state.SetupCacheData["last_visual_category_type" + '_' + state.cache_key];
}
if (state.SetupCacheData.hasOwnProperty("last_visual_filter" + '_' + state.cache_key)) {
savedfilter = state.SetupCacheData['last_visual_filter' + '_' + state.cache_key];
}
if (state.SetupCacheData.hasOwnProperty("last_visual_sort_type" + '_' + state.cache_key)) {
state.sortType = state.SetupCacheData['last_visual_sort_type' + '_' + state.cache_key];
}
if (state.SetupCacheData.hasOwnProperty("last_visual_sort_operator" + '_' + state.cache_key)) {
state.operator = state.SetupCacheData['last_visual_sort_operator' + '_' + state.cache_key];
}
}
}
if (state.AutoFilter === true && state.StackVersion != null && state.ModelVersion != null) {
if (state.StackVersion == 'Auto') {
LastCat = state.ModelVersion;
LastCatType = 'Version';
} else if (state.StackVersion != 'Any' && state.StackVersion != 'Auto') {
LastCat = state.StackVersion;
LastCatType = 'Version';
}
}
if (typeof state.nodeHelper['sortbuttons'] === "object" && typeof state.nodeHelper['sortbuttons'][0] === "object" && state.nodeHelper['sortbuttons'][0].indexOf("Path") == -1) {
LastCat = state.widget_name;
}
state.supportedModels = [];
state.ModelsByVersion = {};
if (ModelType != 'styles') {
state.supportedModels = await getSupportedModels();
injectTypeStyles(state.supportedModels);
state.ModelsByVersion = await getModelData(state.cache_key + '_version');
state.VersionCacheData = await getCacheByKey(state.cache_key + '_version');
}
state.AllPath = await getAllPath(state.source_subdirname);
if (LastCatType == 'Subdir') state.ModelList = await getModelDatabyPath(state.source_subdirname, LastCat);
if (LastCatType == 'Version') state.ModelList = await getModelDatabyVersion(state.source_subdirname, state.cache_key + '_version', LastCat);
if (typeof state.nodeHelper['cache_key'] !== "undefined" && state.nodeHelper['sortbuttons'] !== "undefined") {
if (typeof state.nodeHelper['sortbuttons'] === "object" && typeof state.nodeHelper['sortbuttons'][0] === "object" && state.nodeHelper['sortbuttons'][0].length > 0) {
if (state.nodeHelper['sortbuttons'][0].indexOf("aScore") > -1) {
state.AscoreDataResponse = await ReadAScores(state.nodeHelper['cache_key']);
}
}
}
if (typeof state.nodeHelper['cache_key'] !== "undefined" && state.nodeHelper['sortbuttons'] !== "undefined") {
if (typeof state.nodeHelper['sortbuttons'] === "object" && typeof state.nodeHelper['sortbuttons'][0] === "object" && state.nodeHelper['sortbuttons'][0].length > 0) {
if (state.nodeHelper['sortbuttons'][0].indexOf("STime") > -1) {
state.STimeDataResponse = await ReadSTimes(state.nodeHelper['cache_key']);
}
}
}
if (typeof state.nodeHelper['sortbuttons'] !== "undefined") {
if (typeof state.nodeHelper['sortbuttons'] === "object" && typeof state.nodeHelper['sortbuttons'][0] === "object" && state.nodeHelper['sortbuttons'][0].length > 0) {
if (state.nodeHelper['sortbuttons'][0].indexOf("Date") > -1) {
state.FileDateResponse = await ReadFileDate(state.nodeHelper['subdir']);
}
}
}
if (typeof state.nodeHelper['sortbuttons'] !== "undefined") {
if (typeof state.nodeHelper['sortbuttons'] === "object" && typeof state.nodeHelper['sortbuttons'][0] === "object" && state.nodeHelper['sortbuttons'][0].length > 0) {
if (state.nodeHelper['sortbuttons'][0].indexOf("Similarity") > -1) {
state.SimilarityDataResponse = await ReadSimilarity(state.source_subdirname, PreviewPath, SelectedModel);
}
}
}
if (state.nodeHelper['subdir'] == 'checkpoints' && state.nodeHelper['sortbuttons'][0].indexOf("Symlink") > -1) {
state.FileLinkResponse = await ReadFileSymlink(state.nodeHelper['subdir']);
}
state.RawImageDataResponse = {};
state.RawImageDataResponse = await modelImageData(state.source_subdirname, PreviewPath);
if (state.ModelsByVersion !== false && state.ModelsByVersion != null && Object.keys(state.ModelsByVersion).length > 0 && state.source_subdirname != 'styles' && state.AutoFilter !== true) {
version_html = createTypeMenu(state.ModelsByVersion, state.supportedModels, LastCat, LastCatType);
}
if (state.AllPath !== false && state.AllPath != null && state.AllPath.length > 0 && state.AutoFilter !== true) {
if (typeof state.nodeHelper['sortbuttons'] !== "object" || typeof state.nodeHelper['sortbuttons'][0] !== "object" || state.nodeHelper['sortbuttons'][0].indexOf("Path") > -1) {
type_html = createPathMenu(state.AllPath, ShowHidden, LastCat, LastCatType);
}
}
subdir_tabs.innerHTML = menu_html + type_html + version_html + ' <input type="text" name="ckptfilter" placeholder="filter" class="filter_input"> <button type="button" class="filter_clear">Clear filter</button>';
var sortbuttons = state.nodeHelper['sortbuttons'];
var sort_string = "";
if (typeof sortbuttons === "object" && typeof sortbuttons[0] === "object" && sortbuttons[0].length > 0) {
sort_string = createSortButtons(sortbuttons[0]);
}
subdir_tabs.innerHTML += sort_string;
var fondModel = null;
if (state.ModelList.includes(SelectedModel) || state.ModelList.includes(LastCat + '\\' + SelectedModel)) {
var index_pre = (state.ModelList.indexOf(SelectedModel) + state.ModelList.indexOf(LastCat + '\\' + SelectedModel)) + 1;
fondModel = state.ModelList[index_pre];
if (index_pre !== -1) await state.ModelList.splice(index_pre, 1);
}
if (ModelType == 'styles' && fondModel != null) SelectedModel = fondModel;
if (!state.ModelList.includes(SelectedModel)) {
if (fondModel == null) {
await state.ModelList.unshift(SelectedModel);
} else {
await state.ModelList.unshift(fondModel);
}
}
setText('div#primere_visual_modal div.modal_header label.ckpt-counter', String(state.ModelList.length));
setText('div#primere_visual_modal div.modal_header label.ckpt-name', LastCat);
setText('div#primere_visual_modal div.modal_header label.ckpt-ver', LastCatType);
var CKPTElements = 0;
modal.setAttribute('style','display: block; width: 80%; height: 70%;');
for (var checkpoint of state.ModelList) {
let firstletter = checkpoint.charAt(0);
let pathLastIndex = checkpoint.lastIndexOf('\\');
let ckptName_full = checkpoint.substring(pathLastIndex + 1);
let dotLastIndex = ckptName_full.lastIndexOf('.');
var ckptName = ckptName_full.substring(0, dotLastIndex);
if (((firstletter === '.' && ShowHidden === true) || firstletter !== '.') && ((checkpoint.match('^NSFW') && ShowHidden === true) || !checkpoint.match('^NSFW'))) {
var CategoryName = 'Unknown';
for (const [ver_index, ver_value] of Object.entries(state.ModelsByVersion)) {
if (ver_value.includes(ckptName)) CategoryName = ver_index;
}
if (LastCat === 'Root') {
let isSubdirExist = checkpoint.lastIndexOf('\\');
if (isSubdirExist < 0 || checkpoint == SelectedModel) {
CKPTElements++;
await createCardElement(checkpoint, container, SelectedModel, ModelType, CategoryName);
}
} else {
CKPTElements++;
await createCardElement(checkpoint, container, SelectedModel, ModelType, CategoryName);
}
}
setText('div#primere_visual_modal div.modal_header label.ckpt-counter', String(CKPTElements));
}
if (savedfilter.length > 0) {
const filterInput = getFilterInput();
if (filterInput) filterInput.value = savedfilter;
previewFilter(savedfilter);
}
const sortDirectionButton = document.querySelector('button.preview_sort_direction');
if (sortDirectionButton) sortDirectionButton.textContent = state.operator;
if (state.operator != 'ASC') {
document.querySelector('div.subdirtab button.preview_sort_direction')?.classList.add("selected_path");
}
document.querySelector('div.subdirtab button.preview_sort[data-sortsource="' + state.sortType + '"]')?.classList.add('selected_path');
previewSorter(state.operator, state.sortType);
if (!document.querySelector('#primere_visual_modal div.subdirtab button.selected_path')) {
document.querySelector('#primere_visual_modal div.subdirtab button[data-ckptsubdir="All"]')?.classList.add('selected_path');
}
}
function previewSorter(operator, sortType) {
const container = document.querySelector('div.primere-modal-content.ckpt-container.ckpt-grid-layout');
if (!container) return;
const cards = Array.from(container.querySelectorAll('div.visual-ckpt:not(.visual-ckpt-selected)'));
cards.sort(function (a, b) {
var aVal = a.dataset[sortType] ?? '';
var bVal = b.dataset[sortType] ?? '';
if (isNaN(parseInt(aVal)) && isNaN(parseInt(bVal))) {
aVal = String(aVal).toUpperCase();
bVal = String(bVal).toUpperCase();
if (operator == 'ASC') return aVal < bVal ? -1 : (aVal > bVal ? 1 : 0);
return aVal > bVal ? -1 : (aVal < bVal ? 1 : 0);
}
if (operator == 'ASC') return parseInt(aVal) - parseInt(bVal);
return parseInt(bVal) - parseInt(aVal);
});
for (const card of cards) container.appendChild(card);
}
function previewFilter(filterString) {
var imageContainers = document.querySelectorAll('#primere_visual_modal div.primere-modal-content div.visual-ckpt img');
var filteredCheckpoints = 0;
for (const img_obj of imageContainers) {
var versiontext = img_obj.dataset.ckptver;
var ImageCheckpoint = img_obj.dataset.ckptname + '_' + versiontext;
const parentCard = img_obj.parentElement;
const selectedCard = parentCard?.closest('.visual-ckpt-selected');
if (ImageCheckpoint.toLowerCase().indexOf(filterString.toLowerCase()) >= 0 || selectedCard) {
if (parentCard) parentCard.style.display = '';
filteredCheckpoints++;
} else {
if (parentCard) parentCard.style.display = 'none';
}
setText('div#primere_visual_modal div.modal_header label.ckpt-counter', String(filteredCheckpoints - 1));
}
}
async function createCardElement(checkpoint, container, SelectedModel, ModelType, CategoryName) {
var card = document.createElement("div");
let checkpoint_new = checkpoint.replaceAll('\\', '/');
let dotLastIndex = checkpoint_new.lastIndexOf('.');
if (dotLastIndex > 1) {
var finalName = checkpoint_new.substring(0, dotLastIndex);
} else {
var finalName = checkpoint_new;
}
let pathLastIndex = finalName.lastIndexOf('/');
let ckptName = finalName.substring(pathLastIndex + 1);
let versionWidget = '';
let aesthWidget = '';
let stimeWidget = '';
let symlinkWidget = '';
var path_only = checkpoint.substring(0, checkpoint.indexOf("\\"));
if (ckptName == 'None') return;
var versionString = 'Unknown';
if (CategoryName) {
var titleText = CategoryName + ' checkpoint. Select right version of additional networks.';
versionString = CategoryName;
if (ModelType != 'styles') {
versionWidget = '<div class="ckpt-version ' + versionString + '-ckpt" title="' + titleText + '"></div>';
} else {
var SelModelIndex = SelectedModel.replaceAll(' ', "_");
if (state.RawImageDataResponse.hasOwnProperty(SelectedModel.replaceAll(' ', "_")) === true && checkpoint == SelectedModel) {
var selected_full = state.RawImageDataResponse[SelectedModel.replaceAll(' ', "_")];
var selful_first = selected_full.charAt(0);
var substringStart = 0;
if (selful_first == '\\') substringStart = 1;
path_only = selected_full.substring(substringStart, selected_full.lastIndexOf("\\"));
}
if (path_only == "") path_only = 'Root';
versionWidget = '<div class="ckpt-version" title="Saved prompt on ' + path_only + ' category">&nbsp' + path_only + '&nbsp</div>';
}
}
finalName = finalName.replaceAll(' ', "_");
finalName = finalName.substring(pathLastIndex + 1);
var card_html = '<div class="checkpoint-name background-' + versionString + '">' + ckptName.replaceAll('_', " ") + '</div>' + versionWidget;
var missingimgsrc = prwPath + '/images/missing.jpg';
card.classList.add('visual-ckpt', 'version-' + versionString);
if (state.FileDateResponse.hasOwnProperty(ckptName) === true) {
card.dataset.date = String(state.FileDateResponse[ckptName]);
} else {
card.dataset.date = '0';
}
if (state.FileLinkResponse.hasOwnProperty(ckptName) === true) {
var unetname = state.FileLinkResponse[ckptName];
card.dataset.symlink = unetname;
var unetnameShort = unetname;
if (unetname == 'diffusion_models') unetnameShort = 'DiMo';
if (unetname == 'diffusers') unetnameShort = 'Diff';
symlinkWidget = '<div class="visual-symlink-type" title="Symlinked from: ' + unetname + '">' + unetnameShort + '</div>';
card_html += symlinkWidget;
} else {
card.dataset.symlink = "";
}
if (SelectedModel === checkpoint) card.classList.add('visual-ckpt-selected');
if (state.AscoreDataResponse != null && state.AscoreDataResponse.hasOwnProperty(ckptName) === true) {
var aestString = state.AscoreDataResponse[ckptName];
var aestArray = aestString.split("|");
var aestAVGValue = Math.floor(aestArray[1] / aestArray[0]);
card.dataset.ascore = String(aestAVGValue);
var aeScorePercentLine = Math.floor(((aestAVGValue - state.aeScoreMin) / (state.aeScoreMax - state.aeScoreMin)) * 100);
if (aeScorePercentLine < 0) aeScorePercentLine = 0;
if (aeScorePercentLine > 100) aeScorePercentLine = 100;
aesthWidget = '<div class="visual-aesthetic-score" title="' + aestAVGValue + ' / ' + aeScorePercentLine + '%">' + aestAVGValue + '<span> - ' + aeScorePercentLine + '%</span><hr style="width: ' + aeScorePercentLine + '%"></div>';
card_html += aesthWidget;
} else {
card.dataset.ascore = '0';
}
if (state.STimeDataResponse != null && state.STimeDataResponse.hasOwnProperty(ckptName) === true) {
var stimeString = state.STimeDataResponse[ckptName];
var stimestArray = stimeString.split("|");
var stimestAVGValue = Math.floor(stimestArray[1] / stimestArray[0]);
card.dataset.stime = String(stimestAVGValue);
stimeWidget = '<div class="visual-stime" title="Average sampling time: ' + stimestAVGValue + ' sec">' + stimestAVGValue + 's</div>';
card_html += stimeWidget;
} else {
card.dataset.stime = '0';
}
if (state.SimilarityDataResponse != null && state.SimilarityDataResponse.hasOwnProperty(ckptName) === true) {
card.dataset.similarity = String(Math.floor(state.SimilarityDataResponse[ckptName] * 100));
} else {
card.dataset.similarity = '0';
}
card.dataset.name = ckptName;
if (CategoryName == 'StableCascade') CategoryName = 'Cascade';
card.dataset.version = CategoryName;
card.dataset.path = path_only;
if (state.RawImageDataResponse != null && Object.keys(state.RawImageDataResponse).length > 0) {
if (state.RawImageDataResponse.hasOwnProperty(finalName) === true) {
if (state.PreviewPath == false) {
var imgsrc = 'data:image/jpeg;charset=utf-8;base64, ' + state.RawImageDataResponse[finalName];
card_html += '<img src="data:image/jpeg;charset=utf-8;base64, ' + state.RawImageDataResponse[finalName] + '" title="' + checkpoint_new + '" data-ckptname="' + checkpoint + '" data-ckptver="' + CategoryName + '">';
} else {
var imgsrc = prwPath + '/images/' + state.RawImageDataResponse[finalName] + '?v=' + state.PreviewCacheBust;
card_html += '<img src="' + prwPath + '/images/' + state.RawImageDataResponse[finalName] + '?v=' + state.PreviewCacheBust + '" title="' + checkpoint_new + '" data-ckptname="' + checkpoint + '" data-ckptver="' + CategoryName + '">';
}
card.innerHTML = card_html;
container.appendChild(card);
} else {
var imgsrc = missingimgsrc;
card_html += '<img src="' + missingimgsrc + '" title="' + checkpoint_new + '" data-ckptname="' + checkpoint + '" data-ckptver="' + CategoryName + '">';
card.innerHTML = card_html;
container.appendChild(card);
}
} else {
var imgsrc = missingimgsrc;
card_html += '<img src="' + missingimgsrc + '" title="' + checkpoint_new + '" data-ckptname="' + checkpoint + '" data-ckptver="' + CategoryName + '">';
card.innerHTML = card_html;
container.appendChild(card);
}
const img = new Image();
img.src = imgsrc;
waitForImageToLoad(img).then(() => { return true; });
}
function createSortButtons(buttondata) {
var sort_html = '<label class="sort_by_label"> | Sort by: </label>';
for (const buttonName of buttondata) {
sort_html += '<button type="button" class="preview_sort" data-sortsource="' + buttonName.toLowerCase() + '">' + buttonName + '</button>';
}
sort_html += '<label> | </label><button type="button" class="preview_sort_direction">ASC</button>';
return sort_html;
}
function createTypeMenu(ModelsByVersion, supportedModels, LastCat, LastCatType) {
var version_html = '';
for (const [ver_index, ver_value] of Object.entries(ModelsByVersion)) {
var addWhiteClass = '';
if (supportedModels.includes(ver_index)) {
if (!version_html.includes('data-ckptver="' + ver_index + '"')) {
if (ver_index === LastCat && LastCatType == 'Version') addWhiteClass = ' selected_path';
version_html += '<button type="button" data-ckptver="' + ver_index + '" class="verfilter' + addWhiteClass + '">' + ver_index + '</button>';
}
}
}
version_html += '<label> <hr> </label>';
return version_html;
}
function createPathMenu(AllPath, ShowHidden, LastCat, LastCatType) {
var menu_html = '';
for (var subdir of AllPath) {
var addWhiteClass = "";
let firstletter = subdir.charAt(0);
var subdirName = subdir;
if (firstletter === '.') subdirName = subdir.substring(1);
if (((firstletter === '.' && ShowHidden === true) || firstletter !== '.') && ((subdir.match('^NSFW') && ShowHidden === true) || !subdir.match('^NSFW'))) {
if (subdir === LastCat && LastCatType == 'Subdir') addWhiteClass = ' selected_path';
menu_html += '<button type="button" data-ckptsubdir="' + subdir + '" class="subdirfilter' + addWhiteClass + '">' + subdirName + '</button>';
}
}
return menu_html + '<label> <hr> </label>';
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function waitUntil(condition, time = 100) {
while (!condition()) {
await new Promise((resolve) => setTimeout(resolve, time));
}
}
async function waitUntilEqual(condition1, condition2, time = 100) {
while (condition1 != condition2) {
await new Promise((resolve) => setTimeout(resolve, time));
}
}
async function waitForImageToLoad(imageElement) {
return new Promise(resolve => { imageElement.onload = resolve });
}
export { ModalControl, getCharFactoryModalItems, fetchCharFactoryImages, fetchCharFactorySimilarity };
// Make ModalControl globally accessible for ComfyUI extension module loading
window.__primere = window.__primere || {};
window.__primere.ModalControl = ModalControl;