mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
update album artist page
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
.content-container {
|
.content-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-container {
|
.detail-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--theme-spacing-lg);
|
gap: var(--theme-spacing-2xl);
|
||||||
padding: 1rem 2rem 5rem;
|
padding: 1rem 2rem 5rem;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { Suspense, useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { createSearchParams, generatePath, Link, useParams } from 'react-router';
|
import { createSearchParams, generatePath, Link, useParams } from 'react-router';
|
||||||
@@ -25,225 +25,55 @@ import { Spinner } from '/@/shared/components/spinner/spinner';
|
|||||||
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import {
|
import {
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
|
TopSongListResponse,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const AlbumArtistDetailContent = () => {
|
interface AlbumArtistActionButtonsProps {
|
||||||
const { t } = useTranslation();
|
albumCount: null | number | undefined;
|
||||||
const { artistItems, externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
artistDiscographyLink: string;
|
||||||
const { albumArtistId, artistId } = useParams() as {
|
artistSongsLink: string;
|
||||||
albumArtistId?: string;
|
onFavorite: () => void;
|
||||||
artistId?: string;
|
onMoreOptions: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
};
|
onPlay: () => void;
|
||||||
const routeId = (artistId || albumArtistId) as string;
|
userFavorite?: boolean;
|
||||||
const { ref } = useContainerQuery();
|
}
|
||||||
const { addToQueueByFetch, setFavorite } = usePlayer();
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const genrePath = useGenreRoute();
|
|
||||||
|
|
||||||
const [enabledItem, itemOrder] = useMemo(() => {
|
const AlbumArtistActionButtons = ({
|
||||||
const enabled: { [key in ArtistItem]?: boolean } = {};
|
albumCount,
|
||||||
const order: { [key in ArtistItem]?: number } = {};
|
|
||||||
|
|
||||||
for (const [idx, item] of artistItems.entries()) {
|
|
||||||
enabled[item.id] = !item.disabled;
|
|
||||||
order[item.id] = idx + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [enabled, order];
|
|
||||||
}, [artistItems]);
|
|
||||||
|
|
||||||
const detailQuery = useQuery(
|
|
||||||
artistsQueries.albumArtistDetail({
|
|
||||||
query: { id: routeId },
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const artistDiscographyLink = `${generatePath(
|
|
||||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
|
|
||||||
{
|
|
||||||
albumArtistId: routeId,
|
|
||||||
},
|
|
||||||
)}?${createSearchParams({
|
|
||||||
artistId: routeId,
|
|
||||||
artistName: detailQuery?.data?.name || '',
|
|
||||||
})}`;
|
|
||||||
|
|
||||||
const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, {
|
|
||||||
albumArtistId: routeId,
|
|
||||||
})}?${createSearchParams({
|
|
||||||
artistId: routeId,
|
|
||||||
artistName: detailQuery?.data?.name || '',
|
|
||||||
})}`;
|
|
||||||
|
|
||||||
const topSongsQuery = useQuery(
|
|
||||||
artistsQueries.topSongs({
|
|
||||||
options: {
|
|
||||||
enabled: !!detailQuery?.data?.name && enabledItem.topSongs,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
artist: detailQuery?.data?.name || '',
|
|
||||||
artistId: routeId,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const carousels = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
isHidden: !enabledItem.recentAlbums || !routeId,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
order: itemOrder.recentAlbums,
|
|
||||||
query: {
|
|
||||||
artistIds: routeId ? [routeId] : undefined,
|
|
||||||
compilation: false,
|
|
||||||
},
|
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
title: (
|
|
||||||
<Group align="flex-end">
|
|
||||||
<TextTitle fw={700} order={2}>
|
|
||||||
{t('page.albumArtistDetail.recentReleases', {
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
})}
|
|
||||||
</TextTitle>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
size="compact-md"
|
|
||||||
to={artistDiscographyLink}
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
uniqueId: 'recentReleases',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isHidden:
|
|
||||||
!enabledItem.compilations || server?.type === ServerType.SUBSONIC || !routeId,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
order: itemOrder.compilations,
|
|
||||||
query: {
|
|
||||||
artistIds: routeId ? [routeId] : undefined,
|
|
||||||
compilation: true,
|
|
||||||
},
|
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
title: (
|
|
||||||
<TextTitle fw={700} order={2}>
|
|
||||||
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
|
|
||||||
</TextTitle>
|
|
||||||
),
|
|
||||||
uniqueId: 'compilationAlbums',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: (detailQuery?.data?.similarArtists || []) as AlbumArtist[],
|
|
||||||
isHidden: !detailQuery?.data?.similarArtists || !enabledItem.similarArtists,
|
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
|
||||||
order: itemOrder.similarArtists,
|
|
||||||
title: (
|
|
||||||
<TextTitle fw={700} order={2}>
|
|
||||||
{t('page.albumArtistDetail.relatedArtists', {
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
})}
|
|
||||||
</TextTitle>
|
|
||||||
),
|
|
||||||
uniqueId: 'similarArtists',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [
|
|
||||||
artistDiscographyLink,
|
artistDiscographyLink,
|
||||||
detailQuery?.data?.similarArtists,
|
artistSongsLink,
|
||||||
enabledItem.compilations,
|
onFavorite,
|
||||||
enabledItem.recentAlbums,
|
onMoreOptions,
|
||||||
enabledItem.similarArtists,
|
onPlay,
|
||||||
itemOrder.compilations,
|
userFavorite,
|
||||||
itemOrder.recentAlbums,
|
}: AlbumArtistActionButtonsProps) => {
|
||||||
itemOrder.similarArtists,
|
const { t } = useTranslation();
|
||||||
routeId,
|
|
||||||
server?.type,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const handlePlay = async (playType?: Play) => {
|
|
||||||
if (!server?.id) return;
|
|
||||||
addToQueueByFetch(
|
|
||||||
server.id,
|
|
||||||
[routeId],
|
|
||||||
albumArtistId ? LibraryItem.ALBUM_ARTIST : LibraryItem.ARTIST,
|
|
||||||
playType || playButtonBehavior,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFavorite = () => {
|
|
||||||
if (!detailQuery?.data) return;
|
|
||||||
setFavorite(
|
|
||||||
detailQuery.data._serverId,
|
|
||||||
[detailQuery.data.id],
|
|
||||||
LibraryItem.ALBUM_ARTIST,
|
|
||||||
!detailQuery.data.userFavorite,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
if (!detailQuery?.data) return;
|
|
||||||
ContextMenuController.call({
|
|
||||||
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
|
|
||||||
event: e,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumCount = detailQuery?.data?.albumCount;
|
|
||||||
|
|
||||||
const biography = useMemo(() => {
|
|
||||||
const bio = detailQuery?.data?.biography;
|
|
||||||
|
|
||||||
if (!bio || !enabledItem.biography) return null;
|
|
||||||
return sanitize(bio);
|
|
||||||
}, [detailQuery?.data?.biography, enabledItem.biography]);
|
|
||||||
|
|
||||||
const showTopSongs = topSongsQuery?.data?.items?.length && enabledItem.topSongs;
|
|
||||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
|
||||||
const mbzId = detailQuery?.data?.mbz;
|
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
detailQuery?.isLoading ||
|
|
||||||
(server?.type === ServerType.NAVIDROME && enabledItem.topSongs && topSongsQuery?.isLoading);
|
|
||||||
|
|
||||||
if (isLoading) return <div className={styles.contentContainer} ref={ref} />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.contentContainer} ref={ref}>
|
<>
|
||||||
<div className={styles.detailContainer}>
|
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
<DefaultPlayButton
|
<DefaultPlayButton disabled={albumCount === 0} onClick={onPlay} />
|
||||||
disabled={albumCount === 0}
|
|
||||||
onClick={() => handlePlay(playButtonBehavior)}
|
|
||||||
/>
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="favorite"
|
icon="favorite"
|
||||||
iconProps={{
|
iconProps={{
|
||||||
fill: detailQuery?.data?.userFavorite ? 'primary' : undefined,
|
fill: userFavorite ? 'primary' : undefined,
|
||||||
}}
|
}}
|
||||||
onClick={handleFavorite}
|
onClick={onFavorite}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
/>
|
/>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="ellipsisHorizontal"
|
icon="ellipsisHorizontal"
|
||||||
onClick={handleMoreOptions}
|
onClick={onMoreOptions}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
/>
|
/>
|
||||||
@@ -258,26 +88,45 @@ export const AlbumArtistDetailContent = () => {
|
|||||||
>
|
>
|
||||||
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
|
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button component={Link} size="compact-md" to={artistSongsLink} variant="subtle">
|
||||||
component={Link}
|
|
||||||
size="compact-md"
|
|
||||||
to={artistSongsLink}
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
{String(t('page.albumArtistDetail.viewAllTracks')).toUpperCase()}
|
{String(t('page.albumArtistDetail.viewAllTracks')).toUpperCase()}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
{showGenres ? (
|
</>
|
||||||
<section>
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AlbumArtistMetadataGenresProps {
|
||||||
|
genres?: Array<{ id: string; name: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumArtistMetadataGenres = ({ genres }: AlbumArtistMetadataGenresProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const genrePath = useGenreRoute();
|
||||||
|
|
||||||
|
if (!genres || genres.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} isNoSelect size="sm" tt="uppercase">
|
||||||
|
{t('entity.genre', {
|
||||||
|
count: genres.length,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
{detailQuery?.data?.genres?.map((genre) => (
|
{genres.map((genre) => (
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
key={`genre-${genre.id}`}
|
key={`genre-${genre.id}`}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="compact-md"
|
size="compact-md"
|
||||||
to={generatePath(genrePath, {
|
to={generatePath(genrePath, {
|
||||||
|
albumArtistId: null,
|
||||||
|
albumId: null,
|
||||||
|
artistId: null,
|
||||||
genreId: genre.id,
|
genreId: genre.id,
|
||||||
|
itemType: null,
|
||||||
|
playlistId: null,
|
||||||
})}
|
})}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
@@ -285,17 +134,109 @@ export const AlbumArtistDetailContent = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AlbumArtistMetadataBiographyProps {
|
||||||
|
artistName?: string;
|
||||||
|
biography: null | string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumArtistMetadataBiography = ({
|
||||||
|
artistName,
|
||||||
|
biography,
|
||||||
|
}: AlbumArtistMetadataBiographyProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!biography) return null;
|
||||||
|
|
||||||
|
const sanitizedBiography = sanitize(biography);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section style={{ maxWidth: '1280px' }}>
|
||||||
|
<TextTitle fw={700} order={2}>
|
||||||
|
{t('page.albumArtistDetail.about', {
|
||||||
|
artist: artistName,
|
||||||
|
})}
|
||||||
|
</TextTitle>
|
||||||
|
<Spoiler dangerouslySetInnerHTML={{ __html: sanitizedBiography }} />
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
);
|
||||||
{externalLinks && (lastFM || musicBrainz) ? (
|
};
|
||||||
|
|
||||||
|
interface AlbumArtistMetadataTopSongsProps {
|
||||||
|
routeId: string;
|
||||||
|
topSongsQuery: ReturnType<typeof useQuery<TopSongListResponse>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumArtistMetadataTopSongs = ({
|
||||||
|
routeId,
|
||||||
|
topSongsQuery,
|
||||||
|
}: AlbumArtistMetadataTopSongsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!topSongsQuery?.data?.items?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<section>
|
<section>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Group align="flex-end" wrap="nowrap">
|
||||||
|
<TextTitle fw={700} order={2}>
|
||||||
|
{t('page.albumArtistDetail.topSongs', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</TextTitle>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
size="compact-md"
|
||||||
|
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS, {
|
||||||
|
albumArtistId: routeId,
|
||||||
|
})}
|
||||||
|
uppercase
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{t('page.albumArtistDetail.viewAll', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AlbumArtistMetadataExternalLinksProps {
|
||||||
|
artistName?: string;
|
||||||
|
externalLinks: boolean;
|
||||||
|
lastFM: boolean;
|
||||||
|
mbzId?: null | string;
|
||||||
|
musicBrainz: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumArtistMetadataExternalLinks = ({
|
||||||
|
artistName,
|
||||||
|
externalLinks,
|
||||||
|
lastFM,
|
||||||
|
mbzId,
|
||||||
|
musicBrainz,
|
||||||
|
}: AlbumArtistMetadataExternalLinksProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!externalLinks || (!lastFM && !musicBrainz)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} isNoSelect size="sm" tt="uppercase">
|
||||||
|
{t('common.externalLinks', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
{lastFM && (
|
{lastFM && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
component="a"
|
component="a"
|
||||||
href={`https://www.last.fm/music/${encodeURIComponent(
|
href={`https://www.last.fm/music/${encodeURIComponent(artistName || '')}`}
|
||||||
detailQuery?.data?.name || '',
|
|
||||||
)}`}
|
|
||||||
icon="brandLastfm"
|
icon="brandLastfm"
|
||||||
iconProps={{
|
iconProps={{
|
||||||
fill: 'default',
|
fill: 'default',
|
||||||
@@ -327,54 +268,240 @@ export const AlbumArtistDetailContent = () => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
</section>
|
</Stack>
|
||||||
) : null}
|
);
|
||||||
<Grid gutter="xl">
|
};
|
||||||
{biography ? (
|
|
||||||
<Grid.Col order={itemOrder.biography} span={12}>
|
export const AlbumArtistDetailContent = () => {
|
||||||
<section style={{ maxWidth: '1280px' }}>
|
const { t } = useTranslation();
|
||||||
|
const { artistItems, externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
||||||
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
|
albumArtistId?: string;
|
||||||
|
artistId?: string;
|
||||||
|
};
|
||||||
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
|
const { ref, ...cq } = useContainerQuery();
|
||||||
|
const { addToQueueByFetch, setFavorite } = usePlayer();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
const [enabledItem, itemOrder] = useMemo(() => {
|
||||||
|
const enabled: { [key in ArtistItem]?: boolean } = {};
|
||||||
|
const order: { [key in ArtistItem]?: number } = {};
|
||||||
|
|
||||||
|
for (const [idx, item] of artistItems.entries()) {
|
||||||
|
enabled[item.id] = !item.disabled;
|
||||||
|
order[item.id] = idx + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [enabled, order];
|
||||||
|
}, [artistItems]);
|
||||||
|
|
||||||
|
const detailQuery = useSuspenseQuery(
|
||||||
|
artistsQueries.albumArtistDetail({
|
||||||
|
query: { id: routeId },
|
||||||
|
serverId: server?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const artistDiscographyLink = `${generatePath(
|
||||||
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
|
||||||
|
{
|
||||||
|
albumArtistId: routeId,
|
||||||
|
},
|
||||||
|
)}?${createSearchParams({
|
||||||
|
artistId: routeId,
|
||||||
|
artistName: detailQuery.data?.name || '',
|
||||||
|
})}`;
|
||||||
|
|
||||||
|
const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, {
|
||||||
|
albumArtistId: routeId,
|
||||||
|
})}?${createSearchParams({
|
||||||
|
artistId: routeId,
|
||||||
|
artistName: detailQuery.data?.name || '',
|
||||||
|
})}`;
|
||||||
|
|
||||||
|
const topSongsQuery = useQuery(
|
||||||
|
artistsQueries.topSongs({
|
||||||
|
options: {
|
||||||
|
enabled: !!detailQuery.data?.name && enabledItem.topSongs,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
artist: detailQuery.data?.name || '',
|
||||||
|
artistId: routeId,
|
||||||
|
},
|
||||||
|
serverId: server?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const carousels = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
isHidden: !enabledItem.recentAlbums || !routeId,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
order: itemOrder.recentAlbums,
|
||||||
|
query: {
|
||||||
|
artistIds: routeId ? [routeId] : undefined,
|
||||||
|
compilation: false,
|
||||||
|
},
|
||||||
|
rowCount: 2,
|
||||||
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
title: (
|
||||||
|
<Group align="flex-end">
|
||||||
<TextTitle fw={700} order={2}>
|
<TextTitle fw={700} order={2}>
|
||||||
{t('page.albumArtistDetail.about', {
|
{t('page.albumArtistDetail.recentReleases', {
|
||||||
artist: detailQuery?.data?.name,
|
|
||||||
})}
|
|
||||||
</TextTitle>
|
|
||||||
<Spoiler dangerouslySetInnerHTML={{ __html: biography }} />
|
|
||||||
</section>
|
|
||||||
</Grid.Col>
|
|
||||||
) : null}
|
|
||||||
{showTopSongs ? (
|
|
||||||
<Grid.Col order={itemOrder.topSongs} span={12}>
|
|
||||||
<section>
|
|
||||||
<Group justify="space-between" wrap="nowrap">
|
|
||||||
<Group align="flex-end" wrap="nowrap">
|
|
||||||
<TextTitle fw={700} order={2}>
|
|
||||||
{t('page.albumArtistDetail.topSongs', {
|
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
})}
|
})}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
size="compact-md"
|
size="compact-md"
|
||||||
to={generatePath(
|
to={artistDiscographyLink}
|
||||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS,
|
|
||||||
{
|
|
||||||
albumArtistId: routeId,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
uppercase
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{t('page.albumArtistDetail.viewAll', {
|
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
})}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
),
|
||||||
</section>
|
uniqueId: 'recentReleases',
|
||||||
</Grid.Col>
|
},
|
||||||
) : null}
|
{
|
||||||
|
isHidden:
|
||||||
|
!enabledItem.compilations || server?.type === ServerType.SUBSONIC || !routeId,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
order: itemOrder.compilations,
|
||||||
|
query: {
|
||||||
|
artistIds: routeId ? [routeId] : undefined,
|
||||||
|
compilation: true,
|
||||||
|
},
|
||||||
|
rowCount: 1,
|
||||||
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
title: (
|
||||||
|
<TextTitle fw={700} order={2}>
|
||||||
|
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
|
||||||
|
</TextTitle>
|
||||||
|
),
|
||||||
|
uniqueId: 'compilationAlbums',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: (detailQuery.data?.similarArtists || []) as AlbumArtist[],
|
||||||
|
isHidden: !detailQuery.data?.similarArtists || !enabledItem.similarArtists,
|
||||||
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
|
order: itemOrder.similarArtists,
|
||||||
|
rowCount: 1,
|
||||||
|
title: (
|
||||||
|
<TextTitle fw={700} order={2}>
|
||||||
|
{t('page.albumArtistDetail.relatedArtists', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</TextTitle>
|
||||||
|
),
|
||||||
|
uniqueId: 'similarArtists',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [
|
||||||
|
artistDiscographyLink,
|
||||||
|
detailQuery.data?.similarArtists,
|
||||||
|
enabledItem.compilations,
|
||||||
|
enabledItem.recentAlbums,
|
||||||
|
enabledItem.similarArtists,
|
||||||
|
itemOrder.compilations,
|
||||||
|
itemOrder.recentAlbums,
|
||||||
|
itemOrder.similarArtists,
|
||||||
|
routeId,
|
||||||
|
server?.type,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
{carousels
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
|
const handlePlay = async (playType?: Play) => {
|
||||||
|
if (!server?.id) return;
|
||||||
|
addToQueueByFetch(
|
||||||
|
server.id,
|
||||||
|
[routeId],
|
||||||
|
albumArtistId ? LibraryItem.ALBUM_ARTIST : LibraryItem.ARTIST,
|
||||||
|
playType || playButtonBehavior,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFavorite = () => {
|
||||||
|
if (!detailQuery.data) return;
|
||||||
|
setFavorite(
|
||||||
|
detailQuery.data._serverId,
|
||||||
|
[detailQuery.data.id],
|
||||||
|
LibraryItem.ALBUM_ARTIST,
|
||||||
|
!detailQuery.data.userFavorite,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (!detailQuery.data) return;
|
||||||
|
ContextMenuController.call({
|
||||||
|
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
|
||||||
|
event: e,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const albumCount = detailQuery.data?.albumCount;
|
||||||
|
const biography =
|
||||||
|
detailQuery.data?.biography && enabledItem.biography ? detailQuery.data.biography : null;
|
||||||
|
const showGenres = detailQuery.data?.genres ? detailQuery.data.genres.length !== 0 : false;
|
||||||
|
const mbzId = detailQuery.data?.mbz;
|
||||||
|
|
||||||
|
// Calculate order for genres and external links (show before other sections)
|
||||||
|
// Use a very low order number to ensure they appear first
|
||||||
|
const genresOrder = 0;
|
||||||
|
const externalLinksOrder = 0.5;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.contentContainer} ref={ref}>
|
||||||
|
<div className={styles.detailContainer}>
|
||||||
|
<AlbumArtistActionButtons
|
||||||
|
albumCount={albumCount}
|
||||||
|
artistDiscographyLink={artistDiscographyLink}
|
||||||
|
artistSongsLink={artistSongsLink}
|
||||||
|
onFavorite={handleFavorite}
|
||||||
|
onMoreOptions={handleMoreOptions}
|
||||||
|
onPlay={() => handlePlay(playButtonBehavior)}
|
||||||
|
userFavorite={detailQuery.data?.userFavorite}
|
||||||
|
/>
|
||||||
|
<Grid gutter="xl">
|
||||||
|
{showGenres && (
|
||||||
|
<Grid.Col order={genresOrder} span={12}>
|
||||||
|
<AlbumArtistMetadataGenres genres={detailQuery.data?.genres} />
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{externalLinks && (lastFM || musicBrainz) && (
|
||||||
|
<Grid.Col order={externalLinksOrder} span={12}>
|
||||||
|
<AlbumArtistMetadataExternalLinks
|
||||||
|
artistName={detailQuery.data?.name}
|
||||||
|
externalLinks={externalLinks}
|
||||||
|
lastFM={lastFM}
|
||||||
|
mbzId={mbzId}
|
||||||
|
musicBrainz={musicBrainz}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{biography && (
|
||||||
|
<Grid.Col order={itemOrder.biography} span={12}>
|
||||||
|
<AlbumArtistMetadataBiography
|
||||||
|
artistName={detailQuery.data?.name}
|
||||||
|
biography={biography}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{Boolean(topSongsQuery?.data?.items?.length) && enabledItem.topSongs && (
|
||||||
|
<Grid.Col order={itemOrder.topSongs} span={12}>
|
||||||
|
<AlbumArtistMetadataTopSongs
|
||||||
|
routeId={routeId}
|
||||||
|
topSongsQuery={topSongsQuery}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
{cq.height || cq.width
|
||||||
|
? carousels
|
||||||
.filter((c) => !c.isHidden)
|
.filter((c) => !c.isHidden)
|
||||||
.map((carousel) => (
|
.map((carousel) => (
|
||||||
<Grid.Col
|
<Grid.Col
|
||||||
@@ -382,36 +509,33 @@ export const AlbumArtistDetailContent = () => {
|
|||||||
order={carousel.order}
|
order={carousel.order}
|
||||||
span={12}
|
span={12}
|
||||||
>
|
>
|
||||||
<section>
|
<Suspense fallback={<Spinner container />}>
|
||||||
<Stack gap="xl">
|
|
||||||
{carousel.itemType === LibraryItem.ALBUM ? (
|
{carousel.itemType === LibraryItem.ALBUM ? (
|
||||||
'query' in carousel &&
|
'query' in carousel &&
|
||||||
carousel.query &&
|
carousel.query &&
|
||||||
carousel.sortBy &&
|
carousel.sortBy &&
|
||||||
carousel.sortOrder ? (
|
carousel.sortOrder ? (
|
||||||
<Suspense fallback={<Spinner container />}>
|
|
||||||
<AlbumInfiniteCarousel
|
<AlbumInfiniteCarousel
|
||||||
query={carousel.query}
|
query={carousel.query}
|
||||||
rowCount={1}
|
rowCount={carousel.rowCount}
|
||||||
sortBy={carousel.sortBy}
|
sortBy={carousel.sortBy}
|
||||||
sortOrder={carousel.sortOrder}
|
sortOrder={carousel.sortOrder}
|
||||||
title={carousel.title}
|
title={carousel.title}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
) : null
|
) : null
|
||||||
) : carousel.itemType === LibraryItem.ALBUM_ARTIST ? (
|
) : carousel.itemType === LibraryItem.ALBUM_ARTIST ? (
|
||||||
'data' in carousel && carousel.data ? (
|
'data' in carousel && carousel.data ? (
|
||||||
<AlbumArtistGridCarousel
|
<AlbumArtistGridCarousel
|
||||||
data={carousel.data}
|
data={carousel.data}
|
||||||
rowCount={1}
|
rowCount={carousel.rowCount}
|
||||||
title={carousel.title}
|
title={carousel.title}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</Suspense>
|
||||||
</section>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
))}
|
))
|
||||||
|
: null}
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router';
|
import { useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
@@ -33,11 +33,8 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const detailQuery = useQuery({
|
const detailQuery = useSuspenseQuery({
|
||||||
...artistsQueries.albumArtistDetail({
|
...artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
|
||||||
query: { id: routeId },
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
initialData: location.state?.item,
|
initialData: location.state?.item,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
@@ -49,6 +46,7 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const background = backgroundColor;
|
const background = backgroundColor;
|
||||||
|
|
||||||
const showBlurredImage = Boolean(detailQuery.data?.imageUrl) && artistBackground;
|
const showBlurredImage = Boolean(detailQuery.data?.imageUrl) && artistBackground;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,6 +59,7 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
<LibraryHeaderBar.PlayButton
|
<LibraryHeaderBar.PlayButton
|
||||||
ids={[routeId]}
|
ids={[routeId]}
|
||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
|
variant="default"
|
||||||
/>
|
/>
|
||||||
<LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>
|
||||||
{detailQuery?.data?.name}
|
{detailQuery?.data?.name}
|
||||||
@@ -72,11 +71,11 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
}}
|
}}
|
||||||
ref={scrollAreaRef}
|
ref={scrollAreaRef}
|
||||||
>
|
>
|
||||||
{showBlurredImage && detailQuery.data?.imageUrl ? (
|
{showBlurredImage ? (
|
||||||
<LibraryBackgroundImage
|
<LibraryBackgroundImage
|
||||||
blur={artistBackgroundBlur}
|
blur={artistBackgroundBlur}
|
||||||
headerRef={headerRef}
|
headerRef={headerRef}
|
||||||
imageUrl={detailQuery.data.imageUrl}
|
imageUrl={detailQuery.data?.imageUrl || ''}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
|
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 340px;
|
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
|
|
||||||
:global(.item-image-placeholder) {
|
:global(.item-image-placeholder) {
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export const LibraryHeader = forwardRef(
|
|||||||
alt="cover"
|
alt="cover"
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
containerClassName={styles.image}
|
containerClassName={styles.image}
|
||||||
|
key={imageUrl}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
onError={onImageError}
|
onError={onImageError}
|
||||||
src={imageUrl || ''}
|
src={imageUrl || ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user