diff --git a/frontend/src/lib/stores/filesCache.ts b/frontend/src/lib/stores/filesCache.ts index 338e11b..18fe6d5 100644 --- a/frontend/src/lib/stores/filesCache.ts +++ b/frontend/src/lib/stores/filesCache.ts @@ -1,3 +1,4 @@ +import { browser } from '$app/environment'; import { api } from '$lib/api/client'; import type { File, FileCursorPage } from '$lib/api/types'; @@ -9,13 +10,20 @@ export interface FilesQuery { } /** - * A snapshot of the files grid, kept in memory so that opening a file and - * returning restores the same list (and scroll position) instead of reloading - * page 1 from the top. The file viewer also reads this to derive prev/next and - * extends it as the user pages past the loaded set. + * A snapshot of the files grid, kept so that opening a file and returning + * restores the same list (and scroll position) instead of reloading page 1 from + * the top. The file viewer also reads this to derive prev/next, to find the list + * URL to return to, and extends it as the user pages past the loaded set. + * + * Held in a module variable (survives client-side navigation) AND mirrored to + * sessionStorage (survives a full reload / deep navigation within the tab). */ export interface FilesSnapshot { query: FilesQuery; + /** Search string of the list URL this grid was viewed at (e.g. "?filter=x"), + * so the viewer returns to the exact same filtered list rather than bare + * /files — otherwise the filter is lost and the snapshot no longer matches. */ + listSearch: string; files: File[]; nextCursor: string | null; hasMore: boolean; @@ -30,27 +38,63 @@ export function queryKey(q: FilesQuery): string { return `${q.sort}|${q.order}|${q.filter ?? ''}`; } +const STORAGE_KEY = 'filesSnapshot'; + let snapshot: FilesSnapshot | null = null; +let hydrated = false; let loading = false; +/** Lazily restore the snapshot from sessionStorage the first time it's read so + * the position survives a page reload, not just client-side navigation. */ +function hydrate(): void { + if (hydrated) return; + hydrated = true; + if (!browser) return; + try { + const raw = sessionStorage.getItem(STORAGE_KEY); + if (raw) snapshot = JSON.parse(raw) as FilesSnapshot; + } catch { + // Corrupt/missing — start fresh. + } +} + +function persist(): void { + if (!browser) return; + try { + if (snapshot) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot)); + else sessionStorage.removeItem(STORAGE_KEY); + } catch { + // Quota or serialization failure — non-critical, in-memory copy still works. + } +} + /** Save (replace) the current grid snapshot. */ export function saveFilesSnapshot(s: FilesSnapshot): void { snapshot = s; + hydrated = true; + persist(); } /** Read the snapshot without consuming it. */ export function peekFilesSnapshot(): FilesSnapshot | null { + hydrate(); return snapshot; } /** Forget the snapshot (e.g. on logout). */ export function clearFilesSnapshot(): void { snapshot = null; + hydrated = true; + persist(); } /** Record the file currently being viewed so back-navigation lands on it. */ export function setLastOpened(id: string): void { - if (snapshot) snapshot = { ...snapshot, lastOpenedId: id }; + hydrate(); + if (snapshot) { + snapshot = { ...snapshot, lastOpenedId: id }; + persist(); + } } /** @@ -61,6 +105,7 @@ export function setLastOpened(id: string): void { * a load is already in flight. */ export async function loadMoreIntoSnapshot(limit: number): Promise { + hydrate(); if (!snapshot || !snapshot.hasMore || loading) return; loading = true; try { @@ -77,6 +122,7 @@ export async function loadMoreIntoSnapshot(limit: number): Promise { nextCursor: res.next_cursor ?? null, hasMore: !!res.next_cursor, }; + persist(); } catch { // Non-critical: leave the snapshot unchanged. } finally { diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte index 8ff2bde..6b013ef 100644 --- a/frontend/src/routes/files/+page.svelte +++ b/frontend/src/routes/files/+page.svelte @@ -1,6 +1,6 @@