From e65883cc9ee758951efce86fae8dcf6dae81eaff Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 16 Apr 2026 12:34:55 +0500 Subject: [PATCH] feat(catalogue): UX improvements and bug fixes - Persist audio filter in URL (?audio=1) via history.replaceState - Show total novel count in browse mode subtitle - Close filter panel automatically on Apply - Replace missing-cover SVG placeholder with styled first-letter avatar - Add forbidden scrape badge to list view (was missing, grid had it) - Carry audio param through applyFilters() navigation Co-Authored-By: Claude Sonnet 4.6 --- ui/src/routes/catalogue/+page.server.ts | 4 ++- ui/src/routes/catalogue/+page.svelte | 37 +++++++++++++++---------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/ui/src/routes/catalogue/+page.server.ts b/ui/src/routes/catalogue/+page.server.ts index a727892..90c3afd 100644 --- a/ui/src/routes/catalogue/+page.server.ts +++ b/ui/src/routes/catalogue/+page.server.ts @@ -16,6 +16,7 @@ export const load: PageServerLoad = async ({ url, locals }) => { const sort = url.searchParams.get('sort') ?? 'popular'; const status = url.searchParams.get('status') ?? 'all'; const q = url.searchParams.get('q') ?? ''; + const audioOnly = url.searchParams.get('audio') === '1'; const params = new URLSearchParams({ page, genre, sort, status }); if (q.trim().length >= 2) { @@ -64,7 +65,8 @@ export const load: PageServerLoad = async ({ url, locals }) => { isAdmin: locals.user?.role === 'admin', searchQuery: q.trim().length >= 2 ? q.trim() : '', searchLocalCount: 0, - searchRemoteCount: 0 + searchRemoteCount: 0, + audioOnly }; }; diff --git a/ui/src/routes/catalogue/+page.svelte b/ui/src/routes/catalogue/+page.svelte index 4533cb0..f4affb1 100644 --- a/ui/src/routes/catalogue/+page.svelte +++ b/ui/src/routes/catalogue/+page.svelte @@ -21,11 +21,13 @@ }); function applyFilters() { + filtersOpen = false; const params = new URLSearchParams(); params.set('sort', filterSort); params.set('genre', filterGenre); params.set('status', filterStatus); params.set('page', '1'); + if (filterAudioOnly) params.set('audio', '1'); goto(`/catalogue?${params.toString()}`); } @@ -215,7 +217,7 @@ // ── Audio-available set ─────────────────────────────────────────────────── let audioSlugs = $state>(new Set()); - let filterAudioOnly = $state(false); + let filterAudioOnly = $state(untrack(() => data.audioOnly)); $effect(() => { fetch('/api/audio/slugs') @@ -224,6 +226,17 @@ .catch(() => { /* non-critical */ }); }); + function toggleAudio() { + filterAudioOnly = !filterAudioOnly; + const u = new URL(window.location.href); + if (filterAudioOnly) { + u.searchParams.set('audio', '1'); + } else { + u.searchParams.delete('audio'); + } + history.replaceState({}, '', u.toString()); + } + const displayedNovels = $derived( filterAudioOnly ? novels.filter((n) => audioSlugs.has(n.slug)) : novels ); @@ -249,7 +262,7 @@ {m.catalogue_rank_no_data_body()} {/if} {:else} - {m.catalogue_browse_source()} + {m.catalogue_browse_source()}{#if data.total > 0} {data.total.toLocaleString()} novels{/if} {/if}