From 4f2dec1efeda125fddd24b2e9a7578006044bacf Mon Sep 17 00:00:00 2001 From: "DESKTOP-TVBJISQ\\Primere" Date: Sat, 7 Mar 2026 09:31:20 +0100 Subject: [PATCH] V 2.0.0 - Universal api #67 - js helper and toasts --- front_end/frontend_helper.js | 127 +++++++++++++++++++ front_end/primere_output.js | 35 +---- front_end/primere_uniapi.js | 239 ++++++++--------------------------- 3 files changed, 189 insertions(+), 212 deletions(-) create mode 100644 front_end/frontend_helper.js diff --git a/front_end/frontend_helper.js b/front_end/frontend_helper.js new file mode 100644 index 0000000..aac5ded --- /dev/null +++ b/front_end/frontend_helper.js @@ -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); +} diff --git a/front_end/primere_output.js b/front_end/primere_output.js index a7bef66..6d7ad3f 100644 --- a/front_end/primere_output.js +++ b/front_end/primere_output.js @@ -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; diff --git a/front_end/primere_uniapi.js b/front_end/primere_uniapi.js index 848c0a6..17bbb8a 100644 --- a/front_end/primere_uniapi.js +++ b/front_end/primere_uniapi.js @@ -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({