diff --git a/frontend/src/lib/components/common/InfiniteScroll.svelte b/frontend/src/lib/components/common/InfiniteScroll.svelte index 7320bcc..5a94e34 100644 --- a/frontend/src/lib/components/common/InfiniteScroll.svelte +++ b/frontend/src/lib/components/common/InfiniteScroll.svelte @@ -7,23 +7,44 @@ let { loading = false, hasMore = true, onLoadMore }: Props = $props(); + // Lookahead distance below the viewport at which we start loading. + const MARGIN = 300; + let sentinel = $state(); + // Fire onLoadMore while the sentinel is within MARGIN px of the viewport + // bottom. Measuring the sentinel's viewport rect (rather than a scroll + // container's scrollHeight/clientHeight) makes this correct whether the page + // scrolls on
or on the window — and it loads exactly enough pages to + // reach past the viewport, instead of eagerly loading everything. + function maybeLoad() { + if (loading || !hasMore || !sentinel) return; + const rect = sentinel.getBoundingClientRect(); + if (rect.top <= window.innerHeight + MARGIN) { + onLoadMore(); + } + } + + // Load on scroll: the observer notifies us when the sentinel nears the viewport. $effect(() => { if (!sentinel) return; - const observer = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && !loading && hasMore) { - onLoadMore(); - } + if (entries[0].isIntersecting) maybeLoad(); }, - { rootMargin: '300px' }, + { rootMargin: `${MARGIN}px` }, ); - observer.observe(sentinel); return () => observer.disconnect(); }); + + // After each load settles (loading → false), re-check synchronously: if the + // freshly appended content still didn't push the sentinel past the viewport, + // load again. This fills short pages without the throttled observer lagging + // and over-fetching. + $effect(() => { + if (!loading) maybeLoad(); + }); @@ -59,4 +80,4 @@ @keyframes spin { to { transform: rotate(360deg); } } - \ No newline at end of file + diff --git a/frontend/src/routes/categories/+page.svelte b/frontend/src/routes/categories/+page.svelte index 1d41806..00eaaea 100644 --- a/frontend/src/routes/categories/+page.svelte +++ b/frontend/src/routes/categories/+page.svelte @@ -3,7 +3,6 @@ import { api, ApiError } from '$lib/api/client'; import { categorySorting, type CategorySortField } from '$lib/stores/sorting'; import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte'; - import { tick } from 'svelte'; import type { Category, CategoryOffsetPage } from '$lib/api/types'; const LIMIT = 100; @@ -15,7 +14,6 @@ ]; let categories = $state([]); - let scrollContainer = $state(); let total = $state(0); let offset = $state(0); let loading = $state(false); @@ -64,12 +62,6 @@ loading = false; initialLoaded = true; } - // Keep loading until the content fills the viewport so the infinite-scroll - // sentinel ends up below the fold; then stop. - await tick(); - if (hasMore && scrollContainer && scrollContainer.scrollHeight <= scrollContainer.clientHeight) { - void load(); - } } let hasMore = $derived(categories.length < total); @@ -134,7 +126,7 @@ -
+
{#if error} {/if} diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte index 447caa8..04b7c5e 100644 --- a/frontend/src/routes/files/+page.svelte +++ b/frontend/src/routes/files/+page.svelte @@ -203,12 +203,9 @@ } finally { loading = false; } - // If the loaded content doesn't fill the viewport yet (no scrollbar), - // keep loading until it does or there's nothing left. - await tick(); - if (hasMore && scrollContainer && scrollContainer.scrollHeight <= scrollContainer.clientHeight) { - void loadMore(); - } + // Viewport filling is handled by InfiniteScroll, which re-checks after each + // load — no manual recursion (which over-fetched here because
isn't + // the scroller, so its scrollHeight never exceeds its clientHeight). } function applyFilter(filter: string | null) { diff --git a/frontend/src/routes/files/trash/+page.svelte b/frontend/src/routes/files/trash/+page.svelte index d1f5bab..bc95319 100644 --- a/frontend/src/routes/files/trash/+page.svelte +++ b/frontend/src/routes/files/trash/+page.svelte @@ -1,7 +1,6 @@