diff --git a/www/blobs.html b/www/blobs.html
index e9e0d81..4493e10 100644
--- a/www/blobs.html
+++ b/www/blobs.html
@@ -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 @@
-
+
+
(0)
@@ -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 || {});
diff --git a/www/js/version.json b/www/js/version.json
index 4f66af1..b5b629a 100644
--- a/www/js/version.json
+++ b/www/js/version.json
@@ -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"
}