Add an infinite/page toggle with a pager

This commit is contained in:
Fisto
2026-09-18 16:46:25 +02:00
parent 94ddfdeb31
commit 13ffad34d4
4 changed files with 176 additions and 5 deletions
+46
View File
@@ -839,3 +839,49 @@
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
color: var(--color-neutral-500);
}
/* Infinite/page toggle and pager. */
.vk-media-page-controls {
display: inline-flex;
align-items: center;
gap: 6px;
}
.vk-media-toggle {
display: inline-flex;
border: 1px solid var(--color-divider);
border-radius: 5px;
overflow: hidden;
}
.vk-toggle-half {
border: 0;
background: var(--color-surface);
color: var(--color-neutral-400);
font: 500 11px/1.4 ui-monospace, Menlo, monospace;
padding: 3px 8px;
cursor: pointer;
}
.vk-toggle-half.vk-toggle-on {
background: var(--color-accent-400, #4f8cff);
color: #fff;
}
.vk-media-page-nav {
display: inline-flex;
align-items: center;
gap: 6px;
}
.vk-media-page-input {
width: 58px;
}
.vk-media-page-end {
grid-column: 1 / -1;
padding: 10px 0;
text-align: center;
font: 400 11px/1.5 ui-monospace, Menlo, monospace;
color: var(--color-neutral-500);
}
+118 -4
View File
@@ -33,6 +33,9 @@
),
filters: defaultFilters(),
zoom: Number(localStorage.getItem("vk-media-zoom") ?? 50),
pageMode: localStorage.getItem("vk-media-page-mode") === "page" ? "page" : "infinite",
page: Math.max(1, Number(localStorage.getItem("vk-media-page")) || 1),
pageCooldownUntil: 0,
draft: null,
prefetched: null,
prefetchPromise: null,
@@ -362,6 +365,7 @@
state.stats = null;
}
renderStatus();
if (state.pageMode === "page" && state.view === "media") renderToolbar();
}
/** Append only the cards that are new, so existing images never get rebuilt. */
@@ -389,7 +393,8 @@
grid.replaceChildren();
for (const item of state.items) grid.append(renderCard(item));
if (!state.items.length) grid.append(el("div", "vk-media-status", "No media matches the filters."));
grid.append(buffer);
if (state.pageMode === "infinite") grid.append(buffer);
else grid.append(el("div", "vk-media-page-end", `End of page ${state.page}`));
layoutBuffer();
}
@@ -402,7 +407,7 @@
if (reset) {
state.items = [];
state.itemKeys = new Set();
state.offset = 0;
state.offset = state.pageMode === "page" ? (state.page - 1) * state.pageSize : 0;
state.more = true;
state.prefetched = null;
state.prefetchPromise = null;
@@ -431,12 +436,18 @@
layoutBuffer();
void loadStats();
void prefetchNext();
void fillBuffer();
if (state.pageMode === "infinite") {
void prefetchNext();
void fillBuffer();
} else {
state.pageCooldownUntil = Date.now() + 2000;
renderToolbar();
}
}
/** Warm the next page's JSON and preview images before the user reaches them. */
async function prefetchNext() {
if (state.pageMode !== "infinite") return;
if (!state.loaded || !state.more || state.prefetched || state.prefetchPromise || state.loading) return;
state.prefetchPromise = window.VK.api
@@ -488,6 +499,12 @@
* 1.5 viewport heights and shrinks as the end of the filtered results comes closer.
*/
function layoutBuffer() {
if (state.pageMode !== "infinite") {
buffer.style.height = "0px";
bufferNote.textContent = "";
bufferRetry.hidden = true;
return;
}
if (!state.loaded) {
buffer.style.height = "0px";
bufferNote.textContent = "";
@@ -529,8 +546,46 @@
);
}
function pageCount() {
const total = filteredTotal();
if (total === null) return null;
return Math.max(1, Math.ceil(total / Math.max(1, state.pageSize)));
}
function hasNextPage() {
const count = pageCount();
return count === null ? state.more : state.page < count;
}
function savePage() {
localStorage.setItem("vk-media-page", String(state.page));
}
function setPageMode(mode) {
if (state.pageMode === mode) return;
state.pageMode = mode;
localStorage.setItem("vk-media-page-mode", mode);
state.prefetched = null;
state.page = 1;
savePage();
if (state.loaded) void loadMedia(true);
renderToolbar();
}
function gotoPage(page) {
const count = pageCount();
const wanted = Math.max(1, Math.round(Number(page) || 1));
state.page = count ? Math.min(count, wanted) : wanted;
savePage();
state.loaded = true;
state.pageCooldownUntil = Date.now() + 2000;
void loadMedia(true);
renderToolbar();
}
/** Keep loading while the buffer is on screen, so short filtered pages never dead-stop. */
function fillBuffer() {
if (state.pageMode !== "infinite") return;
if (!state.more || state.loading || !state.loaded) return;
const top = buffer.getBoundingClientRect().top;
const bottom = grid.getBoundingClientRect().bottom;
@@ -1297,6 +1352,50 @@
}
/** Dropdown with the metadata-field checkboxes. */
/** Infinite/page flippy toggle plus the pager shown in page mode. */
function pageControls() {
const wrap = el("span", "vk-media-page-controls");
const toggle = el("span", "vk-media-toggle");
for (const [mode, label] of [
["infinite", "Infinite"],
["page", "Page"]
]) {
const half = el("button", `vk-toggle-half${state.pageMode === mode ? " vk-toggle-on" : ""}`, label);
half.type = "button";
half.addEventListener("click", () => setPageMode(mode));
toggle.append(half);
}
wrap.append(toggle);
if (state.pageMode === "page") {
const count = pageCount();
const nav = el("span", "vk-media-page-nav");
const prev = button("Prev", () => gotoPage(state.page - 1));
prev.disabled = state.page <= 1;
const input = document.createElement("input");
input.type = "number";
input.min = "1";
input.max = count ? String(count) : "";
input.value = String(state.page);
input.className = "vk-media-input vk-media-page-input";
input.addEventListener("change", () => gotoPage(input.value));
input.addEventListener("keydown", event => {
if (event.key === "Enter") gotoPage(input.value);
});
const next = button("Next", () => gotoPage(state.page + 1));
next.disabled = !hasNextPage();
nav.append(prev, input, el("span", "vk-media-status", ` / ${count ?? "?"}`), next);
wrap.append(nav);
}
return wrap;
}
function metaMenu() {
const wrap = el("span", "vk-media-meta");
const panel = el("div", "vk-media-meta-menu");
@@ -1376,6 +1475,8 @@
}
function applyDraft() {
state.page = 1;
savePage();
state.kind = state.draft.kind;
state.filters = {
source: state.draft.source,
@@ -1504,6 +1605,7 @@
toolbar.append(peopleMenu());
toolbar.append(button("Advanced search", toggleFilterDrawer));
toolbar.append(metaMenu());
toolbar.append(pageControls());
toolbar.append(el("span", "vk-media-spacer"));
toolbar.append(button("Apply filters", applyDraft));
toolbar.append(button("Reset", resetMediaView));
@@ -1892,6 +1994,9 @@
state.loaded = false;
state.loading = false;
state.more = false;
state.page = 1;
state.pageCooldownUntil = 0;
savePage();
state.prefetched = null;
state.items = [];
state.offset = 0;
@@ -1992,6 +2097,15 @@
grid.addEventListener("scroll", () => {
if (!state.loaded) return;
const remaining = grid.scrollHeight - grid.scrollTop - grid.clientHeight;
if (state.pageMode === "page") {
// One page at a time, and only after the cooldown following the last load.
if (remaining < 80 && Date.now() >= state.pageCooldownUntil && hasNextPage()) {
gotoPage(state.page + 1);
}
return;
}
if (remaining < 400) void loadNextPage();
});