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,
getAvailableGenres,
getSelectedGenreIds,
getSelectedGenreNames,
loadGenres,
subscribe as subscribeGenre,
type GenreInfo
@@ -46,12 +47,11 @@
});
function updateSelectedName() {
const selectedIds = getSelectedGenreIds();
if (selectedIds.size === 1) {
const genres = getAvailableGenres();
const selectedId = Array.from(selectedIds)[0];
const genre = genres.find(g => g.id === selectedId);
selectedGenreName = genre?.name ?? null;
const selectedNames = getSelectedGenreNames();
if (selectedNames.length === 1) {
selectedGenreName = selectedNames[0];
} else if (selectedNames.length > 1) {
selectedGenreName = `${selectedNames.length} genres`;
} else {
selectedGenreName = null;
}
@@ -81,6 +81,7 @@
function handleClearAll() {
clearSelection();
onClose();
}
function handleRememberToggle() {
+43 -13
View File
@@ -18,7 +18,8 @@
} from '$lib/stores/homeSettingsStore';
import {
getSelectedGenreId,
getSelectedGenreName,
getSelectedGenreIds,
getSelectedGenreNames,
hasActiveFilter as hasGenreFilter
} from '$lib/stores/genreFilterStore';
import { setPlaybackContext } from '$lib/stores/playbackContextStore';
@@ -323,7 +324,7 @@
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 {
const response = await invoke<FeaturedAlbumsResponse>('get_featured_albums', {
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 {
return {
id: album.id,
@@ -447,11 +475,13 @@
}
function filterAlbumsByGenre(albums: AlbumCardData[]): AlbumCardData[] {
const selectedGenreName = getSelectedGenreName();
if (!selectedGenreName) return albums;
// Filter albums whose genre contains the selected genre name (case-insensitive)
const selectedGenreNames = getSelectedGenreNames();
if (selectedGenreNames.length === 0) return albums;
// Filter albums whose genre matches any of the selected genres (case-insensitive)
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)
});
// Get current genre filter
const genreId = getSelectedGenreId();
// Get current genre filter (array of IDs for multi-select)
const genreIds = Array.from(getSelectedGenreIds());
// Start Qobuz API calls in parallel (don't await)
if (isSectionVisible('newReleases')) {
fetchFeaturedAlbums('new-releases', homeLimits.featuredAlbums, genreId).then(async albums => {
fetchFeaturedAlbums('new-releases', homeLimits.featuredAlbums, genreIds).then(async albums => {
newReleases = albums;
await loadAllAlbumDownloadStatuses(albums);
loadingNewReleases = false;
@@ -506,7 +536,7 @@
}
if (isSectionVisible('pressAwards')) {
fetchFeaturedAlbums('press-awards', homeLimits.featuredAlbums, genreId).then(async albums => {
fetchFeaturedAlbums('press-awards', homeLimits.featuredAlbums, genreIds).then(async albums => {
pressAwards = albums;
await loadAllAlbumDownloadStatuses(albums);
loadingPressAwards = false;
@@ -517,7 +547,7 @@
}
if (isSectionVisible('mostStreamed')) {
fetchFeaturedAlbums('most-streamed', homeLimits.featuredAlbums, genreId).then(async albums => {
fetchFeaturedAlbums('most-streamed', homeLimits.featuredAlbums, genreIds).then(async albums => {
mostStreamed = albums;
await loadAllAlbumDownloadStatuses(albums);
loadingMostStreamed = false;
@@ -528,7 +558,7 @@
}
if (isSectionVisible('qobuzissimes')) {
fetchFeaturedAlbums('qobuzissimes', homeLimits.featuredAlbums, genreId).then(async albums => {
fetchFeaturedAlbums('qobuzissimes', homeLimits.featuredAlbums, genreIds).then(async albums => {
qobuzissimes = albums;
await loadAllAlbumDownloadStatuses(albums);
loadingQobuzissimes = false;
@@ -539,7 +569,7 @@
}
if (isSectionVisible('editorPicks')) {
fetchFeaturedAlbums('editor-picks', homeLimits.featuredAlbums, genreId).then(async albums => {
fetchFeaturedAlbums('editor-picks', homeLimits.featuredAlbums, genreIds).then(async albums => {
editorPicks = albums;
await loadAllAlbumDownloadStatuses(albums);
loadingEditorPicks = false;
+7 -2
View File
@@ -116,6 +116,13 @@ export function getSelectedGenreName(): string | undefined {
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 {
return state.selectedGenreIds.has(genreId);
}
@@ -128,8 +135,6 @@ export function toggleGenre(genreId: number): void {
if (state.selectedGenreIds.has(genreId)) {
state.selectedGenreIds.delete(genreId);
} else {
// For now, only allow single selection (API limitation)
state.selectedGenreIds.clear();
state.selectedGenreIds.add(genreId);
}
saveToStorage();