Add view artist discography

This commit is contained in:
jeffvli 2023-01-15 16:22:07 -08:00
parent 67523f1e7b
commit 5614ad54f2
7 changed files with 338 additions and 155 deletions

View file

@ -10,12 +10,12 @@ import {
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ListDisplayType, CardRow } from '/@/renderer/types'; import { ListDisplayType, CardRow } from '/@/renderer/types';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { MutableRefObject, useCallback, useMemo } from 'react'; import { MutableRefObject, useCallback, useMemo, useState } from 'react';
import { ListOnScrollProps } from 'react-window'; import { ListOnScrollProps } from 'react-window';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller'; import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { Album, AlbumListSort, LibraryItem } from '/@/renderer/api/types'; import { Album, AlbumListQuery, AlbumListSort, LibraryItem } from '/@/renderer/api/types';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { import {
useCurrentServer, useCurrentServer,
@ -25,6 +25,7 @@ import {
useSetAlbumTable, useSetAlbumTable,
useSetAlbumTablePagination, useSetAlbumTablePagination,
useAlbumListItemData, useAlbumListItemData,
AlbumListFilter,
} from '/@/renderer/store'; } from '/@/renderer/store';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { import {
@ -44,12 +45,18 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
interface AlbumListContentProps { interface AlbumListContentProps {
customFilters?: Partial<AlbumListFilter>;
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number; itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
} }
export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListContentProps) => { export const AlbumListContent = ({
customFilters,
itemCount,
gridRef,
tableRef,
}: AlbumListContentProps) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const server = useCurrentServer(); const server = useCurrentServer();
@ -58,6 +65,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const { itemData, setItemData } = useAlbumListItemData(); const { itemData, setItemData } = useAlbumListItemData();
const [localItemData, setLocalItemData] = useState<any[]>([]);
const pagination = useAlbumTablePagination(); const pagination = useAlbumTablePagination();
const setPagination = useSetAlbumTablePagination(); const setPagination = useSetAlbumTablePagination();
@ -77,21 +85,28 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
const limit = params.endRow - params.startRow; const limit = params.endRow - params.startRow;
const startIndex = params.startRow; const startIndex = params.startRow;
const queryKey = queryKeys.albums.list(server?.id || '', { const query: AlbumListQuery = {
limit, limit,
startIndex, startIndex,
...page.filter, ...page.filter,
}); ...customFilters,
jfParams: {
...page.filter.jfParams,
...customFilters?.jfParams,
},
ndParams: {
...page.filter.ndParams,
...customFilters?.ndParams,
},
};
const queryKey = queryKeys.albums.list(server?.id || '', query);
const albumsRes = await queryClient.fetchQuery( const albumsRes = await queryClient.fetchQuery(
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumList({ api.controller.getAlbumList({
query: { query,
limit,
startIndex,
...page.filter,
},
server, server,
signal, signal,
}), }),
@ -104,9 +119,12 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
rowCount: undefined, rowCount: undefined,
}; };
params.api.setDatasource(dataSource); params.api.setDatasource(dataSource);
params.api.ensureIndexVisible(page.table.scrollOffset || 0, 'top');
if (!customFilters) {
params.api.ensureIndexVisible(page.table.scrollOffset || 0, 'top');
}
}, },
[page.filter, page.table.scrollOffset, queryClient, server], [customFilters, page.filter, page.table.scrollOffset, queryClient, server],
); );
const onTablePaginationChanged = useCallback( const onTablePaginationChanged = useCallback(
@ -153,25 +171,33 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
const debouncedTableColumnChange = debounce(handleTableColumnChange, 200); const debouncedTableColumnChange = debounce(handleTableColumnChange, 200);
const handleTableScroll = (e: BodyScrollEvent) => { const handleTableScroll = (e: BodyScrollEvent) => {
if (customFilters) return;
const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0)); const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0));
setTable({ scrollOffset }); setTable({ scrollOffset });
}; };
const fetch = useCallback( const fetch = useCallback(
async ({ skip, take }: { skip: number; take: number }) => { async ({ skip, take }: { skip: number; take: number }) => {
const queryKey = queryKeys.albums.list(server?.id || '', { const query: AlbumListQuery = {
limit: take, limit: take,
startIndex: skip, startIndex: skip,
...page.filter, ...page.filter,
}); ...customFilters,
jfParams: {
...page.filter.jfParams,
...customFilters?.jfParams,
},
ndParams: {
...page.filter.ndParams,
...customFilters?.ndParams,
},
};
const queryKey = queryKeys.albums.list(server?.id || '', query);
const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
controller.getAlbumList({ controller.getAlbumList({
query: { query,
limit: take,
startIndex: skip,
...page.filter,
},
server, server,
signal, signal,
}), }),
@ -179,11 +205,12 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
return api.normalize.albumList(albums, server); return api.normalize.albumList(albums, server);
}, },
[page.filter, queryClient, server], [customFilters, page.filter, queryClient, server],
); );
const handleGridScroll = useCallback( const handleGridScroll = useCallback(
(e: ListOnScrollProps) => { (e: ListOnScrollProps) => {
if (customFilters) return;
setPage({ setPage({
list: { list: {
...page, ...page,
@ -194,7 +221,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
}, },
}); });
}, },
[page, setPage], [customFilters, page, setPage],
); );
const cardRows = useMemo(() => { const cardRows = useMemo(() => {
@ -308,9 +335,9 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
handleFavorite={handleFavorite} handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd} handlePlayQueueAdd={handlePlayQueueAdd}
height={height} height={height}
initialScrollOffset={page?.grid.scrollOffset || 0} initialScrollOffset={customFilters ? 0 : page?.grid.scrollOffset || 0}
itemCount={itemCount || 0} itemCount={itemCount || 0}
itemData={itemData} itemData={customFilters ? localItemData : itemData}
itemGap={20} itemGap={20}
itemSize={150 + page.grid?.size} itemSize={150 + page.grid?.size}
itemType={LibraryItem.ALBUM} itemType={LibraryItem.ALBUM}
@ -320,7 +347,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
route: AppRoute.LIBRARY_ALBUMS_DETAIL, route: AppRoute.LIBRARY_ALBUMS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }], slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
}} }}
setItemData={setItemData} setItemData={customFilters ? setLocalItemData : setItemData}
width={width} width={width}
onScroll={handleGridScroll} onScroll={handleGridScroll}
/> />
@ -334,6 +361,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`} key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`}
ref={tableRef} ref={tableRef}
alwaysShowHorizontalScroll alwaysShowHorizontalScroll
suppressRowDrag
autoFitColumns={page.table.autoFit} autoFitColumns={page.table.autoFit}
blockLoadDebounceMillis={200} blockLoadDebounceMillis={200}
columnDefs={columnDefs} columnDefs={columnDefs}

View file

@ -3,6 +3,7 @@ import { useCallback } from 'react';
import { IDatasource } from '@ag-grid-community/core'; import { IDatasource } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core'; import { Flex, Group, Stack } from '@mantine/core';
import { openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { import {
@ -17,7 +18,13 @@ import styled from 'styled-components';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller'; import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types'; import {
AlbumListQuery,
AlbumListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/renderer/api/types';
import { import {
ALBUM_TABLE_COLUMNS, ALBUM_TABLE_COLUMNS,
Badge, Badge,
@ -25,7 +32,6 @@ import {
DropdownMenu, DropdownMenu,
MultiSelect, MultiSelect,
PageHeader, PageHeader,
Popover,
SearchInput, SearchInput,
Slider, Slider,
SpinnerIcon, SpinnerIcon,
@ -92,12 +98,20 @@ const HeaderItems = styled.div`
`; `;
interface AlbumListHeaderProps { interface AlbumListHeaderProps {
customFilters?: Partial<AlbumListFilter>;
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number; itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
title?: string;
} }
export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeaderProps) => { export const AlbumListHeader = ({
itemCount,
gridRef,
tableRef,
title,
customFilters,
}: AlbumListHeaderProps) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const server = useCurrentServer(); const server = useCurrentServer();
const setPage = useSetAlbumStore(); const setPage = useSetAlbumStore();
@ -131,21 +145,28 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
const fetch = useCallback( const fetch = useCallback(
async (skip: number, take: number, filters: AlbumListFilter) => { async (skip: number, take: number, filters: AlbumListFilter) => {
const queryKey = queryKeys.albums.list(server?.id || '', { const query: AlbumListQuery = {
limit: take, limit: take,
startIndex: skip, startIndex: skip,
...filters, ...filters,
}); jfParams: {
...filters.jfParams,
...customFilters?.jfParams,
},
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
},
...customFilters,
};
const queryKey = queryKeys.albums.list(server?.id || '', query);
const albums = await queryClient.fetchQuery( const albums = await queryClient.fetchQuery(
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
controller.getAlbumList({ controller.getAlbumList({
query: { query,
limit: take,
startIndex: skip,
...filters,
},
server, server,
signal, signal,
}), }),
@ -154,7 +175,7 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
return api.normalize.albumList(albums, server); return api.normalize.albumList(albums, server);
}, },
[queryClient, server], [customFilters, queryClient, server],
); );
const handleFilterChange = useCallback( const handleFilterChange = useCallback(
@ -168,21 +189,28 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
const limit = params.endRow - params.startRow; const limit = params.endRow - params.startRow;
const startIndex = params.startRow; const startIndex = params.startRow;
const queryKey = queryKeys.albums.list(server?.id || '', { const query: AlbumListQuery = {
limit, limit,
startIndex, startIndex,
...filters, ...filters,
}); ...customFilters,
jfParams: {
...filters.jfParams,
...customFilters?.jfParams,
},
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
},
};
const queryKey = queryKeys.albums.list(server?.id || '', query);
const albumsRes = await queryClient.fetchQuery( const albumsRes = await queryClient.fetchQuery(
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumList({ api.controller.getAlbumList({
query: { query,
limit,
startIndex,
...filters,
},
server, server,
signal, signal,
}), }),
@ -214,9 +242,30 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
gridRef.current?.setItemData(data.items); gridRef.current?.setItemData(data.items);
} }
}, },
[page.display, tableRef, setPagination, server, queryClient, gridRef, fetch], [page.display, tableRef, customFilters, server, queryClient, setPagination, gridRef, fetch],
); );
const handleOpenFiltersModal = () => {
openModal({
children: (
<>
{server?.type === ServerType.NAVIDROME ? (
<NavidromeAlbumFilters
disableArtistFilter={!!customFilters}
handleFilterChange={handleFilterChange}
/>
) : (
<JellyfinAlbumFilters
disableArtistFilter={!!customFilters}
handleFilterChange={handleFilterChange}
/>
)}
</>
),
title: 'Album Filters',
});
};
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || '')); queryClient.invalidateQueries(queryKeys.albums.list(server?.id || ''));
handleFilterChange(filters); handleFilterChange(filters);
@ -315,7 +364,19 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
const handlePlay = async (play: Play) => { const handlePlay = async (play: Play) => {
if (!itemCount || itemCount === 0) return; if (!itemCount || itemCount === 0) return;
const query = { startIndex: 0, ...filters }; const query = {
startIndex: 0,
...filters,
...customFilters,
jfParams: {
...filters.jfParams,
...customFilters?.jfParams,
},
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
},
};
const queryKey = queryKeys.albums.list(server?.id || '', query); const queryKey = queryKeys.albums.list(server?.id || '', query);
const albumListRes = await queryClient.fetchQuery({ const albumListRes = await queryClient.fetchQuery({
@ -355,9 +416,11 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
<Group noWrap> <Group noWrap>
<TextTitle <TextTitle
fw="bold" fw="bold"
maw="20vw"
order={3} order={3}
overflow="hidden"
> >
Albums {title || 'Albums'}
</TextTitle> </TextTitle>
<Badge <Badge
radius="xl" radius="xl"
@ -509,24 +572,14 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
)} )}
<Popover position="bottom-start"> <Button
<Popover.Target> compact
<Button fw="600"
compact variant="subtle"
fw="600" onClick={handleOpenFiltersModal}
variant="subtle" >
> {cq.isMd ? 'Filters' : <RiFilter3Line size={15} />}
{cq.isMd ? 'Filters' : <RiFilter3Line size={15} />} </Button>
</Button>
</Popover.Target>
<Popover.Dropdown>
{server?.type === ServerType.NAVIDROME ? (
<NavidromeAlbumFilters handleFilterChange={handleFilterChange} />
) : (
<JellyfinAlbumFilters handleFilterChange={handleFilterChange} />
)}
</Popover.Dropdown>
</Popover>
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
<Button <Button

View file

@ -1,15 +1,22 @@
import { ChangeEvent, useMemo } from 'react'; import { ChangeEvent, useMemo, useState } 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, SpinnerIcon, Switch, Text } from '/@/renderer/components';
import { AlbumListFilter, 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';
import { useDebouncedValue } from '@mantine/hooks';
import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
interface JellyfinAlbumFiltersProps { interface JellyfinAlbumFiltersProps {
disableArtistFilter?: boolean;
handleFilterChange: (filters: AlbumListFilter) => void; handleFilterChange: (filters: AlbumListFilter) => void;
} }
export const JellyfinAlbumFilters = ({ handleFilterChange }: JellyfinAlbumFiltersProps) => { export const JellyfinAlbumFilters = ({
disableArtistFilter,
handleFilterChange,
}: JellyfinAlbumFiltersProps) => {
const { filter } = useAlbumListStore(); const { filter } = useAlbumListStore();
const setFilters = useSetAlbumFilters(); const setFilters = useSetAlbumFilters();
@ -74,46 +81,33 @@ export const JellyfinAlbumFilters = ({ handleFilterChange }: JellyfinAlbumFilter
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, 250); }, 250);
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const [debouncedSearchTerm] = useDebouncedValue(albumArtistSearchTerm, 200);
const albumArtistListQuery = useAlbumArtistList(
{
limit: 300,
searchTerm: debouncedSearchTerm,
sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
{
enabled: debouncedSearchTerm ? debouncedSearchTerm !== '' : false,
},
);
const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return [];
return albumArtistListQuery?.data?.items?.map((artist) => ({
label: artist.name,
value: artist.id,
}));
}, [albumArtistListQuery?.data?.items]);
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}`}
@ -127,14 +121,52 @@ export const JellyfinAlbumFilters = ({ handleFilterChange }: JellyfinAlbumFilter
/> />
</Group> </Group>
))} ))}
{/* <Divider my="0.5rem" /> <Divider my="0.5rem" />
<Stack> <Group grow>
<Text>Tags</Text> <NumberInput
<MultiSelect hideControls={false}
disabled label="From year"
data={[]} max={2300}
min={1700}
required={!!filter.jfParams?.maxYear}
value={filter.jfParams?.minYear}
onChange={handleMinYearFilter}
/> />
</Stack> */} <NumberInput
hideControls={false}
label="To year"
max={2300}
min={1700}
required={!!filter.jfParams?.minYear}
value={filter.jfParams?.maxYear}
onChange={handleMaxYearFilter}
/>
</Group>
<Group grow>
<MultiSelect
clearable
searchable
data={genreList}
defaultValue={selectedGenres}
label="Genres"
onChange={handleGenresFilter}
/>
</Group>
<Group grow>
<MultiSelect
clearable
searchable
data={selectableAlbumArtists}
disabled={disableArtistFilter}
label="Artist"
limit={300}
placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchValue={albumArtistSearchTerm}
onSearchChange={setAlbumArtistSearchTerm}
/>
</Group>
</Stack> </Stack>
); );
}; };

View file

@ -1,15 +1,22 @@
import { ChangeEvent, useMemo } from 'react'; import { ChangeEvent, useMemo, useState } from 'react';
import { Divider, Group, Stack } from '@mantine/core'; import { Divider, Group, Stack } from '@mantine/core';
import { NumberInput, Switch, Text, Select } from '/@/renderer/components'; import { NumberInput, Switch, Text, Select, SpinnerIcon } from '/@/renderer/components';
import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store'; import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store';
import { useDebouncedValue } from '@mantine/hooks';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres'; import { useGenreList } from '/@/renderer/features/genres';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types';
interface NavidromeAlbumFiltersProps { interface NavidromeAlbumFiltersProps {
disableArtistFilter?: boolean;
handleFilterChange: (filters: AlbumListFilter) => void; handleFilterChange: (filters: AlbumListFilter) => void;
} }
export const NavidromeAlbumFilters = ({ handleFilterChange }: NavidromeAlbumFiltersProps) => { export const NavidromeAlbumFilters = ({
handleFilterChange,
disableArtistFilter,
}: NavidromeAlbumFiltersProps) => {
const { filter } = useAlbumListStore(); const { filter } = useAlbumListStore();
const setFilters = useSetAlbumFilters(); const setFilters = useSetAlbumFilters();
@ -89,35 +96,33 @@ export const NavidromeAlbumFilters = ({ handleFilterChange }: NavidromeAlbumFilt
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, 500); }, 500);
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const [debouncedSearchTerm] = useDebouncedValue(albumArtistSearchTerm, 200);
const albumArtistListQuery = useAlbumArtistList(
{
limit: 300,
searchTerm: debouncedSearchTerm,
sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
{
enabled: debouncedSearchTerm ? debouncedSearchTerm !== '' : false,
},
);
const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return [];
return albumArtistListQuery?.data?.items?.map((artist) => ({
label: artist.name,
value: artist.id,
}));
}, [albumArtistListQuery?.data?.items]);
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}`}
@ -130,6 +135,39 @@ export const NavidromeAlbumFilters = ({ handleFilterChange }: NavidromeAlbumFilt
/> />
</Group> </Group>
))} ))}
<Divider my="0.5rem" />
<Group grow>
<NumberInput
hideControls={false}
label="Year"
max={5000}
min={0}
value={filter.ndParams?.year}
onChange={handleYearFilter}
/>
<Select
clearable
searchable
data={genreList}
defaultValue={filter.ndParams?.genre_id}
label="Genre"
onChange={handleGenresFilter}
/>
</Group>
<Group grow>
<Select
clearable
searchable
data={selectableAlbumArtists}
disabled={disableArtistFilter}
label="Artist"
limit={300}
placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchValue={albumArtistSearchTerm}
onSearchChange={setAlbumArtistSearchTerm}
/>
</Group>
</Stack> </Stack>
); );
}; };

View file

@ -5,18 +5,49 @@ import { AlbumListContent } from '/@/renderer/features/albums/components/album-l
import { useRef } from 'react'; import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query'; import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
import { useAlbumListFilters } from '/@/renderer/store'; import { useAlbumListFilters, useCurrentServer } from '/@/renderer/store';
import { useSearchParams } from 'react-router-dom';
import { AlbumListQuery, ServerType } from '/@/renderer/api/types';
const AlbumListRoute = () => { const AlbumListRoute = () => {
const gridRef = useRef<VirtualInfiniteGridRef | null>(null); const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const filters = useAlbumListFilters(); const filters = useAlbumListFilters();
const server = useCurrentServer();
const [searchParams] = useSearchParams();
const customFilters: Partial<AlbumListQuery> | undefined = searchParams.get('artistId')
? {
jfParams:
server?.type === ServerType.JELLYFIN
? {
artistIds: searchParams.get('artistId') as string,
}
: undefined,
ndParams:
server?.type === ServerType.NAVIDROME
? {
artist_id: searchParams.get('artistId') as string,
}
: undefined,
}
: undefined;
const itemCountCheck = useAlbumList( const itemCountCheck = useAlbumList(
{ {
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
...filters, ...filters,
...customFilters,
jfParams: {
...filters.jfParams,
...customFilters?.jfParams,
},
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
},
}, },
{ {
cacheTime: 1000 * 60 * 60 * 2, cacheTime: 1000 * 60 * 60 * 2,
@ -33,11 +64,14 @@ const AlbumListRoute = () => {
<AnimatedPage> <AnimatedPage>
<VirtualGridContainer> <VirtualGridContainer>
<AlbumListHeader <AlbumListHeader
customFilters={customFilters}
gridRef={gridRef} gridRef={gridRef}
itemCount={itemCount} itemCount={itemCount}
tableRef={tableRef} tableRef={tableRef}
title={searchParams.get('artistName') || undefined}
/> />
<AlbumListContent <AlbumListContent
customFilters={customFilters}
gridRef={gridRef} gridRef={gridRef}
itemCount={itemCount} itemCount={itemCount}
tableRef={tableRef} tableRef={tableRef}

View file

@ -13,7 +13,7 @@ import { Box, Group, Stack } from '@mantine/core';
import { RiArrowDownSLine, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri'; import { RiArrowDownSLine, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
import { generatePath, useParams } from 'react-router'; import { generatePath, useParams } from 'react-router';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { Link } from 'react-router-dom'; import { createSearchParams, Link } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
@ -66,6 +66,13 @@ export const AlbumArtistDetailContent = () => {
const detailQuery = useAlbumArtistDetail({ id: albumArtistId }); const detailQuery = useAlbumArtistDetail({ id: albumArtistId });
const artistDiscographyLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
albumArtistId,
})}?${createSearchParams({
artistId: albumArtistId,
artistName: detailQuery?.data?.name || '',
})}`;
const recentAlbumsQuery = useAlbumList({ const recentAlbumsQuery = useAlbumList({
jfParams: server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined, jfParams: server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined,
limit: itemsPerPage, limit: itemsPerPage,
@ -146,9 +153,7 @@ export const AlbumArtistDetailContent = () => {
compact compact
uppercase uppercase
component={Link} component={Link}
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, { to={artistDiscographyLink}
albumArtistId,
})}
variant="subtle" variant="subtle"
> >
View discography View discography
@ -283,9 +288,7 @@ export const AlbumArtistDetailContent = () => {
compact compact
uppercase uppercase
component={Link} component={Link}
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, { to={artistDiscographyLink}
albumArtistId,
})}
variant="subtle" variant="subtle"
> >
View discography View discography
@ -302,7 +305,6 @@ export const AlbumArtistDetailContent = () => {
</Group> </Group>
</Group> </Group>
</Box> </Box>
{showGenres && ( {showGenres && (
<Box component="section"> <Box component="section">
<Group> <Group>

View file

@ -58,10 +58,6 @@ const AlbumArtistDetailTopSongsListRoute = lazy(
() => import('../features/artists/routes/album-artist-detail-top-songs-list-route'), () => import('../features/artists/routes/album-artist-detail-top-songs-list-route'),
); );
const AlbumArtistDetailDiscographyRoute = lazy(
() => import('../features/artists/routes/album-artist-detail-discography-route'),
);
const AlbumDetailRoute = lazy( const AlbumDetailRoute = lazy(
() => import('/@/renderer/features/albums/routes/album-detail-route'), () => import('/@/renderer/features/albums/routes/album-detail-route'),
); );
@ -141,7 +137,7 @@ export const AppRouter = () => {
element={<AlbumArtistDetailRoute />} element={<AlbumArtistDetailRoute />}
/> />
<Route <Route
element={<AlbumArtistDetailDiscographyRoute />} element={<AlbumListRoute />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY} path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY}
/> />
<Route <Route