diff --git a/web/src/routes/library/genres/+page.svelte b/web/src/routes/library/genres/+page.svelte index 069de967..70f2423d 100644 --- a/web/src/routes/library/genres/+page.svelte +++ b/web/src/routes/library/genres/+page.svelte @@ -30,6 +30,31 @@ return genres.filter((g: GenreCount) => g.genre.toLowerCase().includes(q)); }); + // Count-first by default because that's what the server returns and it's the + // right default: the head of this list is genuinely where you're going. But a + // real library runs to several hundred genres with a long tail of one-offs + // (391 / ~3.7 per track on the operator's), and at that size "I know roughly + // what it's called" needs A-Z as much as filtering does. + // + // View state only, not a query parameter — same as `filter` above. Neither is + // worth making shareable, and putting one in the URL and not the other would + // be the inconsistent choice. + let sortMode = $state<'count' | 'name'>('count'); + + const visibleGenres = $derived.by(() => { + // Copy before sorting. With no filter applied `filteredGenres` IS the array + // held by the query cache, and Array.sort mutates in place — sorting it + // directly would reorder cached data for every other consumer. + const list = [...filteredGenres]; + if (sortMode === 'name') { + // sensitivity 'base' so case and accents don't split neighbours apart. + return list.sort((a: GenreCount, b: GenreCount) => + a.genre.localeCompare(b.genre, undefined, { sensitivity: 'base' }) + ); + } + return list; // server order: count DESC, then name + }); + let albums = $state([]); let total = $state(0); let loading = $state(false); @@ -153,12 +178,30 @@

Genres

{#if !index.isPending && !index.isError}

- {genres.length} {genres.length === 1 ? 'genre' : 'genres'}, straight from your file tags + {#if filter.trim()} + {visibleGenres.length} of {genres.length} genres + {:else} + {genres.length} {genres.length === 1 ? 'genre' : 'genres'}, straight from your file tags + {/if}

{/if} {#if genres.length > 0} - +
+ + +
{/if} @@ -180,16 +223,17 @@ {/snippet} - {:else if filter.trim() && filteredGenres.length === 0} + {:else if filter.trim() && visibleGenres.length === 0}

No genres match '{filter.trim()}'.

{:else} - +