feishin/src/renderer/features/artists/components/album-artist-detail-content.tsx

571 lines
22 KiB
TypeScript
Raw Normal View History

2024-01-15 22:10:50 -08:00
import { useMemo } from 'react';
2023-01-12 18:43:25 -08:00
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import { Box, Group, Stack } from '@mantine/core';
2024-01-15 22:10:50 -08:00
import { useTranslation } from 'react-i18next';
import { FaLastfmSquare } from 'react-icons/fa';
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
2024-01-15 22:10:50 -08:00
import { SiMusicbrainz } from 'react-icons/si';
2023-01-12 18:43:25 -08:00
import { generatePath, useParams } from 'react-router';
2023-01-15 16:22:07 -08:00
import { createSearchParams, Link } from 'react-router-dom';
2023-01-12 18:43:25 -08:00
import styled from 'styled-components';
import {
2023-07-01 19:10:05 -07:00
Album,
AlbumArtist,
AlbumListSort,
LibraryItem,
QueueSong,
ServerType,
SortOrder,
2023-01-12 18:43:25 -08:00
} from '/@/renderer/api/types';
import { Button, Spoiler, TextTitle } from '/@/renderer/components';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
2023-01-12 18:43:25 -08:00
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query';
import {
useHandleGeneralContextMenu,
useHandleTableContextMenu,
} from '/@/renderer/features/context-menu';
import {
ARTIST_CONTEXT_MENU_ITEMS,
SONG_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { PlayButton, useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
2024-01-15 22:10:50 -08:00
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { CardRow, Play, TableColumn } from '/@/renderer/types';
2024-04-03 07:36:13 -07:00
import { sanitize } from '/@/renderer/utils/sanitize';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
2023-01-12 18:43:25 -08:00
const ContentContainer = styled.div`
2023-07-01 19:10:05 -07:00
position: relative;
z-index: 0;
`;
const DetailContainer = styled.div`
2023-07-01 19:10:05 -07:00
display: flex;
flex-direction: column;
gap: 2rem;
2023-07-01 19:10:05 -07:00
padding: 1rem 2rem 5rem;
overflow: hidden;
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
.ag-theme-alpine-dark {
2023-09-15 20:42:38 -07:00
--ag-header-background-color: rgb(0 0 0 / 0%) !important;
2023-07-01 19:10:05 -07:00
}
2023-01-12 18:43:25 -08:00
`;
interface AlbumArtistDetailContentProps {
background?: string;
}
export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => {
2024-01-15 22:10:50 -08:00
const { t } = useTranslation();
const { externalLinks } = useGeneralSettings();
2023-07-01 19:10:05 -07:00
const { albumArtistId } = useParams() as { albumArtistId: string };
const cq = useContainerQuery();
const handlePlayQueueAdd = usePlayQueueAdd();
const server = useCurrentServer();
const genrePath = useGenreRoute();
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const detailQuery = useAlbumArtistDetail({
query: { id: albumArtistId },
serverId: server?.id,
});
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const artistDiscographyLink = `${generatePath(
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
{
albumArtistId,
},
)}?${createSearchParams({
artistId: albumArtistId,
artistName: detailQuery?.data?.name || '',
})}`;
2023-01-15 16:22:07 -08:00
2023-07-01 19:10:05 -07:00
const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, {
albumArtistId,
})}?${createSearchParams({
artistId: albumArtistId,
artistName: detailQuery?.data?.name || '',
})}`;
2023-01-15 20:39:43 -08:00
2023-07-01 19:10:05 -07:00
const recentAlbumsQuery = useAlbumList({
query: {
_custom: {
jellyfin: {
...(server?.type === ServerType.JELLYFIN
? { ArtistIds: albumArtistId }
: undefined),
},
navidrome: {
...(server?.type === ServerType.NAVIDROME
? { artist_id: albumArtistId, compilation: false }
: undefined),
},
},
2023-07-29 10:55:48 -07:00
limit: 15,
2023-07-01 19:10:05 -07:00
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
startIndex: 0,
},
2023-07-01 19:10:05 -07:00
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),
},
},
2023-07-29 10:55:48 -07:00
limit: 15,
2023-07-01 19:10:05 -07:00
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
startIndex: 0,
},
2023-07-01 19:10:05 -07:00
serverId: server?.id,
});
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const topSongsQuery = useTopSongsList({
options: {
enabled: !!detailQuery?.data?.name,
},
2023-07-01 19:10:05 -07:00
query: {
artist: detailQuery?.data?.name || '',
artistId: albumArtistId,
},
2023-07-01 19:10:05 -07:00
serverId: server?.id,
});
2023-01-15 20:39:43 -08:00
2023-07-01 19:10:05 -07:00
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 },
]),
[],
);
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
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' }],
},
},
],
};
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const cardRoutes = {
album: {
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
2023-01-12 18:43:25 -08:00
},
2023-07-01 19:10:05 -07:00
albumArtist: {
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
2023-01-12 18:43:25 -08:00
},
2023-07-01 19:10:05 -07:00
};
2023-01-12 18:43:25 -08:00
const carousels = useMemo(() => {
return [
{
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}
>
{t('page.albumArtistDetail.recentReleases', {
postProcess: 'sentenceCase',
})}
</TextTitle>
<Button
compact
uppercase
component={Link}
to={artistDiscographyLink}
variant="subtle"
>
{t('page.albumArtistDetail.viewDiscography')}
</Button>
</Group>
),
uniqueId: 'recentReleases',
},
{
data: compilationAlbumsQuery?.data?.items,
isHidden: !compilationAlbumsQuery?.data?.items?.length,
itemType: LibraryItem.ALBUM,
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
title: (
2023-07-01 19:10:05 -07:00
<TextTitle
order={2}
weight={700}
>
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
2023-07-01 19:10:05 -07:00
</TextTitle>
),
uniqueId: 'compilationAlbums',
},
{
data: detailQuery?.data?.similarArtists || [],
isHidden: !detailQuery?.data?.similarArtists,
itemType: LibraryItem.ALBUM_ARTIST,
title: (
<TextTitle
order={2}
weight={700}
2023-07-01 19:10:05 -07:00
>
{t('page.albumArtistDetail.relatedArtists', {
postProcess: 'sentenceCase',
})}
</TextTitle>
),
uniqueId: 'similarArtists',
},
];
}, [
artistDiscographyLink,
compilationAlbumsQuery?.data?.items,
compilationAlbumsQuery.isFetching,
compilationAlbumsQuery?.isLoading,
detailQuery?.data?.similarArtists,
recentAlbumsQuery?.data?.items,
recentAlbumsQuery.isFetching,
recentAlbumsQuery?.isLoading,
t,
]);
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const playButtonBehavior = usePlayButtonBehavior();
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const handlePlay = async (playType?: Play) => {
handlePlayQueueAdd?.({
byItemType: {
id: [albumArtistId],
type: LibraryItem.ALBUM_ARTIST,
},
playType: playType || playButtonBehavior,
});
};
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data || !topSongsQuery?.data) return;
2023-07-01 19:10:05 -07:00
handlePlayQueueAdd?.({
byData: topSongsQuery?.data?.items || [],
initialSongId: e.data.id,
playType: playButtonBehavior,
});
};
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const createFavoriteMutation = useCreateFavorite({});
const deleteFavoriteMutation = useDeleteFavorite({});
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const handleFavorite = () => {
if (!detailQuery?.data) return;
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
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,
});
}
};
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const handleGeneralContextMenu = useHandleGeneralContextMenu(
LibraryItem.ALBUM_ARTIST,
ARTIST_CONTEXT_MENU_ITEMS,
);
2023-07-01 19:10:05 -07:00
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
2023-01-12 18:43:25 -08:00
2024-04-03 07:36:13 -07:00
const biography = useMemo(() => {
const bio = detailQuery?.data?.biography;
if (!bio) return null;
return sanitize(bio);
}, [detailQuery?.data?.biography]);
2023-07-01 19:10:05 -07:00
const showTopSongs = topSongsQuery?.data?.items?.length;
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
2024-01-15 22:10:50 -08:00
const mbzId = detailQuery?.data?.mbz;
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
const isLoading =
detailQuery?.isLoading ||
(server?.type === ServerType.NAVIDROME && topSongsQuery?.isLoading);
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
if (isLoading) return <ContentContainer ref={cq.ref} />;
2023-01-12 18:43:25 -08:00
2023-07-01 19:10:05 -07:00
return (
<ContentContainer ref={cq.ref}>
<LibraryBackgroundOverlay $backgroundColor={background} />
<DetailContainer>
<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>
</Group>
</Group>
<Group spacing="md">
<Button
compact
uppercase
component={Link}
to={artistDiscographyLink}
variant="subtle"
>
{t('page.albumArtistDetail.viewDiscography')}
</Button>
<Button
compact
uppercase
component={Link}
to={artistSongsLink}
variant="subtle"
>
{t('page.albumArtistDetail.viewAllTracks')}
</Button>
</Group>
{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(genrePath, {
2023-08-03 19:04:12 -07:00
genreId: genre.id,
})}
variant="outline"
>
{genre.name}
</Button>
))}
</Group>
</Box>
) : null}
2024-01-15 22:10:50 -08:00
{externalLinks ? (
<Box component="section">
2024-01-15 22:10:50 -08:00
<Group spacing="sm">
<Button
compact
component="a"
href={`https://www.last.fm/music/${encodeURIComponent(
detailQuery?.data?.name || '',
)}`}
radius="md"
rel="noopener noreferrer"
size="md"
target="_blank"
tooltip={{
label: t('action.openIn.lastfm'),
}}
variant="subtle"
>
<FaLastfmSquare size={25} />
</Button>
{mbzId ? (
<Button
compact
component="a"
href={`https://musicbrainz.org/artist/${mbzId}`}
radius="md"
rel="noopener noreferrer"
size="md"
target="_blank"
tooltip={{
label: t('action.openIn.musicbrainz'),
}}
variant="subtle"
>
<SiMusicbrainz size={25} />
</Button>
) : null}
</Group>
</Box>
) : null}
2024-04-03 07:36:13 -07:00
{biography ? (
<Box
component="section"
maw="1280px"
>
<TextTitle
order={2}
weight={700}
>
{t('page.albumArtistDetail.about', {
artist: detailQuery?.data?.name,
})}
</TextTitle>
2024-04-03 07:36:13 -07:00
<Spoiler dangerouslySetInnerHTML={{ __html: biography }} />
</Box>
) : null}
{showTopSongs ? (
<Box component="section">
<Group
noWrap
position="apart"
>
<Group
noWrap
align="flex-end"
>
<TextTitle
order={2}
weight={700}
>
{t('page.albumArtistDetail.topSongs', {
postProcess: 'sentenceCase',
})}
</TextTitle>
<Button
compact
uppercase
component={Link}
to={generatePath(
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS,
{
albumArtistId,
},
)}
variant="subtle"
>
{t('page.albumArtistDetail.viewAll', {
postProcess: 'sentenceCase',
})}
</Button>
</Group>
</Group>
<VirtualTable
autoFitColumns
autoHeight
deselectOnClickOutside
stickyHeader
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) => (
<MemoizedSwiperGridCarousel
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>
</DetailContainer>
2023-07-01 19:10:05 -07:00
</ContentContainer>
);
2023-01-12 18:43:25 -08:00
};