From 7ed7d22b6d592f03e0e63e2ae390e2e26af9b6c2 Mon Sep 17 00:00:00 2001 From: zuevav <34027267+zuevav@users.noreply.github.com> Date: Thu, 14 May 2026 23:29:57 +0300 Subject: [PATCH] Actually fix mobile nav + arm albums infinite scroll for cached loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions from the previous fix: 1. Mobile nav still wrapped into a tall stack. The @media block was placed BEFORE the base `.nav-btn { flex: 1; min-width: 120px }` rule, so source-order specificity made the base rule override the mobile one. Moved the @media block to AFTER all base nav-btn rules so it wins on screens ≤720px. 2. Albums infinite scroll never engaged when the gallery was loaded from cache (the common case). `getAlbumCache` returned only the array, `setAlbumCache` discarded pagination, and the DOMContentLoad auto-load + `refreshAlbumsSilently` both bypassed `setupAlbumsInfiniteScroll`. Now the cache preserves `{ list, page, pages, total }`, a new `getAlbumCacheFull` exposes it, and every cached-render path primes pagination state and arms the observer. Background refresh also re-arms the observer after pulling fresh `pages`, fixing the case where the cache had a stale `pages: 1`. Co-Authored-By: Claude Opus 4.7 (1M context) --- css/style.css | 51 ++++++++++---------- js/app.js | 129 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 119 insertions(+), 61 deletions(-) diff --git a/css/style.css b/css/style.css index b1d9b96..60f575e 100644 --- a/css/style.css +++ b/css/style.css @@ -212,30 +212,6 @@ body { flex-wrap: wrap; } -/* On narrow screens, switch to horizontal scrolling so 5+ tabs stay - on one row instead of breaking into a tall stack. */ -@media (max-width: 720px) { - .main-nav { - flex-wrap: nowrap; - overflow-x: auto; - overflow-y: hidden; - scroll-snap-type: x proximity; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - } - .main-nav::-webkit-scrollbar { - display: none; - } - .nav-btn { - flex: 0 0 auto; - min-width: auto; - white-space: nowrap; - scroll-snap-align: start; - padding: 10px 16px; - font-size: 0.9rem; - } -} - .nav-btn { flex: 1; min-width: 120px; @@ -261,6 +237,33 @@ body { box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3); } +/* On narrow screens, switch to horizontal scrolling so all 6 tabs stay + on a single row instead of breaking into a tall stack. Placed after + the base .nav-btn rules so its specificity-tie-breaker wins. */ +@media (max-width: 720px) { + .main-nav { + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + scroll-snap-type: x proximity; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + gap: 4px; + padding: 6px; + } + .main-nav::-webkit-scrollbar { + display: none; + } + .nav-btn { + flex: 0 0 auto; + min-width: auto; + white-space: nowrap; + scroll-snap-align: start; + padding: 10px 14px; + font-size: 0.88rem; + } +} + /* ============ Panels ============ */ .panel { background: var(--glass-bg); diff --git a/js/app.js b/js/app.js index 821367d..1b149ae 100644 --- a/js/app.js +++ b/js/app.js @@ -482,7 +482,16 @@ document.addEventListener('DOMContentLoaded', function() { const ALBUM_PREFS_KEY = 'vh_album_prefs'; const CACHE_TTL = 60 * 60 * 1000; // 1 hour + // Returns just the albums array (back-compat with callers that destructure + // an array). The full pagination object is available via getAlbumCacheFull. function getAlbumCache() { + const full = getAlbumCacheFull(); + return full ? full.albums : null; + } + + // Returns { albums, page, pages, total } or null. Pagination info may be + // missing on legacy caches. + function getAlbumCacheFull() { try { const cached = localStorage.getItem(ALBUM_CACHE_KEY); if (!cached) return null; @@ -491,12 +500,26 @@ document.addEventListener('DOMContentLoaded', function() { localStorage.removeItem(ALBUM_CACHE_KEY); return null; } - // Handle both old format (array) and new format (object with albums property) const albums = data.albums; if (Array.isArray(albums)) { - return albums; + // Legacy flat-array format — pagination unknown. + return { albums, page: 1, pages: 1, total: albums.length }; } else if (albums && Array.isArray(albums.albums)) { - return albums.albums; + // Older nested format. + return { + albums: albums.albums, + page: albums.page || 1, + pages: albums.pages || 1, + total: albums.total || albums.albums.length, + }; + } else if (Array.isArray(data.list)) { + // Current format: stored as { list, page, pages, total }. + return { + albums: data.list, + page: data.page || 1, + pages: data.pages || 1, + total: data.total || data.list.length, + }; } return null; } catch (e) { @@ -506,12 +529,21 @@ document.addEventListener('DOMContentLoaded', function() { function setAlbumCache(albumsOrData) { try { - // Accept either array or object with albums property - const albums = Array.isArray(albumsOrData) ? albumsOrData : (albumsOrData.albums || []); - localStorage.setItem(ALBUM_CACHE_KEY, JSON.stringify({ - albums: albums, - timestamp: Date.now() - })); + let payload; + if (Array.isArray(albumsOrData)) { + payload = { list: albumsOrData, page: 1, pages: 1, total: albumsOrData.length }; + } else if (albumsOrData && Array.isArray(albumsOrData.albums)) { + payload = { + list: albumsOrData.albums, + page: albumsOrData.page || 1, + pages: albumsOrData.pages || 1, + total: albumsOrData.total || albumsOrData.albums.length, + }; + } else { + return; + } + payload.timestamp = Date.now(); + localStorage.setItem(ALBUM_CACHE_KEY, JSON.stringify(payload)); } catch (e) { console.warn('Failed to cache albums:', e); } @@ -1024,22 +1056,40 @@ document.addEventListener('DOMContentLoaded', function() { refreshAlbumsSilently(); } - // Refresh albums in background without showing loading to user + // Refresh albums in background without showing loading to user. + // IMPORTANT: also saves pagination state and arms the infinite-scroll + // observer, since the cached-render path skips both. async function refreshAlbumsSilently() { if (state.isLoadingAlbums) return; try { - const response = await fetch('api.php?action=flickr_albums'); + const response = await fetch('api.php?action=flickr_albums&page=1&per_page=50'); if (!response.ok) return; const data = await response.json(); if (data.albums && data.albums.length > 0) { - setAlbumCache(data.albums); + state.allAlbums = data.albums; + state.albumsPage = data.page || 1; + state.albumsTotalPages = data.pages || 1; + state.albumsTotal = data.total || data.albums.length; + + setAlbumCache({ + albums: data.albums, + page: state.albumsPage, + pages: state.albumsTotalPages, + total: state.albumsTotal, + }); window._cachedAlbums = data.albums; + // Only re-render if on albums view (photos view is hidden) if (photosView?.classList.contains('hidden')) { renderAlbumsGrid(data.albums); } + + // Arm infinite scroll if there are more pages available. + if (state.albumsPage < state.albumsTotalPages) { + setupAlbumsInfiniteScroll(); + } } } catch (error) { console.log('Silent refresh failed:', error.message); @@ -1071,25 +1121,23 @@ document.addEventListener('DOMContentLoaded', function() { // Check cache first if (!forceRefresh) { - const cached = getAlbumCache(); - if (cached && cached.albums) { - console.log('Using cached albums:', cached.albums.length); - state.allAlbums = cached.albums; - state.albumsPage = cached.page || 1; - state.albumsTotalPages = cached.pages || 1; - state.albumsTotal = cached.total || cached.albums.length; - window._cachedAlbums = cached.albums; - renderAlbumsGrid(cached.albums); - showNotification(`Загружено ${cached.albums.length} альбомов (из кеша)`, 'success'); + const full = getAlbumCacheFull(); + if (full && full.albums && full.albums.length > 0) { + console.log('Using cached albums:', full.albums.length); + state.allAlbums = full.albums; + state.albumsPage = full.page; + state.albumsTotalPages = full.pages; + state.albumsTotal = full.total; + window._cachedAlbums = full.albums; + renderAlbumsGrid(full.albums); + showNotification(`Загружено ${full.albums.length} альбомов (из кеша)`, 'success'); + // Arm scroll observer if there might be more pages, even when + // pagination info is missing from a legacy cache. setupAlbumsInfiniteScroll(); - return; - } else if (cached && Array.isArray(cached)) { - // Old cache format compatibility - console.log('Using old cached albums:', cached.length); - state.allAlbums = cached; - window._cachedAlbums = cached; - renderAlbumsGrid(cached); - showNotification(`Загружено ${cached.length} альбомов (из кеша)`, 'success'); + // Refresh in background to get accurate page count. + if (full.pages <= 1) { + setTimeout(() => refreshAlbumsSilently(), 500); + } return; } } @@ -4317,13 +4365,20 @@ document.addEventListener('DOMContentLoaded', function() { // Auto-load albums on page init if (albumsGrid) { - const cachedAlbums = getAlbumCache(); - if (cachedAlbums && cachedAlbums.length > 0) { - // Use cache first for instant display - console.log('Loading cached albums:', cachedAlbums.length); - window._cachedAlbums = cachedAlbums; - renderAlbumsGrid(cachedAlbums); - // Silently refresh in background + const fullCache = getAlbumCacheFull(); + if (fullCache && fullCache.albums.length > 0) { + // Use cache for instant display, and prime pagination state so + // the infinite-scroll observer can engage immediately. + console.log('Loading cached albums:', fullCache.albums.length); + state.allAlbums = fullCache.albums; + state.albumsPage = fullCache.page; + state.albumsTotalPages = fullCache.pages; + state.albumsTotal = fullCache.total; + window._cachedAlbums = fullCache.albums; + renderAlbumsGrid(fullCache.albums); + setupAlbumsInfiniteScroll(); + // Silently refresh in background — this also corrects pagination + // info if the cache is from a legacy build that lost it. setTimeout(() => refreshAlbumsSilently(), 2000); } else { // No cache or empty - load from API