diff --git a/src/renderer/api/jellyfin/jellyfin-api.ts b/src/renderer/api/jellyfin/jellyfin-api.ts index c2c0c0640..38a05cda7 100644 --- a/src/renderer/api/jellyfin/jellyfin-api.ts +++ b/src/renderer/api/jellyfin/jellyfin-api.ts @@ -413,7 +413,7 @@ export const jfApiClient = (args: { return { body: response?.data, headers: response?.headers as any, - status: response.status, + status: response?.status, }; } throw e; diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 2b54a7d17..bc2489c6d 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -46,9 +46,41 @@ const ALBUM_LIST_SORT_MAPPING: Record( + items: T[], + options: { + limit?: number; + sortBy?: any; + sortFn?: (items: T[], sortBy: any, sortOrder: SortOrder) => T[]; + sortOrder?: SortOrder; + startIndex?: number; + }, +): { + items: T[]; + startIndex: number; + totalRecordCount: number; +} { + let sortedItems = items; + + if (options.sortFn && options.sortBy) { + const sortOrder = options.sortOrder || SortOrder.ASC; + sortedItems = options.sortFn(items, options.sortBy, sortOrder); + } + + const totalCount = sortedItems.length; + const startIndex = options.startIndex || 0; + const limit = options.limit || totalCount; + const paginatedItems = sortedItems.slice(startIndex, startIndex + limit); + + return { + items: paginatedItems, + startIndex: startIndex, + totalRecordCount: totalCount, + }; +} + export const SubsonicController: InternalControllerEndpoint = { addToPlaylist: async ({ apiClientProps, body, query }) => { const res = await ssApiClient(apiClientProps).updatePlaylist({ @@ -246,7 +278,7 @@ export const SubsonicController: InternalControllerEndpoint = { return { items: results, startIndex: query.startIndex, - totalRecordCount: results?.length || 0, + totalRecordCount: artists.length, }; }, getAlbumArtistListCount: (args) => @@ -346,16 +378,18 @@ export const SubsonicController: InternalControllerEndpoint = { throw new Error('Failed to get album list'); } - const results = + const allResults = res.body.starred?.album?.map((album) => ssNormalize.album(album, apiClientProps.server), ) || []; - return { - items: sortAlbumList(results, query.sortBy, query.sortOrder), - startIndex: 0, - totalRecordCount: res.body.starred?.album?.length || 0, - }; + return sortAndPaginate(allResults, { + limit: query.limit, + sortBy: query.sortBy, + sortFn: sortAlbumList, + sortOrder: query.sortOrder, + startIndex: query.startIndex, + }); } if (query.genreIds?.length) { @@ -592,15 +626,13 @@ export const SubsonicController: InternalControllerEndpoint = { results = searchResults; } - if (query.sortBy) { - results = sortAlbumArtistList(results, query.sortBy, query.sortOrder); - } - - return { - items: results, + return sortAndPaginate(results, { + limit: query.limit, + sortBy: query.sortBy, + sortFn: query.sortBy ? sortAlbumArtistList : undefined, + sortOrder: query.sortOrder, startIndex: query.startIndex, - totalRecordCount: results?.length || 0, - }; + }); }, getArtistListCount: async (args) => SubsonicController.getArtistList({ @@ -647,11 +679,10 @@ export const SubsonicController: InternalControllerEndpoint = { const genres = results.map((genre) => ssNormalize.genre(genre, apiClientProps.server)); - return { - items: genres, - startIndex: 0, - totalRecordCount: genres.length, - }; + return sortAndPaginate(genres, { + limit: query.limit, + startIndex: query.startIndex, + }); }, getMusicFolderList: async (args) => { const { apiClientProps } = args; @@ -728,11 +759,14 @@ export const SubsonicController: InternalControllerEndpoint = { break; } - return { - items: results.map((playlist) => ssNormalize.playlist(playlist, apiClientProps.server)), - startIndex: 0, - totalRecordCount: results.length, - }; + const playlists = results.map((playlist) => + ssNormalize.playlist(playlist, apiClientProps.server), + ); + + return sortAndPaginate(playlists, { + limit: query.limit, + startIndex: query.startIndex, + }); }, getPlaylistListCount: async ({ apiClientProps, query }) => { const res = await ssApiClient(apiClientProps).getPlaylists({}); @@ -792,11 +826,14 @@ export const SubsonicController: InternalControllerEndpoint = { } const results = res.body.randomSongs?.song || []; + const normalizedResults = results.map((song) => + ssNormalize.song(song, apiClientProps.server), + ); return { - items: results.map((song) => ssNormalize.song(song, apiClientProps.server)), + items: normalizedResults, startIndex: 0, - totalRecordCount: res.body.randomSongs?.song?.length || 0, + totalRecordCount: normalizedResults.length, }; }, getRoles: async (args) => { @@ -965,16 +1002,18 @@ export const SubsonicController: InternalControllerEndpoint = { throw new Error('Failed to get song list'); } - const results = + const allResults = (res.body.starred?.song || []).map((song) => ssNormalize.song(song, apiClientProps.server), ) || []; - return { - items: sortSongList(results, query.sortBy, query.sortOrder), - startIndex: 0, - totalRecordCount: (res.body.starred?.song || []).length || 0, - }; + return sortAndPaginate(allResults, { + limit: query.limit, + sortBy: query.sortBy, + sortFn: sortSongList, + sortOrder: query.sortOrder, + startIndex: query.startIndex, + }); } const artistIds = query.albumArtistIds || query.artistIds; @@ -1041,7 +1080,7 @@ export const SubsonicController: InternalControllerEndpoint = { } return { - items: results.map((song) => ssNormalize.song(song, apiClientProps.server)), + items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [], startIndex: 0, totalRecordCount: results.length, }; diff --git a/src/renderer/features/albums/components/jellyfin-album-filters.tsx b/src/renderer/features/albums/components/jellyfin-album-filters.tsx index 6bf29d245..9fe7075e1 100644 --- a/src/renderer/features/albums/components/jellyfin-album-filters.tsx +++ b/src/renderer/features/albums/components/jellyfin-album-filters.tsx @@ -1,5 +1,4 @@ import { useQuery } from '@tanstack/react-query'; -import debounce from 'lodash/debounce'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,13 +7,12 @@ import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album 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 { AlbumListFilter } from '/@/renderer/store'; +import { AlbumListFilter, useCurrentServerId } from '/@/renderer/store'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; import { NumberInput } from '/@/shared/components/number-input/number-input'; import { SpinnerIcon } from '/@/shared/components/spinner/spinner'; 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 { AlbumArtistListSort, @@ -27,14 +25,11 @@ interface JellyfinAlbumFiltersProps { customFilters?: Partial; disableArtistFilter?: boolean; onFilterChange: (filters: AlbumListFilter) => void; - serverId: string; } -export const JellyfinAlbumFilters = ({ - disableArtistFilter, - serverId, -}: JellyfinAlbumFiltersProps) => { +export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFiltersProps) => { const { t } = useTranslation(); + const serverId = useCurrentServerId(); const { query, @@ -110,21 +105,50 @@ export const JellyfinAlbumFilters = ({ setCompilation, ]); - const handleMinYearFilter = debounce((e: number | string) => { - if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const year = e === '' ? undefined : (e as number); - setMinYear(year ?? null); - }, 500); + const handleMinYearFilter = useMemo( + () => (e: number | string) => { + // Handle empty string, null, undefined, or invalid numbers as clearing + if (e === '' || e === null || e === undefined || isNaN(Number(e))) { + setMinYear(null); + return; + } - const handleMaxYearFilter = debounce((e: number | string) => { - if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const year = e === '' ? undefined : (e as number); - setMaxYear(year ?? null); - }, 500); + const year = typeof e === 'number' ? e : Number(e); + // If it's a valid number within range, set it; otherwise clear + if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) { + setMinYear(year); + } else { + setMinYear(null); + } + }, + [setMinYear], + ); - const handleGenresFilter = debounce((e: string[] | undefined) => { - setGenreId(e ?? null); - }, 250); + const handleMaxYearFilter = useMemo( + () => (e: number | string) => { + // Handle empty string, null, undefined, or invalid numbers as clearing + if (e === '' || e === null || e === undefined || isNaN(Number(e))) { + setMaxYear(null); + return; + } + + const year = typeof e === 'number' ? e : Number(e); + // If it's a valid number within range, set it; otherwise clear + if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) { + setMaxYear(year); + } else { + setMaxYear(null); + } + }, + [setMaxYear], + ); + + const handleGenresFilter = useMemo( + () => (e: string[] | undefined) => { + setGenreId(e && e.length > 0 ? e : null); + }, + [setGenreId], + ); const albumArtistListQuery = useQuery( artistsQueries.albumArtistList({ @@ -154,24 +178,46 @@ export const JellyfinAlbumFilters = ({ setAlbumArtist(e ?? null); }; - const handleTagFilter = debounce((e: string[] | undefined) => { - setCustom((prev) => ({ - ...prev, - [e?.join('|') || '']: e?.join('|') || undefined, - })); - }, 250); + const handleTagFilter = useMemo( + () => (e: string[] | undefined) => { + setCustom((prev) => { + if (!prev) { + return e && e.length > 0 ? { [e.join('|')]: e.join('|') } : null; + } + + if (!e || e.length === 0) { + // Remove all tag-related properties (they use '|' joined keys) + const rest = Object.fromEntries( + Object.entries(prev).filter(([key]) => !key.includes('|')), + ); + + return Object.keys(rest).length === 0 ? null : rest; + } + + // Remove old tag entries and add new one + const rest = Object.fromEntries( + Object.entries(prev).filter(([key]) => !key.includes('|')), + ); + const tagKey = e.join('|'); + + return { + ...rest, + [tagKey]: tagKey, + }; + }); + }, + [setCustom], + ); return ( {yesNoFilter.map((filter) => ( - - {filter.label} - - + ))} @@ -181,7 +227,7 @@ export const JellyfinAlbumFilters = ({ label={t('filter.fromYear', { postProcess: 'sentenceCase' })} max={2300} min={1700} - onChange={(e) => handleMinYearFilter(e)} + onBlur={(e) => handleMinYearFilter(e.currentTarget.value)} required={!!query.minYear} /> handleMaxYearFilter(e)} + onBlur={(e) => handleMaxYearFilter(e.currentTarget.value)} required={!!query.minYear} /> - - - + handleGenresFilter(e)} + searchable + /> - + : undefined} + searchable + /> + {tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && ( : undefined} + data={tagsQuery.data.boolTags} + defaultValue={query._custom?.[tagsQuery.data.boolTags.join('|')] ?? undefined} + label={t('common.tags', { postProcess: 'sentenceCase' })} + onChange={handleTagFilter} searchable + width={250} /> - - {tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && ( - - - )} ); diff --git a/src/renderer/features/albums/components/subsonic-album-filters.tsx b/src/renderer/features/albums/components/subsonic-album-filters.tsx index ac8363045..ff0e79583 100644 --- a/src/renderer/features/albums/components/subsonic-album-filters.tsx +++ b/src/renderer/features/albums/components/subsonic-album-filters.tsx @@ -1,14 +1,12 @@ import { useQuery } from '@tanstack/react-query'; -import debounce from 'lodash/debounce'; -import { parseAsArrayOf, parseAsBoolean, parseAsInteger, parseAsString, useQueryState } from 'nuqs'; import { ChangeEvent, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; +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 { FILTER_KEYS } from '/@/renderer/features/shared/utils'; -import { AlbumListFilter } from '/@/renderer/store'; +import { useCurrentServerId } from '/@/renderer/store'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; import { NumberInput } from '/@/shared/components/number-input/number-input'; @@ -21,29 +19,15 @@ import { AlbumArtistListSort, GenreListSort, SortOrder } from '/@/shared/types/d interface SubsonicAlbumFiltersProps { disableArtistFilter?: boolean; - onFilterChange: (filters: AlbumListFilter) => void; - serverId: string; } -export const SubsonicAlbumFilters = ({ - disableArtistFilter, - onFilterChange, - serverId, -}: SubsonicAlbumFiltersProps) => { +export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFiltersProps) => { const { t } = useTranslation(); - const [favorite, setFavorite] = useQueryState(FILTER_KEYS.ALBUM.FAVORITE, parseAsBoolean); + const serverId = useCurrentServerId(); - const [minYear, setMinYear] = useQueryState(FILTER_KEYS.ALBUM.MIN_YEAR, parseAsInteger); - - const [maxYear, setMaxYear] = useQueryState(FILTER_KEYS.ALBUM.MAX_YEAR, parseAsInteger); - - const [genreId, setGenreId] = useQueryState(FILTER_KEYS.ALBUM.GENRE_ID, parseAsString); - - const [artistIds, setArtistIds] = useQueryState( - FILTER_KEYS.ALBUM.ARTIST_IDS, - parseAsArrayOf(parseAsString), - ); + const { query, setAlbumArtist, setFavorite, setGenreId, setMaxYear, setMinYear } = + useAlbumListFilters(); const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState(''); @@ -73,13 +57,12 @@ export const SubsonicAlbumFilters = ({ })); }, [items]); - const handleAlbumArtistFilter = (e: null | string[]) => { - setArtistIds(e ?? null); - const updatedFilters: Partial = { - artistIds: e?.length ? e : undefined, - }; - onFilterChange(updatedFilters as AlbumListFilter); - }; + const handleAlbumArtistFilter = useMemo( + () => (e: null | string[]) => { + setAlbumArtist(e ?? null); + }, + [setAlbumArtist], + ); const genreListQuery = useQuery( genresQueries.list({ @@ -104,43 +87,64 @@ export const SubsonicAlbumFilters = ({ })); }, [genreListQuery.data]); - const handleGenresFilter = debounce((e: null | string) => { - setGenreId(e ?? null); - const updatedFilters: Partial = { - genreIds: e ? [e] : undefined, - }; - onFilterChange(updatedFilters as AlbumListFilter); - }, 250); - - const toggleFilters = [ - { - label: t('filter.isFavorited', { postProcess: 'sentenceCase' }), - onChange: (e: ChangeEvent) => { - const favoriteValue = e.target.checked ? true : undefined; - setFavorite(favoriteValue ?? null); - const updatedFilters: Partial = { - favorite: favoriteValue, - }; - onFilterChange(updatedFilters as AlbumListFilter); - }, - value: favorite, + const handleGenresFilter = useMemo( + () => (e: null | string) => { + setGenreId(e ? [e] : null); }, - ]; + [setGenreId], + ); - const handleYearFilter = debounce((e: number | string, type: 'max' | 'min') => { - const year = e ? Number(e) : undefined; + const toggleFilters = useMemo( + () => [ + { + label: t('filter.isFavorited', { postProcess: 'sentenceCase' }), + onChange: (e: ChangeEvent) => { + const favoriteValue = e.target.checked ? true : undefined; + setFavorite(favoriteValue ?? null); + }, + value: query.favorite, + }, + ], + [t, query.favorite, setFavorite], + ); - if (type === 'min') { - setMinYear(year ?? null); - } else { - setMaxYear(year ?? null); - } + const handleMinYearFilter = useMemo( + () => (e: number | string) => { + // Handle empty string, null, undefined, or invalid numbers as clearing + if (e === '' || e === null || e === undefined || isNaN(Number(e))) { + setMinYear(null); + return; + } - const updatedFilters: Partial = { - [type === 'min' ? 'minYear' : 'maxYear']: year, - }; - onFilterChange(updatedFilters as AlbumListFilter); - }, 500); + const year = typeof e === 'number' ? e : Number(e); + // If it's a valid number, set it; otherwise clear + if (!isNaN(year) && isFinite(year) && year > 0) { + setMinYear(year); + } else { + setMinYear(null); + } + }, + [setMinYear], + ); + + const handleMaxYearFilter = useMemo( + () => (e: number | string) => { + // Handle empty string, null, undefined, or invalid numbers as clearing + if (e === '' || e === null || e === undefined || isNaN(Number(e))) { + setMaxYear(null); + return; + } + + const year = typeof e === 'number' ? e : Number(e); + // If it's a valid number, set it; otherwise clear + if (!isNaN(year) && isFinite(year) && year > 0) { + setMaxYear(year); + } else { + setMaxYear(null); + } + }, + [setMaxYear], + ); return ( @@ -153,51 +157,46 @@ export const SubsonicAlbumFilters = ({ 0)} hideControls={false} label={t('filter.fromYear', { postProcess: 'sentenceCase' })} max={5000} min={0} - onChange={(e) => handleYearFilter(e, 'min')} + onBlur={(e) => handleMinYearFilter(e.currentTarget.value)} /> 0)} hideControls={false} label={t('filter.toYear', { postProcess: 'sentenceCase' })} max={5000} min={0} - onChange={(e) => handleYearFilter(e, 'max')} - /> - - - handleGenresFilter(e)} + searchable + /> + : undefined} + searchable + searchValue={albumArtistSearchTerm} + /> ); }; diff --git a/src/renderer/features/genres/api/genres-api.ts b/src/renderer/features/genres/api/genres-api.ts index 13901f8a4..2b9e1d050 100644 --- a/src/renderer/features/genres/api/genres-api.ts +++ b/src/renderer/features/genres/api/genres-api.ts @@ -14,7 +14,7 @@ import { export const genresQueries = { list: (args: QueryHookArgs) => { return queryOptions({ - gcTime: 1000 * 5, + gcTime: 1000 * 60 * 60, queryFn: ({ signal }) => { return api.controller.getGenreList({ apiClientProps: { serverId: args.serverId, signal }, @@ -22,6 +22,7 @@ export const genresQueries = { }); }, queryKey: queryKeys.genres.list(args.serverId, args.query), + staleTime: 1000 * 60 * 60, ...args.options, }); }, diff --git a/src/renderer/features/shared/api/shared-api.ts b/src/renderer/features/shared/api/shared-api.ts index 2c22d9f17..4193737d5 100644 --- a/src/renderer/features/shared/api/shared-api.ts +++ b/src/renderer/features/shared/api/shared-api.ts @@ -38,6 +38,7 @@ export const sharedQueries = { }); }, queryKey: queryKeys.tags.list(args.serverId || '', args.query.type), + staleTime: 1000 * 60, ...args.options, }); }, diff --git a/src/renderer/features/songs/components/jellyfin-song-filters.tsx b/src/renderer/features/songs/components/jellyfin-song-filters.tsx index d280a484f..ca9ca97d7 100644 --- a/src/renderer/features/songs/components/jellyfin-song-filters.tsx +++ b/src/renderer/features/songs/components/jellyfin-song-filters.tsx @@ -1,5 +1,4 @@ import { useQuery } from '@tanstack/react-query'; -import debounce from 'lodash/debounce'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,7 +6,7 @@ import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-i import { useGenreList } from '/@/renderer/features/genres/api/genres-api'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; -import { SongListFilter, useCurrentServer } from '/@/renderer/store'; +import { SongListFilter, useCurrentServerId } from '/@/renderer/store'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; import { NumberInput } from '/@/shared/components/number-input/number-input'; @@ -21,7 +20,7 @@ interface JellyfinSongFiltersProps { } export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) => { - const server = useCurrentServer(); + const serverId = useCurrentServerId(); const { t } = useTranslation(); const { query, setCustom, setFavorite, setMaxYear, setMinYear } = useSongListFilters(); @@ -44,7 +43,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) query: { type: LibraryItem.SONG, }, - serverId: server.id, + serverId, }), ); @@ -66,33 +65,91 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) }, ]; - const handleMinYearFilter = debounce((e: number | string) => { - if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - setMinYear(e === '' ? null : (e as number)); - }, 500); + const handleMinYearFilter = useMemo( + () => (e: number | string) => { + // Handle empty string, null, undefined, or invalid numbers as clearing + if (e === '' || e === null || e === undefined || isNaN(Number(e))) { + setMinYear(null); + return; + } - const handleMaxYearFilter = debounce((e: number | string) => { - if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - setMaxYear(e === '' ? null : (e as number)); - }, 500); + const year = typeof e === 'number' ? e : Number(e); + // If it's a valid number within range, set it; otherwise clear + if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) { + setMinYear(year); + } else { + setMinYear(null); + } + }, + [setMinYear], + ); - const handleGenresFilter = debounce((e: string[] | undefined) => { - setCustom((prev) => ({ - ...prev, - GenreIds: e?.join(',') || undefined, - IncludeItemTypes: 'Audio', - ...prev?.jellyfin, - })); - }, 250); + const handleMaxYearFilter = useMemo( + () => (e: number | string) => { + // Handle empty string, null, undefined, or invalid numbers as clearing + if (e === '' || e === null || e === undefined || isNaN(Number(e))) { + setMaxYear(null); + return; + } - const handleTagFilter = debounce((e: string[] | undefined) => { - setCustom((prev) => ({ - ...prev, - IncludeItemTypes: 'Audio', - Tags: e?.join('|') || undefined, - ...prev?.jellyfin, - })); - }, 250); + const year = typeof e === 'number' ? e : Number(e); + // If it's a valid number within range, set it; otherwise clear + if (!isNaN(year) && isFinite(year) && year >= 1700 && year <= 2300) { + setMaxYear(year); + } else { + setMaxYear(null); + } + }, + [setMaxYear], + ); + + const handleGenresFilter = useMemo( + () => (e: string[] | undefined) => { + setCustom((prev) => { + if (!e || e.length === 0) { + // Remove GenreIds and IncludeItemTypes if genres are cleared + const rest = { ...prev }; + delete rest.GenreIds; + delete rest.IncludeItemTypes; + // Keep jellyfin-specific properties + return Object.keys(rest).length === 0 ? null : rest; + } + + return { + ...prev, + GenreIds: e.join(','), + IncludeItemTypes: 'Audio', + ...prev?.jellyfin, + }; + }); + }, + [setCustom], + ); + + const handleTagFilter = useMemo( + () => (e: string[] | undefined) => { + setCustom((prev) => { + if (!e || e.length === 0) { + // Remove Tags if cleared + const rest = { ...prev }; + delete rest.Tags; + // Keep IncludeItemTypes and jellyfin-specific properties + if (rest.IncludeItemTypes) { + return rest; + } + return Object.keys(rest).length === 0 ? null : rest; + } + + return { + ...prev, + IncludeItemTypes: 'Audio', + Tags: e.join('|'), + ...prev?.jellyfin, + }; + }); + }, + [setCustom], + ); return ( @@ -105,19 +162,21 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) handleMinYearFilter(e.currentTarget.value)} required={!!query.minYear} /> handleMaxYearFilter(e.currentTarget.value)} required={!!query.minYear} /> @@ -128,7 +187,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) data={genreList} defaultValue={selectedGenres} label={t('entity.genre', { count: 1, postProcess: 'sentenceCase' })} - onChange={handleGenresFilter} + onChange={(e) => handleGenresFilter(e)} searchable width={250} /> @@ -141,7 +200,7 @@ export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) data={tagsQuery.data.boolTags} defaultValue={selectedTags} label={t('common.tags', { postProcess: 'sentenceCase' })} - onChange={handleTagFilter} + onChange={(e) => handleTagFilter(e)} searchable width={250} /> diff --git a/src/renderer/features/songs/components/subsonic-song-filters.tsx b/src/renderer/features/songs/components/subsonic-song-filters.tsx index cfde8cede..ccd7a16aa 100644 --- a/src/renderer/features/songs/components/subsonic-song-filters.tsx +++ b/src/renderer/features/songs/components/subsonic-song-filters.tsx @@ -1,5 +1,4 @@ -import debounce from 'lodash/debounce'; -import { useMemo } from 'react'; +import { ChangeEvent, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; @@ -9,8 +8,8 @@ import { SongListFilter } from '/@/renderer/store'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; 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'; interface SubsonicSongFiltersProps { customFilters?: Partial; @@ -32,26 +31,33 @@ export const SubsonicSongFilters = ({ customFilters }: SubsonicSongFiltersProps) })); }, [genreListQuery.data]); - const handleGenresFilter = debounce((e: null | string) => { - setGenreId(e ? [e] : null); - }, 250); - - const toggleFilters = [ - { - label: t('filter.isFavorited', { postProcess: 'sentenceCase' }), - onChange: (favorite: boolean | undefined) => { - setFavorite(favorite ?? null); - }, - value: query.favorite, + const handleGenresFilter = useMemo( + () => (e: null | string) => { + setGenreId(e ? [e] : null); }, - ]; + [setGenreId], + ); + + const toggleFilters = useMemo( + () => [ + { + label: t('filter.isFavorited', { postProcess: 'sentenceCase' }), + onChange: (e: ChangeEvent) => { + const favoriteValue = e.target.checked ? true : undefined; + setFavorite(favoriteValue ?? null); + }, + value: query.favorite, + }, + ], + [t, query.favorite, setFavorite], + ); return ( {toggleFilters.map((filter) => ( {filter.label} - + ))}