From 9ffbc5317b4e4a296e2e1124edd80b5c972a2ae4 Mon Sep 17 00:00:00 2001 From: arlozen Date: Wed, 5 Nov 2025 13:41:34 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=9B=B4=E6=96=B0=EF=BC=9ABase64?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E6=94=AF=E6=8C=81=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E5=86=85=E7=BD=AE=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/misc/__init__.py | 2 +- ..._previewer.py => base64_context_loader.py} | 18 +- ..._previewer.js => base64_context_loader.js} | 175 +++++++++++++++++- 3 files changed, 185 insertions(+), 10 deletions(-) rename nodes/misc/{base64_context_previewer.py => base64_context_loader.py} (79%) rename web/{base64_context_previewer.js => base64_context_loader.js} (57%) diff --git a/nodes/misc/__init__.py b/nodes/misc/__init__.py index 680fb7d..a98f489 100644 --- a/nodes/misc/__init__.py +++ b/nodes/misc/__init__.py @@ -1,3 +1,3 @@ from .base64_downloader import * from .base64_uploader import * -from .base64_context_previewer import * \ No newline at end of file +from .base64_context_loader import * \ No newline at end of file diff --git a/nodes/misc/base64_context_previewer.py b/nodes/misc/base64_context_loader.py similarity index 79% rename from nodes/misc/base64_context_previewer.py rename to nodes/misc/base64_context_loader.py index 6cd5025..67d5397 100644 --- a/nodes/misc/base64_context_previewer.py +++ b/nodes/misc/base64_context_loader.py @@ -7,7 +7,7 @@ from .base64_context import Base64Context routes = PromptServer.instance.routes @register_node -class Base64ContextPreviewer: +class Base64ContextLoader: def __init__(self): pass @@ -17,17 +17,25 @@ class Base64ContextPreviewer: "required": {}, "hidden": { "key": ("STRING", {"default": "NONE"}), + "mode": (["Builtin", "Dialog"], {"default": "Builtin"}), }, } - RETURN_TYPES = () - RETURN_NAMES = () + RETURN_TYPES = ("STRING", "STRING", "STRING") + RETURN_NAMES = ("base64", "basename", "suffix") FUNCTION = "run" CATEGORY = "EasyToolkit/Misc" OUTPUT_NODE = True - def run(self, key): - return {} + def run(self, key, mode): + base64_context = get_persistent_context(key).get_value() + if not base64_context or not isinstance(base64_context, Base64Context): + raise Exception("There is no base64 data at all.") + + base64 = base64_context.get_base64() + basename = base64_context.get_basename() + suffix = base64_context.get_suffix() + return {"result": (base64, basename, suffix,)} @routes.post("/base64_context_previewer/get_keys") async def handle_get_keys(request): diff --git a/web/base64_context_previewer.js b/web/base64_context_loader.js similarity index 57% rename from web/base64_context_previewer.js rename to web/base64_context_loader.js index 18d0bd0..be73f31 100644 --- a/web/base64_context_previewer.js +++ b/web/base64_context_loader.js @@ -1,14 +1,27 @@ import { app } from "../../../scripts/app.js"; app.registerExtension({ - name: "EasyToolkit.Misc.Base64ContextPreviewer", + name: "EasyToolkit.Misc.Base64ContextLoader", async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name !== "Base64ContextPreviewer") return; + if (nodeData.name !== "Base64ContextLoader") return; const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = async function () { if (onNodeCreated) onNodeCreated.apply(this, arguments); + // Add mode selector + let modeWidget = this.addWidget("combo", "mode", "Builtin", (value) => { + // Update preview visibility based on mode + if (value === "Builtin" && this.previewData) { + this.updateBuiltinPreview(this.previewData); + } else if (value === "Dialog") { + // Hide builtin preview + this.previewImage = null; + this.previewText = null; + this.setSize(this.computeSize()); + } + }, { values: ["Builtin", "Dialog"] }); + // Create key combo widget dynamically // This widget name matches the hidden parameter "key" in backend let keyWidget = this.addWidget("combo", "key", "NONE", (value) => { @@ -20,6 +33,11 @@ app.registerExtension({ let previewWidget = this.addWidget("text", "preview_info", "等待预览...", () => {}); previewWidget.disabled = true; + // Store preview data + this.previewData = null; + this.previewImage = null; + this.previewText = null; + // Function to refresh keys from backend const refreshKeys = async () => { try { @@ -80,8 +98,15 @@ app.registerExtension({ const fileInfo = `${data.filename}`; previewWidget.value = fileInfo; - // Create preview dialog - this.showPreviewDialog(data); + // Store preview data + this.previewData = data; + + // Show preview based on mode + if (modeWidget.value === "Builtin") { + this.updateBuiltinPreview(data); + } else { + this.showPreviewDialog(data); + } app.extensionManager.toast.add({ severity: "info", @@ -113,6 +138,148 @@ app.registerExtension({ }); }; + // Add builtin preview update method + nodeType.prototype.updateBuiltinPreview = function(data) { + if (data.content_type === "image") { + // Load image + const img = new Image(); + img.onload = () => { + this.previewImage = img; + this.previewText = null; + this.setSize(this.computeSize()); + }; + img.src = `data:${data.format};base64,${data.base64}`; + } else if (data.content_type === "text") { + // Decode text if needed + let textContent = data.base64; + if (!data.format.startsWith("text/")) { + try { + textContent = atob(data.base64); + } catch (e) { + textContent = data.base64; + } + } + this.previewText = textContent; + this.previewImage = null; + this.setSize(this.computeSize()); + } else { + this.previewImage = null; + this.previewText = null; + this.setSize(this.computeSize()); + } + }; + + // Override onDrawForeground to render preview on node + const onDrawForeground = nodeType.prototype.onDrawForeground; + nodeType.prototype.onDrawForeground = function(ctx) { + if (onDrawForeground) { + onDrawForeground.apply(this, arguments); + } + + // Only draw builtin preview if mode is "Builtin" + const modeWidget = this.widgets?.find(w => w.name === "mode"); + if (!modeWidget || modeWidget.value !== "Builtin") { + return; + } + + if (this.previewImage) { + // Draw image preview + const [w, h] = this.size; + const previewHeight = 300; + const previewY = h - previewHeight - 10; + + // Calculate image dimensions to fit in preview area + const maxWidth = w - 20; + const maxHeight = previewHeight; + let imgWidth = this.previewImage.width; + let imgHeight = this.previewImage.height; + + const aspectRatio = imgWidth / imgHeight; + if (imgWidth > maxWidth) { + imgWidth = maxWidth; + imgHeight = imgWidth / aspectRatio; + } + if (imgHeight > maxHeight) { + imgHeight = maxHeight; + imgWidth = imgHeight * aspectRatio; + } + + const imgX = (w - imgWidth) / 2; + const imgY = previewY + (maxHeight - imgHeight) / 2; + + ctx.save(); + ctx.drawImage(this.previewImage, imgX, imgY, imgWidth, imgHeight); + ctx.restore(); + } else if (this.previewText) { + // Draw text preview + const [w, h] = this.size; + const previewHeight = 200; + const previewY = h - previewHeight - 10; + + ctx.save(); + ctx.fillStyle = "#2d2d2d"; + ctx.fillRect(10, previewY, w - 20, previewHeight); + + ctx.strokeStyle = "#444"; + ctx.strokeRect(10, previewY, w - 20, previewHeight); + + ctx.fillStyle = "#d4d4d4"; + ctx.font = "12px monospace"; + + // Split text into lines and draw + const lines = this.previewText.split('\n'); + const lineHeight = 14; + const maxLines = Math.floor((previewHeight - 20) / lineHeight); + const visibleLines = lines.slice(0, maxLines); + + visibleLines.forEach((line, idx) => { + // Truncate long lines + const maxChars = Math.floor((w - 40) / 7); + const truncatedLine = line.length > maxChars ? line.substring(0, maxChars) + "..." : line; + ctx.fillText(truncatedLine, 15, previewY + 15 + idx * lineHeight); + }); + + if (lines.length > maxLines) { + ctx.fillText("...", 15, previewY + 15 + maxLines * lineHeight); + } + + ctx.restore(); + } + }; + + // Override computeSize to account for preview area + const computeSize = nodeType.prototype.computeSize; + nodeType.prototype.computeSize = function(out) { + const size = computeSize ? computeSize.apply(this, arguments) : [this.size[0], this.size[1]]; + + const modeWidget = this.widgets?.find(w => w.name === "mode"); + if (modeWidget && modeWidget.value === "Builtin") { + let requiredExtraHeight = 0; + let requiredMinWidth = size[0]; + + if (this.previewImage) { + requiredExtraHeight = 310; // Space for image preview + // Calculate minimum width based on image aspect ratio + const imgAspect = this.previewImage.width / this.previewImage.height; + const previewHeight = 300; + requiredMinWidth = Math.max(requiredMinWidth, Math.min(this.previewImage.width, previewHeight * imgAspect) + 40); + } else if (this.previewText) { + requiredExtraHeight = 210; // Space for text preview + requiredMinWidth = Math.max(requiredMinWidth, 300); // Minimum width for text preview + } + + // Calculate minimum required height (base height + preview area) + const minRequiredHeight = size[1] + requiredExtraHeight; + + // Allow user to resize, but enforce minimum dimensions + // Use the larger of: computed minimum size or user's manual resize + size[0] = Math.max(size[0], requiredMinWidth); + size[1] = Math.max(size[1], minRequiredHeight); + } + + return size; + }; + // Add preview dialog method to node prototype nodeType.prototype.showPreviewDialog = function(data) { const dialog = document.createElement("div");