diff --git a/public/app.js b/public/app.js index 1660e51..6b669d0 100644 --- a/public/app.js +++ b/public/app.js @@ -71,6 +71,7 @@ function startApp() { document.getElementById('authScreen').style.display = 'none'; document.querySelector('.container').style.display = ''; if (!appStarted) { setupEventListeners(); appStarted = true; } + selectedScope = scopeFromHash(); loadData(); } @@ -460,8 +461,9 @@ function scopeTitle() { } function updateScopeChrome() { + syncAddress(); // e.g. a deleted collection falling back to All Models contentTitle.textContent = scopeTitle(); - const panel = ['duplicates', 'ai', 'watch', 'printers'].includes(selectedScope); // pages without the model grid + const panel = ['duplicates', 'ai', 'watch', 'filament', 'printers'].includes(selectedScope); // pages without the model grid document.getElementById('filterBar').hidden = panel; document.querySelector('.view-options').hidden = panel; emptyTrashBtn.hidden = !(selectedScope === 'trash' && trashModels.length); @@ -977,12 +979,40 @@ function setupEventListeners() { document.getElementById('addFilesInput').addEventListener('change', handleAddFiles); } -async function selectScope(scope) { +// The page you're on lives in the address — #filament, #collection/12 — so a +// refresh or a bookmark comes back to it, and Back and Forward move between pages. +function scopeHash(scope) { + if (scope === 'all') return ''; + return SPECIAL_SCOPES.includes(scope) ? `#${scope}` : `#collection/${scope}`; +} + +function scopeFromHash() { + const h = decodeURIComponent(location.hash.slice(1)); + const collection = h.match(/^collection\/(\d+)$/); + if (collection) return collection[1]; + return SPECIAL_SCOPES.includes(h) ? h : 'all'; +} + +// Keeps the address in step with the page shown; `push` makes a Back step. +function syncAddress(push = false) { + const hash = scopeHash(String(selectedScope)); + if (location.hash === hash || (!hash && !location.hash)) return; + history[push ? 'pushState' : 'replaceState'](null, '', `${location.pathname}${location.search}${hash}`); +} + +async function selectScope(scope, { push = true } = {}) { selectedScope = scope; + syncAddress(push); if (scope === 'trash') await loadTrash(); renderModels(); } +window.addEventListener('popstate', () => { + if (!appStarted) return; + const scope = scopeFromHash(); + if (String(scope) !== String(selectedScope)) selectScope(scope, { push: false }); +}); + // Tracks the file currently shown in the detail viewer (for thumbnail regen). let currentDetailFile = null; let currentDetailThumb = null; // model image, used as a viewer fallback