import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { Suspense, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { createSearchParams, generatePath, Link, useParams } from 'react-router'; import styles from './album-artist-detail-content.module.css'; import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns'; import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { ItemControls } from '/@/renderer/components/item-list/types'; import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { searchLibraryItems } from '/@/renderer/features/shared/utils'; import { useContainerQuery } from '/@/renderer/hooks'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; import { AppRoute } from '/@/renderer/router/routes'; import { ArtistItem, useCurrentServer, usePlayerSong } from '/@/renderer/store'; import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store'; import { sanitize } from '/@/renderer/utils/sanitize'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Button } from '/@/shared/components/button/button'; import { Grid } from '/@/shared/components/grid/grid'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Stack } from '/@/shared/components/stack/stack'; import { TextInput } from '/@/shared/components/text-input/text-input'; import { TextTitle } from '/@/shared/components/text-title/text-title'; import { Text } from '/@/shared/components/text/text'; import { AlbumArtist, AlbumListSort, LibraryItem, ServerType, Song, SortOrder, TopSongListResponse, } from '/@/shared/types/domain-types'; import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types'; interface AlbumArtistActionButtonsProps { artistDiscographyLink: string; artistSongsLink: string; } const AlbumArtistActionButtons = ({ artistDiscographyLink, artistSongsLink, }: 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(); const [searchTerm, setSearchTerm] = useState(''); const [showAll, setShowAll] = useState(false); const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table); const currentSong = usePlayerSong(); const player = usePlayer(); const songs = useMemo(() => topSongsQuery?.data?.items || [], [topSongsQuery?.data?.items]); const columns = useMemo(() => { return tableConfig?.columns || []; }, [tableConfig?.columns]); const filteredSongs = useMemo(() => { const filtered = searchLibraryItems(songs, searchTerm, LibraryItem.SONG); // When searching, show all results. Otherwise, limit to 5 if not showing all if (searchTerm.trim() || showAll) { return filtered; } return filtered.slice(0, 5); }, [songs, searchTerm, showAll]); const { handleColumnReordered } = useItemListColumnReorder({ itemListKey: ItemListKey.SONG, }); const { handleColumnResized } = useItemListColumnResize({ itemListKey: ItemListKey.SONG, }); const overrideControls: Partial = useMemo(() => { return { onDoubleClick: ({ index, internalState, item, meta }) => { if (!item) { return; } const playType = (meta?.playType as Play) || Play.NOW; const items = internalState?.getData() as Song[]; if (index !== undefined) { player.addToQueueByData(items, playType, item.id); } }, }; }, [player]); if (!topSongsQuery?.data?.items?.length) return null; if (!tableConfig || columns.length === 0) { return (
{t('page.albumArtistDetail.topSongs', { postProcess: 'sentenceCase', })}
); } const currentSongId = currentSong?.id; return (
{t('page.albumArtistDetail.topSongs', { postProcess: 'sentenceCase', })} } onChange={(e) => setSearchTerm(e.target.value)} placeholder={t('common.search', { postProcess: 'sentenceCase' })} radius="xl" rightSection={ searchTerm ? ( setSearchTerm('')} size="sm" variant="transparent" /> ) : null } styles={{ input: { background: 'transparent', border: '1px solid rgba(255, 255, 255, 0.05)', }, }} value={searchTerm} /> {!searchTerm.trim() && songs.length > 5 && !showAll && ( )}
); }; 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(); const { albumArtistId, artistId } = useParams() as { albumArtistId?: string; artistId?: string; }; const routeId = (artistId || albumArtistId) as string; const { ref, ...cq } = useContainerQuery(); 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: ( {t('page.albumArtistDetail.recentReleases', { postProcess: 'sentenceCase', })} ), uniqueId: 'recentReleases', }, { 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: ( {t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })} ), uniqueId: 'compilationAlbums', }, { 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', { postProcess: 'sentenceCase', })} ), uniqueId: 'similarArtists', }, ]; }, [ detailQuery.data?.similarArtists, enabledItem.compilations, enabledItem.recentAlbums, enabledItem.similarArtists, itemOrder.compilations, itemOrder.recentAlbums, itemOrder.similarArtists, routeId, server?.type, t, ]); 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 (
{showGenres && ( )} {externalLinks && (lastFM || musicBrainz) && ( )} {biography && ( )} {Boolean(topSongsQuery?.data?.items?.length) && enabledItem.topSongs && ( )} {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}
); };