V2.0.0 - check #5 - FE

This commit is contained in:
DESKTOP-TVBJISQ\Primere
2026-06-21 16:15:21 +02:00
parent 45b15ddeb8
commit aee1c9842e
6 changed files with 660 additions and 137 deletions
+28 -28
View File
@@ -7,14 +7,14 @@
@import url("../fonts/FUTURATEE_BOOK/stylesheet.css"); /* FUTURATEE BOOK */
/* Primere css for visual selectors */
#primere_visual_modal,
.primere-modal-base,
#primere_promptsaver_modal {
align-content: start;
text-align: left;
background-color: #484848;
font-family: 'NIMBUSSANSDEE BOLD';
}
#primere_visual_modal .visual_modal_title,
.primere-modal-base .visual_modal_title,
#primere_promptsaver_modal .prompt_modal_title {
text-align: center;
padding: 0px;
@@ -27,7 +27,7 @@
height: 10px;
}
#primere_visual_modal button.modal-closer {
.primere-modal-base button.modal-closer {
text-align: center;
padding: 5px;
margin-top: -1em;
@@ -36,12 +36,12 @@
box-shadow: inset 1px 1px 3px #bdbdbd;
}
#primere_visual_modal .primere-modal-content {
.primere-modal-base .primere-modal-content {
z-index: 10000;
overflow: auto;
height: 76%;
}
#primere_visual_modal .visual-ckpt {
.primere-modal-base .visual-ckpt {
width: min-content;
display: inline-grid;
margin-right: 10px;
@@ -59,7 +59,7 @@
-webkit-border-radius: 5px;
border-radius:5px;
}
#primere_visual_modal .visual-ckpt.visual-ckpt-selected {
.primere-modal-base .visual-ckpt.visual-ckpt-selected {
background: #672727 !important;
color: white !important;
border: #ff5151 solid 2px;
@@ -67,7 +67,7 @@
pointer-events: none;
margin-right: 25px;
}
#primere_visual_modal .visual-ckpt.visual-ckpt-selected:after {
.primere-modal-base .visual-ckpt.visual-ckpt-selected:after {
content: "";
background-color: #b3b3b3;
position: absolute;
@@ -77,11 +77,11 @@
right: -15px;
display: block;
}
#primere_visual_modal .visual-ckpt.visual-ckpt-selected .checkpoint-name {
.primere-modal-base .visual-ckpt.visual-ckpt-selected .checkpoint-name {
background: #672727 !important;
color: white !important;
}
#primere_visual_modal .visual-ckpt:hover {
.primere-modal-base .visual-ckpt:hover {
box-shadow: unset;
left: 2px;
top: 2px;
@@ -90,7 +90,7 @@
color: white;
}
#primere_visual_modal .visual-ckpt img {
.primere-modal-base .visual-ckpt img {
height: 220px;
display: block;
-moz-border-radius-bottomleft: 5px;
@@ -101,7 +101,7 @@
border-bottom-right-radius:5px;
}
#primere_visual_modal .checkpoint-name {
.primere-modal-base .checkpoint-name {
overflow: hidden;
display: inline-block;
width: 100%;
@@ -123,7 +123,7 @@
border-top-right-radius: 5px;
}
#primere_visual_modal .ckpt-version {
.primere-modal-base .ckpt-version {
display: block;
-moz-border-radius-bottomright: 4px;
-webkit-border-bottom-right-radius: 4px;
@@ -147,7 +147,7 @@
text-transform: capitalize;
}
#primere_visual_modal .ckpt-version::before {
.primere-modal-base .ckpt-version::before {
display: block;
margin: 0px;
padding-left: 5px;
@@ -157,7 +157,7 @@
border-bottom-right-radius: 5px;
}
#primere_visual_modal .subdirtab {
.primere-modal-base .subdirtab {
background: #656565;
vertical-align: middle;
display: block;
@@ -166,7 +166,7 @@
border-bottom: #aeaeae solid 1px;
border-top: #aeaeae solid 1px;
}
#primere_visual_modal .subdirtab button {
.primere-modal-base .subdirtab button {
min-width: 80px;
margin-right: 5px;
padding-left: 9px;
@@ -178,33 +178,33 @@
box-shadow: inset 1px 1px 3px #bdbdbd;
}
#primere_visual_modal .subdirtab button.subdirfilter {
.primere-modal-base .subdirtab button.subdirfilter {
background-color: #133b51;
}
#primere_visual_modal .subdirtab button.verfilter {
.primere-modal-base .subdirtab button.verfilter {
background-color: #372222;
}
#primere_visual_modal .subdirtab button.preview_sort {
.primere-modal-base .subdirtab button.preview_sort {
background-color: #24471c;
}
#primere_visual_modal .subdirtab button.filter_clear {
.primere-modal-base .subdirtab button.filter_clear {
background-color: #24471c;
}
#primere_visual_modal .subdirtab input.filter_input {
.primere-modal-base .subdirtab input.filter_input {
background-color: #dbda9c;
}
#primere_visual_modal .subdirtab label {
.primere-modal-base .subdirtab label {
color: var(--input-text);
}
#primere_visual_modal .selected_path {
.primere-modal-base .selected_path {
color: #443535 !important;
background: #bdbdbd !important;
border: white solid 1px !important;
}
#primere_visual_modal .visual-aesthetic-score hr {
.primere-modal-base .visual-aesthetic-score hr {
margin: 0px;
height: 2px;
border-top: 1px solid #da2b2b;
@@ -213,7 +213,7 @@
bottom: 0px;
}
#primere_visual_modal div.visual-symlink-type {
.primere-modal-base div.visual-symlink-type {
display: block;
position: absolute;
background-color: #0f5b77;
@@ -238,7 +238,7 @@
letter-spacing: 1px;
}
#primere_visual_modal .visual-aesthetic-score {
.primere-modal-base .visual-aesthetic-score {
display: block;
position: absolute;
-moz-border-radius-bottomright: 4px;
@@ -261,13 +261,13 @@
white-space: nowrap;
}
#primere_visual_modal .visual-aesthetic-score span {
.primere-modal-base .visual-aesthetic-score span {
position: unset !important;
font-size: 11px;
/* font-family: 'NIMBUSSANSDEE LIGHT'; */
}
#primere_visual_modal .visual-stime {
.primere-modal-base .visual-stime {
display: block;
position: absolute;
padding-top: 0px;
@@ -394,4 +394,4 @@ div#primere_title_hover {
#primere_promptsaver_modal .primere-promptsaver-modal-content.prompt-container button.prompt_saver_button {
background-color: #961c22;
margin-left: 10px;
}
}
+202 -13
View File
@@ -1,32 +1,94 @@
const TOAST_DURATION_MS = 6000;
const BTN_HEIGHT = 32;
const BTN_COLOR = "#771a1a";
const BTN_COLOR_ACTIVE = "#932424";
const BTN_RADIUS = 6;
const BTN_FONT = "bold 15px sans-serif";
export function applyPrimereButtonStyle(widget) {
widget.computeSize = () => [0, BTN_HEIGHT];
export function applyPrimereButtonStyle(widget, options = {}) {
const {
height = 32,
radius = 6,
margin = 15,
bgColor = "#771a1a",
activeColor = "#932424",
textColor = "#dad570",
fontSize = "bold 15px sans-serif",
label = null,
} = options;
widget.computeSize = () => [0, height];
widget.draw = function (ctx, node, widget_width, y) {
ctx.save();
const margin = 15;
ctx.fillStyle = this.clicked ? BTN_COLOR_ACTIVE : BTN_COLOR;
ctx.fillStyle = this.clicked ? activeColor : bgColor;
if (this.clicked) {
this.clicked = false;
node.setDirtyCanvas?.(true);
}
ctx.beginPath();
ctx.roundRect(margin, y, widget_width - margin * 2, BTN_HEIGHT, BTN_RADIUS);
ctx.roundRect(margin, y, widget_width - margin * 2, height, radius);
ctx.fill();
ctx.fillStyle = "#dad570";
ctx.font = BTN_FONT;
ctx.fillStyle = textColor;
ctx.font = fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(this.name, widget_width * 0.5, y + BTN_HEIGHT * 0.5);
ctx.fillText(label ?? this.name, widget_width * 0.5, y + height * 0.5);
ctx.restore();
};
}
export function applyPrimereTwinButtonStyle(widget, options = {}) {
const {
height = 30,
radius = 6,
margin = 15,
gap = 4,
bgColor = "#274e7a",
activeColor = "#366294",
textColor = "#ffffff",
fontSize = "bold 13px sans-serif",
leftLabel = "Left",
rightLabel = "Right",
} = options;
widget.computeSize = () => [0, height + 4];
widget.draw = function (ctx, node, widget_width, y) {
ctx.save();
const halfW = (widget_width - margin * 2 - gap) / 2;
const leftX = margin;
const rightX = leftX + halfW + gap;
ctx.fillStyle = this.__twinClickedSide === "left" ? activeColor : bgColor;
ctx.beginPath();
ctx.roundRect(leftX, y + 2, halfW, height, radius);
ctx.fill();
ctx.fillStyle = textColor;
ctx.font = fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(leftLabel, leftX + halfW / 2, y + 2 + height / 2);
ctx.fillStyle = this.__twinClickedSide === "right" ? activeColor : bgColor;
ctx.beginPath();
ctx.roundRect(rightX, y + 2, halfW, height, radius);
ctx.fill();
ctx.fillStyle = textColor;
ctx.fillText(rightLabel, rightX + halfW / 2, y + 2 + height / 2);
ctx.restore();
if (this.__twinClickedSide) {
this.__twinClickedSide = null;
node.setDirtyCanvas?.(true);
}
};
widget.__twinButtonCfg = { margin, gap };
}
export function getTwinButtonSide(widget, node, posX) {
const cfg = widget.__twinButtonCfg || { margin: 15, gap: 4 };
const halfW = (node.size[0] - cfg.margin * 2 - cfg.gap) / 2;
const relX = posX - cfg.margin;
return relX < halfW + cfg.gap / 2 ? "left" : "right";
}
export function showToast(status, message) {
let container = document.getElementById("primere-toast-container");
if (!container) {
@@ -124,3 +186,130 @@ export function showToast(status, message) {
const timer = setTimeout(dismiss, TOAST_DURATION_MS);
closeBtn.addEventListener("click", dismiss);
}
export function applyPrimereDualButtonStyle(widget, options = {}) {
const {
height = 32,
radius = 6,
margin = 15,
gap = 4,
leftLabel = "Left",
rightLabel = "Right",
leftBg = "#2d6e2d",
leftActive = "#3d8e3d",
rightBg = "#771a1a",
rightActive = "#932424",
textColor = "#ffffff",
fontSize = "bold 13px sans-serif",
} = options;
widget.computeSize = () => [0, height + 4];
widget.draw = function (ctx, node, widget_width, y) {
ctx.save();
const halfW = (widget_width - margin * 2 - gap) / 2;
const leftX = margin;
const rightX = leftX + halfW + gap;
ctx.fillStyle = this.__dualClickedSide === "left" ? leftActive : leftBg;
ctx.beginPath();
ctx.roundRect(leftX, y + 2, halfW, height, radius);
ctx.fill();
ctx.fillStyle = textColor;
ctx.font = fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(leftLabel, leftX + halfW / 2, y + 2 + height / 2);
ctx.fillStyle = this.__dualClickedSide === "right" ? rightActive : rightBg;
ctx.beginPath();
ctx.roundRect(rightX, y + 2, halfW, height, radius);
ctx.fill();
ctx.fillStyle = textColor;
ctx.fillText(rightLabel, rightX + halfW / 2, y + 2 + height / 2);
ctx.restore();
if (this.__dualClickedSide) {
this.__dualClickedSide = null;
node.setDirtyCanvas?.(true);
}
};
widget.__dualButtonCfg = { margin, gap };
}
export function getDualButtonSide(widget, node, posX) {
const cfg = widget.__dualButtonCfg || { margin: 15, gap: 4 };
const halfW = (node.size[0] - cfg.margin * 2 - cfg.gap) / 2;
const relX = posX - cfg.margin;
return relX < halfW + cfg.gap / 2 ? "left" : "right";
}
export function applyPrimereTripleButtonStyle(widget, options = {}) {
const {
height = 30,
radius = 6,
margin = 15,
gap = 4,
bgColor = "#274e7a",
activeColor = "#366294",
textColor = "#ffffff",
fontSize = "bold 13px sans-serif",
leftLabel = "Left",
centerLabel = "Center",
rightLabel = "Right",
} = options;
widget.computeSize = () => [0, height + 4];
widget.draw = function (ctx, node, widget_width, y) {
ctx.save();
const thirds = (widget_width - margin * 2 - gap * 2) / 3;
const leftX = margin;
const centerX = leftX + thirds + gap;
const rightX = centerX + thirds + gap;
ctx.fillStyle = this.__tripleClickedSide === "left" ? activeColor : bgColor;
ctx.beginPath();
ctx.roundRect(leftX, y + 2, thirds, height, radius);
ctx.fill();
ctx.fillStyle = textColor;
ctx.font = fontSize;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(leftLabel, leftX + thirds / 2, y + 2 + height / 2);
ctx.fillStyle = this.__tripleClickedSide === "center" ? activeColor : bgColor;
ctx.beginPath();
ctx.roundRect(centerX, y + 2, thirds, height, radius);
ctx.fill();
ctx.fillStyle = textColor;
ctx.fillText(centerLabel, centerX + thirds / 2, y + 2 + height / 2);
ctx.fillStyle = this.__tripleClickedSide === "right" ? activeColor : bgColor;
ctx.beginPath();
ctx.roundRect(rightX, y + 2, thirds, height, radius);
ctx.fill();
ctx.fillStyle = textColor;
ctx.fillText(rightLabel, rightX + thirds / 2, y + 2 + height / 2);
ctx.restore();
if (this.__tripleClickedSide) {
this.__tripleClickedSide = null;
node.setDirtyCanvas?.(true);
}
};
widget.__tripleButtonCfg = { margin, gap };
}
export function getTripleButtonSide(widget, node, posX) {
const cfg = widget.__tripleButtonCfg || { margin: 15, gap: 4 };
const thirds = (node.size[0] - cfg.margin * 2 - cfg.gap * 2) / 3;
const relX = posX - cfg.margin;
if (relX < thirds + cfg.gap / 2) return "left";
if (relX < thirds * 2 + cfg.gap + cfg.gap / 2) return "center";
return "right";
}
-1
View File
@@ -128,5 +128,4 @@ app.registerExtension({
updateSelectWidget(this);
};
},
});
+32 -20
View File
@@ -7,8 +7,8 @@ let hasShownAlertForUpdatingInt = false;
let currentClass = false;
let outputEventListenerInit = false;
let ImagePath = null;
const realPath = "/extensions/ComfyUI_Primere_Nodes";
const prwPath = "/extensions/ComfyUI_Primere_Nodes";
const realPath = "/extensions/Primere_Comfynodes_Private";
const prwPath = "/extensions/Primere_Comfynodes_Private";
let pendingSaveNode = null;
@@ -18,7 +18,8 @@ const NodenameByType = {
'Lora': 'PrimereVisualLORA',
'Lycoris': 'PrimereVisualLYCORIS',
'Hypernetwork': 'PrimereVisualHypernetwork',
'Embedding': 'PrimereVisualEmbedding'
'Embedding': 'PrimereVisualEmbedding',
'Character Factory': 'PrimereCharacterFactory'
}
const NodesubdirByType = {
@@ -27,7 +28,8 @@ const NodesubdirByType = {
'Lora': 'loras',
'Lycoris': 'lycoris',
'Hypernetwork': 'hypernetworks',
'Embedding': 'embeddings'
'Embedding': 'embeddings',
'Character Factory': 'charfactory'
}
const OutputToNode = ['PrimereAnyOutput', 'PrimereTextOutput', 'PrimereAestheticCKPTScorer', 'PrimereFastSeed'];
@@ -264,7 +266,7 @@ app.registerExtension({
previewbox = document.createElement("div");
previewbox.setAttribute('style', 'display:none;');
previewbox.setAttribute("id", "primere_previewbox");
previewbox.innerHTML = '<div class="preview_closebutton">X</div><img src="' + prwPath + '/images/\missing.jpg" ' + 'class="previewbox_image">';
previewbox.innerHTML = '<div class="preview_closebutton">X</div><img src="' + prwPath + '/images/missing.jpg" ' + 'class="previewbox_image">';
document.body.appendChild(previewbox);
}
@@ -544,26 +546,36 @@ function ButtonLabelCreator(node, url = false) {
}
let prwpathNew = state.selectedTarget.replaceAll('\\', '/');
if (wv.previewTarget === 'Character Factory' && state.targetFileName) {
prwpathNew = state.targetFileName;
}
const dotLastIndex = prwpathNew.lastIndexOf('.');
const finalName = (dotLastIndex > 1 ? prwpathNew.substring(0, dotLastIndex) : prwpathNew).replaceAll(' ', '_');
const imgsrc = prwPath + '/images/' + NodesubdirByType[wv.previewTarget] + '/' + finalName + '.jpg';
fetch(imgsrc, { method: 'HEAD' })
.then(response => {
state.previewExist = response.ok;
state.previewURL = response.ok ? imgsrc : null;
const checkPreviewExists = (name) => {
const body = new FormData();
body.append('previewDir', NodesubdirByType[wv.previewTarget]);
body.append('filename', name);
return api.fetchApi("/primere_check_preview", { method: "POST", body })
.then(r => r.json())
.then(data => data.exists);
};
const prwMode = wv.prwSaveMode.split(' ').map(n => n[0]).join('').toUpperCase();
const imgExistLink = state.previewExist ? ' [' + prwMode + ']' : ' [C]';
const title = '🏙️ Save preview as: [' + state.targetFileName + '.jpg] to [' + wv.previewTarget + '] folder.' + imgExistLink;
applyWidgetValues(node, title, state.targetSelValues);
})
.catch(() => {
state.previewExist = false;
state.previewURL = null;
const title = '🏙️ Save preview as: [' + state.targetFileName + '.jpg] to [' + wv.previewTarget + '] folder. [C]';
applyWidgetValues(node, title, state.targetSelValues);
});
checkPreviewExists(finalName).then(exists => {
state.previewExist = exists;
state.previewURL = exists ? imgsrc : null;
const prwMode = wv.prwSaveMode.split(' ').map(n => n[0]).join('').toUpperCase();
const imgExistLink = exists ? ' [' + prwMode + ']' : ' [C]';
const title = '🏙️ Save preview as: [' + state.targetFileName + '.jpg] to [' + wv.previewTarget + '] folder.' + imgExistLink;
applyWidgetValues(node, title, state.targetSelValues);
}).catch(() => {
state.previewExist = false;
state.previewURL = null;
const title = '🏙️ Save preview as: [' + state.targetFileName + '.jpg] to [' + wv.previewTarget + '] folder. [C]';
applyWidgetValues(node, title, state.targetSelValues);
});
}
} else {
const title = '❌ Required node: [' + NodenameByType[wv.previewTarget] + '] not available in workflow for target: [' + wv.previewTarget + ']';
+397 -74
View File
@@ -1,8 +1,8 @@
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 prwPath = "/extensions/Primere_Comfynodes_Private";
const validClasses = ['PrimereVisualCKPT', 'PrimereVisualLORA', 'PrimereVisualEmbedding', 'PrimereVisualHypernetwork', 'PrimereVisualStyle', 'PrimereVisualLYCORIS', 'PrimereVisualPromptOrganizerCSV', 'PrimereCharacterFactory'];
const stackedClasses = ['PrimereVisualLORA', 'PrimereVisualEmbedding', 'PrimereVisualHypernetwork', 'PrimereVisualLYCORIS'];
const state = {
@@ -122,7 +122,7 @@ function getFilterInput() {
}
function clearModalCardsKeepingSelected() {
const cards = document.querySelectorAll('div.primere-modal-content div.visual-ckpt');
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();
}
@@ -163,57 +163,83 @@ app.registerExtension({
function ModalHandler() {
state.eventListenerInit = true;
document.body.addEventListener("click", async function (event) {
let modal = null;
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) {
modal = document.getElementById("primere_visual_modal");
if (modal) modal.setAttribute('style', 'display: none; width: 80%; height: 70%;');
const visModal = document.getElementById("primere_visual_modal");
const cfModal = document.getElementById("primere_charfactory_modal");
var lastDirValue = 'All';
if (state.lastDirObject.hasOwnProperty(state.currentClass) === true) lastDirValue = state.lastDirObject[state.currentClass];
if (visModal && visModal.style.display !== 'none') {
visModal.setAttribute('style', 'display: none; width: 80%; height: 70%;');
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);
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) {
var ckptName = selectedImage.dataset.ckptname;
modal = document.getElementById("primere_visual_modal");
if (modal) modal.setAttribute('style', 'display: none; width: 80%; height: 70%;');
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];
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 (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 (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);
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;
@@ -224,6 +250,8 @@ function ModalHandler() {
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;
@@ -286,6 +314,8 @@ function ModalHandler() {
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;
@@ -320,23 +350,47 @@ function ModalHandler() {
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++;
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));
}
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);
@@ -348,6 +402,16 @@ function ModalHandler() {
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';
@@ -364,7 +428,14 @@ function ModalHandler() {
document.body.addEventListener("keyup", function (event) {
const filterInput = event.target.closest('div.subdirtab input');
if (filterInput) previewFilter(filterInput.value);
if (filterInput) {
const cfModal = document.getElementById("primere_charfactory_modal");
if (cfModal && cfModal.style.display !== 'none') {
cfPreviewFilter(filterInput.value);
} else {
previewFilter(filterInput.value);
}
}
});
}
@@ -377,8 +448,17 @@ class ModalControl {
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;
@@ -423,6 +503,19 @@ class ModalControl {
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);
@@ -431,31 +524,32 @@ class ModalControl {
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;
}
return null;
}
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';
}
// 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);
}
}
return null;
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);
}
}
}
}
@@ -463,6 +557,229 @@ class ModalControl {
}
}
// ── 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/Primere_Comfynodes_Private/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;
@@ -471,7 +788,7 @@ async function setup_visual_modal(combo_name, AllModels, ShowHidden, SelectedMod
modal = document.getElementById("primere_visual_modal");
if (!modal) {
modal = document.createElement("div");
modal.classList.add("comfy-modal");
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>';
@@ -727,7 +1044,7 @@ function previewSorter(operator, sortType) {
}
function previewFilter(filterString) {
var imageContainers = document.querySelectorAll('div.primere-modal-content div.visual-ckpt img');
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;
@@ -789,7 +1106,7 @@ async function createCardElement(checkpoint, container, SelectedModel, ModelType
finalName = finalName.substring(pathLastIndex + 1);
var card_html = '<div class="checkpoint-name background-' + versionString + '">' + ckptName.replaceAll('_', " ") + '</div>' + versionWidget;
var missingimgsrc = prwPath + '/images/\\missing.jpg';
var missingimgsrc = prwPath + '/images/missing.jpg';
card.classList.add('visual-ckpt', 'version-' + versionString);
if (state.FileDateResponse.hasOwnProperty(ckptName) === true) {
@@ -935,3 +1252,9 @@ async function waitUntilEqual(condition1, condition2, time = 100) {
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;
+1 -1
View File
@@ -272,7 +272,7 @@ function buildPreviewHelpText(sectionName) {
function buildPreviewImageSource(sectionName) {
if (!sectionName) return null;
return `/extensions/ComfyUI_Primere_Nodes/images/sections_titles/${encodeURIComponent(sectionName)}.jpg?t=${Date.now()}`;
return `/extensions/Primere_Comfynodes_Private/images/sections_titles/${encodeURIComponent(sectionName)}.jpg?t=${Date.now()}`;
}
function resolvePreviewImageSource(sectionName) {