626 lines
22 KiB
JavaScript
626 lines
22 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 resolveGroupFromWidget(widget, rawValue = undefined) {
|
|
const value = rawValue === undefined ? widget?.value : rawValue;
|
|
if (typeof value === "string") return value;
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
const values = widget?.options?.values;
|
|
if (Array.isArray(values) && values[value] != null) return String(values[value]);
|
|
return "All";
|
|
}
|
|
if (Array.isArray(value) && value.length > 0) {
|
|
const tail = value[value.length - 1];
|
|
if (typeof tail === "string") return tail;
|
|
if (typeof tail === "number") {
|
|
const values = widget?.options?.values;
|
|
if (Array.isArray(values) && values[tail] != null) return String(values[tail]);
|
|
}
|
|
}
|
|
if (value && typeof value === "object") {
|
|
if (typeof value.content === "string") return value.content;
|
|
if (typeof value.value === "string") return value.value;
|
|
}
|
|
if (value == null) return "All";
|
|
return String(value);
|
|
}
|
|
|
|
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 imgWrap = document.createElement("div");
|
|
imgWrap.className = "saa-img-wrap";
|
|
|
|
const img = document.createElement("img");
|
|
img.className = "saa-thumb";
|
|
img.alt = `${item.name_en}`;
|
|
const safeThumbUrl = `/saa_selector/thumb/${encodeURIComponent(item.id || "")}`;
|
|
img.src = safeThumbUrl;
|
|
|
|
const favBtn = document.createElement("div");
|
|
favBtn.className = "saa-fav-btn";
|
|
if (item.is_favorite) favBtn.classList.add("active");
|
|
favBtn.textContent = "★";
|
|
favBtn.title = "Toggle Favorite";
|
|
favBtn.addEventListener("click", async (e) => {
|
|
e.stopPropagation();
|
|
try {
|
|
const res = await apiPost(`/saa_selector/favorite/${encodeURIComponent(item.id)}`);
|
|
item.is_favorite = res.is_favorite;
|
|
if (item.is_favorite) {
|
|
favBtn.classList.add("active");
|
|
} else {
|
|
favBtn.classList.remove("active");
|
|
}
|
|
if (node.__saaFavoritesOnly && !item.is_favorite) {
|
|
card.remove();
|
|
}
|
|
} catch(err) {
|
|
console.error("Failed to toggle favorite", err);
|
|
}
|
|
});
|
|
|
|
imgWrap.appendChild(img);
|
|
imgWrap.appendChild(favBtn);
|
|
|
|
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(imgWrap);
|
|
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; height:100%; box-sizing:border-box; }
|
|
.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-input-wrap { display:flex; gap:4px; align-items:center; min-width:0; }
|
|
.saa-input-wrap input { flex:1; min-width:0; }
|
|
.saa-clear-btn { font-size:12px; padding:3px 6px; line-height:1; }
|
|
.saa-grid-row { display:flex; gap:8px; align-items:stretch; flex:1; min-height:0; }
|
|
.saa-grid { flex:1; display:grid; grid-template-columns:repeat(var(--saa-cols, 2), minmax(0, 1fr)); gap:6px; min-height:0; overflow:auto; padding-right:4px; }
|
|
.saa-scroll-wrap { position:relative; width:18px; height:100%; display:flex; align-items:stretch; justify-content:center; }
|
|
.saa-scroll-rail { position:absolute; z-index:1; top:0; bottom:0; left:50%; transform:translateX(-50%); width:8px; background:#ffffff; border-radius:999px; overflow:hidden; }
|
|
.saa-scroll-fill { position:absolute; top:0; left:0; right:0; height:0%; background:#1e90ff; border-radius:999px; }
|
|
.saa-scroll-thumb { position:absolute; z-index:2; left:50%; transform:translate(-50%, -50%); width:18px; height:18px; border-radius:50%; background:#1e90ff; border:2px solid #ffffff; pointer-events:none; }
|
|
.saa-scroll-progress { position:absolute; z-index:3; inset:0; writing-mode: vertical-lr; -webkit-appearance: slider-vertical; width:18px; height:100%; transform: rotate(180deg); opacity:0; cursor:pointer; }
|
|
.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-img-wrap { position:relative; width:100%; aspect-ratio:2/3; }
|
|
.saa-thumb { width:100%; height:100%; object-fit:cover; background:#111; border-radius:4px; display:block; }
|
|
.saa-fav-btn { position:absolute; top:6px; right:6px; font-size:20px; color:#ccc; cursor:pointer; z-index:10; user-select:none; line-height:1; transition: transform 0.1s; background: rgba(0,0,0,0.5); border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; }
|
|
.saa-fav-btn:hover { transform:scale(1.15); background: rgba(0,0,0,0.7); color:#fff; }
|
|
.saa-fav-btn.active { color:#ffb700; }
|
|
.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 searchWrap = document.createElement("div");
|
|
searchWrap.className = "saa-input-wrap";
|
|
|
|
const search = document.createElement("input");
|
|
search.type = "text";
|
|
search.placeholder = "Search name/origin";
|
|
const searchClearBtn = document.createElement("button");
|
|
searchClearBtn.type = "button";
|
|
searchClearBtn.className = "saa-clear-btn";
|
|
searchClearBtn.textContent = "x";
|
|
searchWrap.appendChild(search);
|
|
searchWrap.appendChild(searchClearBtn);
|
|
|
|
const groupSearchWrap = document.createElement("div");
|
|
groupSearchWrap.className = "saa-input-wrap";
|
|
const groupSearch = document.createElement("input");
|
|
groupSearch.type = "text";
|
|
groupSearch.placeholder = "Search group";
|
|
const groupSearchClearBtn = document.createElement("button");
|
|
groupSearchClearBtn.type = "button";
|
|
groupSearchClearBtn.className = "saa-clear-btn";
|
|
groupSearchClearBtn.textContent = "x";
|
|
groupSearchWrap.appendChild(groupSearch);
|
|
groupSearchWrap.appendChild(groupSearchClearBtn);
|
|
|
|
const group = document.createElement("select");
|
|
const favToggleBtn = document.createElement("button");
|
|
favToggleBtn.type = "button";
|
|
favToggleBtn.textContent = "☆ Fav";
|
|
favToggleBtn.style.color = "#ccc";
|
|
favToggleBtn.addEventListener("click", () => {
|
|
node.__saaFavoritesOnly = !node.__saaFavoritesOnly;
|
|
if (node.__saaFavoritesOnly) {
|
|
favToggleBtn.textContent = "★ Fav";
|
|
favToggleBtn.style.color = "#ffb700";
|
|
} else {
|
|
favToggleBtn.textContent = "☆ Fav";
|
|
favToggleBtn.style.color = "#ccc";
|
|
}
|
|
currentPage = 1;
|
|
currentSeed = "";
|
|
loadCharacters().catch((err) => {
|
|
status.textContent = `Filter failed: ${String(err)}`;
|
|
});
|
|
});
|
|
|
|
const refreshBtn = document.createElement("button");
|
|
refreshBtn.type = "button";
|
|
refreshBtn.textContent = "Reload";
|
|
|
|
top.appendChild(searchWrap);
|
|
top.appendChild(groupSearchWrap);
|
|
top.appendChild(group);
|
|
top.appendChild(favToggleBtn);
|
|
top.appendChild(refreshBtn);
|
|
|
|
const paginationRow = document.createElement("div");
|
|
paginationRow.className = "saa-top";
|
|
|
|
const randomBtn = document.createElement("button");
|
|
randomBtn.type = "button";
|
|
randomBtn.textContent = "Random";
|
|
|
|
const prevBtn = document.createElement("button");
|
|
prevBtn.type = "button";
|
|
prevBtn.textContent = "<";
|
|
|
|
const pageLabel = document.createElement("span");
|
|
pageLabel.style.fontSize = "12px";
|
|
pageLabel.style.color = "#ccc";
|
|
pageLabel.textContent = "1 / 1";
|
|
|
|
const nextBtn = document.createElement("button");
|
|
nextBtn.type = "button";
|
|
nextBtn.textContent = ">";
|
|
|
|
paginationRow.appendChild(randomBtn);
|
|
paginationRow.appendChild(prevBtn);
|
|
paginationRow.appendChild(pageLabel);
|
|
paginationRow.appendChild(nextBtn);
|
|
|
|
randomBtn.addEventListener("click", () => {
|
|
currentSeed = String(Math.floor(Math.random() * 1000000));
|
|
currentPage = 1;
|
|
loadCharacters().catch(err => status.textContent = `Error: ${String(err)}`);
|
|
});
|
|
|
|
prevBtn.addEventListener("click", () => {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
loadCharacters().catch(err => status.textContent = `Error: ${String(err)}`);
|
|
}
|
|
});
|
|
|
|
nextBtn.addEventListener("click", () => {
|
|
currentPage++;
|
|
loadCharacters().catch(err => status.textContent = `Error: ${String(err)}`);
|
|
});
|
|
|
|
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 scrollWrap = document.createElement("div");
|
|
scrollWrap.className = "saa-scroll-wrap";
|
|
const scrollRail = document.createElement("div");
|
|
scrollRail.className = "saa-scroll-rail";
|
|
const scrollFill = document.createElement("div");
|
|
scrollFill.className = "saa-scroll-fill";
|
|
scrollRail.appendChild(scrollFill);
|
|
const scrollThumb = document.createElement("div");
|
|
scrollThumb.className = "saa-scroll-thumb";
|
|
|
|
const scrollProgress = document.createElement("input");
|
|
scrollProgress.type = "range";
|
|
scrollProgress.className = "saa-scroll-progress";
|
|
scrollProgress.min = "0";
|
|
scrollProgress.max = "100";
|
|
scrollProgress.value = "0";
|
|
scrollProgress.orient = "vertical";
|
|
scrollWrap.appendChild(scrollRail);
|
|
scrollWrap.appendChild(scrollThumb);
|
|
scrollWrap.appendChild(scrollProgress);
|
|
|
|
const grid = document.createElement("div");
|
|
grid.className = "saa-grid";
|
|
const gridRow = document.createElement("div");
|
|
gridRow.className = "saa-grid-row";
|
|
gridRow.appendChild(grid);
|
|
gridRow.appendChild(scrollWrap);
|
|
|
|
wrap.appendChild(top);
|
|
wrap.appendChild(paginationRow);
|
|
wrap.appendChild(progressRow);
|
|
wrap.appendChild(status);
|
|
wrap.appendChild(gridRow);
|
|
|
|
node.addDOMWidget("saa_selector", "saa_selector", wrap, {
|
|
getMinHeight: () => 390,
|
|
getMaxHeight: () => 9999,
|
|
getHeight: () => 430,
|
|
});
|
|
|
|
let searchTimer = null;
|
|
let isDraggingProgress = false;
|
|
let lastIsLoading = null;
|
|
let hasHydratedAfterReady = false;
|
|
let lastSyncedGroupValue = "";
|
|
let suppressWidgetCallback = false;
|
|
|
|
let currentPage = 1;
|
|
let currentSeed = "";
|
|
|
|
function updateResponsiveColumns() {
|
|
const width = Math.max(320, wrap.clientWidth || 320);
|
|
// Card minimum width target ~170px, with gap and margins accounted for.
|
|
const cols = Math.max(1, Math.floor((width - 16) / 176));
|
|
grid.style.setProperty("--saa-cols", String(cols));
|
|
}
|
|
|
|
function updateScrollProgressFromGrid() {
|
|
const maxScroll = Math.max(0, grid.scrollHeight - grid.clientHeight);
|
|
if (maxScroll <= 0) {
|
|
scrollProgress.value = "0";
|
|
scrollProgress.disabled = true;
|
|
scrollFill.style.height = "0%";
|
|
scrollThumb.style.top = "0%";
|
|
return;
|
|
}
|
|
scrollProgress.disabled = false;
|
|
if (isDraggingProgress) return;
|
|
const ratio = 100 - (grid.scrollTop / maxScroll) * 100;
|
|
scrollProgress.value = String(Math.max(0, Math.min(100, ratio)));
|
|
const fillRatio = 100 - Number(scrollProgress.value || 0);
|
|
scrollFill.style.height = `${Math.max(0, Math.min(100, fillRatio))}%`;
|
|
scrollThumb.style.top = `${Math.max(0, Math.min(100, fillRatio))}%`;
|
|
}
|
|
|
|
function renderGroupOptions(allGroups, filterText = "") {
|
|
const current = group.value || "All";
|
|
group.innerHTML = "";
|
|
const q = (filterText || "").trim().toLowerCase();
|
|
const filtered = (allGroups || []).filter((g) => {
|
|
const name = typeof g === "string" ? g : g.name;
|
|
if (!q) return true;
|
|
return name.toLowerCase().includes(q);
|
|
});
|
|
const list = filtered.length > 0 ? filtered : allGroups;
|
|
for (const g of list || []) {
|
|
const groupName = typeof g === "string" ? g : g.name;
|
|
const count = typeof g === "string" ? null : g.count;
|
|
const opt = document.createElement("option");
|
|
opt.value = groupName;
|
|
opt.textContent = count === null ? groupName : `${groupName} (${count})`;
|
|
group.appendChild(opt);
|
|
}
|
|
node.__saaGroupNames = (allGroups || []).map((g) => (typeof g === "string" ? g : g.name));
|
|
if (node.__saaGroupNames.includes(current)) {
|
|
group.value = current;
|
|
}
|
|
}
|
|
|
|
function syncSourceWidgetOptions(sourceWidget) {
|
|
if (!sourceWidget || !Array.isArray(node.__saaGroupNames) || node.__saaGroupNames.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const current = resolveGroupFromWidget(sourceWidget);
|
|
if (!sourceWidget.options || typeof sourceWidget.options !== "object") {
|
|
sourceWidget.options = {};
|
|
}
|
|
sourceWidget.options.values = [...node.__saaGroupNames];
|
|
|
|
const next = node.__saaGroupNames.includes(current) ? current : "All";
|
|
const idx = sourceWidget.options.values.indexOf(next);
|
|
sourceWidget.value = idx >= 0 ? next : "All";
|
|
}
|
|
|
|
async function loadGroups() {
|
|
const data = await apiGet("/saa_selector/groups");
|
|
const allGroups = data.groups || [];
|
|
node.__saaGroupsRaw = allGroups;
|
|
renderGroupOptions(allGroups, groupSearch.value);
|
|
|
|
const sourceWidget = byName(node, "source_group");
|
|
if (sourceWidget) {
|
|
syncSourceWidgetOptions(sourceWidget);
|
|
const current = resolveGroupFromWidget(sourceWidget);
|
|
group.value = node.__saaGroupNames.includes(current) ? current : "All";
|
|
}
|
|
}
|
|
|
|
async function loadCharacters() {
|
|
const q = encodeURIComponent(search.value || "");
|
|
const g = encodeURIComponent(group.value || "All");
|
|
const favOnly = node.__saaFavoritesOnly ? "true" : "false";
|
|
const s = encodeURIComponent(currentSeed || "");
|
|
const data = await apiGet(`/saa_selector/characters?search=${q}&group=${g}&limit=100&page=${currentPage}&seed=${s}&favorites_only=${favOnly}`);
|
|
renderCards(node, grid, data.items || []);
|
|
updateResponsiveColumns();
|
|
updateScrollProgressFromGrid();
|
|
|
|
const total = data.total || 0;
|
|
const limit = data.limit || 100;
|
|
const pages = Math.max(1, Math.ceil(total / limit));
|
|
pageLabel.textContent = `${currentPage} / ${pages}`;
|
|
prevBtn.disabled = currentPage <= 1;
|
|
nextBtn.disabled = currentPage >= pages;
|
|
|
|
status.textContent = `Loaded ${data.items?.length || 0} items (Total: ${total})`;
|
|
}
|
|
|
|
async function syncFromSourceWidget(force = false) {
|
|
const sourceWidget = byName(node, "source_group");
|
|
if (!sourceWidget) return;
|
|
const widgetGroup = resolveGroupFromWidget(sourceWidget);
|
|
const allowed = node.__saaGroupNames || [];
|
|
const nextGroup = allowed.includes(widgetGroup) ? widgetGroup : "All";
|
|
if (group.value !== nextGroup) {
|
|
group.value = nextGroup;
|
|
}
|
|
if (force || nextGroup !== lastSyncedGroupValue) {
|
|
lastSyncedGroupValue = nextGroup;
|
|
currentPage = 1;
|
|
currentSeed = "";
|
|
await loadCharacters();
|
|
}
|
|
}
|
|
|
|
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))}%`;
|
|
const isLoading = Boolean(s.is_loading);
|
|
progressRow.style.display = isLoading ? "flex" : "none";
|
|
const becameReady = lastIsLoading === true && isLoading === false;
|
|
lastIsLoading = isLoading;
|
|
if (s.error) {
|
|
status.textContent = `Error: ${s.error}`;
|
|
progressRow.style.display = "flex";
|
|
hasHydratedAfterReady = false;
|
|
} else {
|
|
if (isLoading) {
|
|
status.textContent = s.status || "loading";
|
|
hasHydratedAfterReady = false;
|
|
}
|
|
}
|
|
if (!s.is_loading && (s.progress || 0) >= 100 && (!hasHydratedAfterReady || becameReady)) {
|
|
await loadGroups();
|
|
await loadCharacters();
|
|
hasHydratedAfterReady = true;
|
|
}
|
|
await syncFromSourceWidget(false);
|
|
} catch (err) {
|
|
status.textContent = `Status failed: ${String(err)}`;
|
|
}
|
|
}
|
|
|
|
refreshBtn.addEventListener("click", async () => {
|
|
status.textContent = "Reloading...";
|
|
progressRow.style.display = "flex";
|
|
progress.value = 0;
|
|
pLabel.textContent = "0%";
|
|
hasHydratedAfterReady = false;
|
|
lastIsLoading = true;
|
|
currentPage = 1;
|
|
currentSeed = "";
|
|
await apiPost("/saa_selector/reload");
|
|
refreshStatus();
|
|
});
|
|
|
|
search.addEventListener("input", () => {
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => {
|
|
currentPage = 1;
|
|
currentSeed = "";
|
|
loadCharacters().catch((err) => {
|
|
status.textContent = `Search failed: ${String(err)}`;
|
|
});
|
|
}, 250);
|
|
});
|
|
searchClearBtn.addEventListener("click", () => {
|
|
search.value = "";
|
|
currentPage = 1;
|
|
currentSeed = "";
|
|
loadCharacters().catch((err) => {
|
|
status.textContent = `Search failed: ${String(err)}`;
|
|
});
|
|
});
|
|
|
|
groupSearch.addEventListener("input", () => {
|
|
renderGroupOptions(node.__saaGroupsRaw || [], groupSearch.value);
|
|
});
|
|
groupSearchClearBtn.addEventListener("click", () => {
|
|
groupSearch.value = "";
|
|
renderGroupOptions(node.__saaGroupsRaw || [], "");
|
|
group.value = "All";
|
|
const sourceWidget = byName(node, "source_group");
|
|
if (sourceWidget) {
|
|
sourceWidget.value = "All";
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
currentPage = 1;
|
|
currentSeed = "";
|
|
loadCharacters().catch((err) => {
|
|
status.textContent = `Filter failed: ${String(err)}`;
|
|
});
|
|
});
|
|
|
|
group.addEventListener("change", () => {
|
|
const sourceWidget = byName(node, "source_group");
|
|
if (sourceWidget) {
|
|
const allowed = Array.isArray(node.__saaGroupNames) ? node.__saaGroupNames : ["All"];
|
|
const picked = allowed.includes(group.value) ? group.value : "All";
|
|
syncSourceWidgetOptions(sourceWidget);
|
|
const values = sourceWidget.options?.values;
|
|
suppressWidgetCallback = true;
|
|
if (Array.isArray(values)) {
|
|
const idx = values.indexOf(picked);
|
|
sourceWidget.value = idx >= 0 ? picked : "All";
|
|
} else {
|
|
sourceWidget.value = picked;
|
|
}
|
|
suppressWidgetCallback = false;
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
currentPage = 1;
|
|
currentSeed = "";
|
|
loadCharacters().catch((err) => {
|
|
status.textContent = `Filter failed: ${String(err)}`;
|
|
});
|
|
lastSyncedGroupValue = group.value || "All";
|
|
});
|
|
|
|
// Immediate sync when user changes the built-in source_group widget (left/right arrows).
|
|
const sourceWidget = byName(node, "source_group");
|
|
if (sourceWidget && !sourceWidget.__saaHooked) {
|
|
sourceWidget.__saaHooked = true;
|
|
const oldCb = sourceWidget.callback;
|
|
sourceWidget.callback = function (value) {
|
|
if (typeof oldCb === "function") {
|
|
oldCb.apply(this, arguments);
|
|
}
|
|
if (suppressWidgetCallback) return;
|
|
const mapped = resolveGroupFromWidget(sourceWidget, value);
|
|
const allowed = node.__saaGroupNames || [];
|
|
const nextGroup = allowed.includes(mapped) ? mapped : "All";
|
|
if (group.value !== nextGroup) {
|
|
group.value = nextGroup;
|
|
}
|
|
syncFromSourceWidget(true).catch((err) => {
|
|
status.textContent = `Sync failed: ${String(err)}`;
|
|
});
|
|
};
|
|
}
|
|
|
|
grid.addEventListener("scroll", updateScrollProgressFromGrid);
|
|
|
|
scrollProgress.addEventListener("pointerdown", () => {
|
|
isDraggingProgress = true;
|
|
});
|
|
scrollProgress.addEventListener("pointerup", () => {
|
|
isDraggingProgress = false;
|
|
updateScrollProgressFromGrid();
|
|
});
|
|
scrollProgress.addEventListener("input", () => {
|
|
const maxScroll = Math.max(0, grid.scrollHeight - grid.clientHeight);
|
|
if (maxScroll <= 0) return;
|
|
const ratio = 1 - Number(scrollProgress.value || 0) / 100;
|
|
grid.scrollTop = maxScroll * ratio;
|
|
const fillRatio = 100 - Number(scrollProgress.value || 0);
|
|
scrollFill.style.height = `${Math.max(0, Math.min(100, fillRatio))}%`;
|
|
scrollThumb.style.top = `${Math.max(0, Math.min(100, fillRatio))}%`;
|
|
});
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
updateResponsiveColumns();
|
|
updateScrollProgressFromGrid();
|
|
});
|
|
resizeObserver.observe(wrap);
|
|
|
|
refreshStatus();
|
|
node.__saaTimer = setInterval(refreshStatus, 1400);
|
|
|
|
const oldRemoved = node.onRemoved;
|
|
node.onRemoved = function () {
|
|
if (node.__saaTimer) clearInterval(node.__saaTimer);
|
|
resizeObserver.disconnect();
|
|
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;
|
|
};
|
|
},
|
|
});
|