V 2.0.0 - Universal api #67 - js helper and toasts
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
const TOAST_DURATION_MS = 10000;
|
||||
|
||||
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];
|
||||
widget.draw = function (ctx, node, widget_width, y) {
|
||||
ctx.save();
|
||||
const margin = 15;
|
||||
ctx.fillStyle = this.clicked ? BTN_COLOR_ACTIVE : BTN_COLOR;
|
||||
if (this.clicked) {
|
||||
this.clicked = false;
|
||||
node.setDirtyCanvas?.(true);
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(margin, y, widget_width - margin * 2, BTN_HEIGHT, BTN_RADIUS);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#dad570";
|
||||
ctx.font = BTN_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(this.name, widget_width * 0.5, y + BTN_HEIGHT * 0.5);
|
||||
ctx.restore();
|
||||
};
|
||||
}
|
||||
|
||||
export function showToast(status, message) {
|
||||
let container = document.getElementById("primere-toast-container");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = "primere-toast-container";
|
||||
Object.assign(container.style, {
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: "99999",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const isSuccess = status === "success";
|
||||
const accentColor = isSuccess ? "#4caf50" : "#f44336";
|
||||
const icon = isSuccess ? "✔" : "✖";
|
||||
|
||||
const toast = document.createElement("div");
|
||||
Object.assign(toast.style, {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
background: "#1e1e1e",
|
||||
border: `1px solid ${accentColor}`,
|
||||
borderLeft: `4px solid ${accentColor}`,
|
||||
borderRadius: "6px",
|
||||
padding: "10px 14px 10px 12px",
|
||||
minWidth: "280px",
|
||||
maxWidth: "440px",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: "10px",
|
||||
pointerEvents: "all",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.6)",
|
||||
opacity: "0",
|
||||
transform: "translateY(-12px)",
|
||||
transition: "opacity 0.25s ease, transform 0.25s ease",
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: "15px",
|
||||
color: "#e0e0e0",
|
||||
cursor: "default",
|
||||
});
|
||||
|
||||
const iconEl = document.createElement("span");
|
||||
iconEl.textContent = icon;
|
||||
Object.assign(iconEl.style, { color: accentColor, fontSize: "15px", flexShrink: "0", lineHeight: "1.5" });
|
||||
|
||||
const textEl = document.createElement("span");
|
||||
textEl.textContent = message;
|
||||
Object.assign(textEl.style, { flex: "1", lineHeight: "1.5", wordBreak: "break-all" });
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.textContent = "✕";
|
||||
Object.assign(closeBtn.style, {
|
||||
background: "none", border: "none", color: "#888", cursor: "pointer",
|
||||
fontSize: "13px", padding: "0 0 0 6px", flexShrink: "0", lineHeight: "1.5",
|
||||
});
|
||||
closeBtn.addEventListener("mouseover", () => { closeBtn.style.color = "#ccc"; });
|
||||
closeBtn.addEventListener("mouseout", () => { closeBtn.style.color = "#888"; });
|
||||
|
||||
const progress = document.createElement("div");
|
||||
Object.assign(progress.style, {
|
||||
position: "absolute", bottom: "0", left: "0",
|
||||
height: "3px", width: "100%",
|
||||
background: accentColor,
|
||||
transformOrigin: "left",
|
||||
transform: "scaleX(1)",
|
||||
transition: `transform ${TOAST_DURATION_MS / 1000}s linear`,
|
||||
});
|
||||
|
||||
toast.appendChild(iconEl);
|
||||
toast.appendChild(textEl);
|
||||
toast.appendChild(closeBtn);
|
||||
toast.appendChild(progress);
|
||||
container.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateY(0)";
|
||||
progress.style.transform = "scaleX(0)";
|
||||
}));
|
||||
|
||||
function dismiss() {
|
||||
clearTimeout(timer);
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateY(-12px)";
|
||||
setTimeout(() => toast.remove(), 280);
|
||||
}
|
||||
|
||||
const timer = setTimeout(dismiss, TOAST_DURATION_MS);
|
||||
closeBtn.addEventListener("click", dismiss);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -183,39 +184,15 @@ async function PrimerePreviewSaverWidget(node, inputName) {
|
||||
state.saveIsValid = false;
|
||||
const errTitle = '⛔ Image not available for save. Please load one.';
|
||||
applyWidgetValues(node, errTitle, state.targetSelValues);
|
||||
alert('Current settings is invalid to save image.\n\nERROR: ' + errTitle);
|
||||
showToast("error", 'Current settings is invalid to save image. ERROR: ' + errTitle);
|
||||
}
|
||||
} else {
|
||||
const btn = node.widgets.find(w => w.type === 'button');
|
||||
alert('Current settings is invalid to save image.\n\nERROR: ' + (btn ? btn.name : 'Unknown'));
|
||||
showToast("error", 'Current settings is invalid to save image. ERROR: ' + (btn ? btn.name : 'Unknown'));
|
||||
}
|
||||
});
|
||||
|
||||
const BTN_HEIGHT = 32;
|
||||
const BTN_COLOR = "#771a1a";
|
||||
const BTN_COLOR_ACTIVE = "#932424";
|
||||
const BTN_RADIUS = 6;
|
||||
const BTN_FONT = "bold 15px sans-serif";
|
||||
|
||||
saveBtn.computeSize = () => [0, BTN_HEIGHT];
|
||||
saveBtn.draw = function (ctx, node, widget_width, y) {
|
||||
ctx.save();
|
||||
const margin = 15;
|
||||
ctx.fillStyle = this.clicked ? BTN_COLOR_ACTIVE : BTN_COLOR;
|
||||
if (this.clicked) {
|
||||
this.clicked = false;
|
||||
node.setDirtyCanvas?.(true);
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(margin, y, widget_width - margin * 2, BTN_HEIGHT, BTN_RADIUS);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#dad570";
|
||||
ctx.font = BTN_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(this.name, widget_width * 0.5, y + BTN_HEIGHT * 0.5);
|
||||
ctx.restore();
|
||||
};
|
||||
applyPrimereButtonStyle(saveBtn);
|
||||
|
||||
return { widget: widget };
|
||||
}
|
||||
@@ -437,7 +414,7 @@ class PreviewSaver {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
alert('Failed to load source image: ' + err.message);
|
||||
showToast("error", 'Failed to load source image: ' + err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -596,7 +573,7 @@ function sendPOSTmessage(message) {
|
||||
|
||||
api.addEventListener("PreviewSaveResponse", PreviewSaveResponse);
|
||||
function PreviewSaveResponse(event) {
|
||||
alert(event.detail);
|
||||
showToast("success", event.detail);
|
||||
if (pendingSaveNode) {
|
||||
ButtonLabelCreator(pendingSaveNode);
|
||||
pendingSaveNode = null;
|
||||
|
||||
+56
-183
@@ -1,4 +1,5 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { applyPrimereButtonStyle, showToast } from "./frontend_helper.js";
|
||||
|
||||
const TARGET_NODE_NAME = "PrimereApiProcessor";
|
||||
const SCHEMA_URL = new URL("/extensions/ComfyUI_Primere_Nodes/api_schemas.json", import.meta.url).href;
|
||||
@@ -6,106 +7,6 @@ const SCHEMA_URL = new URL("/extensions/ComfyUI_Primere_Nodes/api_schemas.json",
|
||||
let schemaCache = null;
|
||||
let schemaPromise = null;
|
||||
|
||||
const TOAST_DURATION_MS = 10000; // toast auto-dismiss delay in milliseconds
|
||||
|
||||
function showToast(status, message) {
|
||||
let container = document.getElementById("primere-toast-container");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = "primere-toast-container";
|
||||
Object.assign(container.style, {
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: "99999",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const isSuccess = status === "success";
|
||||
const accentColor = isSuccess ? "#4caf50" : "#f44336";
|
||||
const icon = isSuccess ? "✔" : "✖";
|
||||
|
||||
const toast = document.createElement("div");
|
||||
Object.assign(toast.style, {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
background: "#1e1e1e",
|
||||
border: `1px solid ${accentColor}`,
|
||||
borderLeft: `4px solid ${accentColor}`,
|
||||
borderRadius: "6px",
|
||||
padding: "10px 14px 10px 12px",
|
||||
minWidth: "280px",
|
||||
maxWidth: "440px",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: "10px",
|
||||
pointerEvents: "all",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.6)",
|
||||
opacity: "0",
|
||||
transform: "translateY(-12px)",
|
||||
transition: "opacity 0.25s ease, transform 0.25s ease",
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: "15px",
|
||||
color: "#e0e0e0",
|
||||
cursor: "default",
|
||||
});
|
||||
|
||||
const iconEl = document.createElement("span");
|
||||
iconEl.textContent = icon;
|
||||
Object.assign(iconEl.style, { color: accentColor, fontSize: "15px", flexShrink: "0", lineHeight: "1.5" });
|
||||
|
||||
const textEl = document.createElement("span");
|
||||
textEl.textContent = message;
|
||||
Object.assign(textEl.style, { flex: "1", lineHeight: "1.5", wordBreak: "break-all" });
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.textContent = "✕";
|
||||
Object.assign(closeBtn.style, {
|
||||
background: "none", border: "none", color: "#888", cursor: "pointer",
|
||||
fontSize: "13px", padding: "0 0 0 6px", flexShrink: "0", lineHeight: "1.5",
|
||||
});
|
||||
closeBtn.addEventListener("mouseover", () => { closeBtn.style.color = "#ccc"; });
|
||||
closeBtn.addEventListener("mouseout", () => { closeBtn.style.color = "#888"; });
|
||||
|
||||
const progress = document.createElement("div");
|
||||
Object.assign(progress.style, {
|
||||
position: "absolute", bottom: "0", left: "0",
|
||||
height: "3px", width: "100%",
|
||||
background: accentColor,
|
||||
transformOrigin: "left",
|
||||
transform: "scaleX(1)",
|
||||
transition: `transform ${TOAST_DURATION_MS / 1000}s linear`,
|
||||
});
|
||||
|
||||
toast.appendChild(iconEl);
|
||||
toast.appendChild(textEl);
|
||||
toast.appendChild(closeBtn);
|
||||
toast.appendChild(progress);
|
||||
container.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateY(0)";
|
||||
progress.style.transform = "scaleX(0)";
|
||||
}));
|
||||
|
||||
function dismiss() {
|
||||
clearTimeout(timer);
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateY(-12px)";
|
||||
setTimeout(() => toast.remove(), 280);
|
||||
}
|
||||
|
||||
const timer = setTimeout(dismiss, TOAST_DURATION_MS);
|
||||
closeBtn.addEventListener("click", dismiss);
|
||||
}
|
||||
|
||||
async function loadSchemas() {
|
||||
if (schemaCache) {
|
||||
return schemaCache;
|
||||
@@ -394,92 +295,64 @@ async function initializeUniApiNode(node) {
|
||||
node.__primereOptionalInputNames = new Set();
|
||||
}
|
||||
|
||||
if (!node.__primereUniApiWidgetHooked) {
|
||||
node.__primereUniApiWidgetHooked = true;
|
||||
|
||||
const refreshBtn = node.addWidget("button", "↺ Reload API Schema", null, async () => {
|
||||
schemaCache = null;
|
||||
schemaPromise = null;
|
||||
try {
|
||||
const freshRegistry = await loadSchemas();
|
||||
if (!freshRegistry || Object.keys(freshRegistry).length === 0) {
|
||||
showToast("error", "API Schema reload failed. The schema file returned empty or could not be parsed. Check front_end/api_schemas.json for syntax errors.");
|
||||
return;
|
||||
}
|
||||
updateServiceWidget(node, freshRegistry);
|
||||
updateParameterWidgets(node, freshRegistry);
|
||||
const providerCount = Object.keys(freshRegistry).length;
|
||||
const serviceCount = Object.values(freshRegistry).reduce((sum, p) => sum + Object.keys(p).length, 0);
|
||||
showToast("success", `API Schema reloaded successfully. ${providerCount} provider(s), ${serviceCount} service(s) loaded.`);
|
||||
} catch (error) {
|
||||
showToast("error", `API Schema reload failed. ${error.message}`);
|
||||
}
|
||||
});
|
||||
node.widgets.splice(node.widgets.indexOf(refreshBtn), 1);
|
||||
node.widgets.unshift(refreshBtn);
|
||||
applyPrimereButtonStyle(refreshBtn);
|
||||
|
||||
const onSaveResult = (event) => {
|
||||
const detail = event.detail;
|
||||
if (String(detail?.node_id) !== String(node.id)) return;
|
||||
if (detail.status === "success") {
|
||||
showToast("success", `Saved: ${detail.path}`);
|
||||
} else {
|
||||
showToast("error", `Save failed: ${detail.error}`);
|
||||
}
|
||||
};
|
||||
app.api.addEventListener("primere.save_result", onSaveResult);
|
||||
|
||||
const originalOnRemoved = node.onRemoved;
|
||||
node.onRemoved = function () {
|
||||
originalOnRemoved?.call(this);
|
||||
app.api.removeEventListener("primere.save_result", onSaveResult);
|
||||
};
|
||||
|
||||
const originalOnWidgetChanged = node.onWidgetChanged;
|
||||
node.onWidgetChanged = function (name, value, oldValue, widget) {
|
||||
originalOnWidgetChanged?.call(this, name, value, oldValue, widget);
|
||||
if (name !== "api_provider" && name !== "api_service") {
|
||||
return;
|
||||
}
|
||||
if (name === "api_provider") {
|
||||
updateServiceWidget(this, schemaCache || {});
|
||||
}
|
||||
updateParameterWidgets(this, schemaCache || {});
|
||||
};
|
||||
}
|
||||
|
||||
const schemaRegistry = await loadSchemas();
|
||||
updateServiceWidget(node, schemaRegistry);
|
||||
updateParameterWidgets(node, schemaRegistry);
|
||||
|
||||
if (node.__primereUniApiWidgetHooked) {
|
||||
return;
|
||||
}
|
||||
node.__primereUniApiWidgetHooked = true;
|
||||
|
||||
const refreshBtn = node.addWidget("button", "↺ Reload API Schema", null, async () => {
|
||||
schemaCache = null;
|
||||
schemaPromise = null;
|
||||
try {
|
||||
const freshRegistry = await loadSchemas();
|
||||
if (!freshRegistry || Object.keys(freshRegistry).length === 0) {
|
||||
alert("API Schema reload failed.\nThe schema file returned empty or could not be parsed.\nCheck front_end/api_schemas.json for syntax errors.");
|
||||
return;
|
||||
}
|
||||
updateServiceWidget(node, freshRegistry);
|
||||
updateParameterWidgets(node, freshRegistry);
|
||||
const providerCount = Object.keys(freshRegistry).length;
|
||||
const serviceCount = Object.values(freshRegistry).reduce((sum, p) => sum + Object.keys(p).length, 0);
|
||||
alert(`API Schema reloaded successfully.\n${providerCount} provider(s), ${serviceCount} service(s) loaded.`);
|
||||
} catch (error) {
|
||||
alert(`API Schema reload failed.\n${error.message}`);
|
||||
}
|
||||
});
|
||||
node.widgets.splice(node.widgets.indexOf(refreshBtn), 1);
|
||||
node.widgets.unshift(refreshBtn);
|
||||
|
||||
const BTN_HEIGHT = 32;
|
||||
const BTN_COLOR = "#771a1a";
|
||||
const BTN_COLOR_ACTIVE = "#932424";
|
||||
const BTN_RADIUS = 6;
|
||||
const BTN_FONT = "bold 15px sans-serif";
|
||||
|
||||
refreshBtn.computeSize = () => [0, BTN_HEIGHT];
|
||||
refreshBtn.draw = function (ctx, node, widget_width, y) {
|
||||
ctx.save();
|
||||
const margin = 15;
|
||||
ctx.fillStyle = this.clicked ? BTN_COLOR_ACTIVE : BTN_COLOR;
|
||||
if (this.clicked) {
|
||||
this.clicked = false;
|
||||
node.setDirtyCanvas?.(true);
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(margin, y, widget_width - margin * 2, BTN_HEIGHT, BTN_RADIUS);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#dad570";
|
||||
ctx.font = BTN_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(this.name, widget_width * 0.5, y + BTN_HEIGHT * 0.5);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const onSaveResult = (event) => {
|
||||
const detail = event.detail;
|
||||
if (String(detail?.node_id) !== String(node.id)) return;
|
||||
if (detail.status === "success") {
|
||||
showToast("success", `Saved: ${detail.path}`);
|
||||
} else {
|
||||
showToast("error", `Save failed: ${detail.error}`);
|
||||
}
|
||||
};
|
||||
app.api.addEventListener("primere.save_result", onSaveResult);
|
||||
|
||||
const originalOnRemoved = node.onRemoved;
|
||||
node.onRemoved = function () {
|
||||
originalOnRemoved?.call(this);
|
||||
app.api.removeEventListener("primere.save_result", onSaveResult);
|
||||
};
|
||||
|
||||
const originalOnWidgetChanged = node.onWidgetChanged;
|
||||
node.onWidgetChanged = function (name, value, oldValue, widget) {
|
||||
originalOnWidgetChanged?.call(this, name, value, oldValue, widget);
|
||||
|
||||
if (name !== "api_provider" && name !== "api_service") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "api_provider") {
|
||||
updateServiceWidget(this, schemaCache || {});
|
||||
}
|
||||
updateParameterWidgets(this, schemaCache || {});
|
||||
};
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
Reference in New Issue
Block a user