mirror of
https://github.com/jeffvli/feishin.git
synced 2024-11-20 14:37:06 +01:00
Add view artist discography
This commit is contained in:
parent
67523f1e7b
commit
5614ad54f2
@ -10,12 +10,12 @@ import {
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { ListDisplayType, CardRow } from '/@/renderer/types';
|
||||
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 { api } from '/@/renderer/api';
|
||||
import { controller } from '/@/renderer/api/controller';
|
||||
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 {
|
||||
useCurrentServer,
|
||||
@ -25,6 +25,7 @@ import {
|
||||
useSetAlbumTable,
|
||||
useSetAlbumTablePagination,
|
||||
useAlbumListItemData,
|
||||
AlbumListFilter,
|
||||
} from '/@/renderer/store';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import {
|
||||
@ -44,12 +45,18 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||
|
||||
interface AlbumListContentProps {
|
||||
customFilters?: Partial<AlbumListFilter>;
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount?: number;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListContentProps) => {
|
||||
export const AlbumListContent = ({
|
||||
customFilters,
|
||||
itemCount,
|
||||
gridRef,
|
||||
tableRef,
|
||||
}: AlbumListContentProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const server = useCurrentServer();
|
||||
@ -58,6 +65,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
const { itemData, setItemData } = useAlbumListItemData();
|
||||
const [localItemData, setLocalItemData] = useState<any[]>([]);
|
||||
|
||||
const pagination = useAlbumTablePagination();
|
||||
const setPagination = useSetAlbumTablePagination();
|
||||
@ -77,21 +85,28 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', {
|
||||
const query: AlbumListQuery = {
|
||||
limit,
|
||||
startIndex,
|
||||
...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(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumList({
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...page.filter,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
@ -104,9 +119,12 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
rowCount: undefined,
|
||||
};
|
||||
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(
|
||||
@ -153,25 +171,33 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
const debouncedTableColumnChange = debounce(handleTableColumnChange, 200);
|
||||
|
||||
const handleTableScroll = (e: BodyScrollEvent) => {
|
||||
if (customFilters) return;
|
||||
const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0));
|
||||
setTable({ scrollOffset });
|
||||
};
|
||||
|
||||
const fetch = useCallback(
|
||||
async ({ skip, take }: { skip: number; take: number }) => {
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', {
|
||||
const query: AlbumListQuery = {
|
||||
limit: take,
|
||||
startIndex: skip,
|
||||
...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 }) =>
|
||||
controller.getAlbumList({
|
||||
query: {
|
||||
limit: take,
|
||||
startIndex: skip,
|
||||
...page.filter,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
@ -179,11 +205,12 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
|
||||
return api.normalize.albumList(albums, server);
|
||||
},
|
||||
[page.filter, queryClient, server],
|
||||
[customFilters, page.filter, queryClient, server],
|
||||
);
|
||||
|
||||
const handleGridScroll = useCallback(
|
||||
(e: ListOnScrollProps) => {
|
||||
if (customFilters) return;
|
||||
setPage({
|
||||
list: {
|
||||
...page,
|
||||
@ -194,7 +221,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
},
|
||||
});
|
||||
},
|
||||
[page, setPage],
|
||||
[customFilters, page, setPage],
|
||||
);
|
||||
|
||||
const cardRows = useMemo(() => {
|
||||
@ -308,9 +335,9 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
handleFavorite={handleFavorite}
|
||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||
height={height}
|
||||
initialScrollOffset={page?.grid.scrollOffset || 0}
|
||||
initialScrollOffset={customFilters ? 0 : page?.grid.scrollOffset || 0}
|
||||
itemCount={itemCount || 0}
|
||||
itemData={itemData}
|
||||
itemData={customFilters ? localItemData : itemData}
|
||||
itemGap={20}
|
||||
itemSize={150 + page.grid?.size}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
@ -320,7 +347,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
}}
|
||||
setItemData={setItemData}
|
||||
setItemData={customFilters ? setLocalItemData : setItemData}
|
||||
width={width}
|
||||
onScroll={handleGridScroll}
|
||||
/>
|
||||
@ -334,6 +361,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||
key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`}
|
||||
ref={tableRef}
|
||||
alwaysShowHorizontalScroll
|
||||
suppressRowDrag
|
||||
autoFitColumns={page.table.autoFit}
|
||||
blockLoadDebounceMillis={200}
|
||||
columnDefs={columnDefs}
|
||||
|
@ -3,6 +3,7 @@ import { useCallback } from 'react';
|
||||
import { IDatasource } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Flex, Group, Stack } from '@mantine/core';
|
||||
import { openModal } from '@mantine/modals';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import debounce from 'lodash/debounce';
|
||||
import {
|
||||
@ -17,7 +18,13 @@ import styled from 'styled-components';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { controller } from '/@/renderer/api/controller';
|
||||
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 {
|
||||
ALBUM_TABLE_COLUMNS,
|
||||
Badge,
|
||||
@ -25,7 +32,6 @@ import {
|
||||
DropdownMenu,
|
||||
MultiSelect,
|
||||
PageHeader,
|
||||
Popover,
|
||||
SearchInput,
|
||||
Slider,
|
||||
SpinnerIcon,
|
||||
@ -92,12 +98,20 @@ const HeaderItems = styled.div`
|
||||
`;
|
||||
|
||||
interface AlbumListHeaderProps {
|
||||
customFilters?: Partial<AlbumListFilter>;
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount?: number;
|
||||
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 server = useCurrentServer();
|
||||
const setPage = useSetAlbumStore();
|
||||
@ -131,21 +145,28 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
|
||||
|
||||
const fetch = useCallback(
|
||||
async (skip: number, take: number, filters: AlbumListFilter) => {
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', {
|
||||
const query: AlbumListQuery = {
|
||||
limit: take,
|
||||
startIndex: skip,
|
||||
...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(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
controller.getAlbumList({
|
||||
query: {
|
||||
limit: take,
|
||||
startIndex: skip,
|
||||
...filters,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
@ -154,7 +175,7 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
|
||||
|
||||
return api.normalize.albumList(albums, server);
|
||||
},
|
||||
[queryClient, server],
|
||||
[customFilters, queryClient, server],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
@ -168,21 +189,28 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', {
|
||||
const query: AlbumListQuery = {
|
||||
limit,
|
||||
startIndex,
|
||||
...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(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumList({
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
@ -214,9 +242,30 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
|
||||
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(() => {
|
||||
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || ''));
|
||||
handleFilterChange(filters);
|
||||
@ -315,7 +364,19 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
|
||||
const handlePlay = async (play: Play) => {
|
||||
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 albumListRes = await queryClient.fetchQuery({
|
||||
@ -355,9 +416,11 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
|
||||
<Group noWrap>
|
||||
<TextTitle
|
||||
fw="bold"
|
||||
maw="20vw"
|
||||
order={3}
|
||||
overflow="hidden"
|
||||
>
|
||||
Albums
|
||||
{title || 'Albums'}
|
||||
</TextTitle>
|
||||
<Badge
|
||||
radius="xl"
|
||||
@ -509,24 +572,14 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef }: AlbumListHeade
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<Popover position="bottom-start">
|
||||
<Popover.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
variant="subtle"
|
||||
>
|
||||
{cq.isMd ? 'Filters' : <RiFilter3Line size={15} />}
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
{server?.type === ServerType.NAVIDROME ? (
|
||||
<NavidromeAlbumFilters handleFilterChange={handleFilterChange} />
|
||||
) : (
|
||||
<JellyfinAlbumFilters handleFilterChange={handleFilterChange} />
|
||||
)}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
variant="subtle"
|
||||
onClick={handleOpenFiltersModal}
|
||||
>
|
||||
{cq.isMd ? 'Filters' : <RiFilter3Line size={15} />}
|
||||
</Button>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
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 debounce from 'lodash/debounce';
|
||||
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 {
|
||||
disableArtistFilter?: boolean;
|
||||
handleFilterChange: (filters: AlbumListFilter) => void;
|
||||
}
|
||||
|
||||
export const JellyfinAlbumFilters = ({ handleFilterChange }: JellyfinAlbumFiltersProps) => {
|
||||
export const JellyfinAlbumFilters = ({
|
||||
disableArtistFilter,
|
||||
handleFilterChange,
|
||||
}: JellyfinAlbumFiltersProps) => {
|
||||
const { filter } = useAlbumListStore();
|
||||
const setFilters = useSetAlbumFilters();
|
||||
|
||||
@ -74,46 +81,33 @@ export const JellyfinAlbumFilters = ({ handleFilterChange }: JellyfinAlbumFilter
|
||||
handleFilterChange(updatedFilters);
|
||||
}, 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 (
|
||||
<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) => (
|
||||
<Group
|
||||
key={`nd-filter-${filter.label}`}
|
||||
@ -127,14 +121,52 @@ export const JellyfinAlbumFilters = ({ handleFilterChange }: JellyfinAlbumFilter
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
{/* <Divider my="0.5rem" />
|
||||
<Stack>
|
||||
<Text>Tags</Text>
|
||||
<MultiSelect
|
||||
disabled
|
||||
data={[]}
|
||||
<Divider my="0.5rem" />
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
hideControls={false}
|
||||
label="From year"
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
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 { useDebouncedValue } from '@mantine/hooks';
|
||||
import debounce from 'lodash/debounce';
|
||||
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 {
|
||||
disableArtistFilter?: boolean;
|
||||
handleFilterChange: (filters: AlbumListFilter) => void;
|
||||
}
|
||||
|
||||
export const NavidromeAlbumFilters = ({ handleFilterChange }: NavidromeAlbumFiltersProps) => {
|
||||
export const NavidromeAlbumFilters = ({
|
||||
handleFilterChange,
|
||||
disableArtistFilter,
|
||||
}: NavidromeAlbumFiltersProps) => {
|
||||
const { filter } = useAlbumListStore();
|
||||
const setFilters = useSetAlbumFilters();
|
||||
|
||||
@ -89,35 +96,33 @@ export const NavidromeAlbumFilters = ({ handleFilterChange }: NavidromeAlbumFilt
|
||||
handleFilterChange(updatedFilters);
|
||||
}, 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 (
|
||||
<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) => (
|
||||
<Group
|
||||
key={`nd-filter-${filter.label}`}
|
||||
@ -130,6 +135,39 @@ export const NavidromeAlbumFilters = ({ handleFilterChange }: NavidromeAlbumFilt
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -5,18 +5,49 @@ import { AlbumListContent } from '/@/renderer/features/albums/components/album-l
|
||||
import { useRef } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
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 gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
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(
|
||||
{
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...filters,
|
||||
...customFilters,
|
||||
jfParams: {
|
||||
...filters.jfParams,
|
||||
...customFilters?.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filters.ndParams,
|
||||
...customFilters?.ndParams,
|
||||
},
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60 * 60 * 2,
|
||||
@ -33,11 +64,14 @@ const AlbumListRoute = () => {
|
||||
<AnimatedPage>
|
||||
<VirtualGridContainer>
|
||||
<AlbumListHeader
|
||||
customFilters={customFilters}
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
title={searchParams.get('artistName') || undefined}
|
||||
/>
|
||||
<AlbumListContent
|
||||
customFilters={customFilters}
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
|
@ -13,7 +13,7 @@ import { Box, Group, Stack } from '@mantine/core';
|
||||
import { RiArrowDownSLine, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
||||
import { generatePath, useParams } from 'react-router';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createSearchParams, Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
@ -66,6 +66,13 @@ export const AlbumArtistDetailContent = () => {
|
||||
|
||||
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({
|
||||
jfParams: server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined,
|
||||
limit: itemsPerPage,
|
||||
@ -146,9 +153,7 @@ export const AlbumArtistDetailContent = () => {
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
|
||||
albumArtistId,
|
||||
})}
|
||||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
@ -283,9 +288,7 @@ export const AlbumArtistDetailContent = () => {
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
|
||||
albumArtistId,
|
||||
})}
|
||||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
@ -302,7 +305,6 @@ export const AlbumArtistDetailContent = () => {
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{showGenres && (
|
||||
<Box component="section">
|
||||
<Group>
|
||||
|
@ -58,10 +58,6 @@ const AlbumArtistDetailTopSongsListRoute = lazy(
|
||||
() => 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(
|
||||
() => import('/@/renderer/features/albums/routes/album-detail-route'),
|
||||
);
|
||||
@ -141,7 +137,7 @@ export const AppRouter = () => {
|
||||
element={<AlbumArtistDetailRoute />}
|
||||
/>
|
||||
<Route
|
||||
element={<AlbumArtistDetailDiscographyRoute />}
|
||||
element={<AlbumListRoute />}
|
||||
path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY}
|
||||
/>
|
||||
<Route
|
||||
|
Loading…
Reference in New Issue
Block a user