diff --git a/src/renderer/features/artists/components/album-artist-detail-content.module.css b/src/renderer/features/artists/components/album-artist-detail-content.module.css index 42db7c562..28002029f 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.module.css +++ b/src/renderer/features/artists/components/album-artist-detail-content.module.css @@ -1,12 +1,12 @@ .content-container { position: relative; z-index: 0; + container-type: inline-size; } .detail-container { display: flex; flex-direction: column; - gap: var(--theme-spacing-lg); + gap: var(--theme-spacing-2xl); padding: 1rem 2rem 5rem; - overflow: hidden; } diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index 396778a65..ada04c4fb 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { Suspense, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { createSearchParams, generatePath, Link, useParams } from 'react-router'; @@ -25,15 +25,253 @@ import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Stack } from '/@/shared/components/stack/stack'; import { TextTitle } from '/@/shared/components/text-title/text-title'; +import { Text } from '/@/shared/components/text/text'; import { AlbumArtist, AlbumListSort, LibraryItem, ServerType, SortOrder, + TopSongListResponse, } from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; +interface AlbumArtistActionButtonsProps { + albumCount: null | number | undefined; + artistDiscographyLink: string; + artistSongsLink: string; + onFavorite: () => void; + onMoreOptions: (e: React.MouseEvent) => void; + onPlay: () => void; + userFavorite?: boolean; +} + +const AlbumArtistActionButtons = ({ + albumCount, + artistDiscographyLink, + artistSongsLink, + onFavorite, + onMoreOptions, + onPlay, + userFavorite, +}: AlbumArtistActionButtonsProps) => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + + + + + + ); +}; + +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 ( + + + {t('entity.genre', { + count: genres.length, + })} + + + {genres.map((genre) => ( + + ))} + + + ); +}; + +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 ( +
+ + {t('page.albumArtistDetail.about', { + artist: artistName, + })} + + +
+ ); +}; + +interface AlbumArtistMetadataTopSongsProps { + routeId: string; + topSongsQuery: ReturnType>; +} + +const AlbumArtistMetadataTopSongs = ({ + routeId, + topSongsQuery, +}: AlbumArtistMetadataTopSongsProps) => { + const { t } = useTranslation(); + + if (!topSongsQuery?.data?.items?.length) return null; + + return ( +
+ + + + {t('page.albumArtistDetail.topSongs', { + postProcess: 'sentenceCase', + })} + + + + +
+ ); +}; + +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 ( + + + {t('common.externalLinks', { + postProcess: 'sentenceCase', + })} + + + {lastFM && ( + + )} + {mbzId && musicBrainz ? ( + + ) : null} + + + ); +}; + export const AlbumArtistDetailContent = () => { const { t } = useTranslation(); const { artistItems, externalLinks, lastFM, musicBrainz } = useGeneralSettings(); @@ -42,10 +280,9 @@ export const AlbumArtistDetailContent = () => { artistId?: string; }; const routeId = (artistId || albumArtistId) as string; - const { ref } = useContainerQuery(); + const { ref, ...cq } = useContainerQuery(); const { addToQueueByFetch, setFavorite } = usePlayer(); const server = useCurrentServer(); - const genrePath = useGenreRoute(); const [enabledItem, itemOrder] = useMemo(() => { const enabled: { [key in ArtistItem]?: boolean } = {}; @@ -59,7 +296,7 @@ export const AlbumArtistDetailContent = () => { return [enabled, order]; }, [artistItems]); - const detailQuery = useQuery( + const detailQuery = useSuspenseQuery( artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id, @@ -73,23 +310,23 @@ export const AlbumArtistDetailContent = () => { }, )}?${createSearchParams({ artistId: routeId, - artistName: detailQuery?.data?.name || '', + artistName: detailQuery.data?.name || '', })}`; const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, { albumArtistId: routeId, })}?${createSearchParams({ artistId: routeId, - artistName: detailQuery?.data?.name || '', + artistName: detailQuery.data?.name || '', })}`; const topSongsQuery = useQuery( artistsQueries.topSongs({ options: { - enabled: !!detailQuery?.data?.name && enabledItem.topSongs, + enabled: !!detailQuery.data?.name && enabledItem.topSongs, }, query: { - artist: detailQuery?.data?.name || '', + artist: detailQuery.data?.name || '', artistId: routeId, }, serverId: server?.id, @@ -106,6 +343,7 @@ export const AlbumArtistDetailContent = () => { artistIds: routeId ? [routeId] : undefined, compilation: false, }, + rowCount: 2, sortBy: AlbumListSort.RELEASE_DATE, sortOrder: SortOrder.DESC, title: ( @@ -136,6 +374,7 @@ export const AlbumArtistDetailContent = () => { artistIds: routeId ? [routeId] : undefined, compilation: true, }, + rowCount: 1, sortBy: AlbumListSort.RELEASE_DATE, sortOrder: SortOrder.DESC, title: ( @@ -146,10 +385,11 @@ export const AlbumArtistDetailContent = () => { uniqueId: 'compilationAlbums', }, { - data: (detailQuery?.data?.similarArtists || []) as AlbumArtist[], - isHidden: !detailQuery?.data?.similarArtists || !enabledItem.similarArtists, + data: (detailQuery.data?.similarArtists || []) as AlbumArtist[], + isHidden: !detailQuery.data?.similarArtists || !enabledItem.similarArtists, itemType: LibraryItem.ALBUM_ARTIST, order: itemOrder.similarArtists, + rowCount: 1, title: ( {t('page.albumArtistDetail.relatedArtists', { @@ -162,7 +402,7 @@ export const AlbumArtistDetailContent = () => { ]; }, [ artistDiscographyLink, - detailQuery?.data?.similarArtists, + detailQuery.data?.similarArtists, enabledItem.compilations, enabledItem.recentAlbums, enabledItem.similarArtists, @@ -187,7 +427,7 @@ export const AlbumArtistDetailContent = () => { }; const handleFavorite = () => { - if (!detailQuery?.data) return; + if (!detailQuery.data) return; setFavorite( detailQuery.data._serverId, [detailQuery.data.id], @@ -197,221 +437,105 @@ export const AlbumArtistDetailContent = () => { }; const handleMoreOptions = (e: React.MouseEvent) => { - if (!detailQuery?.data) return; + if (!detailQuery.data) return; ContextMenuController.call({ cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST }, event: e, }); }; - const albumCount = detailQuery?.data?.albumCount; + 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; - 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
; + // 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 (
- - handlePlay(playButtonBehavior)} - /> - - - - - - - - - - {showGenres ? ( -
- - {detailQuery?.data?.genres?.map((genre) => ( - - ))} - -
- ) : null} - {externalLinks && (lastFM || musicBrainz) ? ( -
- - {lastFM && ( - - )} - {mbzId && musicBrainz ? ( - - ) : null} - -
- ) : null} + handlePlay(playButtonBehavior)} + userFavorite={detailQuery.data?.userFavorite} + /> - {biography ? ( + {showGenres && ( + + + + )} + {externalLinks && (lastFM || musicBrainz) && ( + + + + )} + {biography && ( -
- - {t('page.albumArtistDetail.about', { - artist: detailQuery?.data?.name, - })} - - -
+
- ) : null} - {showTopSongs ? ( + )} + {Boolean(topSongsQuery?.data?.items?.length) && enabledItem.topSongs && ( -
- - - - {t('page.albumArtistDetail.topSongs', { - postProcess: 'sentenceCase', - })} - - - - -
+
- ) : null} - - {carousels - .filter((c) => !c.isHidden) - .map((carousel) => ( - -
- - {carousel.itemType === LibraryItem.ALBUM ? ( - 'query' in carousel && - carousel.query && - carousel.sortBy && - carousel.sortOrder ? ( - }> - - - ) : null - ) : carousel.itemType === LibraryItem.ALBUM_ARTIST ? ( - 'data' in carousel && carousel.data ? ( - - ) : null - ) : null} - -
-
- ))} + )} + {cq.height || cq.width + ? carousels + .filter((c) => !c.isHidden) + .map((carousel) => ( + + }> + {carousel.itemType === LibraryItem.ALBUM ? ( + 'query' in carousel && + carousel.query && + carousel.sortBy && + carousel.sortOrder ? ( + + ) : null + ) : carousel.itemType === LibraryItem.ALBUM_ARTIST ? ( + 'data' in carousel && carousel.data ? ( + + ) : null + ) : null} + + + )) + : null}
diff --git a/src/renderer/features/artists/routes/album-artist-detail-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-route.tsx index 38676ee19..d37c7e8ca 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-route.tsx @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { useRef } from 'react'; import { useLocation, useParams } from 'react-router'; @@ -33,11 +33,8 @@ const AlbumArtistDetailRoute = () => { const location = useLocation(); - const detailQuery = useQuery({ - ...artistsQueries.albumArtistDetail({ - query: { id: routeId }, - serverId: server?.id, - }), + const detailQuery = useSuspenseQuery({ + ...artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }), initialData: location.state?.item, staleTime: 0, }); @@ -49,6 +46,7 @@ const AlbumArtistDetailRoute = () => { }); const background = backgroundColor; + const showBlurredImage = Boolean(detailQuery.data?.imageUrl) && artistBackground; return ( @@ -61,6 +59,7 @@ const AlbumArtistDetailRoute = () => { {detailQuery?.data?.name} @@ -72,11 +71,11 @@ const AlbumArtistDetailRoute = () => { }} ref={scrollAreaRef} > - {showBlurredImage && detailQuery.data?.imageUrl ? ( + {showBlurredImage ? ( ) : ( diff --git a/src/renderer/features/shared/components/library-header.module.css b/src/renderer/features/shared/components/library-header.module.css index ef6cc4645..269b56cf4 100644 --- a/src/renderer/features/shared/components/library-header.module.css +++ b/src/renderer/features/shared/components/library-header.module.css @@ -10,7 +10,6 @@ width: 100%; max-width: 100%; height: auto; - min-height: 340px; padding: 2rem 1rem; :global(.item-image-placeholder) { diff --git a/src/renderer/features/shared/components/library-header.tsx b/src/renderer/features/shared/components/library-header.tsx index c25cdc97c..649542703 100644 --- a/src/renderer/features/shared/components/library-header.tsx +++ b/src/renderer/features/shared/components/library-header.tsx @@ -101,6 +101,7 @@ export const LibraryHeader = forwardRef( alt="cover" className={styles.image} containerClassName={styles.image} + key={imageUrl} loading="eager" onError={onImageError} src={imageUrl || ''}