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:
1 parent
22ba76a8d1
commit
8dc1efec5d
4 files changed
+57
-21
No files matched your search
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user