Merge branch 'main' of github.com:choey/ComfyUI-Custom-Scripts into deduplicate-image-feed
This commit is contained in:
+3
-1
@@ -1,3 +1,5 @@
|
||||
__pycache__
|
||||
pysssss.json
|
||||
user/autocomplete.txt
|
||||
user/autocomplete.txt
|
||||
web/js/assets/favicon.user.ico
|
||||
web/js/assets/favicon-active.user.ico
|
||||
+18
-6
@@ -19,12 +19,24 @@ class ShowText:
|
||||
|
||||
CATEGORY = "utils"
|
||||
|
||||
def notify(self, text, unique_id = None, extra_pnginfo=None):
|
||||
if unique_id and extra_pnginfo and "workflow" in extra_pnginfo[0]:
|
||||
workflow = extra_pnginfo[0]["workflow"]
|
||||
node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None)
|
||||
if node:
|
||||
node["widgets_values"] = [text]
|
||||
def notify(self, text, unique_id=None, extra_pnginfo=None):
|
||||
if unique_id is not None and extra_pnginfo is not None:
|
||||
if not isinstance(extra_pnginfo, list):
|
||||
print("Error: extra_pnginfo is not a list")
|
||||
elif (
|
||||
not isinstance(extra_pnginfo[0], dict)
|
||||
or "workflow" not in extra_pnginfo[0]
|
||||
):
|
||||
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
|
||||
else:
|
||||
workflow = extra_pnginfo[0]["workflow"]
|
||||
node = next(
|
||||
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
|
||||
None,
|
||||
)
|
||||
if node:
|
||||
node["widgets_values"] = [text]
|
||||
|
||||
return {"ui": {"text": text}, "result": (text,)}
|
||||
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ class StringFunction:
|
||||
"required": {
|
||||
"action": (["append", "replace"], {}),
|
||||
"tidy_tags": (["yes", "no"], {}),
|
||||
"text_a": ("STRING", {"multiline": True}),
|
||||
"text_b": ("STRING", {"multiline": True}),
|
||||
"text_a": ("STRING", {"multiline": True, "dynamicPrompts": False}),
|
||||
"text_b": ("STRING", {"multiline": True, "dynamicPrompts": False}),
|
||||
},
|
||||
"optional": {
|
||||
"text_c": ("STRING", {"multiline": True})
|
||||
"text_c": ("STRING", {"multiline": True, "dynamicPrompts": False})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+81
-53
@@ -1,7 +1,7 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../../scripts/widgets.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { $el, ComfyDialog } from "../../../scripts/ui.js";
|
||||
import { TextAreaAutoComplete } from "./common/autocomplete.js";
|
||||
import { ModelInfoDialog } from "./common/modelInfoDialog.js";
|
||||
import { LoraInfoDialog } from "./modelInfo.js";
|
||||
@@ -275,57 +275,6 @@ const id = "pysssss.AutoCompleter";
|
||||
app.registerExtension({
|
||||
name: id,
|
||||
init() {
|
||||
async function addEmbeddings() {
|
||||
const embeddings = await api.getEmbeddings();
|
||||
const words = {};
|
||||
words["embedding:"] = { text: "embedding:" };
|
||||
|
||||
for (const emb of embeddings) {
|
||||
const v = `embedding:${emb}`;
|
||||
words[v] = {
|
||||
text: v,
|
||||
info: () => new EmbeddingInfoDialog(emb).show("embeddings", emb),
|
||||
use_replacer: false,
|
||||
};
|
||||
}
|
||||
|
||||
TextAreaAutoComplete.updateWords("pysssss.embeddings", words);
|
||||
}
|
||||
|
||||
async function addLoras() {
|
||||
const loras = await api
|
||||
.fetchApi("/pysssss/loras", { cache: "no-store" })
|
||||
.then(res => res.json());
|
||||
const words = {};
|
||||
words["lora:"] = { text: "lora:" };
|
||||
|
||||
for (const lora of loras) {
|
||||
const v = `<lora:${lora}:1.0>`;
|
||||
words[v] = {
|
||||
text: v,
|
||||
info: () => new LoraInfoDialog(lora).show("loras", lora),
|
||||
use_replacer: false,
|
||||
};
|
||||
}
|
||||
|
||||
TextAreaAutoComplete.updateWords("pysssss.loras", words);
|
||||
}
|
||||
|
||||
// store global words with/without loras
|
||||
Promise.all([addEmbeddings(), addCustomWords()])
|
||||
.then(() => {
|
||||
TextAreaAutoComplete.globalWordsExclLoras = Object.assign(
|
||||
{},
|
||||
TextAreaAutoComplete.globalWords
|
||||
);
|
||||
})
|
||||
.then(addLoras)
|
||||
.then(() => {
|
||||
if (!TextAreaAutoComplete.lorasEnabled) {
|
||||
toggleLoras(); // off by default
|
||||
}
|
||||
});
|
||||
|
||||
const STRING = ComfyWidgets.STRING;
|
||||
const SKIP_WIDGETS = new Set(["ttN xyPlot.x_values", "ttN xyPlot.y_values"]);
|
||||
ComfyWidgets.STRING = function (node, inputName, inputData) {
|
||||
@@ -517,6 +466,29 @@ app.registerExtension({
|
||||
),
|
||||
]
|
||||
),
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
textContent: "Max suggestions: ",
|
||||
style: {
|
||||
display: "block",
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("input", {
|
||||
type: "number",
|
||||
value: +TextAreaAutoComplete.suggestionCount,
|
||||
style: {
|
||||
width: "80px"
|
||||
},
|
||||
onchange: (event) => {
|
||||
const value = +event.target.value;
|
||||
TextAreaAutoComplete.suggestionCount = value;;
|
||||
localStorage.setItem(id + ".SuggestionCount", TextAreaAutoComplete.suggestionCount);
|
||||
},
|
||||
}),
|
||||
]
|
||||
),
|
||||
$el("button", {
|
||||
textContent: "Manage Custom Words",
|
||||
onclick: () => {
|
||||
@@ -539,6 +511,62 @@ app.registerExtension({
|
||||
TextAreaAutoComplete.insertOnTab = localStorage.getItem(id + ".InsertOnTab") !== "false";
|
||||
TextAreaAutoComplete.insertOnEnter = localStorage.getItem(id + ".InsertOnEnter") !== "false";
|
||||
TextAreaAutoComplete.lorasEnabled = localStorage.getItem(id + ".ShowLoras") === "true";
|
||||
TextAreaAutoComplete.suggestionCount = +localStorage.getItem(id + ".SuggestionCount");
|
||||
},
|
||||
setup() {
|
||||
async function addEmbeddings() {
|
||||
const embeddings = await api.getEmbeddings();
|
||||
const words = {};
|
||||
words["embedding:"] = { text: "embedding:" };
|
||||
|
||||
for (const emb of embeddings) {
|
||||
const v = `embedding:${emb}`;
|
||||
words[v] = {
|
||||
text: v,
|
||||
info: () => new EmbeddingInfoDialog(emb).show("embeddings", emb),
|
||||
use_replacer: false,
|
||||
};
|
||||
}
|
||||
|
||||
TextAreaAutoComplete.updateWords("pysssss.embeddings", words);
|
||||
}
|
||||
|
||||
async function addLoras() {
|
||||
let loras;
|
||||
try {
|
||||
loras = LiteGraph.registered_node_types["LoraLoader"]?.nodeData.input.required.lora_name[0];
|
||||
} catch (error) {}
|
||||
|
||||
if (!loras?.length) {
|
||||
loras = await api.fetchApi("/pysssss/loras", { cache: "no-store" }).then((res) => res.json());
|
||||
}
|
||||
|
||||
const words = {};
|
||||
words["lora:"] = { text: "lora:" };
|
||||
|
||||
for (const lora of loras) {
|
||||
const v = `<lora:${lora}:1.0>`;
|
||||
words[v] = {
|
||||
text: v,
|
||||
info: () => new LoraInfoDialog(lora).show("loras", lora),
|
||||
use_replacer: false,
|
||||
};
|
||||
}
|
||||
|
||||
TextAreaAutoComplete.updateWords("pysssss.loras", words);
|
||||
}
|
||||
|
||||
// store global words with/without loras
|
||||
Promise.all([addEmbeddings(), addCustomWords()])
|
||||
.then(() => {
|
||||
TextAreaAutoComplete.globalWordsExclLoras = Object.assign({}, TextAreaAutoComplete.globalWords);
|
||||
})
|
||||
.then(addLoras)
|
||||
.then(() => {
|
||||
if (!TextAreaAutoComplete.lorasEnabled) {
|
||||
toggleLoras(); // off by default
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeRegisterNodeDef(_, def) {
|
||||
// Process each input to see if there is a custom word list for
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
font-family: sans-serif;
|
||||
box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4);
|
||||
z-index: 9999;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pysssss-autocomplete-item {
|
||||
|
||||
@@ -238,6 +238,7 @@ class TextAreaCaretHelper {
|
||||
fontSize: computedStyle.fontSize,
|
||||
fontFamily: computedStyle.fontFamily,
|
||||
padding: "0",
|
||||
position: "absolute",
|
||||
});
|
||||
body.appendChild(tempNode);
|
||||
|
||||
@@ -291,10 +292,46 @@ class TextAreaCaretHelper {
|
||||
const startPos = this.el.selectionStart;
|
||||
const endPos = this.el.selectionEnd;
|
||||
|
||||
this.el.value = this.el.value.substring(0, startPos + offset) + value + this.el.value.substring(endPos, this.el.value.length);
|
||||
// Move selection to beginning of offset
|
||||
this.el.selectionStart = this.el.selectionStart + offset;
|
||||
|
||||
// Using execCommand to support undo, but since it's officially
|
||||
// 'deprecated' we need a backup solution, but it won't support undo :(
|
||||
let pasted = true;
|
||||
try {
|
||||
if (!document.execCommand("insertText", false, value)) {
|
||||
pasted = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error caught during execCommand:", e);
|
||||
pasted = false;
|
||||
}
|
||||
|
||||
if (!pasted) {
|
||||
console.error(
|
||||
"execCommand unsuccessful; not supported. Adding text manually, no undo support.");
|
||||
textarea.setRangeText(modifiedText, this.el.selectionStart, this.el.selectionEnd, 'end');
|
||||
}
|
||||
|
||||
this.el.selectionEnd = this.el.selectionStart = startPos + value.length + offset + (finalOffset ?? 0);
|
||||
} else {
|
||||
this.el.value += value;
|
||||
// Using execCommand to support undo, but since it's officially
|
||||
// 'deprecated' we need a backup solution, but it won't support undo :(
|
||||
let pasted = true;
|
||||
try {
|
||||
if (!document.execCommand("insertText", false, value)) {
|
||||
pasted = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error caught during execCommand:", e);
|
||||
pasted = false;
|
||||
}
|
||||
|
||||
if (!pasted) {
|
||||
console.error(
|
||||
"execCommand unsuccessful; not supported. Adding text manually, no undo support.");
|
||||
this.el.value += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,6 +355,7 @@ export class TextAreaAutoComplete {
|
||||
static insertOnEnter = true;
|
||||
static replacer = undefined;
|
||||
static lorasEnabled = false;
|
||||
static suggestionCount = 20;
|
||||
|
||||
/** @type {Record<string, Record<string, AutoCompleteEntry>>} */
|
||||
static groups = {};
|
||||
@@ -492,7 +530,7 @@ export class TextAreaAutoComplete {
|
||||
);
|
||||
|
||||
const top = priorityMatches.length * 0.2;
|
||||
return priorityMatches.slice(0, top).concat(prefixMatches, priorityMatches.slice(top), includesMatches).slice(0, 20);
|
||||
return priorityMatches.slice(0, top).concat(prefixMatches, priorityMatches.slice(top), includesMatches).slice(0, TextAreaAutoComplete.suggestionCount);
|
||||
}
|
||||
|
||||
#update() {
|
||||
@@ -612,6 +650,7 @@ export class TextAreaAutoComplete {
|
||||
const position = this.helper.getCursorOffset();
|
||||
this.dropdown.style.left = (position.left ?? 0) + "px";
|
||||
this.dropdown.style.top = (position.top ?? 0) + "px";
|
||||
this.dropdown.style.maxHeight = (window.innerHeight - position.top) + "px";
|
||||
}
|
||||
|
||||
#hide() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -107,6 +107,16 @@ export class Lightbox {
|
||||
this.img.src = img;
|
||||
this.img.style.opacity = 1;
|
||||
}
|
||||
|
||||
async updateWithNewImage(img, feedDirection) {
|
||||
// No-op if lightbox is not open
|
||||
if (this.el.style.display === "none" || this.el.style.opacity === "0") return;
|
||||
|
||||
// Ensure currently shown image does not change
|
||||
const [method, shift] = feedDirection === "newest first" ? ["unshift", 1] : ["push", 0];
|
||||
this.images[method](img);
|
||||
await this.update(shift);
|
||||
}
|
||||
}
|
||||
|
||||
export const lightbox = new Lightbox();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.lds-ring {
|
||||
.pysssss-lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-ring div {
|
||||
.pysssss-lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
@@ -16,13 +16,13 @@
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
.pysssss-lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
.pysssss-lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
.pysssss-lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
|
||||
@@ -4,6 +4,6 @@ addStylesheet(import.meta.url);
|
||||
|
||||
export function createSpinner() {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
|
||||
div.innerHTML = `<div class="pysssss-lds-ring"><div></div><div></div><div></div><div></div></div>`;
|
||||
return div.firstElementChild;
|
||||
}
|
||||
|
||||
+16
-2
@@ -6,7 +6,7 @@ import { app } from "../../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "pysssss.FaviconStatus",
|
||||
setup() {
|
||||
async setup() {
|
||||
let link = document.querySelector("link[rel~='icon']");
|
||||
if (!link) {
|
||||
link = document.createElement("link");
|
||||
@@ -14,8 +14,22 @@ app.registerExtension({
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
const getUrl = (active, user) => new URL(`assets/favicon${active ? "-active" : ""}${user ? ".user" : ""}.ico`, import.meta.url);
|
||||
const testUrl = async (active) => {
|
||||
const url = getUrl(active, true);
|
||||
const r = await fetch(url, {
|
||||
method: "HEAD",
|
||||
});
|
||||
if (r.status === 200) {
|
||||
return url;
|
||||
}
|
||||
return getUrl(active, false);
|
||||
};
|
||||
const activeUrl = await testUrl(true);
|
||||
const idleUrl = await testUrl(false);
|
||||
|
||||
let executing = false;
|
||||
const update = () => (link.href = new URL(`assets/favicon${executing ? "-active" : ""}.ico`, import.meta.url));
|
||||
const update = () => (link.href = executing ? activeUrl : idleUrl);
|
||||
|
||||
for (const e of ["execution_start", "progress"]) {
|
||||
api.addEventListener(e, () => {
|
||||
|
||||
+64
-5
@@ -15,6 +15,9 @@ $el("style", {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
div > .pysssss-image-feed {
|
||||
position: static;
|
||||
}
|
||||
.pysssss-image-feed--top, .pysssss-image-feed--bottom {
|
||||
width: 100vw;
|
||||
min-height: 30px;
|
||||
@@ -206,7 +209,7 @@ $el("style", {
|
||||
|
||||
app.registerExtension({
|
||||
name: "pysssss.ImageFeed",
|
||||
setup() {
|
||||
async setup() {
|
||||
let visible = true;
|
||||
const seenImages = new Map();
|
||||
const showButton = $el("button.comfy-settings-btn", {
|
||||
@@ -217,6 +220,17 @@ app.registerExtension({
|
||||
display: "none",
|
||||
},
|
||||
});
|
||||
let showMenuButton;
|
||||
if (!app.menu?.element.style.display && app.menu?.settingsGroup) {
|
||||
showMenuButton = new (await import("../../../scripts/ui/components/button.js")).ComfyButton({
|
||||
icon: "image-multiple",
|
||||
action: () => showButton.click(),
|
||||
tooltip: "Show Image Feed 🐍",
|
||||
content: "Show Image Feed 🐍",
|
||||
});
|
||||
showMenuButton.enabled = false;
|
||||
app.menu.settingsGroup.append(showMenuButton);
|
||||
}
|
||||
|
||||
const getVal = (n, d) => {
|
||||
const v = localStorage.getItem("pysssss.ImageFeed." + n);
|
||||
@@ -230,11 +244,19 @@ app.registerExtension({
|
||||
localStorage.setItem("pysssss.ImageFeed." + n, v);
|
||||
};
|
||||
|
||||
const imageFeed = $el("div.pysssss-image-feed", {
|
||||
parent: document.body,
|
||||
});
|
||||
const imageFeed = $el("div.pysssss-image-feed");
|
||||
const imageList = $el("div.pysssss-image-feed-list");
|
||||
|
||||
function updateMenuParent(location) {
|
||||
if (showMenuButton) {
|
||||
document.querySelector(".comfyui-body-" + location).append(imageFeed);
|
||||
} else {
|
||||
if (!imageFeed.parent) {
|
||||
document.body.append(imageFeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const feedLocation = app.ui.settings.addSetting({
|
||||
id: "pysssss.ImageFeed.Location",
|
||||
name: "🐍 Image Feed Location",
|
||||
@@ -256,6 +278,8 @@ app.registerExtension({
|
||||
oninput: (e) => {
|
||||
feedLocation.value = e.target.value;
|
||||
imageFeed.className = `pysssss-image-feed pysssss-image-feed--${feedLocation.value}`;
|
||||
updateMenuParent(feedLocation.value);
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
},
|
||||
["left", "top", "right", "bottom"].map((m) =>
|
||||
@@ -271,6 +295,7 @@ app.registerExtension({
|
||||
},
|
||||
onChange(value) {
|
||||
imageFeed.className = `pysssss-image-feed pysssss-image-feed--${value}`;
|
||||
updateMenuParent(value);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -324,7 +349,10 @@ by reducing the number of images in the feed.`,
|
||||
|
||||
const clearButton = $el("button.pysssss-image-feed-btn.clear-btn", {
|
||||
textContent: "Clear",
|
||||
onclick: () => imageList.replaceChildren(),
|
||||
onclick: () => {
|
||||
imageList.replaceChildren();
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
});
|
||||
|
||||
const hideButton = $el("button.pysssss-image-feed-btn.hide-btn", {
|
||||
@@ -332,8 +360,10 @@ by reducing the number of images in the feed.`,
|
||||
onclick: () => {
|
||||
imageFeed.style.display = "none";
|
||||
showButton.style.display = "unset";
|
||||
if (showMenuButton) showMenuButton.enabled = true;
|
||||
saveVal("Visible", 0);
|
||||
visible = false;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -344,6 +374,28 @@ by reducing the number of images in the feed.`,
|
||||
saveVal("ImageSize", v);
|
||||
columnInput.max = Math.max(10, v, columnInput.max);
|
||||
columnInput.value = v;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
}
|
||||
|
||||
function addImageToFeed(href) {
|
||||
const method = feedDirection.value === "newest first" ? "prepend" : "append";
|
||||
imageList[method](
|
||||
$el("div", [
|
||||
$el(
|
||||
"a",
|
||||
{
|
||||
target: "_blank",
|
||||
href,
|
||||
onclick: (e) => {
|
||||
const imgs = [...imageList.querySelectorAll("img")].map((img) => img.getAttribute("src"));
|
||||
lightbox.show(imgs, imgs.indexOf(href));
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
[$el("img", { src: href })]
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function addImageToFeed(href) {
|
||||
@@ -384,6 +436,7 @@ by reducing the number of images in the feed.`,
|
||||
e.target.parentElement.title = `Controls the maximum size of the image feed panel (${e.target.value}vh)`;
|
||||
imageFeed.style.setProperty("--max-size", e.target.value);
|
||||
saveVal("FeedSize", e.target.value);
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
$: (el) => {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -432,10 +485,14 @@ by reducing the number of images in the feed.`,
|
||||
showButton.onclick = () => {
|
||||
imageFeed.style.display = "block";
|
||||
showButton.style.display = "none";
|
||||
if (showMenuButton) showMenuButton.enabled = false;
|
||||
|
||||
saveVal("Visible", 1);
|
||||
visible = true;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
};
|
||||
document.querySelector(".comfy-settings-btn").after(showButton);
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
if (!+getVal("Visible", 1)) {
|
||||
hideButton.onclick();
|
||||
@@ -443,10 +500,12 @@ by reducing the number of images in the feed.`,
|
||||
|
||||
api.addEventListener("executed", ({ detail }) => {
|
||||
if (visible && detail?.output?.images) {
|
||||
if (detail.node?.includes?.(":")) {
|
||||
if (detail.node?.includes?.(":")) {
|
||||
// Ignore group nodes
|
||||
const n = app.graph.getNodeById(detail.node.split(":")[0]);
|
||||
if (n?.getInnerNodes) return;
|
||||
if (n?.getInnerNodes) return;
|
||||
}
|
||||
|
||||
for (const src of detail.output.images) {
|
||||
|
||||
Reference in New Issue
Block a user