feat: Add pagination and random display feature

This commit is contained in:
wrt122311
2026-05-05 15:24:02 +08:00
parent 5f90b3fdbb
commit 099cc6f704
2 changed files with 101 additions and 12 deletions
+29 -10
View File
@@ -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):
+72 -2
View File
@@ -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)}`;
});