From 57c34637cf2fde0a6029ddfdd9b09c88cdcd577b Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 22 Dec 2022 01:58:11 -0800 Subject: [PATCH] Add server-specific album filters --- src/renderer/api/jellyfin.api.ts | 14 +- src/renderer/api/jellyfin.types.ts | 5 + src/renderer/api/navidrome.api.ts | 2 +- src/renderer/api/types.ts | 9 +- .../albums/components/album-list-header.tsx | 112 ++++++++++----- .../components/jellyfin-album-filters.tsx | 129 ++++++++++++++++++ .../components/navidrome-album-filters.tsx | 91 ++++++++++++ 7 files changed, 323 insertions(+), 39 deletions(-) create mode 100644 src/renderer/features/albums/components/jellyfin-album-filters.tsx create mode 100644 src/renderer/features/albums/components/navidrome-album-filters.tsx diff --git a/src/renderer/api/jellyfin.api.ts b/src/renderer/api/jellyfin.api.ts index 0c0d0d9f..cf508903 100644 --- a/src/renderer/api/jellyfin.api.ts +++ b/src/renderer/api/jellyfin.api.ts @@ -247,7 +247,16 @@ const getAlbumDetail = async (args: AlbumDetailArgs): Promise => const getAlbumList = async (args: AlbumListArgs): Promise => { const { query, server, signal } = args; - const searchParams: JFAlbumListParams = { + const yearsGroup = []; + if (query.jfParams?.minYear && query.jfParams?.maxYear) { + for (let i = Number(query.jfParams.minYear); i <= Number(query.jfParams.maxYear); i += 1) { + yearsGroup.push(String(i)); + } + } + + const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined; + + const searchParams: JFAlbumListParams & { maxYear?: number; minYear?: number } = { includeItemTypes: 'MusicAlbum', limit: query.limit, parentId: query.musicFolderId, @@ -257,6 +266,9 @@ const getAlbumList = async (args: AlbumListArgs): Promise => { sortOrder: sortOrderMap.jellyfin[query.sortOrder], startIndex: query.startIndex, ...query.jfParams, + maxYear: undefined, + minYear: undefined, + years: yearsFilter, }; const data = await api diff --git a/src/renderer/api/jellyfin.types.ts b/src/renderer/api/jellyfin.types.ts index 6b484f1d..0e114fa4 100644 --- a/src/renderer/api/jellyfin.types.ts +++ b/src/renderer/api/jellyfin.types.ts @@ -499,11 +499,16 @@ export enum JFAlbumListSort { } export type JFAlbumListParams = { + albumArtistIds?: string; + artistIds?: string; filters?: string; + genreIds?: string; genres?: string; includeItemTypes: 'MusicAlbum'; + isFavorite?: boolean; searchTerm?: string; sortBy?: JFAlbumListSort; + tags?: string; years?: string; } & JFBaseParams & JFPaginationParams; diff --git a/src/renderer/api/navidrome.api.ts b/src/renderer/api/navidrome.api.ts index ae00d613..57724fe2 100644 --- a/src/renderer/api/navidrome.api.ts +++ b/src/renderer/api/navidrome.api.ts @@ -215,7 +215,7 @@ const getAlbumList = async (args: AlbumListArgs): Promise => { const res = await api.get('api/album', { headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, prefixUrl: server?.url, - searchParams, + searchParams: parseSearchParams(searchParams), signal, }); diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index b73f701f..e9e9fd5f 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -287,9 +287,15 @@ export enum AlbumListSort { export type AlbumListQuery = { jfParams?: { + albumArtistIds?: string; + artistIds?: string; filters?: string; + genreIds?: string; genres?: string; - years?: string; + isFavorite?: boolean; + maxYear?: number; // Parses to years + minYear?: number; // Parses to years + tags?: string; }; limit?: number; musicFolderId?: string; @@ -299,6 +305,7 @@ export type AlbumListQuery = { genre_id?: string; has_rating?: boolean; name?: string; + recently_played?: boolean; starred?: boolean; year?: number; }; diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index ad95c6f3..740eb163 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -3,9 +3,22 @@ import { useCallback } from 'react'; import { Flex, Slider } from '@mantine/core'; import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; -import { RiArrowDownSLine } from 'react-icons/ri'; -import { AlbumListSort, SortOrder } from '/@/renderer/api/types'; -import { Button, DropdownMenu, PageHeader, SearchInput } from '/@/renderer/components'; +import { + RiArrowDownSLine, + RiFilter3Line, + RiFolder2Line, + RiSortAsc, + RiSortDesc, +} from 'react-icons/ri'; +import { AlbumListSort, ServerType, SortOrder } from '/@/renderer/api/types'; +import { + Button, + DropdownMenu, + PageHeader, + Popover, + SearchInput, + TextTitle, +} from '/@/renderer/components'; import { useCurrentServer, useAlbumListStore, @@ -15,6 +28,8 @@ import { import { CardDisplayType } from '/@/renderer/types'; import { useMusicFolders } from '/@/renderer/features/shared'; import styled from 'styled-components'; +import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters'; +import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters'; const FILTERS = { jellyfin: [ @@ -35,6 +50,7 @@ const FILTERS = { { name: 'Random', value: AlbumListSort.RANDOM }, { name: 'Rating', value: AlbumListSort.RATING }, { name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED }, + { name: 'Recently Played', value: AlbumListSort.RECENTLY_PLAYED }, { name: 'Song Count', value: AlbumListSort.SONG_COUNT }, { name: 'Favorited', value: AlbumListSort.FAVORITED }, { name: 'Year', value: AlbumListSort.YEAR }, @@ -68,8 +84,6 @@ export const AlbumListHeader = () => { )?.name) || 'Unknown'; - const sortOrderLabel = ORDER.find((s) => s.value === filters.sortOrder)?.name; - const setSize = throttle( (e: number) => setPage({ @@ -101,13 +115,9 @@ export const AlbumListHeader = () => { const handleSetOrder = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; - debounce( - () => - setFilter({ - sortOrder: e.currentTarget.value as SortOrder, - }), - 1000, - ); + setFilter({ + sortOrder: e.currentTarget.value as SortOrder, + }); }, [setFilter], ); @@ -153,19 +163,25 @@ export const AlbumListHeader = () => { - + @@ -201,7 +217,7 @@ export const AlbumListHeader = () => { - + @@ -247,29 +267,49 @@ export const AlbumListHeader = () => { ))} - - + {server?.type === ServerType.JELLYFIN && ( + + + + + + {musicFoldersQuery.data?.map((folder) => ( + + {folder.name} + + ))} + + + )} + + - - - {musicFoldersQuery.data?.map((folder) => ( - - {folder.name} - - ))} - - + + + {server?.type === ServerType.NAVIDROME ? ( + + ) : ( + + )} + + { + const { filter } = useAlbumListStore(); + const setFilters = useSetAlbumFilters(); + + // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library + const genreListQuery = useGenreList(null); + + const genreList = useMemo(() => { + if (!genreListQuery?.data) return []; + return genreListQuery.data.map((genre) => ({ + label: genre.name, + value: genre.id, + })); + }, [genreListQuery.data]); + + const selectedGenres = useMemo(() => { + return filter.jfParams?.genreIds?.split(','); + }, [filter.jfParams?.genreIds]); + + const toggleFilters = [ + { + label: 'Is favorited', + onChange: (e: ChangeEvent) => { + setFilters({ + jfParams: { ...filter.jfParams, isFavorite: e.currentTarget.checked ? true : undefined }, + }); + }, + value: filter.jfParams?.isFavorite, + }, + ]; + + const handleMinYearFilter = debounce((e: number | undefined) => { + if (e && (e < 1700 || e > 2300)) return; + setFilters({ + jfParams: { + ...filter.jfParams, + minYear: e, + }, + }); + }, 500); + + const handleMaxYearFilter = debounce((e: number | undefined) => { + if (e && (e < 1700 || e > 2300)) return; + setFilters({ + jfParams: { + ...filter.jfParams, + maxYear: e, + }, + }); + }, 500); + + const handleGenresFilter = debounce((e: string[] | undefined) => { + const genreFilterString = e?.join(','); + setFilters({ + jfParams: { + ...filter.jfParams, + genreIds: genreFilterString, + }, + }); + }, 250); + + return ( + + {toggleFilters.map((filter) => ( + + {filter.label} + + + ))} + + + Year range + + + + + + + + Genres + + + + + Tags + + + + ); +}; diff --git a/src/renderer/features/albums/components/navidrome-album-filters.tsx b/src/renderer/features/albums/components/navidrome-album-filters.tsx new file mode 100644 index 00000000..cfff3f33 --- /dev/null +++ b/src/renderer/features/albums/components/navidrome-album-filters.tsx @@ -0,0 +1,91 @@ +import { ChangeEvent } from 'react'; +import { Divider, Group, Stack } from '@mantine/core'; +import { NumberInput, Switch, Text } from '/@/renderer/components'; +import { useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store'; +import debounce from 'lodash/debounce'; + +export const NavidromeAlbumFilters = () => { + const { filter } = useAlbumListStore(); + const setFilters = useSetAlbumFilters(); + + const toggleFilters = [ + { + label: 'Is rated', + onChange: (e: ChangeEvent) => { + setFilters({ + ndParams: { ...filter.ndParams, has_rating: e.currentTarget.checked ? true : undefined }, + }); + }, + value: filter.ndParams?.has_rating, + }, + { + label: 'Is favorited', + onChange: (e: ChangeEvent) => { + setFilters({ + ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, + }); + }, + value: filter.ndParams?.starred, + }, + { + label: 'Is compilation', + onChange: (e: ChangeEvent) => { + setFilters({ + ndParams: { ...filter.ndParams, compilation: e.currentTarget.checked ? true : undefined }, + }); + }, + value: filter.ndParams?.compilation, + }, + { + label: 'Is recently played', + onChange: (e: ChangeEvent) => { + setFilters({ + ndParams: { + ...filter.ndParams, + recently_played: e.currentTarget.checked ? true : undefined, + }, + }); + }, + value: filter.ndParams?.recently_played, + }, + ]; + + const handleYearFilter = debounce((e: number | undefined) => { + setFilters({ + ndParams: { + ...filter.ndParams, + year: e, + }, + }); + }, 500); + + return ( + + {toggleFilters.map((filter) => ( + + {filter.label} + + + ))} + + + Year + + + + ); +};