diff --git a/AquaNet/src/pages/MaiPhoto.svelte b/AquaNet/src/pages/MaiPhoto.svelte
index 95b99ebf..92cfe60b 100644
--- a/AquaNet/src/pages/MaiPhoto.svelte
+++ b/AquaNet/src/pages/MaiPhoto.svelte
@@ -14,16 +14,46 @@
let loading = true;
let error: any = null;
+ // Monotonic token so a slow response can never overwrite a newer one
+ let requestSeq = 0;
+
+ function readPageFromUrl(): number {
+ const raw = new URL(window.location.toString()).searchParams.get("page");
+ return raw ? parseInt(raw, 10) || 1 : 1;
+ }
+
+ /**
+ * Keep the url in sync with the page the server actually served, since
+ * out-of-range pages are clamped server-side.
+ */
+ function syncUrl(effectivePage: number) {
+ const url = new URL(window.location.toString());
+ const current = url.searchParams.get("page");
+ const target = effectivePage > 1 ? effectivePage.toString() : null;
+ if (current === target) return;
+ if (target === null) url.searchParams.delete("page");
+ else url.searchParams.set("page", target);
+ history.replaceState({}, "", url.toString());
+ }
+
async function loadPhotos(targetPage: number) {
+ const seq = ++requestSeq;
+ // Apply the target immediately so the pager and further clicks act on it
+ // instead of on the page that is still being fetched
+ page = targetPage;
loading = true;
error = null;
try {
- photoData = await GAME.photos(targetPage, pageSize);
- page = photoData.page;
+ const data = await GAME.photos(targetPage, pageSize);
+ if (seq !== requestSeq) return;
+ photoData = data;
+ page = data.page;
+ syncUrl(data.page);
} catch (e) {
+ if (seq !== requestSeq) return;
error = e;
} finally {
- loading = false;
+ if (seq === requestSeq) loading = false;
}
}
@@ -38,18 +68,9 @@
}
onMount(() => {
- const url = new URL(window.location.toString());
- const pageParam = url.searchParams.get("page");
- if (pageParam) {
- page = parseInt(pageParam, 10) || 1;
- }
- loadPhotos(page);
+ loadPhotos(readPageFromUrl());
- const onPopState = () => {
- const currentUrl = new URL(window.location.toString());
- const p = parseInt(currentUrl.searchParams.get("page") || "1", 10) || 1;
- loadPhotos(p);
- };
+ const onPopState = () => loadPhotos(readPageFromUrl());
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
@@ -66,7 +87,7 @@
{:else if error}
{t('maiphoto.none')}{:else} {#if photoData.totalPages > 1} diff --git a/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt b/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt index c040b38d..e2685601 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt @@ -250,9 +250,11 @@ class Maimai2( } } else { val pageSize = (size ?: 12).coerceIn(1, 100) - val pageNum = page.coerceAtLeast(1) val total = files.size val totalPages = if (total == 0) 0 else (total + pageSize - 1) / pageSize + // Clamp to the nearest valid page: out-of-range pages return the closest real page + // instead of an empty grid, and the offset can no longer overflow Int + val pageNum = page.coerceIn(1, maxOf(1, totalPages)) val pagedFiles = files.drop((pageNum - 1) * pageSize).take(pageSize) val photos = pagedFiles.map { photoHashMap.computeIfAbsent(it) { f -> myPhotoGetHash(f) }