diff --git a/front_end/css/visual.css b/front_end/css/visual.css index a4c6d34..e88c5db 100644 --- a/front_end/css/visual.css +++ b/front_end/css/visual.css @@ -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; -} \ No newline at end of file +} diff --git a/front_end/frontend_helper.js b/front_end/frontend_helper.js index c114a88..db5dfad 100644 --- a/front_end/frontend_helper.js +++ b/front_end/frontend_helper.js @@ -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"; +} diff --git a/front_end/primere_dashboard.js b/front_end/primere_dashboard.js index 9a86b78..50fc86e 100644 --- a/front_end/primere_dashboard.js +++ b/front_end/primere_dashboard.js @@ -128,5 +128,4 @@ app.registerExtension({ updateSelectWidget(this); }; }, - }); diff --git a/front_end/primere_output.js b/front_end/primere_output.js index 96a9010..daf798c 100644 --- a/front_end/primere_output.js +++ b/front_end/primere_output.js @@ -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 = '
';
+ previewbox.innerHTML = '
';
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 + ']';
diff --git a/front_end/primere_visuals.js b/front_end/primere_visuals.js
index c7a7709..172c0a2 100644
--- a/front_end/primere_visuals.js
+++ b/front_end/primere_visuals.js
@@ -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 = '