feat(frontend): restore files grid position when returning from a file
Opening a file now snapshots the grid (loaded pages, cursor, scroll offset, opened id) into a shared store, and the viewer derives prev/next from that list instead of a separate anchored request. Returning to the grid restores the cached list and scroll-centres the last-viewed file rather than reloading page 1 from the top. This also fixes two issues: - The viewer's "previous" arrow never appeared: the backend anchor window is forward-inclusive, so the anchor was always item 0 and prev was null. Neighbors now come from the cached list, so paging is symmetric. - Paging forward in the viewer prefetches further pages into the snapshot, so navigation continues past the initially loaded set and the grid still restores correctly. A deep link straight to a file (empty cache) falls back to the anchored API window as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { api } from '$lib/api/client';
|
||||
import type { File, FileCursorPage } from '$lib/api/types';
|
||||
|
||||
/** The sort/order/filter that identifies a particular files listing. */
|
||||
export interface FilesQuery {
|
||||
sort: string;
|
||||
order: string;
|
||||
filter: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export interface FilesSnapshot {
|
||||
query: FilesQuery;
|
||||
files: File[];
|
||||
nextCursor: string | null;
|
||||
hasMore: boolean;
|
||||
scrollTop: number;
|
||||
/** ID of the file the user opened — restore the grid centred on this. */
|
||||
lastOpenedId: string | null;
|
||||
}
|
||||
|
||||
/** Stable string identity for a query, used to tell whether a snapshot still
|
||||
* applies to the current sort/order/filter. */
|
||||
export function queryKey(q: FilesQuery): string {
|
||||
return `${q.sort}|${q.order}|${q.filter ?? ''}`;
|
||||
}
|
||||
|
||||
let snapshot: FilesSnapshot | null = null;
|
||||
let loading = false;
|
||||
|
||||
/** Save (replace) the current grid snapshot. */
|
||||
export function saveFilesSnapshot(s: FilesSnapshot): void {
|
||||
snapshot = s;
|
||||
}
|
||||
|
||||
/** Read the snapshot without consuming it. */
|
||||
export function peekFilesSnapshot(): FilesSnapshot | null {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Forget the snapshot (e.g. on logout). */
|
||||
export function clearFilesSnapshot(): void {
|
||||
snapshot = null;
|
||||
}
|
||||
|
||||
/** Record the file currently being viewed so back-navigation lands on it. */
|
||||
export function setLastOpened(id: string): void {
|
||||
if (snapshot) snapshot = { ...snapshot, lastOpenedId: id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the next page to the snapshot using its own query/cursor. The file
|
||||
* viewer calls this to extend the cached list as the user pages forward, so
|
||||
* prev/next keep working past the originally loaded set and the grid restores
|
||||
* correctly on return. No-op when there is nothing cached, no further pages, or
|
||||
* a load is already in flight.
|
||||
*/
|
||||
export async function loadMoreIntoSnapshot(limit: number): Promise<void> {
|
||||
if (!snapshot || !snapshot.hasMore || loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
const q = snapshot.query;
|
||||
const params = new URLSearchParams({ limit: String(limit), sort: q.sort, order: q.order });
|
||||
if (snapshot.nextCursor) params.set('cursor', snapshot.nextCursor);
|
||||
if (q.filter) params.set('filter', q.filter);
|
||||
const res = await api.get<FileCursorPage>(`/files?${params}`);
|
||||
// Re-read snapshot: it may have been replaced while the request was in flight.
|
||||
if (!snapshot) return;
|
||||
snapshot = {
|
||||
...snapshot,
|
||||
files: [...snapshot.files, ...(res.items ?? [])],
|
||||
nextCursor: res.next_cursor ?? null,
|
||||
hasMore: !!res.next_cursor,
|
||||
};
|
||||
} catch {
|
||||
// Non-critical: leave the snapshot unchanged.
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user