feat(genre): support multi-select genre filter

- Allow multiple genre selection (checkbox behavior)
- Add getSelectedGenreNames() for multiple name retrieval
- Merge results from multiple API calls for featured albums
- Clear button now closes the popup
- Button shows "N genres" when multiple selected
This commit is contained in:
vicrodh committed 2026-02-02 09:37:43 -06:00
1 parent 22ba76a8d1
commit 8dc1efec5d
4 files changed
+57 -21

No files matched your search

+6 -6
View File
@@ -6,6 +6,7 @@
hasActiveFilter, hasActiveFilter,
getAvailableGenres, getAvailableGenres,
getSelectedGenreIds, getSelectedGenreIds,
getSelectedGenreNames,
loadGenres, loadGenres,
subscribe as subscribeGenre, subscribe as subscribeGenre,
type GenreInfo type GenreInfo
@@ -46,12 +47,11 @@
}); });
function updateSelectedName() { function updateSelectedName() {
const selectedIds = getSelectedGenreIds(); const selectedNames = getSelectedGenreNames();
if (selectedIds.size === 1) { if (selectedNames.length === 1) {
const genres = getAvailableGenres(); selectedGenreName = selectedNames[0];
const selectedId = Array.from(selectedIds)[0]; } else if (selectedNames.length > 1) {
const genre = genres.find(g => g.id === selectedId); selectedGenreName = `${selectedNames.length} genres`;
selectedGenreName = genre?.name ?? null;
} else { } else {
selectedGenreName = null; selectedGenreName = null;
} }
@@ -81,6 +81,7 @@
function handleClearAll() { function handleClearAll() {
clearSelection(); clearSelection();
onClose();
} }
function handleRememberToggle() { function handleRememberToggle() {
+43 -13
View File
@@ -18,7 +18,8 @@
} from '$lib/stores/homeSettingsStore'; } from '$lib/stores/homeSettingsStore';
import { import {
getSelectedGenreId, getSelectedGenreId,
getSelectedGenreName, getSelectedGenreIds,
getSelectedGenreNames,
hasActiveFilter as hasGenreFilter hasActiveFilter as hasGenreFilter
} from '$lib/stores/genreFilterStore'; } from '$lib/stores/genreFilterStore';
import { setPlaybackContext } from '$lib/stores/playbackContextStore'; import { setPlaybackContext } from '$lib/stores/playbackContextStore';
@@ -323,7 +324,7 @@
total: number; total: number;
} }
async function fetchFeaturedAlbums(featuredType: string, limit: number, genreId?: number): Promise<AlbumCardData[]> { async function fetchFeaturedAlbumsSingle(featuredType: string, limit: number, genreId?: number): Promise<AlbumCardData[]> {
try { try {
const response = await invoke<FeaturedAlbumsResponse>('get_featured_albums', { const response = await invoke<FeaturedAlbumsResponse>('get_featured_albums', {
featuredType, featuredType,
@@ -337,6 +338,33 @@
} }
} }
async function fetchFeaturedAlbums(featuredType: string, limit: number, genreIds: number[]): Promise<AlbumCardData[]> {
if (genreIds.length === 0) {
// No filter, fetch without genre
return fetchFeaturedAlbumsSingle(featuredType, limit);
}
if (genreIds.length === 1) {
// Single genre, use API filter
return fetchFeaturedAlbumsSingle(featuredType, limit, genreIds[0]);
}
// Multiple genres: fetch each and merge (dedupe by album id)
const perGenreLimit = Math.ceil(limit / genreIds.length) + 2;
const results = await Promise.all(
genreIds.map(gid => fetchFeaturedAlbumsSingle(featuredType, perGenreLimit, gid))
);
const seen = new Set<string>();
const merged: AlbumCardData[] = [];
for (const albums of results) {
for (const album of albums) {
if (!seen.has(album.id)) {
seen.add(album.id);
merged.push(album);
}
}
}
return merged.slice(0, limit);
}
function toAlbumCard(album: QobuzAlbum): AlbumCardData { function toAlbumCard(album: QobuzAlbum): AlbumCardData {
return { return {
id: album.id, id: album.id,
@@ -447,11 +475,13 @@
} }
function filterAlbumsByGenre(albums: AlbumCardData[]): AlbumCardData[] { function filterAlbumsByGenre(albums: AlbumCardData[]): AlbumCardData[] {
const selectedGenreName = getSelectedGenreName(); const selectedGenreNames = getSelectedGenreNames();
if (!selectedGenreName) return albums; if (selectedGenreNames.length === 0) return albums;
// Filter albums whose genre contains the selected genre name (case-insensitive) // Filter albums whose genre matches any of the selected genres (case-insensitive)
return albums.filter(album => return albums.filter(album =>
album.genre.toLowerCase().includes(selectedGenreName.toLowerCase()) selectedGenreNames.some(genreName =>
album.genre.toLowerCase().includes(genreName.toLowerCase())
)
); );
} }
@@ -490,12 +520,12 @@
limitFavorites: Math.max(homeLimits.favoriteAlbums, homeLimits.favoriteTracks) limitFavorites: Math.max(homeLimits.favoriteAlbums, homeLimits.favoriteTracks)
}); });
// Get current genre filter // Get current genre filter (array of IDs for multi-select)
const genreId = getSelectedGenreId(); const genreIds = Array.from(getSelectedGenreIds());
// Start Qobuz API calls in parallel (don't await) // Start Qobuz API calls in parallel (don't await)
if (isSectionVisible('newReleases')) { if (isSectionVisible('newReleases')) {
fetchFeaturedAlbums('new-releases', homeLimits.featuredAlbums, genreId).then(async albums => { fetchFeaturedAlbums('new-releases', homeLimits.featuredAlbums, genreIds).then(async albums => {
newReleases = albums; newReleases = albums;
await loadAllAlbumDownloadStatuses(albums); await loadAllAlbumDownloadStatuses(albums);
loadingNewReleases = false; loadingNewReleases = false;
@@ -506,7 +536,7 @@
} }
if (isSectionVisible('pressAwards')) { if (isSectionVisible('pressAwards')) {
fetchFeaturedAlbums('press-awards', homeLimits.featuredAlbums, genreId).then(async albums => { fetchFeaturedAlbums('press-awards', homeLimits.featuredAlbums, genreIds).then(async albums => {
pressAwards = albums; pressAwards = albums;
await loadAllAlbumDownloadStatuses(albums); await loadAllAlbumDownloadStatuses(albums);
loadingPressAwards = false; loadingPressAwards = false;
@@ -517,7 +547,7 @@
} }
if (isSectionVisible('mostStreamed')) { if (isSectionVisible('mostStreamed')) {
fetchFeaturedAlbums('most-streamed', homeLimits.featuredAlbums, genreId).then(async albums => { fetchFeaturedAlbums('most-streamed', homeLimits.featuredAlbums, genreIds).then(async albums => {
mostStreamed = albums; mostStreamed = albums;
await loadAllAlbumDownloadStatuses(albums); await loadAllAlbumDownloadStatuses(albums);
loadingMostStreamed = false; loadingMostStreamed = false;
@@ -528,7 +558,7 @@
} }
if (isSectionVisible('qobuzissimes')) { if (isSectionVisible('qobuzissimes')) {
fetchFeaturedAlbums('qobuzissimes', homeLimits.featuredAlbums, genreId).then(async albums => { fetchFeaturedAlbums('qobuzissimes', homeLimits.featuredAlbums, genreIds).then(async albums => {
qobuzissimes = albums; qobuzissimes = albums;
await loadAllAlbumDownloadStatuses(albums); await loadAllAlbumDownloadStatuses(albums);
loadingQobuzissimes = false; loadingQobuzissimes = false;
@@ -539,7 +569,7 @@
} }
if (isSectionVisible('editorPicks')) { if (isSectionVisible('editorPicks')) {
fetchFeaturedAlbums('editor-picks', homeLimits.featuredAlbums, genreId).then(async albums => { fetchFeaturedAlbums('editor-picks', homeLimits.featuredAlbums, genreIds).then(async albums => {
editorPicks = albums; editorPicks = albums;
await loadAllAlbumDownloadStatuses(albums); await loadAllAlbumDownloadStatuses(albums);
loadingEditorPicks = false; loadingEditorPicks = false;
+7 -2
View File
@@ -116,6 +116,13 @@ export function getSelectedGenreName(): string | undefined {
return genre?.name; return genre?.name;
} }
export function getSelectedGenreNames(): string[] {
const ids = Array.from(state.selectedGenreIds);
return ids
.map(id => state.availableGenres.find(g => g.id === id)?.name)
.filter((name): name is string => !!name);
}
export function isGenreSelected(genreId: number): boolean { export function isGenreSelected(genreId: number): boolean {
return state.selectedGenreIds.has(genreId); return state.selectedGenreIds.has(genreId);
} }
@@ -128,8 +135,6 @@ export function toggleGenre(genreId: number): void {
if (state.selectedGenreIds.has(genreId)) { if (state.selectedGenreIds.has(genreId)) {
state.selectedGenreIds.delete(genreId); state.selectedGenreIds.delete(genreId);
} else { } else {
// For now, only allow single selection (API limitation)
state.selectedGenreIds.clear();
state.selectedGenreIds.add(genreId); state.selectedGenreIds.add(genreId);
} }
saveToStorage(); saveToStorage();