mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-14 01:39:56 -05:00
Fix: Debounce on LibraryToolbar & Grid-Rendering Optimizations
This commit is contained in:
@@ -31,6 +31,33 @@
|
|||||||
const statsAlways = $derived(settingsState.settings.libraryStatsAlways ?? false)
|
const statsAlways = $derived(settingsState.settings.libraryStatsAlways ?? false)
|
||||||
const cropCovers = $derived(settingsState.settings.libraryCropCovers ?? true)
|
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) {
|
function onDocDown(e: MouseEvent) {
|
||||||
if (movePanelOpen && !(e.target as HTMLElement).closest('.move-wrap')) movePanelOpen = false
|
if (movePanelOpen && !(e.target as HTMLElement).closest('.move-wrap')) movePanelOpen = false
|
||||||
}
|
}
|
||||||
@@ -111,7 +138,7 @@
|
|||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#each items as m (m.id)}
|
{#each renderedItems as m (m.id)}
|
||||||
{@const isSelected = selected.has(m.id)}
|
{@const isSelected = selected.has(m.id)}
|
||||||
{@const isCompleted = m.status === 'COMPLETED' || (!m.unreadCount && (m.chapters?.totalCount ?? 0) > 0)}
|
{@const isCompleted = m.status === 'COMPLETED' || (!m.unreadCount && (m.chapters?.totalCount ?? 0) > 0)}
|
||||||
<button
|
<button
|
||||||
@@ -152,6 +179,9 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if hasMore}
|
||||||
|
<div bind:this={sentinel} class="sentinel" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -289,6 +319,8 @@
|
|||||||
.title-skeleton { height: 12px; margin-top: var(--sp-2); width: 80%; border-radius: var(--radius-sm); }
|
.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; }
|
.skeleton { background: var(--bg-raised); animation: pulse 1.4s ease infinite; }
|
||||||
|
|
||||||
|
.sentinel { height: 1px; width: 100%; }
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
height: 60%; color: var(--text-muted); font-size: var(--text-sm);
|
height: 60%; color: var(--text-muted); font-size: var(--text-sm);
|
||||||
|
|||||||
@@ -63,7 +63,12 @@
|
|||||||
onTabDragStart, onTabDragOver, onTabDragLeave, onTabDrop, onTabDragEnd,
|
onTabDragStart, onTabDragOver, onTabDragLeave, onTabDrop, onTabDragEnd,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
let wheelTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
function onTabsWheel(e: WheelEvent) {
|
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 ids = visibleTabIds.filter(id => id === "library" || id === "downloaded" || visibleCategories.some(c => String(c.id) === id));
|
||||||
const idx = ids.indexOf(tab);
|
const idx = ids.indexOf(tab);
|
||||||
if (e.deltaY > 0 && idx < ids.length - 1) onTabChange(ids[idx + 1]);
|
if (e.deltaY > 0 && idx < ids.length - 1) onTabChange(ids[idx + 1]);
|
||||||
|
|||||||
Reference in New Issue
Block a user