Compare commits

..

3 commits

Author SHA1 Message Date
dlawler489
859554e345 Merge page and model addresses
All checks were successful
build-and-push / docker (push) Successful in 15s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:21:26 +10:00
dlawler489
f3e0a645a2 Keep the open model in the address too
Opening a model adds /model/<id> to the address (#collection/12/model/34),
so a refresh or bookmark reopens it and Back closes it. Moving from one
model to the next doesn't add Back steps, and closing takes back the step
opening added. A model that's gone is dropped from the address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:21:03 +10:00
dlawler489
2aff27cbdd Keep the page you're on in the address
Each page has an address (#filament, #printers, #collection/12), so a refresh
or bookmark returns to it and Back/Forward move between pages. The search
and filter bar is hidden on the Filament page, where it did nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:19:13 +10:00

View file

@ -71,7 +71,13 @@ function startApp() {
document.getElementById('authScreen').style.display = 'none'; document.getElementById('authScreen').style.display = 'none';
document.querySelector('.container').style.display = ''; document.querySelector('.container').style.display = '';
if (!appStarted) { setupEventListeners(); appStarted = true; } if (!appStarted) { setupEventListeners(); appStarted = true; }
loadData(); selectedScope = scopeFromHash();
const model = modelFromHash(); // read now: drawing the page rewrites the address
loadData().then(() => {
if (!model) return;
if (findModel(model)) openModelDetail(model, { push: false });
else syncAddress(); // gone, or in the trash: drop it from the address
});
} }
function showAuth(mode) { function showAuth(mode) {
@ -460,8 +466,9 @@ function scopeTitle() {
} }
function updateScopeChrome() { function updateScopeChrome() {
syncAddress(); // e.g. a deleted collection falling back to All Models
contentTitle.textContent = scopeTitle(); 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.getElementById('filterBar').hidden = panel;
document.querySelector('.view-options').hidden = panel; document.querySelector('.view-options').hidden = panel;
emptyTrashBtn.hidden = !(selectedScope === 'trash' && trashModels.length); emptyTrashBtn.hidden = !(selectedScope === 'trash' && trashModels.length);
@ -977,12 +984,60 @@ function setupEventListeners() {
document.getElementById('addFilesInput').addEventListener('change', handleAddFiles); document.getElementById('addFilesInput').addEventListener('change', handleAddFiles);
} }
async function selectScope(scope) { // The page you're on lives in the address — #filament, #collection/12, and
// /model/34 on the end while a model is open — so a refresh or a bookmark comes
// back to it, and Back and Forward move between pages and models.
let shownModelId = null; // the model open in the detail view
let modelStepPushed = false; // opening it here added a Back step
function scopeHash(scope, modelId = null) {
const page = scope === 'all' ? '' : SPECIAL_SCOPES.includes(scope) ? scope : `collection/${scope}`;
const parts = [page, modelId ? `model/${modelId}` : ''].filter(Boolean);
return parts.length ? `#${parts.join('/')}` : '';
}
function hashParts() {
const h = decodeURIComponent(location.hash.slice(1));
const m = h.match(/^(.*?)\/?model\/(\d+)$/);
return { page: m ? m[1] : h, model: m ? Number(m[2]) : null };
}
function scopeFromHash() {
const { page } = hashParts();
const collection = page.match(/^collection\/(\d+)$/);
if (collection) return collection[1];
return SPECIAL_SCOPES.includes(page) ? page : 'all';
}
const modelFromHash = () => hashParts().model;
// Keeps the address in step with what's shown; `push` makes a Back step.
function syncAddress(push = false) {
const hash = scopeHash(String(selectedScope), shownModelId);
if (location.hash === hash || (!hash && !location.hash)) return;
history[push ? 'pushState' : 'replaceState']({ model: shownModelId }, '', `${location.pathname}${location.search}${hash}`);
}
async function selectScope(scope, { push = true } = {}) {
selectedScope = scope; selectedScope = scope;
syncAddress(push);
if (scope === 'trash') await loadTrash(); if (scope === 'trash') await loadTrash();
renderModels(); renderModels();
} }
window.addEventListener('popstate', () => {
if (!appStarted) return;
const scope = scopeFromHash();
const model = modelFromHash();
if (String(scope) !== String(selectedScope)) selectScope(scope, { push: false });
if (model && model !== shownModelId) {
openModelDetail(model, { push: false });
} else if (!model && shownModelId) {
modelStepPushed = false; // Back already took the step away
closeModal(fileDetailModal);
}
});
// Tracks the file currently shown in the detail viewer (for thumbnail regen). // Tracks the file currently shown in the detail viewer (for thumbnail regen).
let currentDetailFile = null; let currentDetailFile = null;
let currentDetailThumb = null; // model image, used as a viewer fallback let currentDetailThumb = null; // model image, used as a viewer fallback
@ -990,6 +1045,13 @@ let currentDetailThumb = null; // model image, used as a viewer fallback
function closeModal(modal) { function closeModal(modal) {
modal.classList.remove('active'); modal.classList.remove('active');
if (modal === slicerModal) pendingHandoff = null; if (modal === slicerModal) pendingHandoff = null;
if (modal === fileDetailModal && shownModelId) {
shownModelId = null;
// Undo the step opening it added, so Back doesn't reopen what was just closed.
if (modelStepPushed && history.state && history.state.model) history.back();
else syncAddress();
modelStepPushed = false;
}
if (modal === fileDetailModal) { if (modal === fileDetailModal) {
hideSlicerNotice(); hideSlicerNotice();
flushNotes(); flushNotes();
@ -1125,9 +1187,16 @@ function openImportModal() {
importModal.classList.add('active'); importModal.classList.add('active');
} }
function openModelDetail(modelId) { function openModelDetail(modelId, { push = true } = {}) {
const model = findModel(modelId); const model = findModel(modelId);
if (!model) return; if (!model) return;
// Reopening the same model (to refresh it) leaves the address alone.
if (shownModelId !== Number(model.id)) {
const replacing = shownModelId !== null; // one model to the next: no extra Back step
shownModelId = Number(model.id);
syncAddress(push && !replacing);
if (push && !replacing) modelStepPushed = true;
}
flushNotes(); // save any edit still pending for the previously open model flushNotes(); // save any edit still pending for the previously open model
fileDetailModal.dataset.currentModelId = modelId; fileDetailModal.dataset.currentModelId = modelId;
currentDetailThumb = model.thumbnailUrl || null; currentDetailThumb = model.thumbnailUrl || null;