Lint all files

This commit is contained in:
jeffvli 2023-07-01 19:10:05 -07:00
parent 22af76b4d6
commit 30e52ebb54
334 changed files with 76519 additions and 75932 deletions

View file

@ -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>
);
};

View file

@ -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>
);
},
);

View file

@ -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>
</>
);
};

View file

@ -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>
);
};

View file

@ -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>
)}
</>
);
</>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};