Files

229 lines
7.1 KiB
JavaScript

import { app } from "../../scripts/app.js";
const NODE_NAME = "SAACharacterSelector";
function byName(node, name) {
return node.widgets?.find((w) => w.name === name);
}
async function apiGet(url) {
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(`${resp.status} ${resp.statusText}`);
}
return resp.json();
}
async function apiPost(url) {
const resp = await fetch(url, { method: "POST" });
if (!resp.ok) {
throw new Error(`${resp.status} ${resp.statusText}`);
}
return resp.json();
}
function setSelected(node, item) {
const widget = byName(node, "selected_character_id");
if (!widget) return;
widget.value = item?.id || "";
node.setDirtyCanvas(true, true);
}
function renderCards(node, container, items) {
container.innerHTML = "";
const selectedId = byName(node, "selected_character_id")?.value || "";
for (const item of items) {
const card = document.createElement("button");
card.type = "button";
card.className = "saa-card";
if (selectedId === item.id) card.classList.add("active");
const img = document.createElement("img");
img.className = "saa-thumb";
img.alt = `${item.name_en}`;
img.loading = "lazy";
img.src = item.thumb_url;
const title = document.createElement("div");
title.className = "saa-title";
title.textContent = item.name_zh || item.name_en;
const sub = document.createElement("div");
sub.className = "saa-sub";
sub.textContent = `${item.name_en} | ${item.origin}`;
card.appendChild(img);
card.appendChild(title);
card.appendChild(sub);
card.addEventListener("click", () => {
setSelected(node, item);
for (const el of container.querySelectorAll(".saa-card")) {
el.classList.remove("active");
}
card.classList.add("active");
});
container.appendChild(card);
}
}
function ensureStyle() {
if (document.getElementById("saa-selector-style")) return;
const style = document.createElement("style");
style.id = "saa-selector-style";
style.textContent = `
.saa-wrap { display:flex; flex-direction:column; gap:8px; }
.saa-top { display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
.saa-top input, .saa-top select, .saa-top button { font-size:12px; padding:4px 6px; }
.saa-progress-row { display:flex; align-items:center; gap:8px; }
.saa-progress { width:100%; height:10px; }
.saa-grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:6px; max-height:320px; overflow:auto; padding-right:4px; }
.saa-card { display:flex; flex-direction:column; gap:4px; border:1px solid #555; background:#1f1f1f; color:#eee; padding:6px; text-align:left; cursor:pointer; border-radius:6px; }
.saa-card.active { border-color:#58a6ff; box-shadow:0 0 0 1px #58a6ff inset; }
.saa-thumb { width:100%; aspect-ratio:2/3; object-fit:cover; background:#111; border-radius:4px; }
.saa-title { font-size:12px; line-height:1.2; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.saa-sub { font-size:10px; color:#b8b8b8; line-height:1.2; max-height:2.4em; overflow:hidden; }
.saa-status { font-size:11px; color:#cfcfcf; min-height:1.2em; }
`;
document.head.appendChild(style);
}
function attachUI(node) {
if (node.__saaMounted) return;
node.__saaMounted = true;
ensureStyle();
const wrap = document.createElement("div");
wrap.className = "saa-wrap";
const top = document.createElement("div");
top.className = "saa-top";
const search = document.createElement("input");
search.type = "text";
search.placeholder = "Search name/origin";
const group = document.createElement("select");
const refreshBtn = document.createElement("button");
refreshBtn.type = "button";
refreshBtn.textContent = "Reload";
top.appendChild(search);
top.appendChild(group);
top.appendChild(refreshBtn);
const progressRow = document.createElement("div");
progressRow.className = "saa-progress-row";
const progress = document.createElement("progress");
progress.className = "saa-progress";
progress.max = 100;
progress.value = 0;
const pLabel = document.createElement("span");
pLabel.textContent = "0%";
progressRow.appendChild(progress);
progressRow.appendChild(pLabel);
const status = document.createElement("div");
status.className = "saa-status";
status.textContent = "Preparing...";
const grid = document.createElement("div");
grid.className = "saa-grid";
wrap.appendChild(top);
wrap.appendChild(progressRow);
wrap.appendChild(status);
wrap.appendChild(grid);
node.addDOMWidget("saa_selector", "saa_selector", wrap, {
getMinHeight: () => 390,
getMaxHeight: () => 600,
getHeight: () => 430,
});
let searchTimer = null;
async function loadGroups() {
const data = await apiGet("/saa_selector/groups");
group.innerHTML = "";
for (const g of data.groups || []) {
const opt = document.createElement("option");
opt.value = g;
opt.textContent = g;
group.appendChild(opt);
}
}
async function loadCharacters() {
const q = encodeURIComponent(search.value || "");
const g = encodeURIComponent(group.value || "All");
const data = await apiGet(`/saa_selector/characters?search=${q}&group=${g}&limit=200`);
renderCards(node, grid, data.items || []);
status.textContent = `Loaded ${data.items?.length || 0} items`;
}
async function refreshStatus() {
try {
const s = await apiGet("/saa_selector/status");
progress.value = Number(s.progress || 0);
pLabel.textContent = `${Math.round(Number(s.progress || 0))}%`;
if (s.error) {
status.textContent = `Error: ${s.error}`;
} else {
status.textContent = s.status || "idle";
}
if (!s.is_loading && (s.progress || 0) >= 100) {
await loadGroups();
await loadCharacters();
}
} catch (err) {
status.textContent = `Status failed: ${String(err)}`;
}
}
refreshBtn.addEventListener("click", async () => {
status.textContent = "Reloading...";
await apiPost("/saa_selector/reload");
refreshStatus();
});
search.addEventListener("input", () => {
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
loadCharacters().catch((err) => {
status.textContent = `Search failed: ${String(err)}`;
});
}, 250);
});
group.addEventListener("change", () => {
loadCharacters().catch((err) => {
status.textContent = `Filter failed: ${String(err)}`;
});
});
refreshStatus();
node.__saaTimer = setInterval(refreshStatus, 1400);
const oldRemoved = node.onRemoved;
node.onRemoved = function () {
if (node.__saaTimer) clearInterval(node.__saaTimer);
if (oldRemoved) oldRemoved.apply(this, arguments);
};
}
app.registerExtension({
name: "saa.character.selector",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_NAME) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
attachUI(this);
return r;
};
},
});