fix spam re-order

This commit is contained in:
KingKDot
2026-03-17 00:42:57 -05:00
parent 1e45d62502
commit de59664baf
6 changed files with 28 additions and 39 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ export function registerRenderer(fn) {
} }
export async function loadWithOptions(options = {}) { export async function loadWithOptions(options = {}) {
const { force = false } = options; const { force = false, reorder = false } = options;
if (state.isLoading) { if (state.isLoading) {
state.pendingForce = state.pendingForce || force; state.pendingForce = state.pendingForce || force;
return; return;
@@ -38,7 +38,7 @@ export async function loadWithOptions(options = {}) {
return; return;
} }
state.lastDigest = digest; state.lastDigest = digest;
render(data); render(data, { reorder });
if (!state.thumbnailsRequested) { if (!state.thumbnailsRequested) {
state.thumbnailsRequested = true; state.thumbnailsRequested = true;
+8 -8
View File
@@ -411,7 +411,7 @@ wireModalClose();
const debouncedSearch = debounce(() => { const debouncedSearch = debounce(() => {
state.page = 1; state.page = 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
}, 200); }, 200);
searchInput?.addEventListener("input", (e) => { searchInput?.addEventListener("input", (e) => {
@@ -423,7 +423,7 @@ sortSelect?.addEventListener("change", (e) => {
state.sort = e.target.value; state.sort = e.target.value;
state.page = 1; state.page = 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
}); });
filterStatusSelect?.addEventListener("change", (e) => { filterStatusSelect?.addEventListener("change", (e) => {
@@ -436,21 +436,21 @@ filterStatusSelect?.addEventListener("change", (e) => {
} }
state.page = 1; state.page = 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
}); });
filterOsSelect?.addEventListener("change", (e) => { filterOsSelect?.addEventListener("change", (e) => {
state.filterOs = e.target.value; state.filterOs = e.target.value;
state.page = 1; state.page = 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
}); });
initCountryPicker((code) => { initCountryPicker((code) => {
state.filterCountry = code; state.filterCountry = code;
state.page = 1; state.page = 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
}); });
showOfflineToggle?.addEventListener("change", (e) => { showOfflineToggle?.addEventListener("change", (e) => {
@@ -467,7 +467,7 @@ showOfflineToggle?.addEventListener("change", (e) => {
} }
state.page = 1; state.page = 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
}); });
function updateBulkToolbar() { function updateBulkToolbar() {
@@ -686,14 +686,14 @@ prevBtn?.addEventListener("click", () => {
if (state.page > 1) { if (state.page > 1) {
state.page -= 1; state.page -= 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
} }
}); });
nextBtn?.addEventListener("click", () => { nextBtn?.addEventListener("click", () => {
state.page += 1; state.page += 1;
state.lastDigest = ""; state.lastDigest = "";
loadWithOptions({ force: true }); loadWithOptions({ force: true, reorder: true });
}); });
window.addEventListener("click", (e) => { window.addEventListener("click", (e) => {
+14 -28
View File
@@ -33,7 +33,8 @@ export function createRenderer({
const TOUCH_MOVE_CANCEL_PX = 10; const TOUCH_MOVE_CANCEL_PX = 10;
let renderToken = 0; let renderToken = 0;
function renderMerge(data) { function renderMerge(data, options = {}) {
const { reorder = false } = options;
totalPill.textContent = `${data.online ?? data.total} online / ${data.total} total`; totalPill.textContent = `${data.online ?? data.total} online / ${data.total} total`;
const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize));
pageLabel.textContent = `Page ${data.page} of ${totalPages}`; pageLabel.textContent = `Page ${data.page} of ${totalPages}`;
@@ -47,8 +48,8 @@ export function createRenderer({
typeof window !== "undefined" && typeof window !== "undefined" &&
window.matchMedia && window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches; window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const animateLimit = Math.min(items.length, MAX_ANIMATED_CARDS);
const allowAnimation = !prefersReducedMotion && items.length <= 1000; const hadCards = grid.querySelectorAll("article[data-id]").length > 0;
items.forEach((client) => { items.forEach((client) => {
seen.add(client.id); seen.add(client.id);
@@ -64,11 +65,20 @@ export function createRenderer({
.filter((el) => !seen.has(el.dataset.id)) .filter((el) => !seen.has(el.dataset.id))
.forEach((el) => el.remove()); .forEach((el) => el.remove());
if (reorder) {
items.forEach((client) => {
const card = grid.querySelector(`article[data-id="${client.id}"]`);
if (card) grid.appendChild(card);
});
}
if (newClients.length === 0) { if (newClients.length === 0) {
moveOfflineToBottom();
return; return;
} }
const allowAnimation = !hadCards && !prefersReducedMotion && items.length <= 1000;
const animateLimit = Math.min(newClients.length, MAX_ANIMATED_CARDS);
let idx = 0; let idx = 0;
const insertBatch = () => { const insertBatch = () => {
if (renderId !== renderToken) return; if (renderId !== renderToken) return;
@@ -91,35 +101,11 @@ export function createRenderer({
requestAnimationFrame(insertBatch); requestAnimationFrame(insertBatch);
return; return;
} }
moveOfflineToBottom();
}; };
insertBatch(); insertBatch();
} }
function moveOfflineToBottom() {
const cards = Array.from(grid.querySelectorAll("article[data-id]"));
if (cards.length === 0) return;
const online = [];
const offline = [];
cards.forEach((card) => {
if (card.dataset.online === "true") {
online.push(card);
return;
}
offline.push(card);
});
if (offline.length === 0) return;
const fragment = document.createDocumentFragment();
online.forEach((card) => fragment.appendChild(card));
offline.forEach((card) => fragment.appendChild(card));
grid.appendChild(fragment);
}
function prevBtnState(currentPage, totalPages) { function prevBtnState(currentPage, totalPages) {
const prevBtn = document.getElementById("prev"); const prevBtn = document.getElementById("prev");
const nextBtn = document.getElementById("next"); const nextBtn = document.getElementById("next");
+1 -1
View File
@@ -2,7 +2,7 @@ export const state = {
page: 1, page: 1,
pageSize: 12, pageSize: 12,
searchTerm: "", searchTerm: "",
sort: "last_seen_desc", sort: "stable",
filterStatus: "all", filterStatus: "all",
filterOs: "all", filterOs: "all",
filterCountry: "all", filterCountry: "all",
+1
View File
@@ -92,6 +92,7 @@
id="sort" id="sort"
class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-slate-100" class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-slate-100"
> >
<option value="stable">Default</option>
<option value="last_seen_desc">Newest</option> <option value="last_seen_desc">Newest</option>
<option value="host_asc">Host A→Z</option> <option value="host_asc">Host A→Z</option>
<option value="ping_asc">Ping ↑</option> <option value="ping_asc">Ping ↑</option>
+2
View File
@@ -348,6 +348,8 @@ export function listClients(filters: ListFilters): ListResult {
const orderBy = (() => { const orderBy = (() => {
const bookmark = "bookmarked DESC"; const bookmark = "bookmarked DESC";
switch (sort) { switch (sort) {
case "stable":
return `ORDER BY ${bookmark}, online DESC, id ASC`;
case "ping_asc": case "ping_asc":
return `ORDER BY ${bookmark}, ping_ms IS NULL, ping_ms ASC, id ASC`; return `ORDER BY ${bookmark}, ping_ms IS NULL, ping_ms ASC, id ASC`;
case "ping_desc": case "ping_desc":