mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 18:33:33 +00:00
Lint all files
This commit is contained in:
parent
22af76b4d6
commit
30e52ebb54
334 changed files with 76519 additions and 75932 deletions
|
|
@ -11,24 +11,24 @@ import { AppRoute } from '/@/renderer/router/routes';
|
|||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import {
|
||||
useHandleGeneralContextMenu,
|
||||
useHandleTableContextMenu,
|
||||
useHandleGeneralContextMenu,
|
||||
useHandleTableContextMenu,
|
||||
} from '/@/renderer/features/context-menu';
|
||||
import { CardRow, Play, TableColumn } from '/@/renderer/types';
|
||||
import {
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
SONG_CONTEXT_MENU_ITEMS,
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
SONG_CONTEXT_MENU_ITEMS,
|
||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { PlayButton, useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
QueueSong,
|
||||
ServerType,
|
||||
SortOrder,
|
||||
Album,
|
||||
AlbumArtist,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
QueueSong,
|
||||
ServerType,
|
||||
SortOrder,
|
||||
} from '/@/renderer/api/types';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
|
||||
|
|
@ -37,441 +37,455 @@ import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-tabl
|
|||
import { SwiperGridCarousel } from '/@/renderer/components/grid-carousel';
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
padding: 1rem 2rem 5rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
padding: 1rem 2rem 5rem;
|
||||
overflow: hidden;
|
||||
|
||||
.ag-theme-alpine-dark {
|
||||
--ag-header-background-color: rgba(0, 0, 0, 0%) !important;
|
||||
}
|
||||
.ag-theme-alpine-dark {
|
||||
--ag-header-background-color: rgba(0, 0, 0, 0%) !important;
|
||||
}
|
||||
|
||||
.ag-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.ag-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AlbumArtistDetailContent = () => {
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const cq = useContainerQuery();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const server = useCurrentServer();
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const cq = useContainerQuery();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const server = useCurrentServer();
|
||||
|
||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
||||
|
||||
const artistDiscographyLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
|
||||
albumArtistId,
|
||||
})}?${createSearchParams({
|
||||
artistId: albumArtistId,
|
||||
artistName: detailQuery?.data?.name || '',
|
||||
})}`;
|
||||
|
||||
const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, {
|
||||
albumArtistId,
|
||||
})}?${createSearchParams({
|
||||
artistId: albumArtistId,
|
||||
artistName: detailQuery?.data?.name || '',
|
||||
})}`;
|
||||
|
||||
const recentAlbumsQuery = useAlbumList({
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...(server?.type === ServerType.JELLYFIN ? { ArtistIds: albumArtistId } : undefined),
|
||||
},
|
||||
navidrome: {
|
||||
...(server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: false }
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
// limit: 10,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const compilationAlbumsQuery = useAlbumList({
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...(server?.type === ServerType.JELLYFIN
|
||||
? { ContributingArtistIds: albumArtistId }
|
||||
: undefined),
|
||||
},
|
||||
navidrome: {
|
||||
...(server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: true }
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
// limit: 10,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const topSongsQuery = useTopSongsList({
|
||||
options: {
|
||||
enabled: !!detailQuery?.data?.name,
|
||||
},
|
||||
query: {
|
||||
artist: detailQuery?.data?.name || '',
|
||||
artistId: albumArtistId,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const topSongsColumnDefs: ColDef[] = useMemo(
|
||||
() =>
|
||||
getColumnDefs([
|
||||
{ column: TableColumn.ROW_INDEX, width: 0 },
|
||||
{ column: TableColumn.TITLE_COMBINED, width: 0 },
|
||||
{ column: TableColumn.DURATION, width: 0 },
|
||||
{ column: TableColumn.ALBUM, width: 0 },
|
||||
{ column: TableColumn.YEAR, width: 0 },
|
||||
{ column: TableColumn.PLAY_COUNT, width: 0 },
|
||||
{ column: TableColumn.USER_FAVORITE, width: 0 },
|
||||
]),
|
||||
[],
|
||||
);
|
||||
|
||||
const cardRows: Record<string, CardRow<Album>[] | CardRow<AlbumArtist>[]> = {
|
||||
album: [
|
||||
{
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
albumArtist: [
|
||||
{
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const cardRoutes = {
|
||||
album: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
},
|
||||
albumArtist: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
};
|
||||
|
||||
const carousels = [
|
||||
{
|
||||
data: recentAlbumsQuery?.data?.items,
|
||||
isHidden: !recentAlbumsQuery?.data?.items?.length,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
loading: recentAlbumsQuery?.isLoading || recentAlbumsQuery.isFetching,
|
||||
title: (
|
||||
<Group align="flex-end">
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Recent releases
|
||||
</TextTitle>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
uniqueId: 'recentReleases',
|
||||
},
|
||||
{
|
||||
data: compilationAlbumsQuery?.data?.items,
|
||||
isHidden: !compilationAlbumsQuery?.data?.items?.length,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
||||
title: (
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Appears on
|
||||
</TextTitle>
|
||||
),
|
||||
uniqueId: 'compilationAlbums',
|
||||
},
|
||||
{
|
||||
data: detailQuery?.data?.similarArtists || [],
|
||||
isHidden: !detailQuery?.data?.similarArtists,
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
loading: detailQuery?.isLoading || detailQuery.isFetching,
|
||||
title: (
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Related artists
|
||||
</TextTitle>
|
||||
),
|
||||
uniqueId: 'similarArtists',
|
||||
},
|
||||
];
|
||||
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const handlePlay = async (playType?: Play) => {
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [albumArtistId],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
},
|
||||
playType: playType || playButtonBehavior,
|
||||
const detailQuery = useAlbumArtistDetail({
|
||||
query: { id: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
|
||||
const artistDiscographyLink = `${generatePath(
|
||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
|
||||
{
|
||||
albumArtistId,
|
||||
},
|
||||
)}?${createSearchParams({
|
||||
artistId: albumArtistId,
|
||||
artistName: detailQuery?.data?.name || '',
|
||||
})}`;
|
||||
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
||||
if (!e.data || !topSongsQuery?.data) return;
|
||||
const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, {
|
||||
albumArtistId,
|
||||
})}?${createSearchParams({
|
||||
artistId: albumArtistId,
|
||||
artistName: detailQuery?.data?.name || '',
|
||||
})}`;
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byData: topSongsQuery?.data?.items || [],
|
||||
initialSongId: e.data.id,
|
||||
playType: playButtonBehavior,
|
||||
const recentAlbumsQuery = useAlbumList({
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...(server?.type === ServerType.JELLYFIN
|
||||
? { ArtistIds: albumArtistId }
|
||||
: undefined),
|
||||
},
|
||||
navidrome: {
|
||||
...(server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: false }
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
// limit: 10,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
};
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite({});
|
||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||
|
||||
const handleFavorite = () => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
if (detailQuery.data.userFavorite) {
|
||||
deleteFavoriteMutation.mutate({
|
||||
const compilationAlbumsQuery = useAlbumList({
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...(server?.type === ServerType.JELLYFIN
|
||||
? { ContributingArtistIds: albumArtistId }
|
||||
: undefined),
|
||||
},
|
||||
navidrome: {
|
||||
...(server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: true }
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
// limit: 10,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const topSongsQuery = useTopSongsList({
|
||||
options: {
|
||||
enabled: !!detailQuery?.data?.name,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
} else {
|
||||
createFavoriteMutation.mutate({
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
artist: detailQuery?.data?.name || '',
|
||||
artistId: albumArtistId,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
}
|
||||
};
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
);
|
||||
const topSongsColumnDefs: ColDef[] = useMemo(
|
||||
() =>
|
||||
getColumnDefs([
|
||||
{ column: TableColumn.ROW_INDEX, width: 0 },
|
||||
{ column: TableColumn.TITLE_COMBINED, width: 0 },
|
||||
{ column: TableColumn.DURATION, width: 0 },
|
||||
{ column: TableColumn.ALBUM, width: 0 },
|
||||
{ column: TableColumn.YEAR, width: 0 },
|
||||
{ column: TableColumn.PLAY_COUNT, width: 0 },
|
||||
{ column: TableColumn.USER_FAVORITE, width: 0 },
|
||||
]),
|
||||
[],
|
||||
);
|
||||
|
||||
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
|
||||
const cardRows: Record<string, CardRow<Album>[] | CardRow<AlbumArtist>[]> = {
|
||||
album: [
|
||||
{
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
albumArtist: [
|
||||
{
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const showBiography =
|
||||
detailQuery?.data?.biography !== undefined && detailQuery?.data?.biography !== null;
|
||||
const showTopSongs = topSongsQuery?.data?.items?.length;
|
||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||
const cardRoutes = {
|
||||
album: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
},
|
||||
albumArtist: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
},
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
detailQuery?.isLoading || (server?.type === ServerType.NAVIDROME && topSongsQuery?.isLoading);
|
||||
const carousels = [
|
||||
{
|
||||
data: recentAlbumsQuery?.data?.items,
|
||||
isHidden: !recentAlbumsQuery?.data?.items?.length,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
loading: recentAlbumsQuery?.isLoading || recentAlbumsQuery.isFetching,
|
||||
title: (
|
||||
<Group align="flex-end">
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Recent releases
|
||||
</TextTitle>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
uniqueId: 'recentReleases',
|
||||
},
|
||||
{
|
||||
data: compilationAlbumsQuery?.data?.items,
|
||||
isHidden: !compilationAlbumsQuery?.data?.items?.length,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
||||
title: (
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Appears on
|
||||
</TextTitle>
|
||||
),
|
||||
uniqueId: 'compilationAlbums',
|
||||
},
|
||||
{
|
||||
data: detailQuery?.data?.similarArtists || [],
|
||||
isHidden: !detailQuery?.data?.similarArtists,
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
loading: detailQuery?.isLoading || detailQuery.isFetching,
|
||||
title: (
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Related artists
|
||||
</TextTitle>
|
||||
),
|
||||
uniqueId: 'similarArtists',
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) return <ContentContainer ref={cq.ref} />;
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
return (
|
||||
<ContentContainer ref={cq.ref}>
|
||||
<Box component="section">
|
||||
<Group spacing="md">
|
||||
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<Group spacing="xs">
|
||||
<Button
|
||||
compact
|
||||
loading={createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading}
|
||||
variant="subtle"
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
{detailQuery?.data?.userFavorite ? (
|
||||
<RiHeartFill
|
||||
color="red"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<RiHeartLine size={20} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
variant="subtle"
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
>
|
||||
<RiMoreFill size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={artistSongsLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View all songs
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
{showGenres ? (
|
||||
<Box component="section">
|
||||
<Group spacing="sm">
|
||||
{detailQuery?.data?.genres?.map((genre) => (
|
||||
<Button
|
||||
key={`genre-${genre.id}`}
|
||||
compact
|
||||
component={Link}
|
||||
radius="md"
|
||||
size="md"
|
||||
to={generatePath(`${AppRoute.LIBRARY_ALBUM_ARTISTS}?genre=${genre.id}`, {
|
||||
albumArtistId,
|
||||
})}
|
||||
variant="outline"
|
||||
>
|
||||
{genre.name}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</Box>
|
||||
) : null}
|
||||
{showBiography ? (
|
||||
<Box
|
||||
component="section"
|
||||
maw="1280px"
|
||||
>
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
About {detailQuery?.data?.name}
|
||||
</TextTitle>
|
||||
<Text
|
||||
$secondary
|
||||
component="p"
|
||||
dangerouslySetInnerHTML={{ __html: detailQuery?.data?.biography || '' }}
|
||||
sx={{ textAlign: 'justify' }}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
{showTopSongs ? (
|
||||
<Box component="section">
|
||||
<Group
|
||||
noWrap
|
||||
position="apart"
|
||||
>
|
||||
<Group
|
||||
noWrap
|
||||
align="flex-end"
|
||||
>
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Top Songs
|
||||
</TextTitle>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS, {
|
||||
albumArtistId,
|
||||
})}
|
||||
variant="subtle"
|
||||
>
|
||||
View all
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<VirtualTable
|
||||
autoFitColumns
|
||||
autoHeight
|
||||
deselectOnClickOutside
|
||||
suppressCellFocus
|
||||
suppressHorizontalScroll
|
||||
suppressLoadingOverlay
|
||||
suppressRowDrag
|
||||
columnDefs={topSongsColumnDefs}
|
||||
enableCellChangeFlash={false}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
rowData={topSongs}
|
||||
rowHeight={60}
|
||||
rowSelection="multiple"
|
||||
onCellContextMenu={handleContextMenu}
|
||||
onRowDoubleClicked={handleRowDoubleClick}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
<Box component="section">
|
||||
<Stack spacing="xl">
|
||||
{carousels
|
||||
.filter((c) => !c.isHidden)
|
||||
.map((carousel) => (
|
||||
<SwiperGridCarousel
|
||||
key={`carousel-${carousel.uniqueId}`}
|
||||
cardRows={cardRows[carousel.itemType as keyof typeof cardRows]}
|
||||
data={carousel.data}
|
||||
isLoading={carousel.loading}
|
||||
itemType={carousel.itemType}
|
||||
route={cardRoutes[carousel.itemType as keyof typeof cardRoutes]}
|
||||
swiperProps={{
|
||||
grid: {
|
||||
rows: 2,
|
||||
},
|
||||
}}
|
||||
title={{
|
||||
label: carousel.title,
|
||||
}}
|
||||
uniqueId={carousel.uniqueId}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</ContentContainer>
|
||||
);
|
||||
const handlePlay = async (playType?: Play) => {
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [albumArtistId],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
},
|
||||
playType: playType || playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
|
||||
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
||||
if (!e.data || !topSongsQuery?.data) return;
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byData: topSongsQuery?.data?.items || [],
|
||||
initialSongId: e.data.id,
|
||||
playType: playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite({});
|
||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||
|
||||
const handleFavorite = () => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
if (detailQuery.data.userFavorite) {
|
||||
deleteFavoriteMutation.mutate({
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
} else {
|
||||
createFavoriteMutation.mutate({
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
);
|
||||
|
||||
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
|
||||
|
||||
const showBiography =
|
||||
detailQuery?.data?.biography !== undefined && detailQuery?.data?.biography !== null;
|
||||
const showTopSongs = topSongsQuery?.data?.items?.length;
|
||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||
|
||||
const isLoading =
|
||||
detailQuery?.isLoading ||
|
||||
(server?.type === ServerType.NAVIDROME && topSongsQuery?.isLoading);
|
||||
|
||||
if (isLoading) return <ContentContainer ref={cq.ref} />;
|
||||
|
||||
return (
|
||||
<ContentContainer ref={cq.ref}>
|
||||
<Box component="section">
|
||||
<Group spacing="md">
|
||||
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<Group spacing="xs">
|
||||
<Button
|
||||
compact
|
||||
loading={
|
||||
createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading
|
||||
}
|
||||
variant="subtle"
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
{detailQuery?.data?.userFavorite ? (
|
||||
<RiHeartFill
|
||||
color="red"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<RiHeartLine size={20} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
variant="subtle"
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
>
|
||||
<RiMoreFill size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={artistSongsLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View all songs
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
{showGenres ? (
|
||||
<Box component="section">
|
||||
<Group spacing="sm">
|
||||
{detailQuery?.data?.genres?.map((genre) => (
|
||||
<Button
|
||||
key={`genre-${genre.id}`}
|
||||
compact
|
||||
component={Link}
|
||||
radius="md"
|
||||
size="md"
|
||||
to={generatePath(
|
||||
`${AppRoute.LIBRARY_ALBUM_ARTISTS}?genre=${genre.id}`,
|
||||
{
|
||||
albumArtistId,
|
||||
},
|
||||
)}
|
||||
variant="outline"
|
||||
>
|
||||
{genre.name}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</Box>
|
||||
) : null}
|
||||
{showBiography ? (
|
||||
<Box
|
||||
component="section"
|
||||
maw="1280px"
|
||||
>
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
About {detailQuery?.data?.name}
|
||||
</TextTitle>
|
||||
<Text
|
||||
$secondary
|
||||
component="p"
|
||||
dangerouslySetInnerHTML={{ __html: detailQuery?.data?.biography || '' }}
|
||||
sx={{ textAlign: 'justify' }}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
{showTopSongs ? (
|
||||
<Box component="section">
|
||||
<Group
|
||||
noWrap
|
||||
position="apart"
|
||||
>
|
||||
<Group
|
||||
noWrap
|
||||
align="flex-end"
|
||||
>
|
||||
<TextTitle
|
||||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Top Songs
|
||||
</TextTitle>
|
||||
<Button
|
||||
compact
|
||||
uppercase
|
||||
component={Link}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS, {
|
||||
albumArtistId,
|
||||
})}
|
||||
variant="subtle"
|
||||
>
|
||||
View all
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<VirtualTable
|
||||
autoFitColumns
|
||||
autoHeight
|
||||
deselectOnClickOutside
|
||||
suppressCellFocus
|
||||
suppressHorizontalScroll
|
||||
suppressLoadingOverlay
|
||||
suppressRowDrag
|
||||
columnDefs={topSongsColumnDefs}
|
||||
enableCellChangeFlash={false}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
rowData={topSongs}
|
||||
rowHeight={60}
|
||||
rowSelection="multiple"
|
||||
onCellContextMenu={handleContextMenu}
|
||||
onRowDoubleClicked={handleRowDoubleClick}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
<Box component="section">
|
||||
<Stack spacing="xl">
|
||||
{carousels
|
||||
.filter((c) => !c.isHidden)
|
||||
.map((carousel) => (
|
||||
<SwiperGridCarousel
|
||||
key={`carousel-${carousel.uniqueId}`}
|
||||
cardRows={cardRows[carousel.itemType as keyof typeof cardRows]}
|
||||
data={carousel.data}
|
||||
isLoading={carousel.loading}
|
||||
itemType={carousel.itemType}
|
||||
route={cardRoutes[carousel.itemType as keyof typeof cardRoutes]}
|
||||
swiperProps={{
|
||||
grid: {
|
||||
rows: 2,
|
||||
},
|
||||
}}
|
||||
title={{
|
||||
label: carousel.title,
|
||||
}}
|
||||
uniqueId={carousel.uniqueId}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</ContentContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,110 +11,114 @@ import { formatDurationString } from '/@/renderer/utils';
|
|||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
|
||||
interface AlbumArtistDetailHeaderProps {
|
||||
background: string;
|
||||
background: string;
|
||||
}
|
||||
|
||||
export const AlbumArtistDetailHeader = forwardRef(
|
||||
({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = useAlbumArtistDetail({
|
||||
query: { id: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
const cq = useContainerQuery();
|
||||
({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = useAlbumArtistDetail({
|
||||
query: { id: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const metadataItems = [
|
||||
{
|
||||
id: 'albumCount',
|
||||
secondary: false,
|
||||
value: detailQuery?.data?.albumCount && `${detailQuery?.data?.albumCount} albums`,
|
||||
},
|
||||
{
|
||||
id: 'songCount',
|
||||
secondary: false,
|
||||
value: detailQuery?.data?.songCount && `${detailQuery?.data?.songCount} songs`,
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
secondary: true,
|
||||
value: detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
||||
},
|
||||
];
|
||||
const metadataItems = [
|
||||
{
|
||||
id: 'albumCount',
|
||||
secondary: false,
|
||||
value: detailQuery?.data?.albumCount && `${detailQuery?.data?.albumCount} albums`,
|
||||
},
|
||||
{
|
||||
id: 'songCount',
|
||||
secondary: false,
|
||||
value: detailQuery?.data?.songCount && `${detailQuery?.data?.songCount} songs`,
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
secondary: true,
|
||||
value:
|
||||
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
||||
},
|
||||
];
|
||||
|
||||
const updateRatingMutation = useSetRating({});
|
||||
const updateRatingMutation = useSetRating({});
|
||||
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
if (!detailQuery?.data) return;
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating,
|
||||
},
|
||||
serverId: detailQuery?.data.serverId,
|
||||
});
|
||||
};
|
||||
updateRatingMutation.mutate({
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating,
|
||||
},
|
||||
serverId: detailQuery?.data.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearRating = (_e: MouseEvent<HTMLDivElement>, rating?: number) => {
|
||||
if (!detailQuery?.data || !detailQuery?.data.userRating) return;
|
||||
const handleClearRating = (_e: MouseEvent<HTMLDivElement>, rating?: number) => {
|
||||
if (!detailQuery?.data || !detailQuery?.data.userRating) return;
|
||||
|
||||
const isSameRatingAsPrevious = rating === detailQuery.data.userRating;
|
||||
if (!isSameRatingAsPrevious) return;
|
||||
const isSameRatingAsPrevious = rating === detailQuery.data.userRating;
|
||||
if (!isSameRatingAsPrevious) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating: 0,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
};
|
||||
updateRatingMutation.mutate({
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating: 0,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
const showRating = detailQuery?.data?.serverType === ServerType.NAVIDROME;
|
||||
const showRating = detailQuery?.data?.serverType === ServerType.NAVIDROME;
|
||||
|
||||
return (
|
||||
<Stack ref={cq.ref}>
|
||||
<LibraryHeader
|
||||
ref={ref}
|
||||
background={background}
|
||||
imageUrl={detailQuery?.data?.imageUrl}
|
||||
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
|
||||
title={detailQuery?.data?.name || ''}
|
||||
>
|
||||
<Stack>
|
||||
<Group>
|
||||
{metadataItems
|
||||
.filter((i) => i.value)
|
||||
.map((item, index) => (
|
||||
<Fragment key={`item-${item.id}-${index}`}>
|
||||
{index > 0 && <Text $noSelect>•</Text>}
|
||||
<Text $secondary={item.secondary}>{item.value}</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
{showRating && (
|
||||
<>
|
||||
<Text $noSelect>•</Text>
|
||||
<Rating
|
||||
readOnly={detailQuery?.isFetching || updateRatingMutation.isLoading}
|
||||
value={detailQuery?.data?.userRating || 0}
|
||||
onChange={handleUpdateRating}
|
||||
onClick={handleClearRating}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
<Group
|
||||
sx={{
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 2,
|
||||
display: '-webkit-box',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</LibraryHeader>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
return (
|
||||
<Stack ref={cq.ref}>
|
||||
<LibraryHeader
|
||||
ref={ref}
|
||||
background={background}
|
||||
imageUrl={detailQuery?.data?.imageUrl}
|
||||
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
|
||||
title={detailQuery?.data?.name || ''}
|
||||
>
|
||||
<Stack>
|
||||
<Group>
|
||||
{metadataItems
|
||||
.filter((i) => i.value)
|
||||
.map((item, index) => (
|
||||
<Fragment key={`item-${item.id}-${index}`}>
|
||||
{index > 0 && <Text $noSelect>•</Text>}
|
||||
<Text $secondary={item.secondary}>{item.value}</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
{showRating && (
|
||||
<>
|
||||
<Text $noSelect>•</Text>
|
||||
<Rating
|
||||
readOnly={
|
||||
detailQuery?.isFetching ||
|
||||
updateRatingMutation.isLoading
|
||||
}
|
||||
value={detailQuery?.data?.userRating || 0}
|
||||
onChange={handleUpdateRating}
|
||||
onClick={handleClearRating}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
<Group
|
||||
sx={{
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 2,
|
||||
display: '-webkit-box',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</LibraryHeader>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,72 +11,72 @@ import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-gr
|
|||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
|
||||
interface AlbumArtistSongListContentProps {
|
||||
data: QueueSong[];
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
data: QueueSong[];
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const AlbumArtistDetailTopSongsListContent = ({
|
||||
tableRef,
|
||||
data,
|
||||
tableRef,
|
||||
data,
|
||||
}: AlbumArtistSongListContentProps) => {
|
||||
const server = useCurrentServer();
|
||||
const page = useSongListStore();
|
||||
const server = useCurrentServer();
|
||||
const page = useSongListStore();
|
||||
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const columnDefs: ColDef[] = useMemo(
|
||||
() => getColumnDefs(page.table.columns),
|
||||
[page.table.columns],
|
||||
);
|
||||
const columnDefs: ColDef[] = useMemo(
|
||||
() => getColumnDefs(page.table.columns),
|
||||
[page.table.columns],
|
||||
);
|
||||
|
||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
|
||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
|
||||
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
||||
if (!e.data) return;
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
||||
if (!e.data) return;
|
||||
|
||||
const rowData: QueueSong[] = [];
|
||||
e.api.forEachNode((node) => {
|
||||
if (!node.data) return;
|
||||
rowData.push(node.data);
|
||||
});
|
||||
const rowData: QueueSong[] = [];
|
||||
e.api.forEachNode((node) => {
|
||||
if (!node.data) return;
|
||||
rowData.push(node.data);
|
||||
});
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byData: rowData,
|
||||
initialSongId: e.data.id,
|
||||
playType: playButtonBehavior,
|
||||
});
|
||||
};
|
||||
handlePlayQueueAdd?.({
|
||||
byData: rowData,
|
||||
initialSongId: e.data.id,
|
||||
playType: playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<VirtualTable
|
||||
// https://github.com/ag-grid/ag-grid/issues/5284
|
||||
// Key is used to force remount of table when display, rowHeight, or server changes
|
||||
key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`}
|
||||
ref={tableRef}
|
||||
alwaysShowHorizontalScroll
|
||||
animateRows
|
||||
maintainColumnOrder
|
||||
suppressCopyRowsToClipboard
|
||||
suppressMoveWhenRowDragging
|
||||
suppressPaginationPanel
|
||||
suppressRowDrag
|
||||
suppressScrollOnNewData
|
||||
autoFitColumns={page.table.autoFit}
|
||||
columnDefs={columnDefs}
|
||||
enableCellChangeFlash={false}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
rowBuffer={20}
|
||||
rowData={data}
|
||||
rowHeight={page.table.rowHeight || 40}
|
||||
rowModelType="clientSide"
|
||||
rowSelection="multiple"
|
||||
onCellContextMenu={handleContextMenu}
|
||||
onRowDoubleClicked={handleRowDoubleClick}
|
||||
/>
|
||||
</VirtualGridAutoSizerContainer>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<VirtualTable
|
||||
// https://github.com/ag-grid/ag-grid/issues/5284
|
||||
// Key is used to force remount of table when display, rowHeight, or server changes
|
||||
key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`}
|
||||
ref={tableRef}
|
||||
alwaysShowHorizontalScroll
|
||||
animateRows
|
||||
maintainColumnOrder
|
||||
suppressCopyRowsToClipboard
|
||||
suppressMoveWhenRowDragging
|
||||
suppressPaginationPanel
|
||||
suppressRowDrag
|
||||
suppressScrollOnNewData
|
||||
autoFitColumns={page.table.autoFit}
|
||||
columnDefs={columnDefs}
|
||||
enableCellChangeFlash={false}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
rowBuffer={20}
|
||||
rowData={data}
|
||||
rowHeight={page.table.rowHeight || 40}
|
||||
rowModelType="clientSide"
|
||||
rowSelection="multiple"
|
||||
onCellContextMenu={handleContextMenu}
|
||||
onRowDoubleClicked={handleRowDoubleClick}
|
||||
/>
|
||||
</VirtualGridAutoSizerContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,71 +7,71 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|||
import { Play } from '/@/renderer/types';
|
||||
|
||||
interface AlbumArtistDetailTopSongsListHeaderProps {
|
||||
data: QueueSong[];
|
||||
itemCount?: number;
|
||||
title: string;
|
||||
data: QueueSong[];
|
||||
itemCount?: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const AlbumArtistDetailTopSongsListHeader = ({
|
||||
title,
|
||||
itemCount,
|
||||
data,
|
||||
title,
|
||||
itemCount,
|
||||
data,
|
||||
}: AlbumArtistDetailTopSongsListHeaderProps) => {
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const handlePlay = async (playType: Play) => {
|
||||
handlePlayQueueAdd?.({
|
||||
byData: data,
|
||||
playType,
|
||||
});
|
||||
};
|
||||
const handlePlay = async (playType: Play) => {
|
||||
handlePlayQueueAdd?.({
|
||||
byData: data,
|
||||
playType,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageHeader p="1rem">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<LibraryHeaderBar.Title>Top songs from {title}</LibraryHeaderBar.Title>
|
||||
<Paper
|
||||
fw="600"
|
||||
px="1rem"
|
||||
py="0.3rem"
|
||||
radius="sm"
|
||||
>
|
||||
{itemCount === null || itemCount === undefined ? <SpinnerIcon /> : itemCount}
|
||||
</Paper>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size={15} />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiPlayFill />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
>
|
||||
Play
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddBoxFill />}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
>
|
||||
Add to queue
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddCircleFill />}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
>
|
||||
Add to queue next
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</LibraryHeaderBar>
|
||||
</PageHeader>
|
||||
);
|
||||
return (
|
||||
<PageHeader p="1rem">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<LibraryHeaderBar.Title>Top songs from {title}</LibraryHeaderBar.Title>
|
||||
<Paper
|
||||
fw="600"
|
||||
px="1rem"
|
||||
py="0.3rem"
|
||||
radius="sm"
|
||||
>
|
||||
{itemCount === null || itemCount === undefined ? <SpinnerIcon /> : itemCount}
|
||||
</Paper>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size={15} />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiPlayFill />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
>
|
||||
Play
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddBoxFill />}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
>
|
||||
Add to queue
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddCircleFill />}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
>
|
||||
Add to queue next
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</LibraryHeaderBar>
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||
import { useCurrentServer, useAlbumArtistListStore } from '/@/renderer/store';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import {
|
||||
BodyScrollEvent,
|
||||
ColDef,
|
||||
GridReadyEvent,
|
||||
IDatasource,
|
||||
PaginationChangedEvent,
|
||||
RowDoubleClickedEvent,
|
||||
BodyScrollEvent,
|
||||
ColDef,
|
||||
GridReadyEvent,
|
||||
IDatasource,
|
||||
PaginationChangedEvent,
|
||||
RowDoubleClickedEvent,
|
||||
} from '@ag-grid-community/core';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
|
@ -28,311 +28,316 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
|||
import { useAlbumArtistListFilter, useListStoreActions } from '../../../store/list.store';
|
||||
import { useAlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context';
|
||||
import {
|
||||
VirtualInfiniteGridRef,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
VirtualInfiniteGridRef,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
} from '/@/renderer/components/virtual-grid';
|
||||
import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table';
|
||||
|
||||
interface AlbumArtistListContentProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListContentProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const server = useCurrentServer();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const server = useCurrentServer();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
const { id, pageKey } = useAlbumArtistListContext();
|
||||
const filter = useAlbumArtistListFilter({ id, key: pageKey });
|
||||
const { table, grid, display } = useAlbumArtistListStore();
|
||||
const { setTable, setTablePagination, setGrid } = useListStoreActions();
|
||||
const { id, pageKey } = useAlbumArtistListContext();
|
||||
const filter = useAlbumArtistListFilter({ id, key: pageKey });
|
||||
const { table, grid, display } = useAlbumArtistListStore();
|
||||
const { setTable, setTablePagination, setGrid } = useListStoreActions();
|
||||
|
||||
const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED;
|
||||
const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED;
|
||||
|
||||
const checkAlbumArtistList = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: Infinity,
|
||||
staleTime: 60 * 1000 * 5,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...filter,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]);
|
||||
|
||||
const onTableReady = useCallback(
|
||||
(params: GridReadyEvent) => {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
const checkAlbumArtistList = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: Infinity,
|
||||
staleTime: 60 * 1000 * 5,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...filter,
|
||||
});
|
||||
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
params.successCallback(
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
params.api.setDatasource(dataSource);
|
||||
params.api.ensureIndexVisible(table.scrollOffset || 0, 'top');
|
||||
},
|
||||
[filter, table.scrollOffset, queryClient, server],
|
||||
);
|
||||
const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]);
|
||||
|
||||
const onTablePaginationChanged = useCallback(
|
||||
(event: PaginationChangedEvent) => {
|
||||
if (!isPaginationEnabled || !event.api) return;
|
||||
const onTableReady = useCallback(
|
||||
(params: GridReadyEvent) => {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
try {
|
||||
// Scroll to top of page on pagination change
|
||||
const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage;
|
||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
});
|
||||
|
||||
setTablePagination({
|
||||
data: {
|
||||
itemsPerPage: event.api.paginationGetPageSize(),
|
||||
totalItems: event.api.paginationGetRowCount(),
|
||||
totalPages: event.api.paginationGetTotalPages() + 1,
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
params.successCallback(
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
||||
params.api.setDatasource(dataSource);
|
||||
params.api.ensureIndexVisible(table.scrollOffset || 0, 'top');
|
||||
},
|
||||
key: pageKey,
|
||||
});
|
||||
},
|
||||
[
|
||||
isPaginationEnabled,
|
||||
pageKey,
|
||||
setTablePagination,
|
||||
table.pagination.currentPage,
|
||||
table.pagination.itemsPerPage,
|
||||
],
|
||||
);
|
||||
[filter, table.scrollOffset, queryClient, server],
|
||||
);
|
||||
|
||||
const handleTableColumnChange = useCallback(() => {
|
||||
const { columnApi } = tableRef?.current || {};
|
||||
const columnsOrder = columnApi?.getAllGridColumns();
|
||||
const onTablePaginationChanged = useCallback(
|
||||
(event: PaginationChangedEvent) => {
|
||||
if (!isPaginationEnabled || !event.api) return;
|
||||
|
||||
if (!columnsOrder) return;
|
||||
try {
|
||||
// Scroll to top of page on pagination change
|
||||
const currentPageStartIndex =
|
||||
table.pagination.currentPage * table.pagination.itemsPerPage;
|
||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
const columnsInSettings = table.columns;
|
||||
const updatedColumns = [];
|
||||
for (const column of columnsOrder) {
|
||||
const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId);
|
||||
setTablePagination({
|
||||
data: {
|
||||
itemsPerPage: event.api.paginationGetPageSize(),
|
||||
totalItems: event.api.paginationGetRowCount(),
|
||||
totalPages: event.api.paginationGetTotalPages() + 1,
|
||||
},
|
||||
key: pageKey,
|
||||
});
|
||||
},
|
||||
[
|
||||
isPaginationEnabled,
|
||||
pageKey,
|
||||
setTablePagination,
|
||||
table.pagination.currentPage,
|
||||
table.pagination.itemsPerPage,
|
||||
],
|
||||
);
|
||||
|
||||
if (columnInSettings) {
|
||||
updatedColumns.push({
|
||||
...columnInSettings,
|
||||
...(!table.autoFit && {
|
||||
width: column.getColDef().width,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
const handleTableColumnChange = useCallback(() => {
|
||||
const { columnApi } = tableRef?.current || {};
|
||||
const columnsOrder = columnApi?.getAllGridColumns();
|
||||
|
||||
setTable({ data: { columns: updatedColumns }, key: pageKey });
|
||||
}, [tableRef, table.columns, table.autoFit, setTable, pageKey]);
|
||||
if (!columnsOrder) return;
|
||||
|
||||
const debouncedTableColumnChange = debounce(handleTableColumnChange, 200);
|
||||
const columnsInSettings = table.columns;
|
||||
const updatedColumns = [];
|
||||
for (const column of columnsOrder) {
|
||||
const columnInSettings = columnsInSettings.find(
|
||||
(c) => c.column === column.getColDef().colId,
|
||||
);
|
||||
|
||||
const handleTableScroll = (e: BodyScrollEvent) => {
|
||||
const scrollOffset = Number((e.top / table.rowHeight).toFixed(0));
|
||||
setTable({ data: { scrollOffset }, key: pageKey });
|
||||
};
|
||||
if (columnInSettings) {
|
||||
updatedColumns.push({
|
||||
...columnInSettings,
|
||||
...(!table.autoFit && {
|
||||
width: column.getColDef().width,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = useCallback(
|
||||
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
});
|
||||
setTable({ data: { columns: updatedColumns }, key: pageKey });
|
||||
}, [tableRef, table.columns, table.autoFit, setTable, pageKey]);
|
||||
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
const debouncedTableColumnChange = debounce(handleTableColumnChange, 200);
|
||||
|
||||
return albumArtistsRes;
|
||||
},
|
||||
[filter, queryClient, server],
|
||||
);
|
||||
const handleTableScroll = (e: BodyScrollEvent) => {
|
||||
const scrollOffset = Number((e.top / table.rowHeight).toFixed(0));
|
||||
setTable({ data: { scrollOffset }, key: pageKey });
|
||||
};
|
||||
|
||||
const handleGridScroll = useCallback(
|
||||
(e: ListOnScrollProps) => {
|
||||
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
|
||||
},
|
||||
[pageKey, setGrid],
|
||||
);
|
||||
const fetch = useCallback(
|
||||
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
});
|
||||
|
||||
const handleGridSizeChange = () => {
|
||||
if (table.autoFit) {
|
||||
tableRef?.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const cardRows = useMemo(() => {
|
||||
const rows: CardRow<AlbumArtist>[] = [ALBUMARTIST_CARD_ROWS.name];
|
||||
return albumArtistsRes;
|
||||
},
|
||||
[filter, queryClient, server],
|
||||
);
|
||||
|
||||
switch (filter.sortBy) {
|
||||
case AlbumArtistListSort.DURATION:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.duration);
|
||||
break;
|
||||
case AlbumArtistListSort.FAVORITED:
|
||||
break;
|
||||
case AlbumArtistListSort.NAME:
|
||||
break;
|
||||
case AlbumArtistListSort.ALBUM_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.albumCount);
|
||||
break;
|
||||
case AlbumArtistListSort.PLAY_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.playCount);
|
||||
break;
|
||||
case AlbumArtistListSort.RANDOM:
|
||||
break;
|
||||
case AlbumArtistListSort.RATING:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.rating);
|
||||
break;
|
||||
case AlbumArtistListSort.RECENTLY_ADDED:
|
||||
break;
|
||||
case AlbumArtistListSort.SONG_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.songCount);
|
||||
break;
|
||||
case AlbumArtistListSort.RELEASE_DATE:
|
||||
break;
|
||||
}
|
||||
const handleGridScroll = useCallback(
|
||||
(e: ListOnScrollProps) => {
|
||||
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
|
||||
},
|
||||
[pageKey, setGrid],
|
||||
);
|
||||
|
||||
return rows;
|
||||
}, [filter.sortBy]);
|
||||
const handleGridSizeChange = () => {
|
||||
if (table.autoFit) {
|
||||
tableRef?.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenu = useHandleTableContextMenu(
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
);
|
||||
const cardRows = useMemo(() => {
|
||||
const rows: CardRow<AlbumArtist>[] = [ALBUMARTIST_CARD_ROWS.name];
|
||||
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
|
||||
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId: e.data.id }));
|
||||
};
|
||||
switch (filter.sortBy) {
|
||||
case AlbumArtistListSort.DURATION:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.duration);
|
||||
break;
|
||||
case AlbumArtistListSort.FAVORITED:
|
||||
break;
|
||||
case AlbumArtistListSort.NAME:
|
||||
break;
|
||||
case AlbumArtistListSort.ALBUM_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.albumCount);
|
||||
break;
|
||||
case AlbumArtistListSort.PLAY_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.playCount);
|
||||
break;
|
||||
case AlbumArtistListSort.RANDOM:
|
||||
break;
|
||||
case AlbumArtistListSort.RATING:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.rating);
|
||||
break;
|
||||
case AlbumArtistListSort.RECENTLY_ADDED:
|
||||
break;
|
||||
case AlbumArtistListSort.SONG_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.songCount);
|
||||
break;
|
||||
case AlbumArtistListSort.RELEASE_DATE:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<>
|
||||
<VirtualInfiniteGrid
|
||||
ref={gridRef}
|
||||
cardRows={cardRows}
|
||||
display={display || ListDisplayType.CARD}
|
||||
fetchFn={fetch}
|
||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||
height={height}
|
||||
initialScrollOffset={grid?.scrollOffset || 0}
|
||||
itemCount={checkAlbumArtistList?.data?.totalRecordCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
loading={checkAlbumArtistList.isLoading}
|
||||
minimumBatchSize={40}
|
||||
route={{
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
}}
|
||||
width={width}
|
||||
onScroll={handleGridScroll}
|
||||
/>
|
||||
</>
|
||||
return rows;
|
||||
}, [filter.sortBy]);
|
||||
|
||||
const handleContextMenu = useHandleTableContextMenu(
|
||||
LibraryItem.ALBUM_ARTIST,
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
);
|
||||
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
|
||||
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId: e.data.id }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<>
|
||||
<VirtualInfiniteGrid
|
||||
ref={gridRef}
|
||||
cardRows={cardRows}
|
||||
display={display || ListDisplayType.CARD}
|
||||
fetchFn={fetch}
|
||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||
height={height}
|
||||
initialScrollOffset={grid?.scrollOffset || 0}
|
||||
itemCount={checkAlbumArtistList?.data?.totalRecordCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
loading={checkAlbumArtistList.isLoading}
|
||||
minimumBatchSize={40}
|
||||
route={{
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [
|
||||
{ idProperty: 'id', slugProperty: 'albumArtistId' },
|
||||
],
|
||||
}}
|
||||
width={width}
|
||||
onScroll={handleGridScroll}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : (
|
||||
<VirtualTable
|
||||
// https://github.com/ag-grid/ag-grid/issues/5284
|
||||
// Key is used to force remount of table when display, rowHeight, or server changes
|
||||
key={`table-${display}-${table.rowHeight}-${server?.id}`}
|
||||
ref={tableRef}
|
||||
alwaysShowHorizontalScroll
|
||||
suppressRowDrag
|
||||
autoFitColumns={table.autoFit}
|
||||
columnDefs={columnDefs}
|
||||
getRowId={(data) => data.data.id}
|
||||
infiniteInitialRowCount={checkAlbumArtistList.data?.totalRecordCount || 1}
|
||||
pagination={isPaginationEnabled}
|
||||
paginationAutoPageSize={isPaginationEnabled}
|
||||
paginationPageSize={table.pagination.itemsPerPage || 100}
|
||||
rowHeight={table.rowHeight || 40}
|
||||
rowModelType="infinite"
|
||||
onBodyScrollEnd={handleTableScroll}
|
||||
onCellContextMenu={handleContextMenu}
|
||||
onColumnMoved={handleTableColumnChange}
|
||||
onColumnResized={debouncedTableColumnChange}
|
||||
onGridReady={onTableReady}
|
||||
onGridSizeChanged={handleGridSizeChange}
|
||||
onPaginationChanged={onTablePaginationChanged}
|
||||
onRowDoubleClicked={handleRowDoubleClick}
|
||||
/>
|
||||
)}
|
||||
</VirtualGridAutoSizerContainer>
|
||||
{isPaginationEnabled && (
|
||||
<AnimatePresence
|
||||
presenceAffectsLayout
|
||||
initial={false}
|
||||
mode="wait"
|
||||
>
|
||||
{display === ListDisplayType.TABLE_PAGINATED && (
|
||||
<TablePagination
|
||||
pageKey={pageKey}
|
||||
pagination={table.pagination}
|
||||
setPagination={setTablePagination}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : (
|
||||
<VirtualTable
|
||||
// https://github.com/ag-grid/ag-grid/issues/5284
|
||||
// Key is used to force remount of table when display, rowHeight, or server changes
|
||||
key={`table-${display}-${table.rowHeight}-${server?.id}`}
|
||||
ref={tableRef}
|
||||
alwaysShowHorizontalScroll
|
||||
suppressRowDrag
|
||||
autoFitColumns={table.autoFit}
|
||||
columnDefs={columnDefs}
|
||||
getRowId={(data) => data.data.id}
|
||||
infiniteInitialRowCount={checkAlbumArtistList.data?.totalRecordCount || 1}
|
||||
pagination={isPaginationEnabled}
|
||||
paginationAutoPageSize={isPaginationEnabled}
|
||||
paginationPageSize={table.pagination.itemsPerPage || 100}
|
||||
rowHeight={table.rowHeight || 40}
|
||||
rowModelType="infinite"
|
||||
onBodyScrollEnd={handleTableScroll}
|
||||
onCellContextMenu={handleContextMenu}
|
||||
onColumnMoved={handleTableColumnChange}
|
||||
onColumnResized={debouncedTableColumnChange}
|
||||
onGridReady={onTableReady}
|
||||
onGridSizeChanged={handleGridSizeChange}
|
||||
onPaginationChanged={onTablePaginationChanged}
|
||||
onRowDoubleClicked={handleRowDoubleClick}
|
||||
/>
|
||||
)}
|
||||
</VirtualGridAutoSizerContainer>
|
||||
{isPaginationEnabled && (
|
||||
<AnimatePresence
|
||||
presenceAffectsLayout
|
||||
initial={false}
|
||||
mode="wait"
|
||||
>
|
||||
{display === ListDisplayType.TABLE_PAGINATED && (
|
||||
<TablePagination
|
||||
pageKey={pageKey}
|
||||
pagination={table.pagination}
|
||||
setPagination={setTablePagination}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import { Group, Stack, Flex } from '@mantine/core';
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import debounce from 'lodash/debounce';
|
||||
import {
|
||||
RiSortAsc,
|
||||
RiSortDesc,
|
||||
RiFolder2Line,
|
||||
RiMoreFill,
|
||||
RiRefreshLine,
|
||||
RiSettings3Fill,
|
||||
RiSortAsc,
|
||||
RiSortDesc,
|
||||
RiFolder2Line,
|
||||
RiMoreFill,
|
||||
RiRefreshLine,
|
||||
RiSettings3Fill,
|
||||
} from 'react-icons/ri';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
|
|
@ -19,11 +19,11 @@ import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/rend
|
|||
import { useMusicFolders } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
useCurrentServer,
|
||||
useAlbumArtistListStore,
|
||||
AlbumArtistListFilter,
|
||||
useListStoreActions,
|
||||
useAlbumArtistListFilter,
|
||||
useCurrentServer,
|
||||
useAlbumArtistListStore,
|
||||
AlbumArtistListFilter,
|
||||
useListStoreActions,
|
||||
useAlbumArtistListFilter,
|
||||
} from '/@/renderer/store';
|
||||
import { ListDisplayType, TableColumn, ServerType } from '/@/renderer/types';
|
||||
import { useAlbumArtistListContext } from '../context/album-artist-list-context';
|
||||
|
|
@ -31,455 +31,468 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
|||
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Album', value: AlbumArtistListSort.ALBUM },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Duration', value: AlbumArtistListSort.DURATION },
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME },
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumArtistListSort.RANDOM },
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: 'Recently Added',
|
||||
value: AlbumArtistListSort.RECENTLY_ADDED,
|
||||
},
|
||||
// { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE },
|
||||
],
|
||||
navidrome: [
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Album Count', value: AlbumArtistListSort.ALBUM_COUNT },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumArtistListSort.FAVORITED },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Most Played', value: AlbumArtistListSort.PLAY_COUNT },
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumArtistListSort.RATING },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumArtistListSort.SONG_COUNT },
|
||||
],
|
||||
jellyfin: [
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Album', value: AlbumArtistListSort.ALBUM },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Duration', value: AlbumArtistListSort.DURATION },
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME },
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumArtistListSort.RANDOM },
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: 'Recently Added',
|
||||
value: AlbumArtistListSort.RECENTLY_ADDED,
|
||||
},
|
||||
// { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE },
|
||||
],
|
||||
navidrome: [
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: 'Album Count',
|
||||
value: AlbumArtistListSort.ALBUM_COUNT,
|
||||
},
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumArtistListSort.FAVORITED },
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: 'Most Played',
|
||||
value: AlbumArtistListSort.PLAY_COUNT,
|
||||
},
|
||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumArtistListSort.RATING },
|
||||
{ defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumArtistListSort.SONG_COUNT },
|
||||
],
|
||||
};
|
||||
|
||||
const ORDER = [
|
||||
{ name: 'Ascending', value: SortOrder.ASC },
|
||||
{ name: 'Descending', value: SortOrder.DESC },
|
||||
{ name: 'Ascending', value: SortOrder.ASC },
|
||||
{ name: 'Descending', value: SortOrder.DESC },
|
||||
];
|
||||
|
||||
interface AlbumArtistListHeaderFiltersProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const AlbumArtistListHeaderFilters = ({
|
||||
gridRef,
|
||||
tableRef,
|
||||
gridRef,
|
||||
tableRef,
|
||||
}: AlbumArtistListHeaderFiltersProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useAlbumArtistListContext();
|
||||
const { display, table, grid } = useAlbumArtistListStore();
|
||||
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
|
||||
useListStoreActions();
|
||||
const filter = useAlbumArtistListFilter({ key: pageKey });
|
||||
const cq = useContainerQuery();
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useAlbumArtistListContext();
|
||||
const { display, table, grid } = useAlbumArtistListStore();
|
||||
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
|
||||
useListStoreActions();
|
||||
const filter = useAlbumArtistListFilter({ key: pageKey });
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)?.name) ||
|
||||
'Unknown';
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
|
||||
?.name) ||
|
||||
'Unknown';
|
||||
|
||||
const sortOrderLabel = ORDER.find((o) => o.value === filter.sortOrder)?.name || 'Unknown';
|
||||
const sortOrderLabel = ORDER.find((o) => o.value === filter.sortOrder)?.name || 'Unknown';
|
||||
|
||||
const handleItemSize = (e: number) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||
} else {
|
||||
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||
}
|
||||
};
|
||||
const handleItemSize = (e: number) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||
} else {
|
||||
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const fetch = useCallback(
|
||||
async (startIndex: number, limit: number, filters: AlbumArtistListFilter) => {
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const albums = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return albums;
|
||||
},
|
||||
[queryClient, server],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
async (filters: AlbumArtistListFilter) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const fetch = useCallback(
|
||||
async (startIndex: number, limit: number, filters: AlbumArtistListFilter) => {
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
const albums = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
params.successCallback(
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
tableRef.current?.api.setDatasource(dataSource);
|
||||
tableRef.current?.api.purgeInfiniteCache();
|
||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
return albums;
|
||||
},
|
||||
[queryClient, server],
|
||||
);
|
||||
|
||||
if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||
const handleFilterChange = useCallback(
|
||||
async (filters: AlbumArtistListFilter) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
params.successCallback(
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
tableRef.current?.api.setDatasource(dataSource);
|
||||
tableRef.current?.api.purgeInfiniteCache();
|
||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
|
||||
if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||
}
|
||||
} else {
|
||||
gridRef.current?.scrollTo(0);
|
||||
gridRef.current?.resetLoadMoreItemsCache();
|
||||
|
||||
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
||||
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
||||
// and then set the grid's data here.
|
||||
const data = await fetch(0, 200, filters);
|
||||
|
||||
if (!data?.items) return;
|
||||
gridRef.current?.setItemData(data.items);
|
||||
}
|
||||
},
|
||||
[display, tableRef, server, queryClient, setTablePagination, pageKey, gridRef, fetch],
|
||||
);
|
||||
|
||||
const handleSetSortBy = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value || !server?.type) return;
|
||||
|
||||
const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
|
||||
(f) => f.value === e.currentTarget.value,
|
||||
)?.defaultOrder;
|
||||
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
sortBy: e.currentTarget.value as AlbumArtistListSort,
|
||||
sortOrder: sortOrder || SortOrder.ASC,
|
||||
},
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
[handleFilterChange, pageKey, server?.type, setFilter],
|
||||
);
|
||||
|
||||
const handleSetMusicFolder = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
|
||||
let updatedFilters = null;
|
||||
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
||||
updatedFilters = setFilter({
|
||||
data: { musicFolderId: undefined },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
} else {
|
||||
updatedFilters = setFilter({
|
||||
data: { musicFolderId: e.currentTarget.value },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
}
|
||||
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
[filter.musicFolderId, handleFilterChange, setFilter, pageKey],
|
||||
);
|
||||
|
||||
const handleToggleSortOrder = useCallback(() => {
|
||||
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||
const updatedFilters = setFilter({
|
||||
data: { sortOrder: newSortOrder },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
}, [filter.sortOrder, handleFilterChange, pageKey, setFilter]);
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
|
||||
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
|
||||
},
|
||||
[pageKey, setDisplayType],
|
||||
);
|
||||
|
||||
const handleTableColumns = (values: TableColumn[]) => {
|
||||
const existingColumns = table.columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
return setTable({
|
||||
data: {
|
||||
columns: [],
|
||||
},
|
||||
key: pageKey,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
gridRef.current?.scrollTo(0);
|
||||
gridRef.current?.resetLoadMoreItemsCache();
|
||||
|
||||
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
||||
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
||||
// and then set the grid's data here.
|
||||
const data = await fetch(0, 200, filters);
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||
|
||||
if (!data?.items) return;
|
||||
gridRef.current?.setItemData(data.items);
|
||||
}
|
||||
},
|
||||
[display, tableRef, server, queryClient, setTablePagination, pageKey, gridRef, fetch],
|
||||
);
|
||||
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
||||
} else {
|
||||
// If removing a column
|
||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||
|
||||
const handleSetSortBy = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value || !server?.type) return;
|
||||
setTable({ data: { columns: newColumns }, key: pageKey });
|
||||
}
|
||||
|
||||
const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
|
||||
(f) => f.value === e.currentTarget.value,
|
||||
)?.defaultOrder;
|
||||
return tableRef.current?.api.sizeColumnsToFit();
|
||||
};
|
||||
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
sortBy: e.currentTarget.value as AlbumArtistListSort,
|
||||
sortOrder: sortOrder || SortOrder.ASC,
|
||||
},
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
|
||||
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
[handleFilterChange, pageKey, server?.type, setFilter],
|
||||
);
|
||||
if (e.currentTarget.checked) {
|
||||
tableRef.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetMusicFolder = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
const handleRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries(queryKeys.albumArtists.list(server?.id || ''));
|
||||
handleFilterChange(filter);
|
||||
}, [filter, handleFilterChange, queryClient, server?.id]);
|
||||
|
||||
let updatedFilters = null;
|
||||
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
||||
updatedFilters = setFilter({
|
||||
data: { musicFolderId: undefined },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
} else {
|
||||
updatedFilters = setFilter({
|
||||
data: { musicFolderId: e.currentTarget.value },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
}
|
||||
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
[filter.musicFolderId, handleFilterChange, setFilter, pageKey],
|
||||
);
|
||||
|
||||
const handleToggleSortOrder = useCallback(() => {
|
||||
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||
const updatedFilters = setFilter({
|
||||
data: { sortOrder: newSortOrder },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
}, [filter.sortOrder, handleFilterChange, pageKey, setFilter]);
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
|
||||
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
|
||||
},
|
||||
[pageKey, setDisplayType],
|
||||
);
|
||||
|
||||
const handleTableColumns = (values: TableColumn[]) => {
|
||||
const existingColumns = table.columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
return setTable({
|
||||
data: {
|
||||
columns: [],
|
||||
},
|
||||
key: pageKey,
|
||||
});
|
||||
}
|
||||
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||
|
||||
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
||||
} else {
|
||||
// If removing a column
|
||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||
|
||||
setTable({ data: { columns: newColumns }, key: pageKey });
|
||||
}
|
||||
|
||||
return tableRef.current?.api.sizeColumnsToFit();
|
||||
};
|
||||
|
||||
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
tableRef.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries(queryKeys.albumArtists.list(server?.id || ''));
|
||||
handleFilterChange(filter);
|
||||
}, [filter, handleFilterChange, queryClient, server?.id]);
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
ref={cq.ref}
|
||||
spacing="sm"
|
||||
w="100%"
|
||||
>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
ref={cq.ref}
|
||||
spacing="sm"
|
||||
w="100%"
|
||||
>
|
||||
{sortByLabel}
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
||||
<DropdownMenu.Item
|
||||
key={`filter-${f.name}`}
|
||||
$isActive={f.value === filter.sortBy}
|
||||
value={f.value}
|
||||
onClick={handleSetSortBy}
|
||||
>
|
||||
{f.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
onClick={handleToggleSortOrder}
|
||||
>
|
||||
{cq.isMd ? (
|
||||
sortOrderLabel
|
||||
) : (
|
||||
<>
|
||||
{filter.sortOrder === SortOrder.ASC ? (
|
||||
<RiSortAsc size={15} />
|
||||
) : (
|
||||
<RiSortDesc size={15} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
<DropdownMenu.Item
|
||||
key={`musicFolder-${folder.id}`}
|
||||
$isActive={filter.musicFolderId === folder.id}
|
||||
value={folder.id}
|
||||
onClick={handleSetMusicFolder}
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{sortByLabel}
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
||||
<DropdownMenu.Item
|
||||
key={`filter-${f.name}`}
|
||||
$isActive={f.value === filter.sortBy}
|
||||
value={f.value}
|
||||
onClick={handleSetSortBy}
|
||||
>
|
||||
{f.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
onClick={handleToggleSortOrder}
|
||||
>
|
||||
{folder.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size={15} />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiRefreshLine />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group>
|
||||
<DropdownMenu position="bottom-end">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings3Fill size="1.3rem" />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Label>Display type</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.CARD}
|
||||
value={ListDisplayType.CARD}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Card
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.POSTER}
|
||||
value={ListDisplayType.POSTER}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Poster
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE}
|
||||
value={ListDisplayType.TABLE}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE_PAGINATED}
|
||||
value={ListDisplayType.TABLE_PAGINATED}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table (paginated)
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
||||
<Slider
|
||||
defaultValue={grid?.itemsPerRow}
|
||||
label={null}
|
||||
max={10}
|
||||
min={2}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
) : (
|
||||
<Slider
|
||||
defaultValue={table.rowHeight}
|
||||
label={null}
|
||||
max={100}
|
||||
min={30}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
{(display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) && (
|
||||
<>
|
||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
closeMenuOnClick={false}
|
||||
component="div"
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
clearable
|
||||
data={ALBUMARTIST_TABLE_COLUMNS}
|
||||
defaultValue={table?.columns.map((column) => column.column)}
|
||||
width={300}
|
||||
onChange={handleTableColumns}
|
||||
/>
|
||||
<Group position="apart">
|
||||
<Text>Auto Fit Columns</Text>
|
||||
<Switch
|
||||
defaultChecked={table.autoFit}
|
||||
onChange={handleAutoFitColumns}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
{cq.isMd ? (
|
||||
sortOrderLabel
|
||||
) : (
|
||||
<>
|
||||
{filter.sortOrder === SortOrder.ASC ? (
|
||||
<RiSortAsc size={15} />
|
||||
) : (
|
||||
<RiSortDesc size={15} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
<DropdownMenu.Item
|
||||
key={`musicFolder-${folder.id}`}
|
||||
$isActive={filter.musicFolderId === folder.id}
|
||||
value={folder.id}
|
||||
onClick={handleSetMusicFolder}
|
||||
>
|
||||
{folder.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size={15} />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiRefreshLine />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group>
|
||||
<DropdownMenu position="bottom-end">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings3Fill size="1.3rem" />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Label>Display type</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.CARD}
|
||||
value={ListDisplayType.CARD}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Card
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.POSTER}
|
||||
value={ListDisplayType.POSTER}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Poster
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE}
|
||||
value={ListDisplayType.TABLE}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE_PAGINATED}
|
||||
value={ListDisplayType.TABLE_PAGINATED}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table (paginated)
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
{display === ListDisplayType.CARD ||
|
||||
display === ListDisplayType.POSTER ? (
|
||||
<Slider
|
||||
defaultValue={grid?.itemsPerRow}
|
||||
label={null}
|
||||
max={10}
|
||||
min={2}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
) : (
|
||||
<Slider
|
||||
defaultValue={table.rowHeight}
|
||||
label={null}
|
||||
max={100}
|
||||
min={30}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
{(display === ListDisplayType.TABLE ||
|
||||
display === ListDisplayType.TABLE_PAGINATED) && (
|
||||
<>
|
||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
closeMenuOnClick={false}
|
||||
component="div"
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
clearable
|
||||
data={ALBUMARTIST_TABLE_COLUMNS}
|
||||
defaultValue={table?.columns.map(
|
||||
(column) => column.column,
|
||||
)}
|
||||
width={300}
|
||||
onChange={handleTableColumns}
|
||||
/>
|
||||
<Group position="apart">
|
||||
<Text>Auto Fit Columns</Text>
|
||||
<Switch
|
||||
defaultChecked={table.autoFit}
|
||||
onChange={handleAutoFitColumns}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import { PageHeader, SearchInput } from '/@/renderer/components';
|
|||
import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
AlbumArtistListFilter,
|
||||
useAlbumArtistListStore,
|
||||
useCurrentServer,
|
||||
useListStoreActions,
|
||||
AlbumArtistListFilter,
|
||||
useAlbumArtistListStore,
|
||||
useCurrentServer,
|
||||
useListStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { ListDisplayType } from '/@/renderer/types';
|
||||
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
||||
|
|
@ -24,156 +24,158 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
|||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
|
||||
interface AlbumArtistListHeaderProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount?: number;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount?: number;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const AlbumArtistListHeader = ({
|
||||
itemCount,
|
||||
gridRef,
|
||||
tableRef,
|
||||
itemCount,
|
||||
gridRef,
|
||||
tableRef,
|
||||
}: AlbumArtistListHeaderProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useAlbumArtistListContext();
|
||||
const { display, filter } = useAlbumArtistListStore();
|
||||
const { setFilter, setTablePagination } = useListStoreActions();
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const fetch = useCallback(
|
||||
async (startIndex: number, limit: number, filters: AlbumArtistListFilter) => {
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const albums = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return albums;
|
||||
},
|
||||
[queryClient, server],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
async (filters: AlbumArtistListFilter) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useAlbumArtistListContext();
|
||||
const { display, filter } = useAlbumArtistListStore();
|
||||
const { setFilter, setTablePagination } = useListStoreActions();
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const fetch = useCallback(
|
||||
async (startIndex: number, limit: number, filters: AlbumArtistListFilter) => {
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
const albums = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
params.successCallback(
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
tableRef.current?.api.setDatasource(dataSource);
|
||||
tableRef.current?.api.purgeInfiniteCache();
|
||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
return albums;
|
||||
},
|
||||
[queryClient, server],
|
||||
);
|
||||
|
||||
if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||
}
|
||||
} else {
|
||||
gridRef.current?.scrollTo(0);
|
||||
gridRef.current?.resetLoadMoreItemsCache();
|
||||
const handleFilterChange = useCallback(
|
||||
async (filters: AlbumArtistListFilter) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
||||
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
||||
// and then set the grid's data here.
|
||||
const data = await fetch(0, 200, filters);
|
||||
const queryKey = queryKeys.albumArtists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
if (!data?.items) return;
|
||||
gridRef.current?.setItemData(data.items);
|
||||
}
|
||||
},
|
||||
[display, tableRef, server, queryClient, setTablePagination, pageKey, gridRef, fetch],
|
||||
);
|
||||
const albumArtistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const previousSearchTerm = filter.searchTerm;
|
||||
const searchTerm = e.target.value === '' ? undefined : e.target.value;
|
||||
const updatedFilters = setFilter({
|
||||
data: { searchTerm },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters);
|
||||
}, 500);
|
||||
params.successCallback(
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
tableRef.current?.api.setDatasource(dataSource);
|
||||
tableRef.current?.api.purgeInfiniteCache();
|
||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={cq.ref}
|
||||
spacing={0}
|
||||
>
|
||||
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||
}
|
||||
} else {
|
||||
gridRef.current?.scrollTo(0);
|
||||
gridRef.current?.resetLoadMoreItemsCache();
|
||||
|
||||
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
||||
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
||||
// and then set the grid's data here.
|
||||
const data = await fetch(0, 200, filters);
|
||||
|
||||
if (!data?.items) return;
|
||||
gridRef.current?.setItemData(data.items);
|
||||
}
|
||||
},
|
||||
[display, tableRef, server, queryClient, setTablePagination, pageKey, gridRef, fetch],
|
||||
);
|
||||
|
||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const previousSearchTerm = filter.searchTerm;
|
||||
const searchTerm = e.target.value === '' ? undefined : e.target.value;
|
||||
const updatedFilters = setFilter({
|
||||
data: { searchTerm },
|
||||
itemType: LibraryItem.ALBUM_ARTIST,
|
||||
key: pageKey,
|
||||
}) as AlbumArtistListFilter;
|
||||
if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={cq.ref}
|
||||
spacing={0}
|
||||
>
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>Album Artists</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Badge isLoading={itemCount === null || itemCount === undefined}>
|
||||
{itemCount}
|
||||
</LibraryHeaderBar.Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<AlbumArtistListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>Album Artists</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Badge
|
||||
isLoading={itemCount === null || itemCount === undefined}
|
||||
>
|
||||
{itemCount}
|
||||
</LibraryHeaderBar.Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<AlbumArtistListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { createContext, useContext } from 'react';
|
|||
import { ListKey } from '/@/renderer/store';
|
||||
|
||||
export const AlbumArtistDetailSongListContext = createContext<{ id?: string; pageKey: ListKey }>({
|
||||
pageKey: 'albumArtist',
|
||||
pageKey: 'albumArtist',
|
||||
});
|
||||
|
||||
export const useAlbumArtistDetailSongListContext = () => {
|
||||
const ctxValue = useContext(AlbumArtistDetailSongListContext);
|
||||
return ctxValue;
|
||||
const ctxValue = useContext(AlbumArtistDetailSongListContext);
|
||||
return ctxValue;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { createContext, useContext } from 'react';
|
|||
import { ListKey } from '/@/renderer/store';
|
||||
|
||||
export const AlbumArtistListContext = createContext<{ id?: string; pageKey: ListKey }>({
|
||||
pageKey: 'albumArtist',
|
||||
pageKey: 'albumArtist',
|
||||
});
|
||||
|
||||
export const useAlbumArtistListContext = () => {
|
||||
const ctxValue = useContext(AlbumArtistListContext);
|
||||
return ctxValue;
|
||||
const ctxValue = useContext(AlbumArtistListContext);
|
||||
return ctxValue;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,16 +6,19 @@ import { api } from '/@/renderer/api';
|
|||
import { QueryHookArgs } from '../../../lib/react-query';
|
||||
|
||||
export const useAlbumArtistDetail = (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!server?.id && !!query.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumArtistDetail({ apiClientProps: { server, signal }, query });
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
return useQuery({
|
||||
enabled: !!server?.id && !!query.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumArtistDetail({
|
||||
apiClientProps: { server, signal },
|
||||
query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ import { api } from '/@/renderer/api';
|
|||
import { QueryHookArgs } from '../../../lib/react-query';
|
||||
|
||||
export const useAlbumArtistList = (args: QueryHookArgs<AlbumArtistListQuery>) => {
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!server?.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumArtistList({ apiClientProps: { server, signal }, query });
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.list(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
return useQuery({
|
||||
enabled: !!server?.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumArtistList({ apiClientProps: { server, signal }, query });
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.list(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,16 +6,19 @@ import { api } from '/@/renderer/api';
|
|||
import { QueryHookArgs } from '../../../lib/react-query';
|
||||
|
||||
export const useAlbumArtistInfo = (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!server?.id && !!query.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumArtistDetail({ apiClientProps: { server, signal }, query });
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
return useQuery({
|
||||
enabled: !!server?.id && !!query.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getAlbumArtistDetail({
|
||||
apiClientProps: { server, signal },
|
||||
query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ import { getServerById } from '/@/renderer/store';
|
|||
import { api } from '/@/renderer/api';
|
||||
|
||||
export const useTopSongsList = (args: QueryHookArgs<TopSongListQuery>) => {
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
const { options, query, serverId } = args || {};
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!server?.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getTopSongList({ apiClientProps: { server, signal }, query });
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.topSongs(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
return useQuery({
|
||||
enabled: !!server?.id,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getTopSongList({ apiClientProps: { server, signal }, query });
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.topSongs(server?.id || '', query),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,51 +12,56 @@ import { AlbumArtistDetailContent } from '/@/renderer/features/artists/component
|
|||
import { useCurrentServer } from '/@/renderer/store';
|
||||
|
||||
const AlbumArtistDetailRoute = () => {
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const server = useCurrentServer();
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const server = useCurrentServer();
|
||||
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
||||
const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
|
||||
|
||||
const handlePlay = () => {
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [albumArtistId],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
},
|
||||
playType: playButtonBehavior,
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const detailQuery = useAlbumArtistDetail({
|
||||
query: { id: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
};
|
||||
const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
|
||||
|
||||
if (detailQuery.isLoading || !background) return null;
|
||||
const handlePlay = () => {
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [albumArtistId],
|
||||
type: LibraryItem.ALBUM_ARTIST,
|
||||
},
|
||||
playType: playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatedPage key={`album-artist-detail-${albumArtistId}`}>
|
||||
<NativeScrollArea
|
||||
ref={scrollAreaRef}
|
||||
pageHeaderProps={{
|
||||
backgroundColor: background,
|
||||
children: (
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
||||
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
||||
</LibraryHeaderBar>
|
||||
),
|
||||
target: headerRef,
|
||||
}}
|
||||
>
|
||||
<AlbumArtistDetailHeader
|
||||
ref={headerRef}
|
||||
background={background}
|
||||
/>
|
||||
<AlbumArtistDetailContent />
|
||||
</NativeScrollArea>
|
||||
</AnimatedPage>
|
||||
);
|
||||
if (detailQuery.isLoading || !background) return null;
|
||||
|
||||
return (
|
||||
<AnimatedPage key={`album-artist-detail-${albumArtistId}`}>
|
||||
<NativeScrollArea
|
||||
ref={scrollAreaRef}
|
||||
pageHeaderProps={{
|
||||
backgroundColor: background,
|
||||
children: (
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
||||
<LibraryHeaderBar.Title>
|
||||
{detailQuery?.data?.name}
|
||||
</LibraryHeaderBar.Title>
|
||||
</LibraryHeaderBar>
|
||||
),
|
||||
target: headerRef,
|
||||
}}
|
||||
>
|
||||
<AlbumArtistDetailHeader
|
||||
ref={headerRef}
|
||||
background={background}
|
||||
/>
|
||||
<AlbumArtistDetailContent />
|
||||
</NativeScrollArea>
|
||||
</AnimatedPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlbumArtistDetailRoute;
|
||||
|
|
|
|||
|
|
@ -9,35 +9,38 @@ import { AnimatedPage } from '/@/renderer/features/shared';
|
|||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
|
||||
const AlbumArtistDetailTopSongsListRoute = () => {
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const server = useCurrentServer();
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const server = useCurrentServer();
|
||||
|
||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
||||
const detailQuery = useAlbumArtistDetail({
|
||||
query: { id: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const topSongsQuery = useTopSongsList({
|
||||
options: { enabled: !!detailQuery?.data?.name },
|
||||
query: { artist: detailQuery?.data?.name || '', artistId: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
const topSongsQuery = useTopSongsList({
|
||||
options: { enabled: !!detailQuery?.data?.name },
|
||||
query: { artist: detailQuery?.data?.name || '', artistId: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount = topSongsQuery?.data?.items?.length || 0;
|
||||
const itemCount = topSongsQuery?.data?.items?.length || 0;
|
||||
|
||||
if (detailQuery.isLoading || topSongsQuery?.isLoading) return null;
|
||||
if (detailQuery.isLoading || topSongsQuery?.isLoading) return null;
|
||||
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<AlbumArtistDetailTopSongsListHeader
|
||||
data={topSongsQuery?.data?.items || []}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
<AlbumArtistDetailTopSongsListContent
|
||||
data={topSongsQuery?.data?.items || []}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</AnimatedPage>
|
||||
);
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<AlbumArtistDetailTopSongsListHeader
|
||||
data={topSongsQuery?.data?.items || []}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
<AlbumArtistDetailTopSongsListContent
|
||||
data={topSongsQuery?.data?.items || []}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</AnimatedPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlbumArtistDetailTopSongsListRoute;
|
||||
|
|
|
|||
|
|
@ -10,46 +10,46 @@ import { useCurrentServer } from '../../../store/auth.store';
|
|||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
const AlbumArtistListRoute = () => {
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const pageKey = generatePageKey('albumArtist', undefined);
|
||||
const server = useCurrentServer();
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const pageKey = generatePageKey('albumArtist', undefined);
|
||||
const server = useCurrentServer();
|
||||
|
||||
const albumArtistListFilter = useAlbumArtistListFilter({ id: undefined, key: pageKey });
|
||||
const albumArtistListFilter = useAlbumArtistListFilter({ id: undefined, key: pageKey });
|
||||
|
||||
const itemCountCheck = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...albumArtistListFilter,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
const itemCountCheck = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...albumArtistListFilter,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
? undefined
|
||||
: itemCountCheck.data?.totalRecordCount;
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
? undefined
|
||||
: itemCountCheck.data?.totalRecordCount;
|
||||
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<AlbumArtistListContext.Provider value={{ id: undefined, pageKey }}>
|
||||
<AlbumArtistListHeader
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AlbumArtistListContent
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</AlbumArtistListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<AlbumArtistListContext.Provider value={{ id: undefined, pageKey }}>
|
||||
<AlbumArtistListHeader
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<AlbumArtistListContent
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</AlbumArtistListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlbumArtistListRoute;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue