Add infinite scroll, gone/private tiles, and live-only scrape strip
This commit is contained in:
@@ -136,6 +136,12 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vk-media-load-more {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.vk-media-prompt {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
|
||||
+37
-8
@@ -24,6 +24,8 @@
|
||||
authors: null,
|
||||
errors: [],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
more: false,
|
||||
stats: null,
|
||||
metaFields: JSON.parse(
|
||||
localStorage.getItem("vk-media-meta-fields") ??
|
||||
@@ -227,24 +229,29 @@
|
||||
renderStatus();
|
||||
}
|
||||
|
||||
async function loadMedia(reset) {
|
||||
if (!state.loaded) return;
|
||||
const PAGE_SIZE = 60;
|
||||
|
||||
async function loadMedia(reset) {
|
||||
if (!state.loaded || state.loading) return;
|
||||
|
||||
state.loading = true;
|
||||
if (reset) {
|
||||
state.items = [];
|
||||
state.offset = 0;
|
||||
state.more = false;
|
||||
}
|
||||
|
||||
try {
|
||||
const items = await window.VK.api.get(
|
||||
`/api/media?limit=60&offset=${state.offset}&${filterQuery()}`
|
||||
`/api/media?limit=${PAGE_SIZE}&offset=${state.offset}&${filterQuery()}`
|
||||
);
|
||||
state.items = state.items.concat(items);
|
||||
state.offset += items.length;
|
||||
state.more = items.length >= PAGE_SIZE;
|
||||
} catch (error) {
|
||||
setStatus(`media load failed: ${error.message}`);
|
||||
renderGrid();
|
||||
return;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
renderGrid();
|
||||
@@ -281,7 +288,8 @@
|
||||
|
||||
function renderScrapes() {
|
||||
scrapeList.replaceChildren();
|
||||
const active = state.scrapes.filter(scrape => ["queued", "running", "paused"].includes(scrape.state));
|
||||
// Only live jobs: a paused scrape is not running and belongs in History, not the strip.
|
||||
const active = state.scrapes.filter(scrape => ["queued", "running"].includes(scrape.state));
|
||||
|
||||
for (const scrape of active) {
|
||||
const row = el("div", "vk-media-scrape");
|
||||
@@ -421,7 +429,14 @@
|
||||
}
|
||||
|
||||
function placeholder(media, item) {
|
||||
media.replaceChildren(el("span", "vk-media-card-kind", item.kind.toUpperCase()));
|
||||
const error = item.previewError ?? "";
|
||||
let label = item.kind.toUpperCase();
|
||||
if (/\b403\b/.test(error)) label = "PRIVATE";
|
||||
else if (/\b(404|410)\b/.test(error)) label = "GONE";
|
||||
|
||||
const node = el("span", "vk-media-card-kind", label);
|
||||
if (error) node.title = error;
|
||||
media.replaceChildren(node);
|
||||
}
|
||||
|
||||
/** Only real video files can be framed; embed pages (YouTube etc.) never are. */
|
||||
@@ -535,7 +550,15 @@
|
||||
}
|
||||
|
||||
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."));
|
||||
|
||||
if (!state.items.length) {
|
||||
grid.append(el("div", "vk-media-status", "No media matches the filters."));
|
||||
} else if (state.more) {
|
||||
const more = el("button", "btn btn-secondary vk-media-load-more", "Load more");
|
||||
more.type = "button";
|
||||
more.addEventListener("click", () => void loadMedia(false));
|
||||
grid.append(more);
|
||||
}
|
||||
}
|
||||
|
||||
function filterSelect(options, value, onChange) {
|
||||
@@ -774,6 +797,12 @@
|
||||
if (state.view === "media") void loadScrapes();
|
||||
}, 4000);
|
||||
|
||||
// Infinite scroll: fetch the next page before the grid bottom is reached.
|
||||
grid.addEventListener("scroll", () => {
|
||||
if (state.loading || !state.more) return;
|
||||
if (grid.scrollTop + grid.clientHeight >= grid.scrollHeight - 240) void loadMedia(false);
|
||||
});
|
||||
|
||||
// ---- Scrape popup: server or group-chat trees ----
|
||||
|
||||
const scrapeModal = document.createElement("div");
|
||||
|
||||
Reference in New Issue
Block a user