fix(frontend): open pool files in an overlay so back returns to the pool

Tapping a file in a pool did a full goto('/files/<id>') to the standalone
viewer route, whose close button always routes to /files — so returning from
a file viewed inside a pool dropped the user on the global files list instead
of back in the pool.

Open the viewer as an overlay over the still-mounted pool grid via shallow
routing, mirroring the files grid: pushState keeps the pool URL (the overlay
is driven by page.state.fileId), and the back button / close does
history.back(), returning to the pool with its list and scroll intact.
Neighbours follow the pool's own order, paging in more pool files near the
end, and closing scrolls the grid back to the last-viewed file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 10:22:34 +03:00
parent 5968a7b593
commit 1f3bc2acf4
+81 -2
View File
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { goto } from '$app/navigation'; import { goto, pushState, replaceState } from '$app/navigation';
import { api, ApiError } from '$lib/api/client'; import { api, ApiError } from '$lib/api/client';
import { tick } from 'svelte'; import { tick } from 'svelte';
import FileCard from '$lib/components/file/FileCard.svelte'; import FileCard from '$lib/components/file/FileCard.svelte';
import FileViewer from '$lib/components/file/FileViewer.svelte';
import FilterBar from '$lib/components/file/FilterBar.svelte'; import FilterBar from '$lib/components/file/FilterBar.svelte';
import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte'; import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@@ -164,7 +165,7 @@
// ---- Selection ---- // ---- Selection ----
function handleTap(file: PoolFile, idx: number, e: MouseEvent) { function handleTap(file: PoolFile, idx: number, e: MouseEvent) {
if (!selectionMode) { if (!selectionMode) {
goto(`/files/${file.id}`); openFile(file);
return; return;
} }
if (e.shiftKey && lastSelectedIdx !== null) { if (e.shiftKey && lastSelectedIdx !== null) {
@@ -208,6 +209,60 @@
} }
} }
// ---- File viewer overlay (shallow routing) ----
// Open the viewer on top of the still-mounted pool grid so the back button (and
// the viewer's own close) returns here — with the pool's list and scroll intact —
// instead of navigating to the standalone /files/<id> route, whose close drops
// the user on the global files list. Neighbours follow the pool's own order.
let activeFileId = $derived(page.state.fileId);
let activeIdx = $derived(activeFileId ? files.findIndex((f) => f.id === activeFileId) : -1);
let viewerPrevId = $derived(activeIdx > 0 ? (files[activeIdx - 1]?.id ?? null) : null);
let viewerNextId = $derived(
activeIdx >= 0 && activeIdx < files.length - 1 ? (files[activeIdx + 1]?.id ?? null) : null,
);
function openFile(file: PoolFile) {
if (!file.id) return;
// Keep the pool URL; the overlay is driven by page.state, so a back press (or
// a reload, which clears page.state) reveals the pool untouched.
pushState(`${page.url.pathname}${page.url.search}`, { fileId: file.id });
}
function pageTo(id: string) {
// Replace (not push) so one back press returns to the grid rather than
// stepping back through every file paged.
replaceState(`${page.url.pathname}${page.url.search}`, { fileId: id });
}
function closeViewer() {
history.back();
}
// Page in more pool files when the viewer nears the end of the loaded set.
$effect(() => {
if (activeIdx >= 0 && activeIdx >= files.length - 3 && hasMore && !filesLoading) {
void loadMore();
}
});
// On close, bring the grid back to the last-viewed file (the list never unmounted).
let lastOverlayId: string | null = null;
$effect(() => {
const id = activeFileId;
if (id) {
lastOverlayId = id;
} else if (lastOverlayId) {
const target = lastOverlayId;
lastOverlayId = null;
const idx = files.findIndex((f) => f.id === target);
if (idx >= 0) {
document
.querySelector<HTMLElement>(`[data-file-index="${idx}"]`)
?.scrollIntoView({ block: 'center' });
}
}
});
// ---- Drag-to-reorder ---- // ---- Drag-to-reorder ----
function onDragStart(idx: number, e: DragEvent) { function onDragStart(idx: number, e: DragEvent) {
dragSrcIdx = idx; dragSrcIdx = idx;
@@ -553,6 +608,20 @@
{/if} {/if}
</div> </div>
<!-- File viewer overlay (shallow routing): renders on top of the still-mounted
pool grid, so closing it reveals the pool untouched. -->
{#if activeFileId}
<div class="viewer-overlay">
<FileViewer
fileId={activeFileId}
prevId={viewerPrevId}
nextId={viewerNextId}
onNavigate={pageTo}
onClose={closeViewer}
/>
</div>
{/if}
<!-- Selection bar (remove mode) --> <!-- Selection bar (remove mode) -->
{#if selectionMode && !addMode} {#if selectionMode && !addMode}
<div class="selection-bar" role="toolbar"> <div class="selection-bar" role="toolbar">
@@ -598,6 +667,16 @@
flex-direction: column; flex-direction: column;
} }
/* Full-screen viewer overlay covering the grid and the bottom navbar. */
.viewer-overlay {
position: fixed;
inset: 0;
z-index: 200;
background-color: var(--color-bg-primary);
overflow-y: auto;
overscroll-behavior: contain;
}
/* ---- Shared top bar ---- */ /* ---- Shared top bar ---- */
.top-bar { .top-bar {
position: sticky; position: sticky;