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" }