From aee1c9842e56cdc537f4aefc0fec7d021c075b8c Mon Sep 17 00:00:00 2001 From: "DESKTOP-TVBJISQ\\Primere" Date: Sun, 21 Jun 2026 16:15:21 +0200 Subject: [PATCH] V2.0.0 - check #5 - FE --- front_end/css/visual.css | 56 ++-- front_end/frontend_helper.js | 215 ++++++++++++++- front_end/primere_dashboard.js | 1 - front_end/primere_output.js | 52 ++-- front_end/primere_visuals.js | 471 +++++++++++++++++++++++++++------ front_end/title_manager.js | 2 +- 6 files changed, 660 insertions(+), 137 deletions(-) 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 = '
X
'; + previewbox.innerHTML = '
X
'; 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 = ''; + + 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} :: :: `; + } + + const subdirContainer = modal.querySelector("div.subdirtab"); + if (subdirContainer) { + let html = ' '; + for (const btn of sortButtonsList) { + const isActive = btn.toLowerCase() === "name"; + html += ``; + } + html += ''; + 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 = `
${item.label}
`; + 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 = ''; @@ -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 = '
' + ckptName.replaceAll('_', " ") + '
' + 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; diff --git a/front_end/title_manager.js b/front_end/title_manager.js index 8d8707d..7e24cbe 100644 --- a/front_end/title_manager.js +++ b/front_end/title_manager.js @@ -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) {