Add URL-based pagination and editable page input to blobs.html

This commit is contained in:
Laan Tungir
2026-09-24 17:55:29 -04:00
parent 7eae0cee90
commit 427b8c1f0d
2 changed files with 124 additions and 8 deletions
+121 -5
View File
@@ -127,6 +127,30 @@
min-width: 8em;
}
/* Editable page number input between the pagination arrows */
.inpPage {
width: 4.5em;
padding: 4px 6px;
text-align: center;
font-family: var(--font-family);
font-size: 0.9em;
color: var(--primary-color);
background-color: var(--secondary-color);
border: var(--border-width) solid var(--muted-color);
border-radius: var(--border-radius);
-moz-appearance: textfield;
appearance: textfield;
}
.inpPage:focus {
outline: none;
border-color: var(--accent-color);
}
.inpPage::-webkit-outer-spin-button,
.inpPage::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Text blob preview */
.divBlobTextPreview {
position: absolute;
@@ -596,7 +620,8 @@
<button id="btnFilterBinary" class="btn btnServer btnFilter active">Binary</button>
<span style="margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 90%; color: #666;">
<button id="btnPagePrev" class="btn btnServer btnMuted btnPageNav">◀</button>
<span id="pageIndicator"></span>
<input id="inpPage" class="inpPage" type="number" min="1" value="1" inputmode="numeric" title="Jump to page" />
<span id="pageTotal"></span>
&nbsp;(<span id="totalBlobCount">0</span>)
<button id="btnPageNext" class="btn btnServer btnMuted btnPageNav">▶</button>
</span>
@@ -713,6 +738,7 @@
let rowsPerPage = 0; // 0 = auto; loaded from user settings on init
let currentPage = 0; // zero-indexed
let initialBlobRefreshTriggered = false;
let preservePageOnNextRefresh = false; // honor URL page on initial load
// hiddenFilters: categories the user has clicked off. Empty = show all.
let hiddenFilters = new Set();
@@ -757,7 +783,8 @@
const divPostComposerHost = document.getElementById("divPostComposerHost");
const btnClosePostPopup = document.getElementById("btnClosePostPopup");
const totalBlobCount = document.getElementById("totalBlobCount");
const pageIndicator = document.getElementById("pageIndicator");
const inpPage = document.getElementById("inpPage");
const pageTotal = document.getElementById("pageTotal");
const tblBlobsBody = document.getElementById("tblBlobsBody");
const divBlobList = document.getElementById("divBlobList");
const divBlobGrid = document.getElementById("divBlobGrid");
@@ -1624,6 +1651,7 @@
currentPage = 0;
updateFilterButtons();
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
syncPageToUrl(false);
};
const updateFilterButtons = () => {
@@ -1667,10 +1695,40 @@
return getFilteredBlobs().slice(currentPage * size, (currentPage + 1) * size);
};
// Read the 1-indexed `page` query param and convert to a 0-indexed page.
const getPageFromUrl = () => {
const params = new URLSearchParams(window.location.search || '');
const raw = parseInt(params.get('page'), 10);
return Number.isFinite(raw) && raw > 0 ? raw - 1 : 0;
};
// Reflect the current page into the URL as a `page` query param (1-indexed).
// Uses replaceState by default so the URL stays shareable without spamming
// history; pass push=true for user-initiated navigation so Back/Forward work.
const syncPageToUrl = (push = false) => {
const params = new URLSearchParams(window.location.search || '');
if (currentPage > 0) {
params.set('page', String(currentPage + 1));
} else {
params.delete('page');
}
const query = params.toString();
const nextUrl = `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash || ''}`;
if (push) {
window.history.pushState({ page: currentPage }, '', nextUrl);
} else {
window.history.replaceState({ page: currentPage }, '', nextUrl);
}
};
const updatePaginationControls = () => {
const total = getTotalPages();
if (pageIndicator) {
pageIndicator.textContent = total > 1 ? `${currentPage + 1} / ${total}` : '';
if (inpPage) {
inpPage.value = String(currentPage + 1);
inpPage.max = String(total);
}
if (pageTotal) {
pageTotal.textContent = `/ ${total}`;
}
if (btnPagePrev) {
btnPagePrev.classList.toggle('btnMuted', currentPage === 0);
@@ -1682,10 +1740,27 @@
}
};
// Jump to a specific 1-indexed page (clamped to the valid range)
const goToPage = (pageNum) => {
const total = getTotalPages();
let target = parseInt(pageNum, 10);
if (!Number.isFinite(target)) target = currentPage + 1;
target = Math.min(Math.max(target, 1), total) - 1;
if (target !== currentPage) {
currentPage = target;
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
syncPageToUrl(true);
} else {
// Normalize the input display even when the page didn't change
updatePaginationControls();
}
};
const goToPrevPage = () => {
if (currentPage > 0) {
currentPage--;
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
syncPageToUrl(true);
}
};
@@ -1693,6 +1768,7 @@
if (currentPage < getTotalPages() - 1) {
currentPage++;
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
syncPageToUrl(true);
}
};
@@ -1700,6 +1776,7 @@
if (currentPage !== 0) {
currentPage = 0;
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
syncPageToUrl(true);
}
};
@@ -1708,6 +1785,7 @@
if (currentPage !== last) {
currentPage = last;
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
syncPageToUrl(true);
}
};
@@ -2189,7 +2267,15 @@
}, []);
USER_BLOBS = uniqueBlobs.sort((a, b) => b.uploaded - a.uploaded);
currentPage = 0;
if (preservePageOnNextRefresh) {
// Initial load: honor the page requested in the URL, clamped to range
preservePageOnNextRefresh = false;
currentPage = Math.min(currentPage, getTotalPages() - 1);
if (currentPage < 0) currentPage = 0;
} else {
currentPage = 0;
}
console.log(`Total unique blobs across all servers: ${USER_BLOBS.length}`);
@@ -2199,6 +2285,9 @@
updateBlobGrid();
}
// Keep the URL in sync with the resolved page (normalizes/clamps)
syncPageToUrl(false);
console.log(`Loaded ${USER_BLOBS.length} blobs from ${uploadServers.length} server(s)`);
} catch (error) {
console.error(`Failed to load blobs: ${error.message}`);
@@ -2452,6 +2541,7 @@
currentPage = 0;
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
updateRowsButtons();
syncPageToUrl(false);
patchUserSettings({ blobs: { gridColumns, rowsPerPage: val } }).catch(() => {});
};
@@ -2574,11 +2664,33 @@
if (e.target === divPostPopup) closePostPopup();
});
// Editable page input: commit on Enter or blur/change
if (inpPage) {
inpPage.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
goToPage(inpPage.value);
inpPage.blur();
}
});
inpPage.addEventListener('change', () => goToPage(inpPage.value));
inpPage.addEventListener('blur', () => goToPage(inpPage.value));
}
document.getElementById('divGutterLeft').addEventListener('click', goToPrevPage);
document.getElementById('divGutterRight').addEventListener('click', goToNextPage);
document.getElementById('divGutterLeft').addEventListener('dblclick', goToFirstPage);
document.getElementById('divGutterRight').addEventListener('dblclick', goToLastPage);
// Restore the page when the user navigates with Back/Forward
window.addEventListener('popstate', () => {
const page = getPageFromUrl();
if (page !== currentPage) {
currentPage = page;
BLOB_VIEW_MODE === 'list' ? updateBlobTable() : updateBlobGrid();
}
});
window.addEventListener('blossomServerDrop', async (event) => {
const { serverUrl, files } = event.detail || {};
if (!serverUrl || !Array.isArray(files) || files.length === 0) return;
@@ -2695,6 +2807,10 @@
}
applyGridColumns();
// Restore page from URL (e.g. a shared link) before loading blobs
currentPage = getPageFromUrl();
preservePageOnNextRefresh = true;
// Subscribe to live settings updates (e.g. changed on another device)
onUserSettings((settings) => {
applyBlobSettings(settings || {});
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.107",
"VERSION_NUMBER": "0.7.107",
"BUILD_DATE": "2026-09-24T20:29:17.829Z"
"VERSION": "v0.7.108",
"VERSION_NUMBER": "0.7.108",
"BUILD_DATE": "2026-09-24T21:55:29.385Z"
}