diff --git a/README.md b/README.md index 15bdc5c..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,2 +0,0 @@ -# ComfyUI-FetchMe -ComfyUI-FetchMe diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..4b8a02d --- /dev/null +++ b/__init__.py @@ -0,0 +1,20 @@ +WEB_DIRECTORY = "js" + +from .fetch_filter import FetchMeFilter +from .fetch_codex import FetchMeCodex + +NODE_CLASS_MAPPINGS = { + "FetchMeFilter": FetchMeFilter, + "FetchMeCodex": FetchMeCodex, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "FetchMeFilter": "🍖Fetch Filter", + "FetchMeCodex": "🍖Fetch Codex", +} + +__all__ = [ + "WEB_DIRECTORY", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] diff --git a/fetch_codex.py b/fetch_codex.py new file mode 100644 index 0000000..9a567fa --- /dev/null +++ b/fetch_codex.py @@ -0,0 +1,48 @@ +from __future__ import annotations +from typing import Dict, Tuple + + +class FetchMeCodex: + MAX_KEYS = 64 + + @classmethod + def INPUT_TYPES(cls): + required = { + "value": ("STRING", {"default": "", "multiline": False}), + "key_1": ("STRING", {"default": "", "multiline": False}), + "key_2": ("STRING", {"default": "", "multiline": False}), + } + optional = { + f"key_{idx}": ("STRING", {"default": "", "multiline": False}) + for idx in range(3, cls.MAX_KEYS + 1) + } + return {"required": required, "optional": optional} + + RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("codex",) + FUNCTION = "run" + CATEGORY = "FetchMe/Text" + + def _put(self, out: Dict[str, str], key: str, value: str) -> None: + if key is None: + return + k = str(key).strip() + if k == "": + return + out[k] = value + + def run(self, value: str, key_1: str, key_2: str, **kwargs) -> Tuple[Dict[str, str]]: + replacement = "" if value is None else str(value) + out: Dict[str, str] = {} + + self._put(out, key_1, replacement) + self._put(out, key_2, replacement) + + for idx in range(3, self.MAX_KEYS + 1): + self._put(out, kwargs.get(f"key_{idx}", ""), replacement) + + return (out,) + + +# Backward-compatible alias. +fetch_codex = FetchMeCodex diff --git a/fetch_filter.py b/fetch_filter.py new file mode 100644 index 0000000..9ca1a33 --- /dev/null +++ b/fetch_filter.py @@ -0,0 +1,71 @@ +from __future__ import annotations +import re +from typing import Dict, Tuple + + +class FetchMeFilter: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"forceInput": True}), + }, + "optional": { + "codex_1": ("DICT",), + "codex_2": ("DICT",), + "codex_3": ("DICT",), + "codex_4": ("DICT",), + "codex_5": ("DICT",), + "codex_6": ("DICT",), + "codex_7": ("DICT",), + "codex_8": ("DICT",), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "run" + CATEGORY = "FetchMe/Text" + + def _merge_codex(self, out: Dict[str, str], value: Dict | None) -> None: + if value is None or not isinstance(value, dict): + return + out.update(value) + + def run( + self, + text: str, + codex_1: Dict | None = None, + codex_2: Dict | None = None, + codex_3: Dict | None = None, + codex_4: Dict | None = None, + codex_5: Dict | None = None, + codex_6: Dict | None = None, + codex_7: Dict | None = None, + codex_8: Dict | None = None, + ) -> Tuple[str]: + out = "" if text is None else str(text) + + merged: Dict[str, str] = {} + self._merge_codex(merged, codex_1) + self._merge_codex(merged, codex_2) + self._merge_codex(merged, codex_3) + self._merge_codex(merged, codex_4) + self._merge_codex(merged, codex_5) + self._merge_codex(merged, codex_6) + self._merge_codex(merged, codex_7) + self._merge_codex(merged, codex_8) + + for key, value in merged.items(): + token = str(key) + if token == "": + continue + replacement = "" if value is None else str(value) + pattern = re.compile(re.escape(token), re.IGNORECASE) + out = pattern.sub(replacement, out) + + return (out,) + + +# Backward-compatible alias. +fetch_filter = FetchMeFilter diff --git a/js/(2).gitkeep b/js/(2).gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/js/(2).gitkeep @@ -0,0 +1 @@ + diff --git a/js/fetch_theme (2).js b/js/fetch_theme (2).js new file mode 100644 index 0000000..6dd0149 --- /dev/null +++ b/js/fetch_theme (2).js @@ -0,0 +1,156 @@ +import { app } from "../../scripts/app.js"; + +const BRAND = "#5e2db3"; +const FETCHME_CLASSES = new Set(["FetchMeFilter", "FetchMeCodex"]); +const DYNAMIC_KEY_CLASSES = new Set(["FetchMeCodex"]); + +const FILTER_INPUT_LABELS = { + codex_1: "codex_I", + codex_2: "codex_II", + codex_3: "codex_III", + codex_4: "codex_IV", + codex_5: "codex_V", + codex_6: "codex_VI", + codex_7: "codex_VII", + codex_8: "codex_VIII", +}; + +function applyBrandColor(node) { + node.color = BRAND; + node.bgcolor = BRAND; +} + +function relabelInputs(node, labels) { + if (!Array.isArray(node.inputs)) { + return; + } + for (const input of node.inputs) { + if (!input || typeof input.name !== "string") { + continue; + } + const label = labels[input.name]; + if (label) { + input.label = label; + } + } +} + +function keyIndex(name) { + if (typeof name !== "string") { + return null; + } + const match = /^key_(\d+)$/.exec(name); + if (!match) { + return null; + } + return Number(match[1]); +} + +function isFilled(widget) { + const v = widget?.value; + return typeof v === "string" ? v.trim() !== "" : !!v; +} + +function installDynamicKeyFields(node) { + if (!Array.isArray(node.widgets) || node.widgets.length === 0) { + return; + } + + const allWidgets = [...node.widgets]; + const keyEntries = allWidgets + .map((widget, pos) => ({ widget, idx: keyIndex(widget?.name), pos })) + .filter((entry) => Number.isFinite(entry.idx)) + .sort((a, b) => a.idx - b.idx); + + if (keyEntries.length === 0) { + return; + } + + const firstKeyPos = Math.min(...keyEntries.map((e) => e.pos)); + const lastKeyPos = Math.max(...keyEntries.map((e) => e.pos)); + + const prefix = allWidgets.slice(0, firstKeyPos); + const suffix = allWidgets.slice(lastKeyPos + 1); + const keyWidgets = keyEntries.map((e) => e.widget); + const fullWidgets = [...prefix, ...keyWidgets, ...suffix]; + + const applyValuesFromConfig = (cfg) => { + const values = cfg?.widgets_values; + if (!Array.isArray(values)) { + return; + } + const n = Math.min(values.length, fullWidgets.length); + for (let i = 0; i < n; i += 1) { + fullWidgets[i].value = values[i]; + } + }; + + const refresh = () => { + let lastFilled = 0; + for (let i = 0; i < keyWidgets.length; i += 1) { + if (isFilled(keyWidgets[i])) { + lastFilled = i + 1; + } + } + + const minVisible = 2; + const desiredCount = Math.max(minVisible, Math.min(keyWidgets.length, lastFilled + 1)); + node.widgets = [...prefix, ...keyWidgets.slice(0, desiredCount), ...suffix]; + + if (typeof node.computeSize === "function" && typeof node.setSize === "function") { + node.setSize(node.computeSize()); + } + node.setDirtyCanvas(true, true); + }; + + for (const widget of keyWidgets) { + if (widget._fetchDynamicBound) { + continue; + } + const originalCallback = widget.callback; + widget.callback = (...args) => { + if (typeof originalCallback === "function") { + originalCallback(...args); + } + refresh(); + }; + widget._fetchDynamicBound = true; + } + + const originalSerialize = node.onSerialize; + node.onSerialize = function (o) { + if (typeof originalSerialize === "function") { + originalSerialize.call(this, o); + } + o.widgets_values = fullWidgets.map((w) => w.value); + }; + + const originalConfigure = node.onConfigure; + node.onConfigure = function (...args) { + const result = originalConfigure ? originalConfigure.apply(this, args) : undefined; + applyValuesFromConfig(args[0]); + refresh(); + return result; + }; + + refresh(); +} + +app.registerExtension({ + name: "FetchMe.ThemeAndLabels", + nodeCreated(node) { + if (!node || !FETCHME_CLASSES.has(node.comfyClass)) { + return; + } + + applyBrandColor(node); + + if (node.comfyClass === "FetchMeFilter") { + relabelInputs(node, FILTER_INPUT_LABELS); + } + + if (DYNAMIC_KEY_CLASSES.has(node.comfyClass)) { + installDynamicKeyFields(node); + } + }, +}); diff --git a/js/fetch_theme.js b/js/fetch_theme.js new file mode 100644 index 0000000..6dd0149 --- /dev/null +++ b/js/fetch_theme.js @@ -0,0 +1,156 @@ +import { app } from "../../scripts/app.js"; + +const BRAND = "#5e2db3"; +const FETCHME_CLASSES = new Set(["FetchMeFilter", "FetchMeCodex"]); +const DYNAMIC_KEY_CLASSES = new Set(["FetchMeCodex"]); + +const FILTER_INPUT_LABELS = { + codex_1: "codex_I", + codex_2: "codex_II", + codex_3: "codex_III", + codex_4: "codex_IV", + codex_5: "codex_V", + codex_6: "codex_VI", + codex_7: "codex_VII", + codex_8: "codex_VIII", +}; + +function applyBrandColor(node) { + node.color = BRAND; + node.bgcolor = BRAND; +} + +function relabelInputs(node, labels) { + if (!Array.isArray(node.inputs)) { + return; + } + for (const input of node.inputs) { + if (!input || typeof input.name !== "string") { + continue; + } + const label = labels[input.name]; + if (label) { + input.label = label; + } + } +} + +function keyIndex(name) { + if (typeof name !== "string") { + return null; + } + const match = /^key_(\d+)$/.exec(name); + if (!match) { + return null; + } + return Number(match[1]); +} + +function isFilled(widget) { + const v = widget?.value; + return typeof v === "string" ? v.trim() !== "" : !!v; +} + +function installDynamicKeyFields(node) { + if (!Array.isArray(node.widgets) || node.widgets.length === 0) { + return; + } + + const allWidgets = [...node.widgets]; + const keyEntries = allWidgets + .map((widget, pos) => ({ widget, idx: keyIndex(widget?.name), pos })) + .filter((entry) => Number.isFinite(entry.idx)) + .sort((a, b) => a.idx - b.idx); + + if (keyEntries.length === 0) { + return; + } + + const firstKeyPos = Math.min(...keyEntries.map((e) => e.pos)); + const lastKeyPos = Math.max(...keyEntries.map((e) => e.pos)); + + const prefix = allWidgets.slice(0, firstKeyPos); + const suffix = allWidgets.slice(lastKeyPos + 1); + const keyWidgets = keyEntries.map((e) => e.widget); + const fullWidgets = [...prefix, ...keyWidgets, ...suffix]; + + const applyValuesFromConfig = (cfg) => { + const values = cfg?.widgets_values; + if (!Array.isArray(values)) { + return; + } + const n = Math.min(values.length, fullWidgets.length); + for (let i = 0; i < n; i += 1) { + fullWidgets[i].value = values[i]; + } + }; + + const refresh = () => { + let lastFilled = 0; + for (let i = 0; i < keyWidgets.length; i += 1) { + if (isFilled(keyWidgets[i])) { + lastFilled = i + 1; + } + } + + const minVisible = 2; + const desiredCount = Math.max(minVisible, Math.min(keyWidgets.length, lastFilled + 1)); + node.widgets = [...prefix, ...keyWidgets.slice(0, desiredCount), ...suffix]; + + if (typeof node.computeSize === "function" && typeof node.setSize === "function") { + node.setSize(node.computeSize()); + } + node.setDirtyCanvas(true, true); + }; + + for (const widget of keyWidgets) { + if (widget._fetchDynamicBound) { + continue; + } + const originalCallback = widget.callback; + widget.callback = (...args) => { + if (typeof originalCallback === "function") { + originalCallback(...args); + } + refresh(); + }; + widget._fetchDynamicBound = true; + } + + const originalSerialize = node.onSerialize; + node.onSerialize = function (o) { + if (typeof originalSerialize === "function") { + originalSerialize.call(this, o); + } + o.widgets_values = fullWidgets.map((w) => w.value); + }; + + const originalConfigure = node.onConfigure; + node.onConfigure = function (...args) { + const result = originalConfigure ? originalConfigure.apply(this, args) : undefined; + applyValuesFromConfig(args[0]); + refresh(); + return result; + }; + + refresh(); +} + +app.registerExtension({ + name: "FetchMe.ThemeAndLabels", + nodeCreated(node) { + if (!node || !FETCHME_CLASSES.has(node.comfyClass)) { + return; + } + + applyBrandColor(node); + + if (node.comfyClass === "FetchMeFilter") { + relabelInputs(node, FILTER_INPUT_LABELS); + } + + if (DYNAMIC_KEY_CLASSES.has(node.comfyClass)) { + installDynamicKeyFields(node); + } + }, +});