diff --git a/src/renderer/components/grid-carousel/grid-carousel-v2.tsx b/src/renderer/components/grid-carousel/grid-carousel-v2.tsx index eba2cc639..338047982 100644 --- a/src/renderer/components/grid-carousel/grid-carousel-v2.tsx +++ b/src/renderer/components/grid-carousel/grid-carousel-v2.tsx @@ -6,10 +6,24 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import styles from './grid-carousel.module.css'; +import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card'; import { useContainerQuery } from '/@/renderer/hooks'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Group } from '/@/shared/components/group/group'; import { TextTitle } from '/@/shared/components/text-title/text-title'; +import { LibraryItem } from '/@/shared/types/domain-types'; + +export const useGridCarouselContainerQuery = () => { + return useContainerQuery({ + '2xl': 1280, + '3xl': 1440, + lg: 960, + md: 720, + sm: 520, + xl: 1152, + xs: 360, + }); +}; interface Card { content: ReactNode; @@ -18,12 +32,16 @@ interface Card { interface GridCarouselProps { cards: Card[]; + containerQuery?: ReturnType; enableRefresh?: boolean; hasNextPage?: boolean; + isFetchingNextPage?: boolean; loadNextPage?: () => void; onNextPage: (page: number) => void; onPrevPage: (page: number) => void; onRefresh?: () => void; + placeholderItemType?: LibraryItem; + placeholderRows?: DataRow[]; rowCount?: number; title?: ReactNode | string; } @@ -47,24 +65,22 @@ const pageVariants: Variants = { function BaseGridCarousel(props: GridCarouselProps) { const { cards, + containerQuery: providedContainerQuery, enableRefresh = false, hasNextPage, + isFetchingNextPage, loadNextPage, onNextPage, onPrevPage, onRefresh, + placeholderItemType, + placeholderRows, rowCount = 1, title, } = props; - const { ref, ...cq } = useContainerQuery({ - '2xl': 1280, - '3xl': 1440, - lg: 960, - md: 720, - sm: 520, - xl: 1152, - xs: 360, - }); + const defaultContainerQuery = useGridCarouselContainerQuery(); + const containerQuery = providedContainerQuery || defaultContainerQuery; + const { ref, ...cq } = containerQuery; const [currentPage, setCurrentPage] = useState({ isNext: false, @@ -97,11 +113,48 @@ function BaseGridCarousel(props: GridCarouselProps) { }); const visibleCards = useMemo(() => { - return cards.slice( - currentPage.page * cardsToShow * rowCount, - (currentPage.page + 1) * cardsToShow * rowCount, - ); - }, [cards, currentPage, cardsToShow, rowCount]); + const startIndex = currentPage.page * cardsToShow * rowCount; + const endIndex = (currentPage.page + 1) * cardsToShow * rowCount; + const slicedCards = cards.slice(startIndex, endIndex); + const expectedCardCount = cardsToShow * rowCount; + const missingCardCount = expectedCardCount - slicedCards.length; + + // Add placeholder cards during loading state + if ( + missingCardCount > 0 && + hasNextPage && + isFetchingNextPage && + placeholderItemType && + placeholderRows + ) { + const placeholderCards: Card[] = Array.from( + { length: missingCardCount }, + (_, index) => ({ + content: ( + + ), + id: `placeholder-${startIndex + slicedCards.length + index}`, + }), + ); + return [...slicedCards, ...placeholderCards]; + } + + return slicedCards; + }, [ + currentPage.page, + cardsToShow, + rowCount, + cards, + hasNextPage, + isFetchingNextPage, + placeholderItemType, + placeholderRows, + ]); const shouldLoadNextPage = visibleCards.length < cardsToShow * rowCount; @@ -249,6 +302,74 @@ export const GridCarousel = memo(BaseGridCarousel); GridCarousel.displayName = 'GridCarousel'; +interface GridCarouselSkeletonProps { + containerQuery?: ReturnType; + enableRefresh?: boolean; + placeholderItemType: LibraryItem; + placeholderRows: DataRow[]; + rowCount?: number; + title?: ReactNode | string; +} + +const GridCarouselSkeleton = (props: GridCarouselSkeletonProps) => { + const { + containerQuery: providedContainerQuery, + enableRefresh = false, + placeholderItemType, + placeholderRows, + rowCount = 1, + title, + } = props; + + const { ...cq } = providedContainerQuery; + + const cardsToShow = cq.isCalculated + ? getCardsToShow({ + isLargerThan2xl: cq.is2xl, + isLargerThan3xl: cq.is3xl, + isLargerThanLg: cq.isLg, + isLargerThanMd: cq.isMd, + isLargerThanSm: cq.isSm, + isLargerThanXl: cq.isXl, + }) + : 6; + + const placeholderCards = useMemo(() => { + const cardCount = cardsToShow * rowCount; + return Array.from({ length: cardCount }, (_, index) => ({ + content: ( + + ), + id: `skeleton-${index}`, + })); + }, [cardsToShow, rowCount, placeholderItemType, placeholderRows]); + + return ( + {}} + onPrevPage={() => {}} + placeholderItemType={placeholderItemType} + placeholderRows={placeholderRows} + rowCount={rowCount} + title={title} + /> + ); +}; + +export const GridCarouselSkeletonFallback = memo(GridCarouselSkeleton); + +GridCarouselSkeletonFallback.displayName = 'GridCarouselSkeletonFallback'; + function getCardsToShow(breakpoints: { isLargerThan2xl: boolean; isLargerThan3xl: boolean; diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index d5ef8c1c8..31f156571 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -1,10 +1,11 @@ -import { useQuery } from '@tanstack/react-query'; -import { ReactNode, Suspense, useMemo, useRef, useState } from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { ReactNode, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, useParams } from 'react-router'; import styles from './album-detail-content.module.css'; +import { useGridCarouselContainerQuery } from '/@/renderer/components/grid-carousel/grid-carousel-v2'; 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'; @@ -18,7 +19,6 @@ import { ListConfigMenu } from '/@/renderer/features/shared/components/list-conf import { ListSortByDropdownControlled } from '/@/renderer/features/shared/components/list-sort-by-dropdown'; import { ListSortOrderToggleButtonControlled } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; import { searchLibraryItems } from '/@/renderer/features/shared/utils'; -import { useContainerQuery } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, usePlayerSong } from '/@/renderer/store'; import { useExternalLinks, useSettingsStore } from '/@/renderer/store/settings.store'; @@ -31,7 +31,6 @@ import { Checkbox } from '/@/shared/components/checkbox/checkbox'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Pill, PillLink } from '/@/shared/components/pill/pill'; -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'; @@ -304,68 +303,14 @@ const AlbumMetadataExternalLinks = ({ }; export const AlbumDetailContent = () => { - const { t } = useTranslation(); const { albumId } = useParams() as { albumId: string }; const server = useCurrentServer(); - const detailQuery = useQuery( + const detailQuery = useSuspenseQuery( albumQueries.detail({ query: { id: albumId }, serverId: server.id }), ); - const { ref, ...cq } = useContainerQuery(); const { externalLinks, lastFM, musicBrainz } = useExternalLinks(); - const genreCarousels = useMemo(() => { - const genreLimit = 2; - const selectedGenres = detailQuery?.data?.genres?.slice(0, genreLimit); - - if (!selectedGenres || selectedGenres.length === 0) return []; - - return selectedGenres - .map((genre) => { - const uniqueId = `moreFromGenre-${genre.id}`; - return { - enableRefresh: true, - excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined, - isHidden: !genre, - query: { - genreIds: [genre.id], - }, - rowCount: 1, - sortBy: AlbumListSort.RANDOM, - sortOrder: SortOrder.ASC, - title: sentenceCase( - t('page.albumDetail.moreFromGeneric', { - item: genre.name, - }), - ), - uniqueId, - }; - }) - .filter((carousel) => !carousel.isHidden); - }, [detailQuery.data, t]); - - const carousels = useMemo(() => { - const moreFromArtistUniqueId = 'moreFromArtist'; - return [ - { - enableRefresh: false, - excludeIds: detailQuery?.data?.id ? [detailQuery.data.id] : undefined, - isHidden: !detailQuery?.data?.albumArtists?.[0]?.id, - query: { - artistIds: detailQuery?.data?.albumArtists.length - ? [detailQuery?.data?.albumArtists[0].id] - : undefined, - }, - rowCount: 1, - sortBy: AlbumListSort.YEAR, - sortOrder: SortOrder.DESC, - title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }), - uniqueId: moreFromArtistUniqueId, - }, - ...genreCarousels, - ]; - }, [detailQuery.data, genreCarousels, t]); - const comment = detailQuery?.data?.comment; const releaseYear = detailQuery?.data?.releaseYear; @@ -374,7 +319,7 @@ export const AlbumDetailContent = () => { const mbzId = detailQuery?.data?.mbzId; return ( -
+
{comment && ( @@ -388,7 +333,6 @@ export const AlbumDetailContent = () => { )}
- {/* */} { ))} )} - - {cq.height || cq.width ? ( - }> - {carousels - .filter((c) => !c.isHidden) - .map((carousel) => ( - - ))} - - ) : null} - +
); @@ -439,6 +364,82 @@ interface AlbumDetailSongsTableProps { songs: Song[]; } +function AlbumDetailCarousels({ data }: { data: Album }) { + const { t } = useTranslation(); + + const genreCarousels = useMemo(() => { + const genreLimit = 2; + const selectedGenres = data?.genres?.slice(0, genreLimit); + + if (!selectedGenres || selectedGenres.length === 0) return []; + + return selectedGenres + .map((genre) => { + const uniqueId = `moreFromGenre-${genre.id}`; + return { + enableRefresh: true, + excludeIds: data?.id ? [data.id] : undefined, + isHidden: !genre, + query: { + genreIds: [genre.id], + }, + rowCount: 1, + sortBy: AlbumListSort.RANDOM, + sortOrder: SortOrder.ASC, + title: sentenceCase( + t('page.albumDetail.moreFromGeneric', { + item: genre.name, + }), + ), + uniqueId, + }; + }) + .filter((carousel) => !carousel.isHidden); + }, [data, t]); + + const carousels = useMemo(() => { + const moreFromArtistUniqueId = 'moreFromArtist'; + return [ + { + enableRefresh: false, + excludeIds: data?.id ? [data.id] : undefined, + isHidden: !data?.albumArtists?.[0]?.id, + query: { + artistIds: data?.albumArtists.length ? [data?.albumArtists[0].id] : undefined, + }, + rowCount: 1, + sortBy: AlbumListSort.YEAR, + sortOrder: SortOrder.DESC, + title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }), + uniqueId: moreFromArtistUniqueId, + }, + ...genreCarousels, + ]; + }, [data.albumArtists, data.id, genreCarousels, t]); + + const cq = useGridCarouselContainerQuery(); + + return ( + + {carousels + .filter((c) => !c.isHidden) + .map((carousel) => ( + + ))} + + ); +} + const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); diff --git a/src/renderer/features/albums/components/album-infinite-carousel.tsx b/src/renderer/features/albums/components/album-infinite-carousel.tsx index fcc8f7841..70cba4dea 100644 --- a/src/renderer/features/albums/components/album-infinite-carousel.tsx +++ b/src/renderer/features/albums/components/album-infinite-carousel.tsx @@ -1,10 +1,14 @@ import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; +import { Suspense, useCallback, useMemo } from 'react'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2'; -import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card'; +import { + GridCarousel, + GridCarouselSkeletonFallback, + useGridCarouselContainerQuery, +} from '/@/renderer/components/grid-carousel/grid-carousel-v2'; +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 { useCurrentServerId } from '/@/renderer/store'; @@ -19,6 +23,7 @@ import { import { ItemListKey } from '/@/shared/types/types'; interface AlbumCarouselProps { + containerQuery?: ReturnType; enableRefresh?: boolean; excludeIds?: string[]; query?: Partial>; @@ -28,21 +33,23 @@ interface AlbumCarouselProps { title: React.ReactNode | string; } -const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => { +const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps & { rows: DataRow[] }) => { const { + containerQuery, enableRefresh, excludeIds, query: additionalQuery, rowCount = 1, + rows, sortBy, sortOrder, title, } = props; - const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM); const { data: albums, fetchNextPage, hasNextPage, + isFetchingNextPage, refetch, } = useAlbumListInfinite(sortBy, sortOrder, 20, additionalQuery); @@ -50,7 +57,7 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => { const cards = useMemo(() => { // Flatten all pages and filter excluded IDs - const allItems = albums.pages.flatMap((page: AlbumListResponse) => page.items); + const allItems = albums?.pages.flatMap((page: AlbumListResponse) => page.items) || []; const filteredItems = excludeIds ? allItems.filter((album) => !excludeIds.includes(album.id)) : allItems; @@ -69,7 +76,7 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => { ), id: album.id, })); - }, [albums.pages, controls, excludeIds, rows]); + }, [albums, controls, excludeIds, rows]); const handleNextPage = useCallback(() => {}, []); @@ -80,8 +87,8 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => { }, [refetch]); const firstPageItems = excludeIds - ? albums.pages[0]?.items.filter((album) => !excludeIds.includes(album.id)) || [] - : albums.pages[0]?.items || []; + ? albums?.pages[0]?.items.filter((album) => !excludeIds.includes(album.id)) || [] + : albums?.pages[0]?.items || []; if (firstPageItems.length === 0) { return null; @@ -90,12 +97,16 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => { return ( @@ -103,7 +114,22 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => { }; export const AlbumInfiniteCarousel = (props: AlbumCarouselProps) => { - return ; + const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM); + + return ( + + } + > + + + ); }; function useAlbumListInfinite( diff --git a/src/renderer/features/artists/components/album-artist-infinite-carousel.tsx b/src/renderer/features/artists/components/album-artist-infinite-carousel.tsx index 229abd939..e0c702e74 100644 --- a/src/renderer/features/artists/components/album-artist-infinite-carousel.tsx +++ b/src/renderer/features/artists/components/album-artist-infinite-carousel.tsx @@ -1,10 +1,14 @@ import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; +import { Suspense, useCallback, useMemo } from 'react'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2'; -import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card'; +import { + GridCarousel, + GridCarouselSkeletonFallback, + useGridCarouselContainerQuery, +} from '/@/renderer/components/grid-carousel/grid-carousel-v2'; +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 { useCurrentServerId } from '/@/renderer/store'; @@ -19,6 +23,7 @@ import { import { ItemListKey } from '/@/shared/types/types'; interface AlbumArtistCarouselProps { + containerQuery?: ReturnType; excludeIds?: string[]; query?: Partial>; rowCount?: number; @@ -27,13 +32,22 @@ interface AlbumArtistCarouselProps { title: React.ReactNode | string; } -export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) => { - const { excludeIds, query: additionalQuery, rowCount = 1, sortBy, sortOrder, title } = props; - const rows = useGridRows(LibraryItem.ALBUM_ARTIST, ItemListKey.ALBUM_ARTIST); +const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps & { rows: DataRow[] }) => { + const { + containerQuery, + excludeIds, + query: additionalQuery, + rowCount = 1, + rows, + sortBy, + sortOrder, + title, + } = props; const { data: albumArtists, fetchNextPage, hasNextPage, + isFetchingNextPage, refetch, } = useAlbumArtistListInfinite(sortBy, sortOrder, 20, additionalQuery); @@ -41,7 +55,8 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) const cards = useMemo(() => { // Flatten all pages and filter excluded IDs - const allItems = albumArtists.pages.flatMap((page: AlbumArtistListResponse) => page.items); + const allItems = + albumArtists?.pages.flatMap((page: AlbumArtistListResponse) => page.items) || []; const filteredItems = excludeIds ? allItems.filter((albumArtist) => !excludeIds.includes(albumArtist.id)) : allItems; @@ -60,7 +75,7 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) ), id: albumArtist.id, })); - }, [albumArtists.pages, controls, excludeIds, rows]); + }, [albumArtists, controls, excludeIds, rows]); const handleNextPage = useCallback(() => {}, []); @@ -71,10 +86,10 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) }, [refetch]); const firstPageItems = excludeIds - ? albumArtists.pages[0]?.items.filter( + ? albumArtists?.pages[0]?.items.filter( (albumArtist) => !excludeIds.includes(albumArtist.id), ) || [] - : albumArtists.pages[0]?.items || []; + : albumArtists?.pages[0]?.items || []; if (firstPageItems.length === 0) { return null; @@ -83,11 +98,15 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) return ( @@ -95,7 +114,22 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) }; export const AlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) => { - return ; + const rows = useGridRows(LibraryItem.ALBUM_ARTIST, ItemListKey.ALBUM_ARTIST); + + return ( + + } + > + + + ); }; function useAlbumArtistListInfinite( diff --git a/src/renderer/features/home/routes/home-route.tsx b/src/renderer/features/home/routes/home-route.tsx index ab0314698..d4b25eb20 100644 --- a/src/renderer/features/home/routes/home-route.tsx +++ b/src/renderer/features/home/routes/home-route.tsx @@ -1,6 +1,7 @@ import { Suspense, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { useGridCarouselContainerQuery } from '/@/renderer/components/grid-carousel/grid-carousel-v2'; import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area'; import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel'; import { AlbumInfiniteSingleFeatureCarousel } from '/@/renderer/features/home/components/album-infinite-single-feature-carousel'; @@ -35,6 +36,7 @@ const HomeRoute = () => { const { windowBarStyle } = useWindowSettings(); const homeFeature = useHomeFeature(); const homeItems = useHomeItems(); + const containerQuery = useGridCarouselContainerQuery(); const isJellyfin = server?.type === ServerType.JELLYFIN; @@ -112,6 +114,7 @@ const HomeRoute = () => { mb="5rem" pt={windowBarStyle === Platform.WEB ? '5rem' : '3rem'} px="2rem" + ref={containerQuery.ref} > {homeFeature && } {sortedItems.map((item) => { @@ -127,6 +130,7 @@ const HomeRoute = () => { if (carousel.itemType === LibraryItem.ALBUM) { return ( { if (carousel.itemType === LibraryItem.SONG) { return ( ; enableRefresh?: boolean; excludeIds?: string[]; query?: Partial>; @@ -30,21 +35,23 @@ interface SongCarouselProps { title: React.ReactNode | string; } -const BaseSongInfiniteCarousel = (props: SongCarouselProps) => { +const BaseSongInfiniteCarousel = (props: SongCarouselProps & { rows: DataRow[] }) => { const { + containerQuery, enableRefresh, excludeIds, query: additionalQuery, rowCount = 1, + rows, sortBy, sortOrder, title, } = props; - const rows = useGridRows(LibraryItem.SONG, ItemListKey.SONG); const { data: songs, fetchNextPage, hasNextPage, + isFetchingNextPage, refetch, } = useSongListInfinite(sortBy, sortOrder, 20, additionalQuery); @@ -66,7 +73,7 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => { const cards = useMemo(() => { // Flatten all pages and filter excluded IDs - const allItems = songs.pages.flatMap((page: SongListResponse) => page.items); + const allItems = songs?.pages.flatMap((page: SongListResponse) => page.items) || []; const filteredItems = excludeIds ? allItems.filter((song) => !excludeIds.includes(song.id)) : allItems; @@ -85,7 +92,7 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => { ), id: song.id, })); - }, [songs.pages, controls, excludeIds, rows]); + }, [songs, controls, excludeIds, rows]); const handleNextPage = useCallback(() => {}, []); @@ -96,8 +103,8 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => { }, [refetch]); const firstPageItems = excludeIds - ? songs.pages[0]?.items.filter((song) => !excludeIds.includes(song.id)) || [] - : songs.pages[0]?.items || []; + ? songs?.pages[0]?.items.filter((song) => !excludeIds.includes(song.id)) || [] + : songs?.pages[0]?.items || []; if (firstPageItems.length === 0) { return null; @@ -106,12 +113,16 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => { return ( @@ -119,7 +130,22 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => { }; export const SongInfiniteCarousel = (props: SongCarouselProps) => { - return ; + const rows = useGridRows(LibraryItem.SONG, ItemListKey.SONG); + + return ( + + } + > + + + ); }; function useSongListInfinite(