Adjust album list header/filters

This commit is contained in:
jeffvli 2022-12-26 05:08:01 -08:00
parent 5ed06f79b3
commit 926d7f714e
3 changed files with 173 additions and 125 deletions

View file

@ -136,7 +136,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
); );
const handleFilterChange = useCallback( const handleFilterChange = useCallback(
async (filters: any) => { async (filters: AlbumListFilter) => {
gridRef.current?.scrollTo(0); gridRef.current?.scrollTo(0);
gridRef.current?.resetLoadMoreItemsCache(); gridRef.current?.resetLoadMoreItemsCache();
@ -185,14 +185,11 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
[handleFilterChange, page.filter.musicFolderId, setFilter], [handleFilterChange, page.filter.musicFolderId, setFilter],
); );
const handleSetOrder = useCallback( const handleToggleSortOrder = useCallback(() => {
(e: MouseEvent<HTMLButtonElement>) => { const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
if (!e.currentTarget?.value) return; const updatedFilters = setFilter({ sortOrder: newSortOrder });
const updatedFilters = setFilter({ sortOrder: e.currentTarget.value as SortOrder });
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, }, [filters.sortOrder, handleFilterChange, setFilter]);
[handleFilterChange, setFilter],
);
const handleSetViewType = useCallback( const handleSetViewType = useCallback(
(e: MouseEvent<HTMLButtonElement>) => { (e: MouseEvent<HTMLButtonElement>) => {
@ -210,7 +207,9 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
); );
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => { const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({ searchTerm: e.target.value }); const updatedFilters = setFilter({
searchTerm: e.target.value === '' ? undefined : e.target.value,
});
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, 500); }, 500);
@ -222,7 +221,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
gap="md" gap="md"
justify="center" justify="center"
> >
<DropdownMenu position="bottom"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button <Button
compact compact
@ -240,6 +239,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</Button> </Button>
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
<DropdownMenu.Label>Item size</DropdownMenu.Label>
<DropdownMenu.Item> <DropdownMenu.Item>
<Slider <Slider
defaultValue={page.grid.size || 0} defaultValue={page.grid.size || 0}
@ -248,16 +248,17 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
/> />
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Divider /> <DropdownMenu.Divider />
<DropdownMenu.Label>Display type</DropdownMenu.Label>
<DropdownMenu.Item <DropdownMenu.Item
$isActive={page.display === CardDisplayType.CARD} $isActive={page.display === ListDisplayType.CARD}
value={CardDisplayType.CARD} value={ListDisplayType.CARD}
onClick={handleSetViewType} onClick={handleSetViewType}
> >
Card Card
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Item
$isActive={page.display === CardDisplayType.POSTER} $isActive={page.display === ListDisplayType.POSTER}
value={CardDisplayType.POSTER} value={ListDisplayType.POSTER}
onClick={handleSetViewType} onClick={handleSetViewType}
> >
Poster Poster
@ -272,7 +273,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
<DropdownMenu position="bottom"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button <Button
compact compact
@ -295,12 +296,12 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
))} ))}
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
<DropdownMenu position="bottom">
<DropdownMenu.Target>
<Button <Button
compact compact
fw="normal" fw="normal"
tooltip={!cq.isMd ? { label: sortOrderLabel } : undefined}
variant="subtle" variant="subtle"
onClick={handleToggleSortOrder}
> >
{cq.isMd ? ( {cq.isMd ? (
sortOrderLabel sortOrderLabel
@ -314,26 +315,13 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</> </>
)} )}
</Button> </Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{ORDER.map((sort) => (
<DropdownMenu.Item
key={`sort-${sort.value}`}
$isActive={sort.value === filters.sortOrder}
value={sort.value}
onClick={handleSetOrder}
>
{sort.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
{server?.type === ServerType.JELLYFIN && ( {server?.type === ServerType.JELLYFIN && (
<DropdownMenu position="bottom"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button <Button
compact compact
fw="normal" fw="normal"
tooltip={!cq.isMd ? { label: 'Folder' } : undefined}
variant="subtle" variant="subtle"
> >
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />} {cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
@ -353,11 +341,15 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
)} )}
<Popover> <Popover
closeOnClickOutside={false}
position="bottom-start"
>
<Popover.Target> <Popover.Target>
<Button <Button
compact compact
fw="normal" fw="normal"
tooltip={!cq.isMd ? { label: 'Filters' } : undefined}
variant="subtle" variant="subtle"
> >
{cq.isMd ? 'Filters' : <RiFilter3Line size={15} />} {cq.isMd ? 'Filters' : <RiFilter3Line size={15} />}
@ -365,23 +357,17 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
{server?.type === ServerType.NAVIDROME ? ( {server?.type === ServerType.NAVIDROME ? (
<NavidromeAlbumFilters /> <NavidromeAlbumFilters handleFilterChange={handleFilterChange} />
) : ( ) : (
<JellyfinAlbumFilters /> <JellyfinAlbumFilters handleFilterChange={handleFilterChange} />
)} )}
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
</Flex> <DropdownMenu position="bottom-start">
<Flex gap="md">
<SearchInput
defaultValue={page.filter.searchTerm}
openedWidth={cq.isLg ? 300 : cq.isMd ? 250 : cq.isSm ? 150 : 75}
onChange={handleSearch}
/>
<DropdownMenu>
<DropdownMenu.Target> <DropdownMenu.Target>
<Button <Button
px="sm" compact
tooltip={{ label: 'More' }}
variant="subtle" variant="subtle"
> >
<RiMoreFill size={15} /> <RiMoreFill size={15} />
@ -395,6 +381,13 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
</Flex> </Flex>
<Flex gap="md">
<SearchInput
defaultValue={page.filter.searchTerm}
openedWidth={cq.isLg ? 300 : cq.isMd ? 250 : cq.isSm ? 150 : 75}
onChange={handleSearch}
/>
</Flex>
</HeaderItems> </HeaderItems>
</PageHeader> </PageHeader>
); );

View file

@ -1,11 +1,15 @@
import { ChangeEvent, useMemo } from 'react'; import { ChangeEvent, useMemo } from 'react';
import { Divider, Group, Stack } from '@mantine/core'; import { Divider, Group, Stack } from '@mantine/core';
import { MultiSelect, NumberInput, Switch, Text } from '/@/renderer/components'; import { MultiSelect, NumberInput, Switch, Text } from '/@/renderer/components';
import { useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store'; import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres'; import { useGenreList } from '/@/renderer/features/genres';
export const JellyfinAlbumFilters = () => { interface JellyfinAlbumFiltersProps {
handleFilterChange: (filters: AlbumListFilter) => void;
}
export const JellyfinAlbumFilters = ({ handleFilterChange }: JellyfinAlbumFiltersProps) => {
const { filter } = useAlbumListStore(); const { filter } = useAlbumListStore();
const setFilters = useSetAlbumFilters(); const setFilters = useSetAlbumFilters();
@ -28,9 +32,10 @@ export const JellyfinAlbumFilters = () => {
{ {
label: 'Is favorited', label: 'Is favorited',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
setFilters({ const updatedFilters = setFilters({
jfParams: { ...filter.jfParams, isFavorite: e.currentTarget.checked ? true : undefined }, jfParams: { ...filter.jfParams, isFavorite: e.currentTarget.checked ? true : undefined },
}); });
handleFilterChange(updatedFilters);
}, },
value: filter.jfParams?.isFavorite, value: filter.jfParams?.isFavorite,
}, },
@ -38,36 +43,77 @@ export const JellyfinAlbumFilters = () => {
const handleMinYearFilter = debounce((e: number | undefined) => { const handleMinYearFilter = debounce((e: number | undefined) => {
if (e && (e < 1700 || e > 2300)) return; if (e && (e < 1700 || e > 2300)) return;
setFilters({ const updatedFilters = setFilters({
jfParams: { jfParams: {
...filter.jfParams, ...filter.jfParams,
minYear: e, minYear: e,
}, },
}); });
handleFilterChange(updatedFilters);
}, 500); }, 500);
const handleMaxYearFilter = debounce((e: number | undefined) => { const handleMaxYearFilter = debounce((e: number | undefined) => {
if (e && (e < 1700 || e > 2300)) return; if (e && (e < 1700 || e > 2300)) return;
setFilters({ const updatedFilters = setFilters({
jfParams: { jfParams: {
...filter.jfParams, ...filter.jfParams,
maxYear: e, maxYear: e,
}, },
}); });
handleFilterChange(updatedFilters);
}, 500); }, 500);
const handleGenresFilter = debounce((e: string[] | undefined) => { const handleGenresFilter = debounce((e: string[] | undefined) => {
const genreFilterString = e?.join(','); const genreFilterString = e?.join(',');
setFilters({ const updatedFilters = setFilters({
jfParams: { jfParams: {
...filter.jfParams, ...filter.jfParams,
genreIds: genreFilterString, genreIds: genreFilterString,
}, },
}); });
handleFilterChange(updatedFilters);
}, 250); }, 250);
return ( return (
<Stack p="0.8rem"> <Stack p="0.8rem">
<Group position="apart">
<Text>Year range</Text>
<Group>
<NumberInput
required
hideControls={false}
max={2300}
min={1700}
value={filter.jfParams?.minYear}
width={80}
onChange={handleMinYearFilter}
/>
<NumberInput
hideControls={false}
max={2300}
min={1700}
value={filter.jfParams?.maxYear}
width={80}
onChange={handleMaxYearFilter}
/>
</Group>
</Group>
<Divider my="0.5rem" />
<Group
position="apart"
spacing={20}
>
<Text>Genres</Text>
<MultiSelect
clearable
searchable
data={genreList}
defaultValue={selectedGenres}
width={250}
onChange={handleGenresFilter}
/>
</Group>
<Divider my="0.5rem" />
{toggleFilters.map((filter) => ( {toggleFilters.map((filter) => (
<Group <Group
key={`nd-filter-${filter.label}`} key={`nd-filter-${filter.label}`}
@ -81,49 +127,14 @@ export const JellyfinAlbumFilters = () => {
/> />
</Group> </Group>
))} ))}
<Divider my="0.5rem" /> {/* <Divider my="0.5rem" />
<Group position="apart">
<Text>Year range</Text>
<Group>
<NumberInput
required
max={2300}
min={1700}
size="sm"
value={filter.jfParams?.minYear}
width={60}
onChange={handleMinYearFilter}
/>
<NumberInput
max={2300}
min={1700}
size="sm"
value={filter.jfParams?.maxYear}
width={60}
onChange={handleMaxYearFilter}
/>
</Group>
</Group>
<Divider my="0.5rem" />
<Stack>
<Text>Genres</Text>
<MultiSelect
clearable
searchable
data={genreList}
defaultValue={selectedGenres}
width={250}
onChange={handleGenresFilter}
/>
</Stack>
<Divider my="0.5rem" />
<Stack> <Stack>
<Text>Tags</Text> <Text>Tags</Text>
<MultiSelect <MultiSelect
disabled disabled
data={[]} data={[]}
/> />
</Stack> </Stack> */}
</Stack> </Stack>
); );
}; };

View file

@ -1,66 +1,123 @@
import { ChangeEvent } from 'react'; import { ChangeEvent, useMemo } from 'react';
import { Divider, Group, Stack } from '@mantine/core'; import { Divider, Group, Stack } from '@mantine/core';
import { NumberInput, Switch, Text } from '/@/renderer/components'; import { NumberInput, Switch, Text, Select } from '/@/renderer/components';
import { useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store'; import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres';
export const NavidromeAlbumFilters = () => { interface NavidromeAlbumFiltersProps {
handleFilterChange: (filters: AlbumListFilter) => void;
}
export const NavidromeAlbumFilters = ({ handleFilterChange }: NavidromeAlbumFiltersProps) => {
const { filter } = useAlbumListStore(); const { filter } = useAlbumListStore();
const setFilters = useSetAlbumFilters(); const setFilters = useSetAlbumFilters();
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 handleGenresFilter = debounce((e: string | null) => {
const updatedFilters = setFilters({
ndParams: {
...filter.ndParams,
genre_id: e || undefined,
},
});
handleFilterChange(updatedFilters);
}, 250);
const toggleFilters = [ const toggleFilters = [
{ {
label: 'Is rated', label: 'Is rated',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
setFilters({ const updatedFilters = setFilters({
ndParams: { ...filter.ndParams, has_rating: e.currentTarget.checked ? true : undefined }, ndParams: { ...filter.ndParams, has_rating: e.currentTarget.checked ? true : undefined },
}); });
handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.has_rating, value: filter.ndParams?.has_rating,
}, },
{ {
label: 'Is favorited', label: 'Is favorited',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
setFilters({ const updatedFilters = setFilters({
ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined },
}); });
handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.starred, value: filter.ndParams?.starred,
}, },
{ {
label: 'Is compilation', label: 'Is compilation',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
setFilters({ const updatedFilters = setFilters({
ndParams: { ...filter.ndParams, compilation: e.currentTarget.checked ? true : undefined }, ndParams: { ...filter.ndParams, compilation: e.currentTarget.checked ? true : undefined },
}); });
handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.compilation, value: filter.ndParams?.compilation,
}, },
{ {
label: 'Is recently played', label: 'Is recently played',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
setFilters({ const updatedFilters = setFilters({
ndParams: { ndParams: {
...filter.ndParams, ...filter.ndParams,
recently_played: e.currentTarget.checked ? true : undefined, recently_played: e.currentTarget.checked ? true : undefined,
}, },
}); });
handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.recently_played, value: filter.ndParams?.recently_played,
}, },
]; ];
const handleYearFilter = debounce((e: number | undefined) => { const handleYearFilter = debounce((e: number | undefined) => {
setFilters({ const updatedFilters = setFilters({
ndParams: { ndParams: {
...filter.ndParams, ...filter.ndParams,
year: e, year: e,
}, },
}); });
handleFilterChange(updatedFilters);
}, 500); }, 500);
return ( return (
<Stack p="0.8rem"> <Stack p="0.8rem">
<Group position="apart">
<Text>Year</Text>
<NumberInput
hideControls={false}
max={5000}
min={0}
value={filter.ndParams?.year}
width={80}
onChange={handleYearFilter}
/>
</Group>
<Divider my="0.5rem" />
<Group
position="apart"
spacing={20}
>
<Text>Genre</Text>
<Select
clearable
searchable
data={genreList}
defaultValue={filter.ndParams?.genre_id}
width={150}
onChange={handleGenresFilter}
/>
</Group>
<Divider my="0.5rem" />
{toggleFilters.map((filter) => ( {toggleFilters.map((filter) => (
<Group <Group
key={`nd-filter-${filter.label}`} key={`nd-filter-${filter.label}`}
@ -69,23 +126,10 @@ export const NavidromeAlbumFilters = () => {
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<Switch <Switch
checked={filter?.value || false} checked={filter?.value || false}
size="xs"
onChange={filter.onChange} onChange={filter.onChange}
/> />
</Group> </Group>
))} ))}
<Divider my="0.5rem" />
<Group position="apart">
<Text>Year</Text>
<NumberInput
max={5000}
min={0}
size="xs"
value={filter.ndParams?.year}
width={50}
onChange={handleYearFilter}
/>
</Group>
</Stack> </Stack>
); );
}; };