import { useQuery, useQueryClient, useSuspenseQuery, UseSuspenseQueryResult, } from '@tanstack/react-query'; import { motion } from 'motion/react'; import { memo, Suspense, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { createSearchParams, generatePath, Link, useLocation, useParams } from 'react-router'; import styles from './album-artist-detail-content.module.css'; import { queryKeys } from '/@/renderer/api/query-keys'; import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows'; 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 { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel'; import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context'; import { ListConfigMenu, SONG_DISPLAY_TYPES, } from '/@/renderer/features/shared/components/list-config-menu'; import { CLIENT_SIDE_ALBUM_FILTERS, CLIENT_SIDE_SONG_FILTERS, ListSortByDropdownControlled, } from '/@/renderer/features/shared/components/list-sort-by-dropdown'; import { ListSortOrderToggleButtonControlled } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; import { LONG_PRESS_PLAY_BEHAVIOR, PlayTooltip, } from '/@/renderer/features/shared/components/play-button-group'; import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click'; import { searchLibraryItems } from '/@/renderer/features/shared/utils'; import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; import { useContainerQuery } from '/@/renderer/hooks'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; import { AppRoute } from '/@/renderer/router/routes'; import { ArtistItem, useAppStore, useCurrentServer, useCurrentServerId, usePlayerSong, } from '/@/renderer/store'; import { useArtistItems, useArtistRadioCount, useExternalLinks, useSettingsStore, } from '/@/renderer/store/settings.store'; import { sanitize } from '/@/renderer/utils/sanitize'; import { sortAlbumList, sortSongList } from '/@/shared/api/utils'; import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon'; import { Badge } from '/@/shared/components/badge/badge'; import { Button } from '/@/shared/components/button/button'; import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu'; import { Grid } from '/@/shared/components/grid/grid'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; import { Skeleton } from '/@/shared/components/skeleton/skeleton'; 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 { useDebouncedValue } from '/@/shared/hooks/use-debounced-value'; import { useHotkeys } from '/@/shared/hooks/use-hotkeys'; import { useLocalStorage } from '/@/shared/hooks/use-local-storage'; import { Album, AlbumArtist, AlbumArtistDetailResponse, AlbumListResponse, AlbumListSort, LibraryItem, RelatedArtist, ServerType, Song, SongListSort, SortOrder, } from '/@/shared/types/domain-types'; import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types'; interface AlbumArtistActionButtonsProps { artistDiscographyLink: string; artistSongsLink: string; onArtistRadio?: () => void; } const AlbumArtistActionButtons = ({ artistDiscographyLink, artistSongsLink, onArtistRadio, }: AlbumArtistActionButtonsProps) => { const { t } = useTranslation(); const isPlayerFetching = useIsPlayerFetching(); return ( <> {onArtistRadio && ( )} ); }; interface AlbumArtistMetadataGenresProps { genres?: Array<{ id: string; name: string }>; order?: number; } const AlbumArtistMetadataGenres = ({ genres, order }: 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; order?: number; routeId: string; } const AlbumArtistMetadataBiography = ({ artistName, order, routeId, }: AlbumArtistMetadataBiographyProps) => { const { t } = useTranslation(); const server = useCurrentServer(); const artistInfoQuery = useQuery({ ...artistsQueries.albumArtistInfo({ query: { id: routeId, limit: 10 }, serverId: server?.id, }), enabled: Boolean(server?.id && routeId), }); const detailQuery = useQuery({ ...artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id, }), enabled: Boolean(server?.id && routeId), }); const biography = artistInfoQuery.data?.biography || detailQuery.data?.biography; const isLoading = !biography && (artistInfoQuery.isLoading || detailQuery.isLoading); const sanitizedBiography = biography ? sanitize(biography) : ''; if (isLoading) { return (
{t('page.albumArtistDetail.about', { artist: artistName, })}
); } if (!biography) { return null; } return (
{t('page.albumArtistDetail.about', { artist: artistName, })}
); }; const TABLE_ROW_HEIGHT = { compact: 40, default: 64, large: 88, } as const; const TABLE_HEADER_HEIGHT = 40; interface SongTableListContainerProps { children: React.ReactNode; enableHeader?: boolean; itemCount: number; maxRows?: number; tableSize?: 'compact' | 'default' | 'large'; } function getTableRowHeight(size: 'compact' | 'default' | 'large' | undefined): number { return size ? TABLE_ROW_HEIGHT[size] : TABLE_ROW_HEIGHT.default; } const SongTableListContainer = ({ children, enableHeader = true, itemCount, maxRows = 5, tableSize = 'default', }: SongTableListContainerProps) => { const rowHeight = getTableRowHeight(tableSize); const headerOffset = enableHeader ? TABLE_HEADER_HEIGHT : 0; const height = headerOffset + rowHeight * Math.min(itemCount, maxRows); return
{children}
; }; interface AlbumArtistMetadataTopSongsProps { detailQuery: ReturnType>; order?: number; routeId: string; } const AlbumArtistMetadataTopSongsContent = ({ detailQuery, order, routeId, }: AlbumArtistMetadataTopSongsProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300); const [topSongsQueryType, setTopSongsQueryType] = useLocalStorage<'community' | 'personal'>({ defaultValue: 'community', key: 'album-artist-top-songs-query-type', }); const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table); const currentSong = usePlayerSong(); const player = usePlayer(); const serverId = useCurrentServerId(); const server = useCurrentServer(); const canStartQuery = server?.type === ServerType.JELLYFIN || !!detailQuery.data?.name; const topSongsQuery = useQuery({ ...artistsQueries.topSongs({ query: { artist: detailQuery.data?.name || '', artistId: routeId, type: topSongsQueryType, }, serverId: serverId, }), enabled: canStartQuery, }); const songs = useMemo(() => topSongsQuery.data?.items || [], [topSongsQuery.data?.items]); const columns = useMemo(() => { return tableConfig?.columns || []; }, [tableConfig?.columns]); const filteredSongs = useMemo(() => { return searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG); }, [songs, debouncedSearchTerm]); 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]); const handlePlay = useCallback( (playType: Play) => { if (songs.length === 0) return; player.addToQueueByData(songs, playType); }, [songs, player], ); const handlePlayNext = usePlayButtonClick({ onClick: () => handlePlay(Play.NEXT), onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]), }); const handlePlayNow = usePlayButtonClick({ onClick: () => handlePlay(Play.NOW), onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]), }); const handlePlayLast = usePlayButtonClick({ onClick: () => handlePlay(Play.LAST), onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]), }); const isLoading = topSongsQuery.isLoading || !topSongsQuery.data; if (!isLoading && !tableConfig) return null; if (!isLoading && songs.length === 0) return null; const currentSongId = currentSong?.id; return (
{t('page.albumArtistDetail.topSongs', { postProcess: 'sentenceCase', })} {!isLoading && {songs.length}}
{songs.length > 0 && ( )}
{isLoading ? ( ) : tableConfig ? ( <> } 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} /> setTopSongsQueryType(value as 'community' | 'personal') } size="xs" value={topSongsQueryType} /> ) : null}
); }; const AlbumArtistMetadataTopSongs = ({ detailQuery, order, routeId, }: AlbumArtistMetadataTopSongsProps) => { const server = useCurrentServer(); const location = useLocation(); const artistName = location.state?.item?.name || detailQuery.data?.name; const canStartQuery = server?.type === ServerType.JELLYFIN || !!artistName; return ( {canStartQuery ? ( ) : null} ); }; interface AlbumArtistMetadataFavoriteSongsProps { order?: number; routeId: string; } const AlbumArtistMetadataFavoriteSongs = ({ order, routeId, }: AlbumArtistMetadataFavoriteSongsProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300); const albumArtistDetailFavoriteSongsSort = useAppStore( (state) => state.albumArtistDetailFavoriteSongsSort, ); const setAlbumArtistDetailFavoriteSongsSort = useAppStore( (state) => state.actions.setAlbumArtistDetailFavoriteSongsSort, ); const sortBy = albumArtistDetailFavoriteSongsSort.sortBy; const sortOrder = albumArtistDetailFavoriteSongsSort.sortOrder; const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table); const currentSong = usePlayerSong(); const player = usePlayer(); const serverId = useCurrentServerId(); const favoriteSongsQuery = useQuery({ ...artistsQueries.favoriteSongs({ query: { artistId: routeId, }, serverId: serverId, }), }); const songs = useMemo( () => favoriteSongsQuery.data?.items || [], [favoriteSongsQuery.data?.items], ); const columns = useMemo(() => { return tableConfig?.columns || []; }, [tableConfig?.columns]); const filteredSongs = useMemo(() => { return sortSongList( searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG), sortBy, sortOrder, ); }, [songs, debouncedSearchTerm, sortBy, sortOrder]); 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]); const handlePlay = useCallback( (playType: Play) => { if (songs.length === 0) return; player.addToQueueByData(songs, playType); }, [songs, player], ); const handlePlayNext = usePlayButtonClick({ onClick: () => handlePlay(Play.NEXT), onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]), }); const handlePlayNow = usePlayButtonClick({ onClick: () => handlePlay(Play.NOW), onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]), }); const handlePlayLast = usePlayButtonClick({ onClick: () => handlePlay(Play.LAST), onLongPress: () => handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]), }); const isLoading = favoriteSongsQuery.isLoading || !favoriteSongsQuery.data; if (!isLoading && !tableConfig) return null; if (!isLoading && songs.length === 0) return null; const currentSongId = currentSong?.id; return (
{t('page.albumArtistDetail.favoriteSongs', { postProcess: 'sentenceCase', })} {!isLoading && {songs.length}}
{songs.length > 0 && ( )}
{isLoading ? ( ) : tableConfig ? ( <> } 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} /> setAlbumArtistDetailFavoriteSongsSort( value as SongListSort, sortOrder, ) } sortBy={sortBy} /> setAlbumArtistDetailFavoriteSongsSort( sortBy, value as SortOrder, ) } sortOrder={sortOrder} /> ) : null}
); }; interface AlbumArtistMetadataExternalLinksProps { artistName?: string; externalLinks: boolean; lastFM: boolean; listenBrainz: boolean; mbzId?: null | string; musicBrainz: boolean; nativeSpotify: boolean; order?: number; qobuz: boolean; spotify: boolean; } const getListenBrainzUrl = (mbzId: null | string, artistName?: string) => { if (mbzId) { return `https://listenbrainz.org/artist/${mbzId}`; } if (artistName) { return `https://listenbrainz.org/search/?search_term=${encodeURIComponent(artistName)}`; } return null; }; const getQobuzUrl = (artistName?: string) => { if (artistName) { return `https://www.qobuz.com/us-en/search/artists/${encodeURIComponent(artistName)}`; } return null; }; const AlbumArtistMetadataExternalLinks = ({ artistName, externalLinks, lastFM, listenBrainz, mbzId, musicBrainz, nativeSpotify, order, qobuz, spotify, }: AlbumArtistMetadataExternalLinksProps) => { const { t } = useTranslation(); const listenBrainzUrl = getListenBrainzUrl(mbzId || null, artistName); const qobuzUrl = getQobuzUrl(artistName); if (!externalLinks || (!lastFM && !listenBrainz && !musicBrainz && !qobuz && !spotify)) { return null; } return ( {t('common.externalLinks', { postProcess: 'sentenceCase', })} {lastFM && ( )} {mbzId && musicBrainz ? ( ) : null} {listenBrainz && listenBrainzUrl && ( )} {qobuz && qobuzUrl && ( )} {spotify && ( )} ); }; interface AlbumArtistMetadataSimilarArtistsProps { order?: number; routeId: string; } const AlbumArtistMetadataSimilarArtists = ({ order, routeId, }: AlbumArtistMetadataSimilarArtistsProps) => { const { t } = useTranslation(); const server = useCurrentServer(); const serverId = useCurrentServerId(); const artistInfoQuery = useQuery({ ...artistsQueries.albumArtistInfo({ query: { id: routeId, limit: 10 }, serverId: server?.id, }), enabled: Boolean(server?.id && routeId), }); const relatedArtists = artistInfoQuery.data?.similarArtists ?? null; const similarArtists = useMemo(() => { if (!relatedArtists || relatedArtists.length === 0) { return []; } return relatedArtists.map( (relatedArtist: RelatedArtist): AlbumArtist => ({ _itemType: LibraryItem.ALBUM_ARTIST, _serverId: serverId || '', _serverType: (server?.type as ServerType) || ServerType.JELLYFIN, albumCount: null, biography: null, duration: null, genres: [], id: relatedArtist.id, imageId: relatedArtist.imageId, imageUrl: relatedArtist.imageUrl, lastPlayedAt: null, mbz: null, name: relatedArtist.name, playCount: null, similarArtists: null, songCount: null, userFavorite: relatedArtist.userFavorite, userRating: relatedArtist.userRating, }), ); }, [relatedArtists, server?.type, serverId]); const carouselTitle = useMemo( () => (
{t('page.albumArtistDetail.relatedArtists', { postProcess: 'sentenceCase', })}
), [t], ); if (!artistInfoQuery.isLoading && similarArtists.length === 0) { return null; } return ( ); }; interface AlbumArtistDetailContentProps { albumsQuery: UseSuspenseQueryResult; detailQuery: UseSuspenseQueryResult; } export const AlbumArtistDetailContent = ({ albumsQuery, detailQuery, }: AlbumArtistDetailContentProps) => { const artistItems = useArtistItems(); const artistRadioCount = useArtistRadioCount(); const { externalLinks, lastFM, listenBrainz, musicBrainz, nativeSpotify, qobuz, spotify } = useExternalLinks(); const { albumArtistId, artistId } = useParams() as { albumArtistId?: string; artistId?: string; }; const routeId = (artistId || albumArtistId) as string; const server = useCurrentServer(); const { addToQueueByData } = usePlayer(); const queryClient = useQueryClient(); 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 artistDiscographyLink = useMemo( () => `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, { albumArtistId: routeId, })}?${createSearchParams({ artistId: routeId, artistName: detailQuery.data?.name || '', })}`, [routeId, detailQuery.data?.name], ); const artistSongsLink = useMemo( () => `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, { albumArtistId: routeId, })}?${createSearchParams({ artistId: routeId, artistName: detailQuery.data?.name || '', })}`, [routeId, detailQuery.data?.name], ); const mbzId = detailQuery.data?.mbz; const handleArtistRadio = useCallback(async () => { if (!server?.id || !routeId) return; try { const artistRadioSongs = await queryClient.fetchQuery({ ...songsQueries.artistRadio({ query: { artistId: routeId, count: artistRadioCount, }, serverId: server.id, }), queryKey: queryKeys.player.fetch({ artistId: routeId }), }); if (artistRadioSongs && artistRadioSongs.length > 0) { addToQueueByData(artistRadioSongs, Play.NOW); } } catch (error) { console.error('Failed to load artist radio:', error); } }, [addToQueueByData, artistRadioCount, queryClient, routeId, server.id]); // 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 (
{externalLinks && (lastFM || listenBrainz || musicBrainz || qobuz || spotify) && ( )} {enabledItem.biography && ( )} {enabledItem.similarArtists && ( )} {enabledItem.topSongs && ( )} {enabledItem.favoriteSongs && ( )}
); }; interface AlbumSectionProps { albums: Album[]; controls: ItemControls; enableExpansion?: boolean; itemsPerRow: number; releaseType: string; rows: DataRow[] | undefined; title: React.ReactNode | string; } const MAX_SECTION_CARDS = 100; const getItemsPerRow = (cq: ReturnType) => { // Match grid carousel breakpoints: is3xl: 8, is2xl: 7, isXl: 6, isLg: 5, isMd: 4, isSm: 3, default: 2 if (cq.is3xl) return 8; if (cq.is2xl) return 7; if (cq.isXl) return 6; if (cq.isLg) return 5; if (cq.isMd) return 4; if (cq.isSm) return 3; if (cq.isXs) return 2; return 2; }; const AlbumSection = memo(function AlbumSection({ albums, controls, enableExpansion, itemsPerRow, releaseType, rows, title, }: AlbumSectionProps) { const { t } = useTranslation(); const albumCount = albums.length; const [showAll, setShowAll] = useState(false); const player = usePlayer(); const serverId = useCurrentServerId(); const displayedAlbums = showAll ? albums : albums.slice(0, MAX_SECTION_CARDS); const hasMoreAlbums = albums.length > MAX_SECTION_CARDS; const handlePlay = useCallback( (playType: Play) => { if (albums.length === 0) return; const albumIds = albums.map((album) => album.id); player.addToQueueByFetch(serverId, albumIds, LibraryItem.ALBUM, playType); }, [albums, player, serverId], ); const handlePlayNext = usePlayButtonClick({ onClick: () => { handlePlay(Play.NEXT); }, onLongPress: () => { handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]); }, }); const handlePlayNow = usePlayButtonClick({ onClick: () => { handlePlay(Play.NOW); }, onLongPress: () => { handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]); }, }); const handlePlayLast = usePlayButtonClick({ onClick: () => { handlePlay(Play.LAST); }, onLongPress: () => { handlePlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]); }, }); const DisplayedAlbumsMemo = useMemo(() => { return displayedAlbums.map((album) => ( )); }, [controls, displayedAlbums, enableExpansion, releaseType, rows]); return (
{title} {albumCount}
{albumCount > 0 && ( )}
{DisplayedAlbumsMemo}
{hasMoreAlbums && !showAll && ( )} ); }); import { useArtistAlbumsGrouped } from '/@/renderer/features/artists/hooks/use-artist-albums-grouped'; interface ArtistAlbumsProps { albumsQuery: UseSuspenseQueryResult; order?: number; } const ArtistAlbums = ({ albumsQuery, order }: ArtistAlbumsProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300); const albumArtistDetailSort = useAppStore((state) => state.albumArtistDetailSort); const setAlbumArtistDetailSort = useAppStore((state) => state.actions.setAlbumArtistDetailSort); const sortBy = albumArtistDetailSort.sortBy; const sortOrder = albumArtistDetailSort.sortOrder; const { albumArtistId, artistId } = useParams() as { albumArtistId?: string; artistId?: string; }; const routeId = (artistId || albumArtistId) as string; const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM); const filteredAndSortedAlbums = useMemo(() => { const albums = albumsQuery.data?.items || []; const searched = searchLibraryItems(albums, debouncedSearchTerm, LibraryItem.ALBUM); return sortAlbumList(searched, sortBy, sortOrder); }, [albumsQuery.data?.items, debouncedSearchTerm, sortBy, sortOrder]); const controls = useDefaultItemListControls(); const { releaseTypeEntries } = useArtistAlbumsGrouped(filteredAndSortedAlbums, routeId); const cq = useContainerQuery({ '2xl': 1280, '3xl': 1440, lg: 960, md: 720, sm: 520, xl: 1152, xs: 360, }); const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch); const searchInputRef = useRef(null); useHotkeys([ [ binding.hotkey, () => { searchInputRef.current?.focus(); }, ], ]); const itemsPerRow = getItemsPerRow(cq); const ReleaseTypeEntriesMemo = useMemo(() => { return releaseTypeEntries.map(({ albums, displayName, releaseType }) => ( )); }, [releaseTypeEntries, itemsPerRow, controls, rows]); return ( } onChange={(e) => setSearchTerm(e.target.value)} placeholder={t('common.search', { postProcess: 'sentenceCase' })} radius="xl" ref={searchInputRef} rightSection={ searchTerm ? ( setSearchTerm('')} size="sm" variant="transparent" /> ) : null } styles={{ input: { background: 'transparent', border: '1px solid rgba(255, 255, 255, 0.05)', }, }} value={searchTerm} /> setAlbumArtistDetailSort(value as AlbumListSort, sortOrder) } sortBy={sortBy} /> setAlbumArtistDetailSort(sortBy, value as SortOrder) } sortOrder={sortOrder} /> {releaseTypeEntries.length > 0 && (
{cq.isCalculated && <>{ReleaseTypeEntriesMemo}}
)}
); }; function GroupingTypeSelector() { const { t } = useTranslation(); const groupingType = useAppStore((state) => state.albumArtistDetailSort.groupingType); const setAlbumArtistDetailGroupingType = useAppStore( (state) => state.actions.setAlbumArtistDetailGroupingType, ); return ( setAlbumArtistDetailGroupingType('all')} > {t('page.albumArtistDetail.groupingTypeAll', { postProcess: 'sentenceCase', })} setAlbumArtistDetailGroupingType('primary')} > {t('page.albumArtistDetail.groupingTypePrimary', { postProcess: 'sentenceCase', })} ); }