From 099cc6f704b4b1aad4202a4e8efb397e1484e00a Mon Sep 17 00:00:00 2001 From: wrt122311 <1720122311@qq.com> Date: Tue, 5 May 2026 15:24:02 +0800 Subject: [PATCH] feat: Add pagination and random display feature --- saa_character_selector.py | 39 +++++++++++++++------ web/saa_selector.js | 74 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/saa_character_selector.py b/saa_character_selector.py index fc17652..a4c555e 100644 --- a/saa_character_selector.py +++ b/saa_character_selector.py @@ -253,12 +253,13 @@ class _SAADataStore: result.append({"name": g, "count": counts.get(g, 0)}) return result - def list_characters(self, search="", group="All", limit=120, favorites_only=False): + def list_characters(self, search="", group="All", limit=100, page=1, favorites_only=False, seed=None): with self._lock: chars = self._characters favs = self._favorites q = (search or "").strip().lower() - result = [] + + filtered = [] for item in chars: if favorites_only and item["id"] not in favs: continue @@ -267,6 +268,19 @@ class _SAADataStore: if q: if q not in item["name_en"].lower() and q not in item["name_zh"].lower() and q not in item["origin"].lower(): continue + filtered.append(item) + + if seed is not None and seed != "": + import random + rng = random.Random(seed) + rng.shuffle(filtered) + + total = len(filtered) + start_idx = (page - 1) * limit + end_idx = start_idx + limit + + result = [] + for item in filtered[start_idx:end_idx]: result.append( { "id": item["id"], @@ -277,9 +291,7 @@ class _SAADataStore: "is_favorite": item["id"] in favs, } ) - if len(result) >= limit: - break - return result + return {"items": result, "total": total, "page": page, "limit": limit} def get_character(self, char_id): item = self._resolve_character(char_id) @@ -387,13 +399,20 @@ async def saa_selector_characters(request): search = request.query.get("search", "") group = request.query.get("group", "All") fav_only = request.query.get("favorites_only", "false").lower() == "true" + seed = request.query.get("seed", "") try: - limit = int(request.query.get("limit", "120")) + limit = int(request.query.get("limit", "100")) except ValueError: - limit = 120 - limit = max(1, min(600, limit)) - items = STORE.list_characters(search=search, group=group, limit=limit, favorites_only=fav_only) - return web.json_response({"items": items}) + limit = 100 + limit = max(1, limit) + try: + page = int(request.query.get("page", "1")) + except ValueError: + page = 1 + page = max(1, page) + + data = STORE.list_characters(search=search, group=group, limit=limit, page=page, favorites_only=fav_only, seed=seed) + return web.json_response(data) @PromptServer.instance.routes.post("/saa_selector/favorite/{char_id}") async def saa_selector_toggle_favorite(request): diff --git a/web/saa_selector.js b/web/saa_selector.js index bcc249b..3ac4451 100644 --- a/web/saa_selector.js +++ b/web/saa_selector.js @@ -206,6 +206,8 @@ function attachUI(node) { favToggleBtn.textContent = "☆ Fav"; favToggleBtn.style.color = "#ccc"; } + currentPage = 1; + currentSeed = ""; loadCharacters().catch((err) => { status.textContent = `Filter failed: ${String(err)}`; }); @@ -221,6 +223,49 @@ function attachUI(node) { 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"); @@ -265,6 +310,7 @@ function attachUI(node) { gridRow.appendChild(scrollWrap); wrap.appendChild(top); + wrap.appendChild(paginationRow); wrap.appendChild(progressRow); wrap.appendChild(status); wrap.appendChild(gridRow); @@ -281,6 +327,9 @@ function attachUI(node) { let hasHydratedAfterReady = false; let lastSyncedGroupValue = ""; let suppressWidgetCallback = false; + + let currentPage = 1; + let currentSeed = ""; function updateResponsiveColumns() { const width = Math.max(320, wrap.clientWidth || 320); @@ -365,11 +414,20 @@ function attachUI(node) { const q = encodeURIComponent(search.value || ""); const g = encodeURIComponent(group.value || "All"); const favOnly = node.__saaFavoritesOnly ? "true" : "false"; - const data = await apiGet(`/saa_selector/characters?search=${q}&group=${g}&limit=200&favorites_only=${favOnly}`); + 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(); - status.textContent = `Loaded ${data.items?.length || 0} items`; + + 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) { @@ -383,6 +441,8 @@ function attachUI(node) { } if (force || nextGroup !== lastSyncedGroupValue) { lastSyncedGroupValue = nextGroup; + currentPage = 1; + currentSeed = ""; await loadCharacters(); } } @@ -424,6 +484,8 @@ function attachUI(node) { pLabel.textContent = "0%"; hasHydratedAfterReady = false; lastIsLoading = true; + currentPage = 1; + currentSeed = ""; await apiPost("/saa_selector/reload"); refreshStatus(); }); @@ -431,6 +493,8 @@ function attachUI(node) { search.addEventListener("input", () => { if (searchTimer) clearTimeout(searchTimer); searchTimer = setTimeout(() => { + currentPage = 1; + currentSeed = ""; loadCharacters().catch((err) => { status.textContent = `Search failed: ${String(err)}`; }); @@ -438,6 +502,8 @@ function attachUI(node) { }); searchClearBtn.addEventListener("click", () => { search.value = ""; + currentPage = 1; + currentSeed = ""; loadCharacters().catch((err) => { status.textContent = `Search failed: ${String(err)}`; }); @@ -455,6 +521,8 @@ function attachUI(node) { sourceWidget.value = "All"; node.setDirtyCanvas(true, true); } + currentPage = 1; + currentSeed = ""; loadCharacters().catch((err) => { status.textContent = `Filter failed: ${String(err)}`; }); @@ -477,6 +545,8 @@ function attachUI(node) { suppressWidgetCallback = false; node.setDirtyCanvas(true, true); } + currentPage = 1; + currentSeed = ""; loadCharacters().catch((err) => { status.textContent = `Filter failed: ${String(err)}`; });