enhance album/song list filters

This commit is contained in:
jeffvli
2026-01-17 16:56:35 -08:00
parent 790782b799
commit 5b519320c2
10 changed files with 880 additions and 167 deletions
@@ -2,18 +2,26 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useListContext } from '/@/renderer/context/list-context';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import {
ArtistMultiSelectRow,
GenreMultiSelectRow,
} from '/@/renderer/features/shared/components/multi-select-rows';
import { useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
import {
@@ -61,7 +69,9 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
const genreList = useMemo(() => {
if (!genreListQuery?.data) return [];
return genreListQuery.data.items.map((genre) => ({
albumCount: genre.albumCount,
label: genre.name,
songCount: genre.songCount,
value: genre.id,
}));
}, [genreListQuery.data]);
@@ -173,7 +183,14 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
if (!albumArtistListQuery?.data?.items) return [];
return albumArtistListQuery?.data?.items?.map((artist) => ({
albumCount: artist.albumCount,
imageUrl: getItemImageUrl({
id: artist.id,
itemType: LibraryItem.ARTIST,
type: 'table',
}),
label: artist.name,
songCount: artist.songCount,
value: artist.id,
}));
}, [albumArtistListQuery.data?.items]);
@@ -195,6 +212,87 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
const debouncedHandleMinYearFilter = useDebouncedCallback(handleMinYearFilter, 300);
const debouncedHandleMaxYearFilter = useDebouncedCallback(handleMaxYearFilter, 300);
const artistSelectMode = useAppStore((state) => state.artistSelectMode);
const genreSelectMode = useAppStore((state) => state.genreSelectMode);
const { setArtistSelectMode, setGenreSelectMode } = useAppStoreActions();
const selectedArtistIds = useMemo(() => query.artistIds || [], [query.artistIds]);
const selectedGenreIds = useMemo(() => query.genreIds || [], [query.genreIds]);
const handleArtistSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setArtistSelectMode(newMode);
if (newMode === 'single' && selectedArtistIds.length > 1) {
setAlbumArtist([selectedArtistIds[0]]);
}
},
[selectedArtistIds, setAlbumArtist, setArtistSelectMode],
);
const handleGenreSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setGenreSelectMode(newMode);
if (newMode === 'single' && selectedGenreIds.length > 1) {
setGenreId([selectedGenreIds[0]]);
}
},
[selectedGenreIds, setGenreId, setGenreSelectMode],
);
const artistFilterLabel = useMemo(() => {
return (
<Group gap="xs" justify="space-between" w="100%">
<Text fw={500} size="sm">
{t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={[
{
label: t('common.filter_single', { postProcess: 'titleCase' }),
value: 'single',
},
{
label: t('common.filter_multiple', { postProcess: 'titleCase' }),
value: 'multi',
},
]}
onChange={handleArtistSelectModeChange}
size="xs"
value={artistSelectMode}
/>
</Group>
);
}, [artistSelectMode, handleArtistSelectModeChange, t]);
const genreFilterLabel = useMemo(() => {
return (
<Group gap="xs" justify="space-between" w="100%">
<Text fw={500} size="sm">
{t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={[
{
label: t('common.filter_single', { postProcess: 'titleCase' }),
value: 'single',
},
{
label: t('common.filter_multiple', { postProcess: 'titleCase' }),
value: 'multi',
},
]}
onChange={handleGenreSelectModeChange}
size="xs"
value={genreSelectMode}
/>
</Group>
);
}, [genreSelectMode, handleGenreSelectModeChange, t]);
return (
<Stack px="md" py="md">
{yesNoFilter.map((filter) => (
@@ -205,6 +303,38 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
onChange={(e) => filter.onChange(e ? e === 'true' : undefined)}
/>
))}
{!disableArtistFilter && (
<>
<Divider my="md" />
<VirtualMultiSelect
displayCountType="album"
height={300}
isLoading={albumArtistListQuery.isFetching}
label={artistFilterLabel}
onChange={handleAlbumArtistFilter}
options={selectableAlbumArtists}
RowComponent={ArtistMultiSelectRow}
singleSelect={artistSelectMode === 'single'}
value={selectedArtistIds}
/>
</>
)}
{!isGenrePage && (
<>
<Divider my="md" />
<VirtualMultiSelect
displayCountType="album"
height={220}
isLoading={genreListQuery.isFetching}
label={genreFilterLabel}
onChange={handleGenresFilter}
options={genreList}
RowComponent={GenreMultiSelectRow}
singleSelect={genreSelectMode === 'single'}
value={selectedGenreIds}
/>
</>
)}
<Divider my="md" />
<Group grow>
<NumberInput
@@ -226,27 +356,6 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
required={!!query.minYear}
/>
</Group>
{!isGenrePage && (
<MultiSelectWithInvalidData
clearable
data={genreList}
defaultValue={query.genreIds || []}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={handleGenresFilter}
searchable
/>
)}
<MultiSelectWithInvalidData
clearable
data={selectableAlbumArtists}
defaultValue={query.artistIds || []}
disabled={disableArtistFilter}
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
limit={300}
onChange={handleAlbumArtistFilter}
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchable
/>
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
<MultiSelectWithInvalidData
clearable
@@ -2,23 +2,29 @@ import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { ChangeEvent, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useListContext } from '/@/renderer/context/list-context';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import {
ArtistMultiSelectRow,
GenreMultiSelectRow,
} from '/@/renderer/features/shared/components/multi-select-rows';
import { useCurrentServer, useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { titleCase } from '/@/renderer/utils';
import { NDSongQueryFieldsLabelMap } from '/@/shared/api/navidrome/navidrome-types';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
@@ -32,6 +38,9 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
const serverId = server.id;
const { customFilters } = useListContext();
const artistSelectMode = useAppStore((state) => state.artistSelectMode);
const genreSelectMode = useAppStore((state) => state.genreSelectMode);
const { setArtistSelectMode, setGenreSelectMode } = useAppStoreActions();
const isGenrePage = customFilters?.genreIds !== undefined;
@@ -52,29 +61,43 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
const genreList = useMemo(() => {
if (!genreListQuery?.data) return [];
return genreListQuery.data.items.map((genre) => ({
albumCount: genre.albumCount,
label: genre.name,
songCount: genre.songCount,
value: genre.id,
}));
}, [genreListQuery.data]);
const yesNoUndefinedFilters = useMemo(
// Helper function to convert boolean/null to segment value
const booleanToSegmentValue = (value: boolean | null | undefined): string => {
if (value === true) return 'true';
if (value === false) return 'false';
return 'none';
};
// Helper function to convert segment value to boolean/null
const segmentValueToBoolean = (value: string): boolean | null => {
if (value === 'true') return true;
if (value === 'false') return false;
return null;
};
const segmentedControlData = useMemo(
() => [
{
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (favorite?: boolean) => {
setFavorite(favorite ?? null);
},
value: query.favorite,
label: t('common.none', { postProcess: 'titleCase' }),
value: 'none',
},
{
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
onChange: (compilation?: boolean) => {
setCompilation(compilation ?? null);
},
value: query.compilation,
label: t('common.yes', { postProcess: 'titleCase' }),
value: 'true',
},
{
label: t('common.no', { postProcess: 'titleCase' }),
value: 'false',
},
],
[t, query.favorite, query.compilation, setFavorite, setCompilation],
[t],
);
const toggleFilters = useMemo(
@@ -141,7 +164,14 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
if (!albumArtistListQuery?.data?.items) return [];
return albumArtistListQuery?.data?.items?.map((artist) => ({
albumCount: artist.albumCount,
imageUrl: getItemImageUrl({
id: artist.id,
itemType: LibraryItem.ARTIST,
type: 'table',
}),
label: artist.name,
songCount: artist.songCount,
value: artist.id,
}));
}, [albumArtistListQuery.data?.items]);
@@ -159,6 +189,9 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
[setGenreId],
);
const selectedArtistIds = useMemo(() => query.artistIds || [], [query.artistIds]);
const selectedGenreIds = useMemo(() => query.genreIds || [], [query.genreIds]);
const handleAlbumArtistChange = useCallback(
(e: null | string[]) => {
if (e && e.length > 0) {
@@ -170,23 +203,147 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
[setAlbumArtist],
);
const handleArtistSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setArtistSelectMode(newMode);
if (newMode === 'single' && selectedArtistIds.length > 1) {
setAlbumArtist([selectedArtistIds[0]]);
}
},
[selectedArtistIds, setAlbumArtist, setArtistSelectMode],
);
const handleGenreSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setGenreSelectMode(newMode);
if (newMode === 'single' && selectedGenreIds.length > 1) {
setGenreId([selectedGenreIds[0]]);
}
},
[selectedGenreIds, setGenreId, setGenreSelectMode],
);
const artistFilterLabel = useMemo(() => {
return (
<Group gap="xs" justify="space-between" w="100%">
<Text fw={500} size="sm">
{t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={[
{
label: t('common.filter_single', { postProcess: 'titleCase' }),
value: 'single',
},
{
label: t('common.filter_multiple', { postProcess: 'titleCase' }),
value: 'multi',
},
]}
onChange={handleArtistSelectModeChange}
size="xs"
value={artistSelectMode}
/>
</Group>
);
}, [artistSelectMode, handleArtistSelectModeChange, t]);
const genreFilterLabel = useMemo(() => {
return (
<Group gap="xs" justify="space-between" w="100%">
<Text fw={500} size="sm">
{t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={[
{
label: t('common.filter_single', { postProcess: 'titleCase' }),
value: 'single',
},
{
label: t('common.filter_multiple', { postProcess: 'titleCase' }),
value: 'multi',
},
]}
onChange={handleGenreSelectModeChange}
size="xs"
value={genreSelectMode}
/>
</Group>
);
}, [genreSelectMode, handleGenreSelectModeChange, t]);
return (
<Stack px="md" py="md">
{yesNoUndefinedFilters.map((filter) => (
<YesNoSelect
clearable
defaultValue={filter.value ? filter.value.toString() : undefined}
key={`nd-filter-${filter.label}`}
label={filter.label}
onChange={(e) => filter.onChange(e ? e === 'true' : undefined)}
<Stack gap="xs">
<Text size="sm" weight={500}>
{t('filter.isFavorited', { postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={segmentedControlData}
defaultValue={booleanToSegmentValue(query.favorite)}
onChange={(value) => {
setFavorite(segmentValueToBoolean(value));
}}
size="sm"
w="100%"
/>
))}
</Stack>
<Stack gap="xs">
<Text size="sm" weight={500}>
{t('filter.isCompilation', { postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={segmentedControlData}
defaultValue={booleanToSegmentValue(query.compilation)}
onChange={(value) => {
setCompilation(segmentValueToBoolean(value));
}}
size="sm"
w="100%"
/>
</Stack>
{toggleFilters.map((filter) => (
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
<Text>{filter.label}</Text>
<Switch defaultChecked={filter?.value ?? false} onChange={filter.onChange} />
</Group>
))}
{!disableArtistFilter && (
<>
<Divider my="md" />
<VirtualMultiSelect
displayCountType="album"
height={300}
isLoading={albumArtistListQuery.isFetching}
label={artistFilterLabel}
onChange={handleAlbumArtistChange}
options={selectableAlbumArtists}
RowComponent={ArtistMultiSelectRow}
singleSelect={artistSelectMode === 'single'}
value={selectedArtistIds}
/>
</>
)}
{!isGenrePage && (
<>
<Divider my="md" />
<VirtualMultiSelect
displayCountType="album"
height={220}
label={genreFilterLabel}
onChange={handleGenreChange}
options={genreList}
RowComponent={GenreMultiSelectRow}
singleSelect={genreSelectMode === 'single'}
value={selectedGenreIds}
/>
</>
)}
<Divider my="md" />
<NumberInput
defaultValue={query.minYear ?? undefined}
@@ -196,27 +353,6 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
min={0}
onChange={(e) => debouncedHandleYearFilter(e)}
/>
{!isGenrePage && (
<MultiSelectWithInvalidData
clearable
data={genreList}
defaultValue={query.genreIds || []}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={handleGenreChange}
searchable
/>
)}
<MultiSelectWithInvalidData
clearable
data={selectableAlbumArtists}
defaultValue={query.artistIds || []}
disabled={disableArtistFilter}
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
limit={300}
onChange={handleAlbumArtistChange}
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchable
/>
<Divider my="md" />
<TagFilters />
</Stack>
@@ -1,23 +1,28 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { ChangeEvent, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { useListContext } from '/@/renderer/context/list-context';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import {
ArtistMultiSelectRow,
GenreMultiSelectRow,
} from '/@/renderer/features/shared/components/multi-select-rows';
import { useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Select } from '/@/shared/components/select/select';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text';
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
import { AlbumArtistListSort, SortOrder } from '/@/shared/types/domain-types';
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
interface SubsonicAlbumFiltersProps {
disableArtistFilter?: boolean;
@@ -35,8 +40,6 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
const { query, setAlbumArtist, setFavorite, setGenreId, setMaxYear, setMinYear } =
useAlbumListFilters();
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const albumArtistListQuery = useSuspenseQuery(
artistsQueries.albumArtistList({
options: {
@@ -58,7 +61,14 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
if (!items) return [];
return items.map((artist) => ({
albumCount: artist.albumCount,
imageUrl: getItemImageUrl({
id: artist.id,
itemType: LibraryItem.ARTIST,
type: 'table',
}),
label: artist.name,
songCount: artist.songCount,
value: artist.id,
}));
}, [items]);
@@ -75,18 +85,34 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
const genreList = useMemo(() => {
if (!genreListQuery?.data) return [];
return genreListQuery.data.items.map((genre) => ({
albumCount: genre.albumCount,
label: genre.name,
songCount: genre.songCount,
value: genre.id,
}));
}, [genreListQuery.data]);
const selectedGenreIds = useMemo(() => query.genreIds || [], [query.genreIds]);
const handleGenresFilter = useCallback(
(e: null | string) => {
setGenreId(e ? [e] : null);
(e: null | string[]) => {
if (e && e.length > 0) {
setGenreId([e[0]]);
} else {
setGenreId(null);
}
},
[setGenreId],
);
const genreFilterLabel = useMemo(() => {
return (
<Text fw={500} size="sm">
{t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
</Text>
);
}, [t]);
const toggleFilters = useMemo(
() => [
{
@@ -142,6 +168,48 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
const debouncedHandleMinYearFilter = useDebouncedCallback(handleMinYearFilter, 300);
const debouncedHandleMaxYearFilter = useDebouncedCallback(handleMaxYearFilter, 300);
const artistSelectMode = useAppStore((state) => state.artistSelectMode);
const { setArtistSelectMode } = useAppStoreActions();
const selectedArtistIds = useMemo(() => query.artistIds || [], [query.artistIds]);
const handleArtistSelectModeChange = useCallback(
(value: string) => {
const newMode = value as 'multi' | 'single';
setArtistSelectMode(newMode);
if (newMode === 'single' && selectedArtistIds.length > 1) {
setAlbumArtist([selectedArtistIds[0]]);
}
},
[selectedArtistIds, setAlbumArtist, setArtistSelectMode],
);
const artistFilterLabel = useMemo(() => {
return (
<Group gap="xs" justify="space-between" w="100%">
<Text fw={500} size="sm">
{t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={[
{
label: t('common.filter_single', { postProcess: 'titleCase' }),
value: 'single',
},
{
label: t('common.filter_multiple', { postProcess: 'titleCase' }),
value: 'multi',
},
]}
onChange={handleArtistSelectModeChange}
size="xs"
value={artistSelectMode}
/>
</Group>
);
}, [artistSelectMode, handleArtistSelectModeChange, t]);
return (
<Stack px="md" py="md">
{toggleFilters.map((filter) => (
@@ -150,6 +218,36 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
<Switch defaultChecked={filter.value ?? false} onChange={filter.onChange} />
</Group>
))}
{!disableArtistFilter && (
<>
<Divider my="md" />
<VirtualMultiSelect
displayCountType="album"
height={300}
label={artistFilterLabel}
onChange={handleAlbumArtistFilter}
options={selectableAlbumArtists}
RowComponent={ArtistMultiSelectRow}
singleSelect={artistSelectMode === 'single'}
value={selectedArtistIds}
/>
</>
)}
{!isGenrePage && (
<>
<Divider my="md" />
<VirtualMultiSelect
displayCountType="album"
height={220}
label={genreFilterLabel}
onChange={handleGenresFilter}
options={genreList}
RowComponent={GenreMultiSelectRow}
singleSelect={true}
value={selectedGenreIds}
/>
</>
)}
<Divider my="md" />
<Group grow>
<NumberInput
@@ -171,30 +269,6 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
onChange={(e) => debouncedHandleMaxYearFilter(e)}
/>
</Group>
{!isGenrePage && (
<Select
clearable
data={genreList}
defaultValue={query.genreIds?.[0] ?? undefined}
disabled={Boolean(query.minYear || query.maxYear)}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={handleGenresFilter}
searchable
/>
)}
<MultiSelectWithInvalidData
clearable
data={selectableAlbumArtists}
defaultValue={query.artistIds ?? []}
disabled={disableArtistFilter}
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
limit={300}
onChange={handleAlbumArtistFilter}
onSearchChange={setAlbumArtistSearchTerm}
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchable
searchValue={albumArtistSearchTerm}
/>
</Stack>
);
};