diff --git a/src/lib/components/library/LibraryGrid.svelte b/src/lib/components/library/LibraryGrid.svelte
index c672537..3602d10 100644
--- a/src/lib/components/library/LibraryGrid.svelte
+++ b/src/lib/components/library/LibraryGrid.svelte
@@ -31,6 +31,33 @@
const statsAlways = $derived(settingsState.settings.libraryStatsAlways ?? false)
const cropCovers = $derived(settingsState.settings.libraryCropCovers ?? true)
+ // Virtual rendering — only mount cards up to visibleCount
+ const PAGE = 48
+ let visibleCount = $state(PAGE)
+ let sentinel: HTMLDivElement | undefined = $state()
+ let observer: IntersectionObserver | null = null
+
+ const renderedItems = $derived(items.slice(0, visibleCount))
+ const hasMore = $derived(visibleCount < items.length)
+
+ // Reset when items list changes (tab switch, filter, etc)
+ $effect(() => {
+ items
+ visibleCount = PAGE
+ })
+
+ $effect(() => {
+ observer?.disconnect()
+ if (!sentinel) return
+ observer = new IntersectionObserver((entries) => {
+ if (entries[0]?.isIntersecting && hasMore) {
+ visibleCount = Math.min(visibleCount + PAGE, items.length)
+ }
+ }, { rootMargin: '200px' })
+ observer.observe(sentinel)
+ return () => observer?.disconnect()
+ })
+
function onDocDown(e: MouseEvent) {
if (movePanelOpen && !(e.target as HTMLElement).closest('.move-wrap')) movePanelOpen = false
}
@@ -111,7 +138,7 @@
{:else}
- {#each items as m (m.id)}
+ {#each renderedItems as m (m.id)}
{@const isSelected = selected.has(m.id)}
{@const isCompleted = m.status === 'COMPLETED' || (!m.unreadCount && (m.chapters?.totalCount ?? 0) > 0)}
+ {#if hasMore}
+
+ {/if}
{/if}
@@ -289,6 +319,8 @@
.title-skeleton { height: 12px; margin-top: var(--sp-2); width: 80%; border-radius: var(--radius-sm); }
.skeleton { background: var(--bg-raised); animation: pulse 1.4s ease infinite; }
+ .sentinel { height: 1px; width: 100%; }
+
.empty {
display: flex; align-items: center; justify-content: center;
height: 60%; color: var(--text-muted); font-size: var(--text-sm);
diff --git a/src/lib/components/library/LibraryToolbar.svelte b/src/lib/components/library/LibraryToolbar.svelte
index 13b0022..1e382ae 100644
--- a/src/lib/components/library/LibraryToolbar.svelte
+++ b/src/lib/components/library/LibraryToolbar.svelte
@@ -63,7 +63,12 @@
onTabDragStart, onTabDragOver, onTabDragLeave, onTabDrop, onTabDragEnd,
}: Props = $props();
+ let wheelTimer: ReturnType | null = null
+
function onTabsWheel(e: WheelEvent) {
+ e.preventDefault()
+ if (wheelTimer) return
+ wheelTimer = setTimeout(() => { wheelTimer = null }, 180)
const ids = visibleTabIds.filter(id => id === "library" || id === "downloaded" || visibleCategories.some(c => String(c.id) === id));
const idx = ids.indexOf(tab);
if (e.deltaY > 0 && idx < ids.length - 1) onTabChange(ids[idx + 1]);