diff --git a/__init__.py b/__init__.py index d4224f8..7304c53 100644 --- a/__init__.py +++ b/__init__.py @@ -196,6 +196,7 @@ from .nodes.utility.FL_RandomRange import FL_RandomNumber from .nodes.utility.FL_RandomShapeGenerator import FL_RandomShapeGenerator from .nodes.utility.FL_SD_Slices import FL_SDUltimate_Slices from .nodes.utility.FL_SeparateMasks import FL_SeparateMaskComponents +from .nodes.utility.FL_ShowText import FL_ShowText from .nodes.utility.FL_StringToLoraName import FL_StringToLoraName from .nodes.utility.FL_Switch import FL_Switch from .nodes.utility.FL_Switch_Big import FL_Switch_Big @@ -449,6 +450,7 @@ NODE_CLASS_MAPPINGS = { "FL_RandomShapeGenerator": FL_RandomShapeGenerator, "FL_KartelJobInput": FL_KartelJobInput, "FL_KartelJobOutput": FL_KartelJobOutput, + "FL_ShowText": FL_ShowText, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -652,6 +654,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FL_RandomShapeGenerator": "FL Random Shape Generator", "FL_KartelJobInput": "FL Kartel Job Input", "FL_KartelJobOutput": "FL Kartel Job Output", + "FL_ShowText": "FL Show Text", } # Conditionally register google-genai nodes if the SDK loaded successfully diff --git a/nodes/utility/FL_ShowText.py b/nodes/utility/FL_ShowText.py new file mode 100644 index 0000000..78d24be --- /dev/null +++ b/nodes/utility/FL_ShowText.py @@ -0,0 +1,22 @@ +class FL_ShowText: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"forceInput": True}), + }, + "hidden": {"unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "show" + OUTPUT_NODE = True + CATEGORY = "🏵️Fill Nodes/Utility" + + def show(self, text, unique_id=None): + if text is None: + text = "" + if not isinstance(text, str): + text = str(text) + return {"ui": {"text": [text]}, "result": (text,)} diff --git a/pyproject.toml b/pyproject.toml index 47bdd77..c50cd8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_fill-nodes" description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection." -version = "2.7.9" +version = "2.8.0" license = "LICENSE" dependencies = ["librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"] diff --git a/web/nodes/utility/FL_ShowText.js b/web/nodes/utility/FL_ShowText.js new file mode 100644 index 0000000..709009d --- /dev/null +++ b/web/nodes/utility/FL_ShowText.js @@ -0,0 +1,192 @@ +/** + * File: FL_ShowText.js + * Project: ComfyUI_Fill-Nodes + * + * Displays the incoming text on the node via a styled DOM widget and + * passes the same string through to the output socket. + */ + +import { app } from "../../../../scripts/app.js"; + +const STYLES = ` + .flshowtext-host { + box-sizing: border-box; + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + } + .flshowtext-widget { + background: #17181c; + border: 1px solid #2a2d34; + border-radius: 8px; + color: #f4f4f5; + display: flex; + flex: 1 1 auto; + flex-direction: column; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 12px; + line-height: 1.4; + min-height: 0; + overflow: hidden; + padding: 0; + box-sizing: border-box; + width: 100%; + } + .flshowtext-widget * { box-sizing: border-box; } + .flshowtext-header { + align-items: center; + background: #101114; + border-bottom: 1px solid #2a2d34; + color: #cbd5e1; + display: flex; + flex: 0 0 auto; + font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 10px; + font-weight: 700; + gap: 8px; + justify-content: space-between; + letter-spacing: 0.04em; + padding: 6px 10px; + text-transform: uppercase; + } + .flshowtext-badge { + background: #27272a; + border-radius: 999px; + color: #a1a1aa; + font-size: 9px; + font-variant-numeric: tabular-nums; + font-weight: 700; + letter-spacing: 0; + padding: 2px 7px; + text-transform: none; + } + .flshowtext-body { + color: #e5e7eb; + flex: 1 1 auto; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 10px; + white-space: pre-wrap; + word-break: break-word; + scrollbar-width: thin; + scrollbar-color: #3f3f46 #17181c; + } + .flshowtext-body::-webkit-scrollbar { width: 8px; } + .flshowtext-body::-webkit-scrollbar-track { background: #17181c; } + .flshowtext-body::-webkit-scrollbar-thumb { + background: #3f3f46; + border-radius: 999px; + } + .flshowtext-body::-webkit-scrollbar-thumb:hover { background: #52525b; } + .flshowtext-body.empty { + color: #71717a; + font-style: italic; + } +`; + +const PLACEHOLDER = "Waiting for input. Run the workflow to display text."; + +function injectStyles() { + const id = "flshowtext-styles"; + if (document.getElementById(id)) return; + const style = document.createElement("style"); + style.id = id; + style.textContent = STYLES; + document.head.appendChild(style); +} + +function formatLength(text) { + const chars = text.length; + const lines = text ? text.split("\n").length : 0; + return `${chars} chars · ${lines} line${lines === 1 ? "" : "s"}`; +} + +function buildPanel() { + const root = document.createElement("div"); + root.className = "flshowtext-widget"; + root.innerHTML = ` +
+ Show Text + empty +
+
+ `; + const body = root.querySelector('[data-role="body"]'); + const badge = root.querySelector('[data-role="length"]'); + body.textContent = PLACEHOLDER; + return { root, body, badge }; +} + +function attachWheelCapture(body) { + body.addEventListener( + "wheel", + (event) => { + const canScrollDown = + event.deltaY > 0 && body.scrollTop + body.clientHeight < body.scrollHeight - 1; + const canScrollUp = event.deltaY < 0 && body.scrollTop > 0; + if (canScrollDown || canScrollUp) { + body.scrollTop += event.deltaY; + event.preventDefault(); + event.stopPropagation(); + } + }, + { passive: false } + ); +} + +app.registerExtension({ + name: "fl.node.FL_ShowText", + nodeCreated(node) { + const comfyClass = (node.constructor && node.constructor.comfyClass) || ""; + if (comfyClass !== "FL_ShowText") return; + + injectStyles(); + + const host = document.createElement("div"); + host.className = "flshowtext-host"; + + const panel = buildPanel(); + host.appendChild(panel.root); + + attachWheelCapture(panel.body); + + node.addDOMWidget("fl_showtext_display", "fl-showtext", host, { + getMinHeight: () => 80, + hideOnZoom: false, + serialize: false, + }); + + const setText = (text) => { + const value = text == null ? "" : String(text); + if (value === "") { + panel.body.classList.add("empty"); + panel.body.textContent = PLACEHOLDER; + panel.badge.textContent = "empty"; + } else { + panel.body.classList.remove("empty"); + panel.body.textContent = value; + panel.badge.textContent = formatLength(value); + } + }; + + const [oldW, oldH] = node.size; + node.setSize([Math.max(oldW, 320), Math.max(oldH, 220)]); + + const originalOnExecuted = node.onExecuted; + node.onExecuted = function (message) { + originalOnExecuted?.apply(this, arguments); + const lines = message?.text; + if (Array.isArray(lines) && lines.length > 0) { + setText(lines.join("\n")); + } + }; + + const originalOnConfigure = node.onConfigure; + node.onConfigure = function () { + originalOnConfigure?.apply(this, arguments); + setText(""); + }; + }, +});