mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-16 05:36:00 +02:00
enhance album/song list filters
This commit is contained in:
@@ -1,25 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, useSuspenseQuery } 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 { 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 { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
import { useCurrentServer } 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 { 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 { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
|
||||
|
||||
export const JellyfinSongFilters = () => {
|
||||
const serverId = useCurrentServerId();
|
||||
interface JellyfinSongFiltersProps {
|
||||
disableArtistFilter?: boolean;
|
||||
}
|
||||
|
||||
export const JellyfinSongFilters = ({ disableArtistFilter }: JellyfinSongFiltersProps) => {
|
||||
const server = useCurrentServer();
|
||||
const serverId = server.id;
|
||||
const { t } = useTranslation();
|
||||
const { query, setCustom, setFavorite, setMaxYear, setMinYear } = useSongListFilters();
|
||||
const { query, setArtistIds, setCustom, setFavorite, setMaxYear, setMinYear } =
|
||||
useSongListFilters();
|
||||
|
||||
const { customFilters } = useListContext();
|
||||
|
||||
@@ -32,11 +48,44 @@ export const JellyfinSongFilters = () => {
|
||||
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 albumArtistListQuery = useSuspenseQuery(
|
||||
artistsQueries.albumArtistList({
|
||||
options: {
|
||||
gcTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
sortBy: AlbumArtistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId,
|
||||
}),
|
||||
);
|
||||
|
||||
const selectableAlbumArtists = useMemo(() => {
|
||||
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]);
|
||||
|
||||
const tagsQuery = useQuery(
|
||||
sharedQueries.tagList({
|
||||
query: {
|
||||
@@ -46,8 +95,10 @@ export const JellyfinSongFilters = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const selectedArtistIds = useMemo(() => query.artistIds || [], [query.artistIds]);
|
||||
|
||||
const selectedGenres = useMemo(() => {
|
||||
return query._custom?.GenreIds?.split(',');
|
||||
return query._custom?.GenreIds?.split(',') || [];
|
||||
}, [query._custom?.GenreIds]);
|
||||
|
||||
const selectedTags = useMemo(() => {
|
||||
@@ -136,6 +187,95 @@ export const JellyfinSongFilters = () => {
|
||||
[setCustom],
|
||||
);
|
||||
|
||||
const artistSelectMode = useAppStore((state) => state.artistSelectMode);
|
||||
const genreSelectMode = useAppStore((state) => state.genreSelectMode);
|
||||
const { setArtistSelectMode, setGenreSelectMode } = useAppStoreActions();
|
||||
|
||||
const handleArtistSelectModeChange = useCallback(
|
||||
(value: string) => {
|
||||
const newMode = value as 'multi' | 'single';
|
||||
setArtistSelectMode(newMode);
|
||||
|
||||
if (newMode === 'single' && selectedArtistIds.length > 1) {
|
||||
setArtistIds([selectedArtistIds[0]]);
|
||||
}
|
||||
},
|
||||
[selectedArtistIds, setArtistIds, 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]);
|
||||
|
||||
const handleArtistChange = useCallback(
|
||||
(e: null | string[]) => {
|
||||
if (e && e.length > 0) {
|
||||
setArtistIds(e);
|
||||
} else {
|
||||
setArtistIds(null);
|
||||
}
|
||||
},
|
||||
[setArtistIds],
|
||||
);
|
||||
|
||||
const handleGenreSelectModeChange = useCallback(
|
||||
(value: string) => {
|
||||
const newMode = value as 'multi' | 'single';
|
||||
setGenreSelectMode(newMode);
|
||||
|
||||
if (newMode === 'single' && selectedGenres.length > 1) {
|
||||
handleGenresFilter([selectedGenres[0]]);
|
||||
}
|
||||
},
|
||||
[selectedGenres, handleGenresFilter, setGenreSelectMode],
|
||||
);
|
||||
|
||||
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">
|
||||
{yesNoFilters.map((filter) => (
|
||||
@@ -146,6 +286,37 @@ export const JellyfinSongFilters = () => {
|
||||
onChange={(e) => filter.onChange(e ? e === 'true' : undefined)}
|
||||
/>
|
||||
))}
|
||||
{!disableArtistFilter && (
|
||||
<>
|
||||
<Divider my="md" />
|
||||
<VirtualMultiSelect
|
||||
displayCountType="song"
|
||||
height={300}
|
||||
label={artistFilterLabel}
|
||||
onChange={handleArtistChange}
|
||||
options={selectableAlbumArtists}
|
||||
RowComponent={ArtistMultiSelectRow}
|
||||
singleSelect={artistSelectMode === 'single'}
|
||||
value={selectedArtistIds}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isGenrePage && (
|
||||
<>
|
||||
<Divider my="md" />
|
||||
<VirtualMultiSelect
|
||||
displayCountType="song"
|
||||
height={220}
|
||||
isLoading={genreListQuery.isFetching}
|
||||
label={genreFilterLabel}
|
||||
onChange={handleGenresFilter}
|
||||
options={genreList}
|
||||
RowComponent={GenreMultiSelectRow}
|
||||
singleSelect={genreSelectMode === 'single'}
|
||||
value={selectedGenres}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Divider my="md" />
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
@@ -167,16 +338,6 @@ export const JellyfinSongFilters = () => {
|
||||
required={!!query.minYear}
|
||||
/>
|
||||
</Group>
|
||||
{!isGenrePage && (
|
||||
<MultiSelectWithInvalidData
|
||||
clearable
|
||||
data={genreList}
|
||||
defaultValue={selectedGenres}
|
||||
label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })}
|
||||
onChange={handleGenresFilter}
|
||||
searchable
|
||||
/>
|
||||
)}
|
||||
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
|
||||
<MultiSelectWithInvalidData
|
||||
clearable
|
||||
|
||||
@@ -2,24 +2,37 @@ import { useSuspenseQuery } 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 { 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 { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
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 { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
|
||||
|
||||
export const NavidromeSongFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const { query, setFavorite, setGenreId, setMaxYear, setMinYear } = useSongListFilters();
|
||||
const server = useCurrentServer();
|
||||
const serverId = server.id;
|
||||
const { query, setArtistIds, setFavorite, setGenreId, setMaxYear, setMinYear } =
|
||||
useSongListFilters();
|
||||
|
||||
const { customFilters } = useListContext();
|
||||
|
||||
@@ -30,22 +43,74 @@ export const NavidromeSongFilters = () => {
|
||||
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(
|
||||
const albumArtistListQuery = useSuspenseQuery(
|
||||
artistsQueries.albumArtistList({
|
||||
options: {
|
||||
gcTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
sortBy: AlbumArtistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId,
|
||||
}),
|
||||
);
|
||||
|
||||
const selectableAlbumArtists = useMemo(() => {
|
||||
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]);
|
||||
|
||||
// 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('common.yes', { postProcess: 'titleCase' }),
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
label: t('common.no', { postProcess: 'titleCase' }),
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
[t, query.favorite, setFavorite],
|
||||
[t],
|
||||
);
|
||||
|
||||
const handleYearFilter = useMemo(
|
||||
@@ -73,6 +138,48 @@ export const NavidromeSongFilters = () => {
|
||||
|
||||
const debouncedHandleYearFilter = useDebouncedCallback(handleYearFilter, 300);
|
||||
|
||||
const genreSelectMode = useAppStore((state) => state.genreSelectMode);
|
||||
const { setGenreSelectMode } = useAppStoreActions();
|
||||
|
||||
const selectedGenreIds = useMemo(() => query.genreIds || [], [query.genreIds]);
|
||||
|
||||
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 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]);
|
||||
|
||||
const handleGenreChange = useCallback(
|
||||
(e: null | string[]) => {
|
||||
if (e && e.length > 0) {
|
||||
@@ -84,18 +191,98 @@ export const NavidromeSongFilters = () => {
|
||||
[setGenreId],
|
||||
);
|
||||
|
||||
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) {
|
||||
setArtistIds([selectedArtistIds[0]]);
|
||||
}
|
||||
},
|
||||
[selectedArtistIds, setArtistIds, 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]);
|
||||
|
||||
const handleArtistChange = useCallback(
|
||||
(e: null | string[]) => {
|
||||
if (e && e.length > 0) {
|
||||
setArtistIds(e);
|
||||
} else {
|
||||
setArtistIds(null);
|
||||
}
|
||||
},
|
||||
[setArtistIds],
|
||||
);
|
||||
|
||||
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>
|
||||
<Divider my="md" />
|
||||
<VirtualMultiSelect
|
||||
displayCountType="song"
|
||||
height={300}
|
||||
label={artistFilterLabel}
|
||||
onChange={handleArtistChange}
|
||||
options={selectableAlbumArtists}
|
||||
RowComponent={ArtistMultiSelectRow}
|
||||
singleSelect={artistSelectMode === 'single'}
|
||||
value={selectedArtistIds}
|
||||
/>
|
||||
{!isGenrePage && (
|
||||
<VirtualMultiSelect
|
||||
displayCountType="song"
|
||||
height={220}
|
||||
label={genreFilterLabel}
|
||||
onChange={handleGenreChange}
|
||||
options={genreList}
|
||||
RowComponent={GenreMultiSelectRow}
|
||||
singleSelect={genreSelectMode === 'single'}
|
||||
value={selectedGenreIds}
|
||||
/>
|
||||
)}
|
||||
<NumberInput
|
||||
defaultValue={query.minYear ?? undefined}
|
||||
hideControls={false}
|
||||
@@ -104,16 +291,6 @@ export const NavidromeSongFilters = () => {
|
||||
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
|
||||
/>
|
||||
)}
|
||||
<Divider my="md" />
|
||||
<TagFilters />
|
||||
</Stack>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ChangeEvent, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
||||
import { GenreMultiSelectRow } from '/@/renderer/features/shared/components/multi-select-rows';
|
||||
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||
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 { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
@@ -24,18 +25,34 @@ export const SubsonicSongFilters = () => {
|
||||
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(
|
||||
() => [
|
||||
{
|
||||
@@ -61,13 +78,15 @@ export const SubsonicSongFilters = () => {
|
||||
{!isGenrePage && (
|
||||
<>
|
||||
<Divider my="md" />
|
||||
<SelectWithInvalidData
|
||||
clearable
|
||||
data={genreList}
|
||||
defaultValue={query.genreIds ? query.genreIds[0] : undefined}
|
||||
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||
<VirtualMultiSelect
|
||||
displayCountType="song"
|
||||
height={220}
|
||||
label={genreFilterLabel}
|
||||
onChange={handleGenresFilter}
|
||||
searchable
|
||||
options={genreList}
|
||||
RowComponent={GenreMultiSelectRow}
|
||||
singleSelect={true}
|
||||
value={selectedGenreIds}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user