Files

649 lines
26 KiB
JavaScript

import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { ComfyWidgets } from "/scripts/widgets.js";
import { applyPrimereButtonStyle, showToast } from "./frontend_helper.js";
let hasShownAlertForUpdatingInt = false;
let currentClass = false;
let outputEventListenerInit = false;
let ImagePath = null;
const realPath = "/extensions/comfyui_primere_nodes";
const prwPath = "/extensions/comfyui_primere_nodes";
let pendingSaveNode = null;
const NodenameByType = {
'Checkpoint': 'PrimereVisualCKPT',
'CSV Prompt': 'PrimereVisualStyle',
'Lora': 'PrimereVisualLORA',
'Lycoris': 'PrimereVisualLYCORIS',
'Hypernetwork': 'PrimereVisualHypernetwork',
'Embedding': 'PrimereVisualEmbedding',
'Character Factory': 'PrimereCharacterFactory'
}
const NodesubdirByType = {
'Checkpoint': 'checkpoints',
'CSV Prompt': 'styles',
'Lora': 'loras',
'Lycoris': 'lycoris',
'Hypernetwork': 'hypernetworks',
'Embedding': 'embeddings',
'Character Factory': 'charfactory'
}
const OutputToNode = ['PrimereAnyOutput', 'PrimereTextOutput', 'PrimereAestheticCKPTScorer', 'PrimereFastSeed'];
// ── Per-node state ────────────────────────────────────────────────────────────
function initNodeState() {
return {
workflowData: {},
origSizeString: '',
saveIsValid: false,
targetFileName: null,
targetSelValues: ['select target...'],
selectedTarget: null,
previewExist: false,
previewURL: null,
};
}
function ensureNodeState(node) {
if (!node.psState) node.psState = initNodeState();
return node.psState;
}
function getWidgetValues(node) {
const state = ensureNodeState(node);
const vals = {
previewTarget: 'Checkpoint',
saveMode: true,
imgType: 'jpeg',
maxSide: -1,
targetQuality: 95,
prwSaveMode: 'Overwrite',
};
for (const w of node.widgets) {
switch (w.name) {
case 'preview_target': vals.previewTarget = w.value; break;
case 'image_save_as': vals.saveMode = w.value; break;
case 'image_type': vals.imgType = w.value; break;
case 'image_resize': vals.maxSide = w.value; break;
case 'image_quality': vals.targetQuality = w.value; break;
case 'preview_save_mode': vals.prwSaveMode = w.value; break;
case 'target_selection': state.selectedTarget = w.value; break;
}
}
return vals;
}
// ── Node registration ─────────────────────────────────────────────────────────
app.registerExtension({
name: "Primere.PrimereOutputs",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (OutputToNode.includes(nodeData.name) === true) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
this.showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget;
this.showValueWidget.inputEl.readOnly = true;
this.showValueWidget.serializeValue = async (node, index) => {
if (typeof node.widgets_values != "undefined") {
node.widgets_values[index] = "";
return "";
}
};
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]);
this.showValueWidget.value = message.text[0];
app.canvas.setDirty(true);
};
}
if (nodeData.name === "PrimerePreviewImage") {
if (nodeData.input.hasOwnProperty('hidden') === true) {
ImagePath = nodeData.input.hidden['image_path'][0];
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
new MiniPreviewControl(this);
PrimerePreviewSaverWidget.apply(this, [this, 'PrimerePreviewSaver']);
normalizeTargetSelectionWidget(this, true);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure ? onConfigure.apply(this, arguments) : undefined;
normalizeTargetSelectionWidget(this);
};
}
},
});
class MiniPreviewControl {
constructor(node) {
node.onMouseDown = function(event, pos, graphcanvas) {
if (event.type == 'pointerdown') {
if (pos[1] >= 212 && pos[1] <= 265) {
showPreviewIfExist(node, event.clientX, event.clientY);
} else {
checkPreviewExample();
}
}
};
node.onMouseMove = function(event, pos, graphcanvas) {
if (event.type == 'pointermove') {
const previewBox = document.querySelector('div#primere_previewbox');
const btn = node.widgets.find(w => w.type === 'button');
if (!previewBox || previewBox.style.display === 'none') {
if (pos[1] >= btn.last_y && pos[1] <= btn.last_y + 30 && pos[0] >= 14 && pos[0] <= 160) {
showPreviewIfExist(node, event.clientX, event.clientY);
} else {
checkPreviewExample();
}
} else {
if (pos[1] <= btn.last_y || pos[1] >= btn.last_y + 30 || pos[0] <= 14 || pos[0] >= 160) {
checkPreviewExample();
}
}
}
};
}
}
function showPreviewIfExist(node, coordX, coordY) {
const state = ensureNodeState(node);
const wv = getWidgetValues(node);
if (state.saveIsValid && wv.saveMode && state.previewExist) {
const previewBox = document.querySelector('div#primere_previewbox');
const previewImage = document.querySelector('div#primere_previewbox img.previewbox_image');
if (!previewBox || !previewImage) return;
previewBox.style.top = coordY + 'px';
previewBox.style.left = coordX + 'px';
previewImage.src = state.previewURL;
previewBox.style.display = 'block';
}
}
function checkPreviewExample() {
const previewBox = document.querySelector('div#primere_previewbox');
if (previewBox && previewBox.style.display !== 'none') {
previewBox.style.display = 'none';
}
}
function normalizeTargetSelectionWidget(node, forceDefault = false) {
const state = ensureNodeState(node);
const targetWidget = node.widgets?.find(w => w.name === 'target_selection');
if (!targetWidget) return;
const values = Array.isArray(targetWidget.options?.values) ? targetWidget.options.values : ['select target...'];
const hasOnlyDefaultValue = values.length === 1 && values[0] === 'select target...';
const shouldReset = forceDefault || hasOnlyDefaultValue || !values.includes(targetWidget.value);
if (shouldReset && values.length > 0) {
targetWidget.value = values[0];
state.selectedTarget = values[0];
}
}
async function PrimerePreviewSaverWidget(node, inputName) {
if (inputName == 'PrimerePreviewSaver') {
node.name = inputName;
ensureNodeState(node);
const widget = {
type: "preview_saver_widget",
name: `w${inputName}`,
callback: () => {},
};
node.onWidgetChanged = function (name, value, old_value) {
ButtonLabelCreator(node);
return false;
};
node.addWidget("combo", "target_selection", 'select target...', () => {}, {
values: ["select target..."],
});
normalizeTargetSelectionWidget(node, true);
const saveBtn = node.addWidget("button", '⛔ Image not available for save. Please load one.', null, () => {
const state = ensureNodeState(node);
if (state.saveIsValid === true) {
if (typeof node['imgs'] != "undefined") {
pendingSaveNode = node;
node.PreviewSaver = new PreviewSaver(node);
} else {
state.saveIsValid = false;
const errTitle = '⛔ Image not available for save. Please load one.';
applyWidgetValues(node, errTitle, state.targetSelValues);
showToast("error", 'Current settings is invalid to save image. ERROR: ' + errTitle);
}
} else {
const btn = node.widgets.find(w => w.type === 'button');
showToast("error", 'Current settings is invalid to save image. ERROR: ' + (btn ? btn.name : 'Unknown'));
}
});
applyPrimereButtonStyle(saveBtn);
return { widget: widget };
}
}
app.registerExtension({
name: "Primere.PrimerePreviewImage",
async init(app) {
function PreviewHandler(app) {
outputEventListenerInit = true;
let head = document.getElementsByTagName('HEAD')[0];
let js1 = document.createElement("script");
js1.src = realPath + "/vendor/LoadImage/load-image.js";
head.appendChild(js1);
let js2 = document.createElement("script");
js2.src = realPath + "/vendor/LoadImage/load-image-scale.js";
head.appendChild(js2);
let link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = realPath + '/css/visual.css';
head.appendChild(link);
let previewbox = document.getElementById("primere_previewbox");
if (!previewbox) {
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">';
document.body.appendChild(previewbox);
}
document.addEventListener('click', function(e) {
if (e.target.closest('div#primere_previewbox div.preview_closebutton')) {
checkPreviewExample();
return;
}
if (e.target.closest('body')) {
checkPreviewExample();
}
});
}
if (outputEventListenerInit == false) {
PreviewHandler(app);
}
},
});
// ── Event: node executed ──────────────────────────────────────────────────────
api.addEventListener("getVisualTargets", VisualDataReceiver);
function VisualDataReceiver(event) {
const data = event.detail;
let targetNode = null;
const nodeId = data['node_id'];
if (nodeId && app.graph) {
targetNode = app.graph.getNodeById(parseInt(nodeId));
}
if (!targetNode && app.graph && app.graph._nodes) {
targetNode = app.graph._nodes.find(n => n.type === 'PrimerePreviewImage');
}
if (!targetNode) return;
const state = ensureNodeState(targetNode);
state.workflowData = data;
if (data['ImagePath']) ImagePath = data['ImagePath'];
const imgs = data['SaveImages'];
if (!imgs || imgs.length === 0) return;
const newLoadedURL = window.location.origin + '/view?filename=' + imgs[0]['filename'] + '&type=' + imgs[0]['type'] + '&subfolder=' + imgs[0]['subfolder'];
UrlExists(newLoadedURL, function (status) {
if (status === 200) {
ButtonLabelCreator(targetNode, newLoadedURL);
} else {
state.origSizeString = '[Unknown dimensions]';
console.log('new image loaded - ERROR status ' + status + ': - ' + newLoadedURL);
}
});
}
// ── Utility ───────────────────────────────────────────────────────────────────
function UrlExists(url, cb) {
fetch(url, {
method: 'HEAD',
}).then((response) => {
if (typeof cb === 'function') {
cb.apply(this, [response.status]);
}
}).catch(() => {
if (typeof cb === 'function') {
cb.apply(this, [0]);
}
});
}
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[arr.length - 1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type:mime});
}
function downloadImage(url, extension, PreviewTarget) {
fetch(url, {
mode : 'no-cors',
})
.then(response => response.blob())
.then(blob => {
let blobUrl = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.download = PreviewTarget + '.' + extension;
a.href = blobUrl;
document.body.appendChild(a);
a.click();
a.remove();
});
}
// ── PreviewSaver ──────────────────────────────────────────────────────────────
class PreviewSaver {
constructor(node) {
const state = ensureNodeState(node);
const wv = getWidgetValues(node);
let maxWidth = null;
let maxHeight = null;
if (wv.saveMode === true) {
maxWidth = 400;
maxHeight = 220;
}
let imgMime = "image/jpeg";
let extension = 'jpg';
if (wv.saveMode === false) {
if (wv.imgType === 'jpeg') {
imgMime = "image/jpeg";
extension = 'jpg';
} else if (wv.imgType === 'png') {
imgMime = "image/png";
extension = 'png';
} else if (wv.imgType === 'webp') {
imgMime = "image/webp";
extension = 'webp';
}
}
if (wv.maxSide >= 64 && wv.saveMode === false) {
maxWidth = wv.maxSide;
maxHeight = wv.maxSide;
}
const sizeStringFN = wv.maxSide >= 64 ? wv.maxSide + 'px_' : '';
const imageSource = node['imgs'][0]['src'];
const imageName = node['images'][0]['filename'];
let saveImageName = 'PreviewImage_' + sizeStringFN + '_QTY' + wv.targetQuality + '_' + (Math.random() + 1).toString(36).substring(5);
if (state.targetFileName !== null) {
saveImageName = state.targetFileName;
}
const renderToCanvas = (blob) => new Promise((resolve, reject) => {
const blobUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(blobUrl);
const canvas = document.createElement('canvas');
let w = img.naturalWidth;
let h = img.naturalHeight;
if (maxWidth && w > maxWidth) { h = Math.round(h * maxWidth / w); w = maxWidth; }
if (maxHeight && h > maxHeight) { w = Math.round(w * maxHeight / h); h = maxHeight; }
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, w, h);
resolve(canvas);
};
img.onerror = () => {
URL.revokeObjectURL(blobUrl);
reject(new Error('Failed to decode image: ' + imageName));
};
img.src = blobUrl;
});
fetch(imageSource)
.then((res) => res.blob())
.then((blob) => renderToCanvas(blob))
.then((canvas) => {
const resampledImage = canvas.toDataURL(imgMime, wv.targetQuality / 100);
if (wv.saveMode === false) {
downloadImage(resampledImage, extension, saveImageName);
} else {
sendPOSTmessage(JSON.stringify({
"PreviewTarget": wv.previewTarget,
"PreviewTargetOriginal": state.selectedTarget,
"extension": extension,
"ImageName": imageName,
"ImagePath": ImagePath,
"SaveImageName": saveImageName,
"maxWidth": canvas.width,
"maxHeight": canvas.height,
"TargetQuality": wv.targetQuality,
"PrwSaveMode": wv.prwSaveMode,
}));
}
})
.catch((err) => {
showToast("error", 'Failed to load source image: ' + err.message);
});
}
}
// ── Button label / target list ────────────────────────────────────────────────
function TargetListCreator(node) {
const state = ensureNodeState(node);
const wv = getWidgetValues(node);
const workflowData = state.workflowData;
const origKey = NodenameByType[wv.previewTarget] + '_ORIGINAL';
if (workflowData[origKey] !== undefined) {
state.targetSelValues = workflowData[origKey];
if (state.targetSelValues.length === 0) {
state.saveIsValid = false;
state.targetSelValues = ['ERROR: Cannot list target for ' + wv.previewTarget];
}
} else {
state.saveIsValid = false;
state.targetSelValues = ['ERROR: Cannot list target for ' + wv.previewTarget];
}
return state.targetSelValues;
}
function ButtonLabelCreator(node, url = false) {
const state = ensureNodeState(node);
state.previewExist = false;
state.previewURL = null;
const wv = getWidgetValues(node);
state.targetSelValues = TargetListCreator(node);
if (typeof state.targetSelValues === "object") {
let targetIndexChanged = 0;
if (state.targetSelValues.includes(state.selectedTarget)) {
targetIndexChanged = state.targetSelValues.indexOf(state.selectedTarget);
}
state.selectedTarget = state.targetSelValues[targetIndexChanged];
}
const imgTypeString = wv.imgType.toUpperCase() + ' format';
const imgSizeString = wv.maxSide < 64 ? "at original size" : "resized to " + wv.maxSide + 'px';
const targetQuality = wv.imgType === 'png' ? 100 : wv.targetQuality;
state.targetFileName = null;
state.saveIsValid = false;
const workflowData = state.workflowData;
if (Object.keys(workflowData).length < 1) {
if (wv.saveMode === true) {
applyWidgetValues(node, state.selectedTarget, state.targetSelValues);
} else {
state.saveIsValid = true;
if (url !== false) {
getMeta(url).then(img => {
if (img) state.origSizeString = '[' + img.naturalHeight + ' X ' + img.naturalWidth + ']';
const title = '💾 Save image as ' + imgTypeString + ' | ' + state.origSizeString + ' ' + imgSizeString + ' | QTY: ' + targetQuality + '%';
applyWidgetValues(node, title, state.targetSelValues);
});
} else {
applyWidgetValues(node, '⛔ Image not available for save. Please load one.', state.targetSelValues);
}
}
} else {
if (wv.saveMode === true) {
if (url !== false) {
getMeta(url).then(img => {
if (img) state.origSizeString = '[' + img.naturalHeight + ' X ' + img.naturalWidth + ']';
});
}
const nodeName = NodenameByType[wv.previewTarget];
if (workflowData[nodeName] !== undefined && state.selectedTarget !== undefined) {
if (workflowData[nodeName].length < 1) {
const title = '❌ No resource selected for preview target: [' + wv.previewTarget + ']';
applyWidgetValues(node, title, state.targetSelValues);
} else {
state.saveIsValid = true;
const origKey = nodeName + '_ORIGINAL';
const targetIndex = workflowData[origKey].indexOf(state.selectedTarget);
if (targetIndex > -1) {
state.targetFileName = workflowData[nodeName][targetIndex];
}
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';
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);
};
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 + ']';
applyWidgetValues(node, title, state.targetSelValues);
}
} else {
state.saveIsValid = true;
if (url !== false) {
getMeta(url).then(img => {
if (img) state.origSizeString = '[' + img.naturalHeight + ' X ' + img.naturalWidth + ']';
const title = '💾 Save image as ' + imgTypeString + ' | ' + state.origSizeString + ' ' + imgSizeString + ' | QTY: ' + targetQuality + '%';
applyWidgetValues(node, title, state.targetSelValues);
});
} else {
const title = '💾 Save image as ' + imgTypeString + ' | ' + state.origSizeString + ' ' + imgSizeString + ' | QTY: ' + targetQuality + '%';
applyWidgetValues(node, title, state.targetSelValues);
}
}
}
}
function applyWidgetValues(node, buttontitle, targetSelValues) {
const state = ensureNodeState(node);
for (const w of node.widgets) {
if (w.type == 'button') {
w.name = buttontitle;
}
if (w.name == 'target_selection') {
w.options.values = targetSelValues;
if (targetSelValues.length > 0) {
let targetIndexChanged = 0;
if (targetSelValues.includes(state.selectedTarget)) {
targetIndexChanged = targetSelValues.indexOf(state.selectedTarget);
}
w.value = targetSelValues[targetIndexChanged];
state.selectedTarget = w.value;
}
}
}
normalizeTargetSelectionWidget(node);
app.canvas.setDirty(true);
}
// ── POST save ─────────────────────────────────────────────────────────────────
function sendPOSTmessage(message) {
const body = new FormData();
body.append('previewdata', message);
api.fetchApi("/primere_preview_post", {method: "POST", body,});
}
api.addEventListener("PreviewSaveResponse", PreviewSaveResponse);
function PreviewSaveResponse(event) {
showToast("success", event.detail);
if (pendingSaveNode) {
ButtonLabelCreator(pendingSaveNode);
pendingSaveNode = null;
}
}
// ── getMeta ───────────────────────────────────────────────────────────────────
const getMeta = (url) => new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = url;
}).catch(function() {
return false;
});