mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add loading placeholder cards to grid carousel
This commit is contained in:
@@ -6,10 +6,24 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import styles from './grid-carousel.module.css';
|
import styles from './grid-carousel.module.css';
|
||||||
|
|
||||||
|
import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
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 {
|
interface Card {
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
@@ -18,12 +32,16 @@ interface Card {
|
|||||||
|
|
||||||
interface GridCarouselProps {
|
interface GridCarouselProps {
|
||||||
cards: Card[];
|
cards: Card[];
|
||||||
|
containerQuery?: ReturnType<typeof useGridCarouselContainerQuery>;
|
||||||
enableRefresh?: boolean;
|
enableRefresh?: boolean;
|
||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
|
isFetchingNextPage?: boolean;
|
||||||
loadNextPage?: () => void;
|
loadNextPage?: () => void;
|
||||||
onNextPage: (page: number) => void;
|
onNextPage: (page: number) => void;
|
||||||
onPrevPage: (page: number) => void;
|
onPrevPage: (page: number) => void;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
|
placeholderItemType?: LibraryItem;
|
||||||
|
placeholderRows?: DataRow[];
|
||||||
rowCount?: number;
|
rowCount?: number;
|
||||||
title?: ReactNode | string;
|
title?: ReactNode | string;
|
||||||
}
|
}
|
||||||
@@ -47,24 +65,22 @@ const pageVariants: Variants = {
|
|||||||
function BaseGridCarousel(props: GridCarouselProps) {
|
function BaseGridCarousel(props: GridCarouselProps) {
|
||||||
const {
|
const {
|
||||||
cards,
|
cards,
|
||||||
|
containerQuery: providedContainerQuery,
|
||||||
enableRefresh = false,
|
enableRefresh = false,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
loadNextPage,
|
loadNextPage,
|
||||||
onNextPage,
|
onNextPage,
|
||||||
onPrevPage,
|
onPrevPage,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
placeholderItemType,
|
||||||
|
placeholderRows,
|
||||||
rowCount = 1,
|
rowCount = 1,
|
||||||
title,
|
title,
|
||||||
} = props;
|
} = props;
|
||||||
const { ref, ...cq } = useContainerQuery({
|
const defaultContainerQuery = useGridCarouselContainerQuery();
|
||||||
'2xl': 1280,
|
const containerQuery = providedContainerQuery || defaultContainerQuery;
|
||||||
'3xl': 1440,
|
const { ref, ...cq } = containerQuery;
|
||||||
lg: 960,
|
|
||||||
md: 720,
|
|
||||||
sm: 520,
|
|
||||||
xl: 1152,
|
|
||||||
xs: 360,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState({
|
const [currentPage, setCurrentPage] = useState({
|
||||||
isNext: false,
|
isNext: false,
|
||||||
@@ -97,11 +113,48 @@ function BaseGridCarousel(props: GridCarouselProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const visibleCards = useMemo(() => {
|
const visibleCards = useMemo(() => {
|
||||||
return cards.slice(
|
const startIndex = currentPage.page * cardsToShow * rowCount;
|
||||||
currentPage.page * cardsToShow * rowCount,
|
const endIndex = (currentPage.page + 1) * cardsToShow * rowCount;
|
||||||
(currentPage.page + 1) * cardsToShow * rowCount,
|
const slicedCards = cards.slice(startIndex, endIndex);
|
||||||
);
|
const expectedCardCount = cardsToShow * rowCount;
|
||||||
}, [cards, currentPage, 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: (
|
||||||
|
<MemoizedItemCard
|
||||||
|
data={undefined}
|
||||||
|
itemType={placeholderItemType}
|
||||||
|
rows={placeholderRows}
|
||||||
|
type="poster"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
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;
|
const shouldLoadNextPage = visibleCards.length < cardsToShow * rowCount;
|
||||||
|
|
||||||
@@ -249,6 +302,74 @@ export const GridCarousel = memo(BaseGridCarousel);
|
|||||||
|
|
||||||
GridCarousel.displayName = 'GridCarousel';
|
GridCarousel.displayName = 'GridCarousel';
|
||||||
|
|
||||||
|
interface GridCarouselSkeletonProps {
|
||||||
|
containerQuery?: ReturnType<typeof useGridCarouselContainerQuery>;
|
||||||
|
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: (
|
||||||
|
<MemoizedItemCard
|
||||||
|
data={undefined}
|
||||||
|
itemType={placeholderItemType}
|
||||||
|
rows={placeholderRows}
|
||||||
|
type="poster"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: `skeleton-${index}`,
|
||||||
|
}));
|
||||||
|
}, [cardsToShow, rowCount, placeholderItemType, placeholderRows]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCarousel
|
||||||
|
cards={placeholderCards}
|
||||||
|
containerQuery={providedContainerQuery}
|
||||||
|
enableRefresh={enableRefresh}
|
||||||
|
hasNextPage={false}
|
||||||
|
isFetchingNextPage={false}
|
||||||
|
onNextPage={() => {}}
|
||||||
|
onPrevPage={() => {}}
|
||||||
|
placeholderItemType={placeholderItemType}
|
||||||
|
placeholderRows={placeholderRows}
|
||||||
|
rowCount={rowCount}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GridCarouselSkeletonFallback = memo(GridCarouselSkeleton);
|
||||||
|
|
||||||
|
GridCarouselSkeletonFallback.displayName = 'GridCarouselSkeletonFallback';
|
||||||
|
|
||||||
function getCardsToShow(breakpoints: {
|
function getCardsToShow(breakpoints: {
|
||||||
isLargerThan2xl: boolean;
|
isLargerThan2xl: boolean;
|
||||||
isLargerThan3xl: boolean;
|
isLargerThan3xl: boolean;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { ReactNode, Suspense, useMemo, useRef, useState } from 'react';
|
import { ReactNode, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath, useParams } from 'react-router';
|
import { generatePath, useParams } from 'react-router';
|
||||||
|
|
||||||
import styles from './album-detail-content.module.css';
|
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 { 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 { 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 { 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 { ListSortByDropdownControlled } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||||
import { ListSortOrderToggleButtonControlled } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
import { ListSortOrderToggleButtonControlled } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||||
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer, usePlayerSong } from '/@/renderer/store';
|
import { useCurrentServer, usePlayerSong } from '/@/renderer/store';
|
||||||
import { useExternalLinks, useSettingsStore } from '/@/renderer/store/settings.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 { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Pill, PillLink } from '/@/shared/components/pill/pill';
|
import { Pill, PillLink } from '/@/shared/components/pill/pill';
|
||||||
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 { TextInput } from '/@/shared/components/text-input/text-input';
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
@@ -304,68 +303,14 @@ const AlbumMetadataExternalLinks = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AlbumDetailContent = () => {
|
export const AlbumDetailContent = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { albumId } = useParams() as { albumId: string };
|
const { albumId } = useParams() as { albumId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const detailQuery = useQuery(
|
const detailQuery = useSuspenseQuery(
|
||||||
albumQueries.detail({ query: { id: albumId }, serverId: server.id }),
|
albumQueries.detail({ query: { id: albumId }, serverId: server.id }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { ref, ...cq } = useContainerQuery();
|
|
||||||
const { externalLinks, lastFM, musicBrainz } = useExternalLinks();
|
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 comment = detailQuery?.data?.comment;
|
||||||
|
|
||||||
const releaseYear = detailQuery?.data?.releaseYear;
|
const releaseYear = detailQuery?.data?.releaseYear;
|
||||||
@@ -374,7 +319,7 @@ export const AlbumDetailContent = () => {
|
|||||||
const mbzId = detailQuery?.data?.mbzId;
|
const mbzId = detailQuery?.data?.mbzId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.contentContainer} ref={ref}>
|
<div className={styles.contentContainer}>
|
||||||
<div className={styles.detailContainer}>
|
<div className={styles.detailContainer}>
|
||||||
{comment && (
|
{comment && (
|
||||||
<Spoiler maxHeight={75}>
|
<Spoiler maxHeight={75}>
|
||||||
@@ -388,7 +333,6 @@ export const AlbumDetailContent = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.metadataColumn}>
|
<div className={styles.metadataColumn}>
|
||||||
{/* <AlbumMetadataArtists artists={detailQuery?.data?.albumArtists} /> */}
|
|
||||||
<AlbumMetadataGenres genres={detailQuery?.data?.genres} />
|
<AlbumMetadataGenres genres={detailQuery?.data?.genres} />
|
||||||
<AlbumMetadataTags album={detailQuery?.data} />
|
<AlbumMetadataTags album={detailQuery?.data} />
|
||||||
<AlbumMetadataExternalLinks
|
<AlbumMetadataExternalLinks
|
||||||
@@ -410,26 +354,7 @@ export const AlbumDetailContent = () => {
|
|||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<Stack gap="lg" mt="3rem">
|
<AlbumDetailCarousels data={detailQuery?.data} />
|
||||||
{cq.height || cq.width ? (
|
|
||||||
<Suspense fallback={<Spinner container />}>
|
|
||||||
{carousels
|
|
||||||
.filter((c) => !c.isHidden)
|
|
||||||
.map((carousel) => (
|
|
||||||
<AlbumInfiniteCarousel
|
|
||||||
enableRefresh={carousel.enableRefresh}
|
|
||||||
excludeIds={carousel.excludeIds}
|
|
||||||
key={`carousel-${carousel.uniqueId}`}
|
|
||||||
query={carousel.query}
|
|
||||||
rowCount={carousel.rowCount}
|
|
||||||
sortBy={carousel.sortBy}
|
|
||||||
sortOrder={carousel.sortOrder}
|
|
||||||
title={carousel.title}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Suspense>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -439,6 +364,82 @@ interface AlbumDetailSongsTableProps {
|
|||||||
songs: Song[];
|
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 (
|
||||||
|
<Stack gap="lg" mt="3rem" ref={cq.ref}>
|
||||||
|
{carousels
|
||||||
|
.filter((c) => !c.isHidden)
|
||||||
|
.map((carousel) => (
|
||||||
|
<AlbumInfiniteCarousel
|
||||||
|
containerQuery={cq}
|
||||||
|
enableRefresh={carousel.enableRefresh}
|
||||||
|
excludeIds={carousel.excludeIds}
|
||||||
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
|
query={carousel.query}
|
||||||
|
rowCount={carousel.rowCount}
|
||||||
|
sortBy={carousel.sortBy}
|
||||||
|
sortOrder={carousel.sortOrder}
|
||||||
|
title={carousel.title}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { Suspense, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
import {
|
||||||
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
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 { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
import { useCurrentServerId } from '/@/renderer/store';
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
@@ -19,6 +23,7 @@ import {
|
|||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumCarouselProps {
|
interface AlbumCarouselProps {
|
||||||
|
containerQuery?: ReturnType<typeof useGridCarouselContainerQuery>;
|
||||||
enableRefresh?: boolean;
|
enableRefresh?: boolean;
|
||||||
excludeIds?: string[];
|
excludeIds?: string[];
|
||||||
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
|
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
|
||||||
@@ -28,21 +33,23 @@ interface AlbumCarouselProps {
|
|||||||
title: React.ReactNode | string;
|
title: React.ReactNode | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps & { rows: DataRow[] }) => {
|
||||||
const {
|
const {
|
||||||
|
containerQuery,
|
||||||
enableRefresh,
|
enableRefresh,
|
||||||
excludeIds,
|
excludeIds,
|
||||||
query: additionalQuery,
|
query: additionalQuery,
|
||||||
rowCount = 1,
|
rowCount = 1,
|
||||||
|
rows,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
title,
|
title,
|
||||||
} = props;
|
} = props;
|
||||||
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
|
||||||
const {
|
const {
|
||||||
data: albums,
|
data: albums,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
refetch,
|
refetch,
|
||||||
} = useAlbumListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
} = useAlbumListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
||||||
|
|
||||||
@@ -50,7 +57,7 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
|||||||
|
|
||||||
const cards = useMemo(() => {
|
const cards = useMemo(() => {
|
||||||
// Flatten all pages and filter excluded IDs
|
// 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
|
const filteredItems = excludeIds
|
||||||
? allItems.filter((album) => !excludeIds.includes(album.id))
|
? allItems.filter((album) => !excludeIds.includes(album.id))
|
||||||
: allItems;
|
: allItems;
|
||||||
@@ -69,7 +76,7 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
|||||||
),
|
),
|
||||||
id: album.id,
|
id: album.id,
|
||||||
}));
|
}));
|
||||||
}, [albums.pages, controls, excludeIds, rows]);
|
}, [albums, controls, excludeIds, rows]);
|
||||||
|
|
||||||
const handleNextPage = useCallback(() => {}, []);
|
const handleNextPage = useCallback(() => {}, []);
|
||||||
|
|
||||||
@@ -80,8 +87,8 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
|||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
const firstPageItems = excludeIds
|
const firstPageItems = excludeIds
|
||||||
? albums.pages[0]?.items.filter((album) => !excludeIds.includes(album.id)) || []
|
? albums?.pages[0]?.items.filter((album) => !excludeIds.includes(album.id)) || []
|
||||||
: albums.pages[0]?.items || [];
|
: albums?.pages[0]?.items || [];
|
||||||
|
|
||||||
if (firstPageItems.length === 0) {
|
if (firstPageItems.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -90,12 +97,16 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
|||||||
return (
|
return (
|
||||||
<GridCarousel
|
<GridCarousel
|
||||||
cards={cards}
|
cards={cards}
|
||||||
|
containerQuery={containerQuery}
|
||||||
enableRefresh={enableRefresh}
|
enableRefresh={enableRefresh}
|
||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
loadNextPage={fetchNextPage}
|
loadNextPage={fetchNextPage}
|
||||||
onNextPage={handleNextPage}
|
onNextPage={handleNextPage}
|
||||||
onPrevPage={handlePrevPage}
|
onPrevPage={handlePrevPage}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
placeholderItemType={LibraryItem.ALBUM}
|
||||||
|
placeholderRows={rows}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
@@ -103,7 +114,22 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
export const AlbumInfiniteCarousel = (props: AlbumCarouselProps) => {
|
||||||
return <BaseAlbumInfiniteCarousel {...props} />;
|
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<GridCarouselSkeletonFallback
|
||||||
|
containerQuery={props.containerQuery}
|
||||||
|
placeholderItemType={LibraryItem.ALBUM}
|
||||||
|
placeholderRows={rows}
|
||||||
|
title={props.title}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BaseAlbumInfiniteCarousel {...props} rows={rows} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function useAlbumListInfinite(
|
function useAlbumListInfinite(
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { Suspense, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
import {
|
||||||
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
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 { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
import { useCurrentServerId } from '/@/renderer/store';
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
@@ -19,6 +23,7 @@ import {
|
|||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistCarouselProps {
|
interface AlbumArtistCarouselProps {
|
||||||
|
containerQuery?: ReturnType<typeof useGridCarouselContainerQuery>;
|
||||||
excludeIds?: string[];
|
excludeIds?: string[];
|
||||||
query?: Partial<Omit<AlbumArtistListQuery, 'startIndex'>>;
|
query?: Partial<Omit<AlbumArtistListQuery, 'startIndex'>>;
|
||||||
rowCount?: number;
|
rowCount?: number;
|
||||||
@@ -27,13 +32,22 @@ interface AlbumArtistCarouselProps {
|
|||||||
title: React.ReactNode | string;
|
title: React.ReactNode | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) => {
|
const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps & { rows: DataRow[] }) => {
|
||||||
const { excludeIds, query: additionalQuery, rowCount = 1, sortBy, sortOrder, title } = props;
|
const {
|
||||||
const rows = useGridRows(LibraryItem.ALBUM_ARTIST, ItemListKey.ALBUM_ARTIST);
|
containerQuery,
|
||||||
|
excludeIds,
|
||||||
|
query: additionalQuery,
|
||||||
|
rowCount = 1,
|
||||||
|
rows,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
title,
|
||||||
|
} = props;
|
||||||
const {
|
const {
|
||||||
data: albumArtists,
|
data: albumArtists,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
refetch,
|
refetch,
|
||||||
} = useAlbumArtistListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
} = useAlbumArtistListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
||||||
|
|
||||||
@@ -41,7 +55,8 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps)
|
|||||||
|
|
||||||
const cards = useMemo(() => {
|
const cards = useMemo(() => {
|
||||||
// Flatten all pages and filter excluded IDs
|
// 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
|
const filteredItems = excludeIds
|
||||||
? allItems.filter((albumArtist) => !excludeIds.includes(albumArtist.id))
|
? allItems.filter((albumArtist) => !excludeIds.includes(albumArtist.id))
|
||||||
: allItems;
|
: allItems;
|
||||||
@@ -60,7 +75,7 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps)
|
|||||||
),
|
),
|
||||||
id: albumArtist.id,
|
id: albumArtist.id,
|
||||||
}));
|
}));
|
||||||
}, [albumArtists.pages, controls, excludeIds, rows]);
|
}, [albumArtists, controls, excludeIds, rows]);
|
||||||
|
|
||||||
const handleNextPage = useCallback(() => {}, []);
|
const handleNextPage = useCallback(() => {}, []);
|
||||||
|
|
||||||
@@ -71,10 +86,10 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps)
|
|||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
const firstPageItems = excludeIds
|
const firstPageItems = excludeIds
|
||||||
? albumArtists.pages[0]?.items.filter(
|
? albumArtists?.pages[0]?.items.filter(
|
||||||
(albumArtist) => !excludeIds.includes(albumArtist.id),
|
(albumArtist) => !excludeIds.includes(albumArtist.id),
|
||||||
) || []
|
) || []
|
||||||
: albumArtists.pages[0]?.items || [];
|
: albumArtists?.pages[0]?.items || [];
|
||||||
|
|
||||||
if (firstPageItems.length === 0) {
|
if (firstPageItems.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -83,11 +98,15 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps)
|
|||||||
return (
|
return (
|
||||||
<GridCarousel
|
<GridCarousel
|
||||||
cards={cards}
|
cards={cards}
|
||||||
|
containerQuery={containerQuery}
|
||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
loadNextPage={fetchNextPage}
|
loadNextPage={fetchNextPage}
|
||||||
onNextPage={handleNextPage}
|
onNextPage={handleNextPage}
|
||||||
onPrevPage={handlePrevPage}
|
onPrevPage={handlePrevPage}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
placeholderItemType={LibraryItem.ALBUM_ARTIST}
|
||||||
|
placeholderRows={rows}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
@@ -95,7 +114,22 @@ export const BaseAlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps)
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) => {
|
export const AlbumArtistInfiniteCarousel = (props: AlbumArtistCarouselProps) => {
|
||||||
return <BaseAlbumArtistInfiniteCarousel {...props} />;
|
const rows = useGridRows(LibraryItem.ALBUM_ARTIST, ItemListKey.ALBUM_ARTIST);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<GridCarouselSkeletonFallback
|
||||||
|
containerQuery={props.containerQuery}
|
||||||
|
placeholderItemType={LibraryItem.ALBUM_ARTIST}
|
||||||
|
placeholderRows={rows}
|
||||||
|
title={props.title}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BaseAlbumArtistInfiniteCarousel {...props} rows={rows} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function useAlbumArtistListInfinite(
|
function useAlbumArtistListInfinite(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Suspense, useRef } from 'react';
|
import { Suspense, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
||||||
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
|
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
|
||||||
import { AlbumInfiniteSingleFeatureCarousel } from '/@/renderer/features/home/components/album-infinite-single-feature-carousel';
|
import { AlbumInfiniteSingleFeatureCarousel } from '/@/renderer/features/home/components/album-infinite-single-feature-carousel';
|
||||||
@@ -35,6 +36,7 @@ const HomeRoute = () => {
|
|||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const homeFeature = useHomeFeature();
|
const homeFeature = useHomeFeature();
|
||||||
const homeItems = useHomeItems();
|
const homeItems = useHomeItems();
|
||||||
|
const containerQuery = useGridCarouselContainerQuery();
|
||||||
|
|
||||||
const isJellyfin = server?.type === ServerType.JELLYFIN;
|
const isJellyfin = server?.type === ServerType.JELLYFIN;
|
||||||
|
|
||||||
@@ -112,6 +114,7 @@ const HomeRoute = () => {
|
|||||||
mb="5rem"
|
mb="5rem"
|
||||||
pt={windowBarStyle === Platform.WEB ? '5rem' : '3rem'}
|
pt={windowBarStyle === Platform.WEB ? '5rem' : '3rem'}
|
||||||
px="2rem"
|
px="2rem"
|
||||||
|
ref={containerQuery.ref}
|
||||||
>
|
>
|
||||||
{homeFeature && <AlbumInfiniteSingleFeatureCarousel />}
|
{homeFeature && <AlbumInfiniteSingleFeatureCarousel />}
|
||||||
{sortedItems.map((item) => {
|
{sortedItems.map((item) => {
|
||||||
@@ -127,6 +130,7 @@ const HomeRoute = () => {
|
|||||||
if (carousel.itemType === LibraryItem.ALBUM) {
|
if (carousel.itemType === LibraryItem.ALBUM) {
|
||||||
return (
|
return (
|
||||||
<AlbumInfiniteCarousel
|
<AlbumInfiniteCarousel
|
||||||
|
containerQuery={containerQuery}
|
||||||
enableRefresh={carousel.enableRefresh}
|
enableRefresh={carousel.enableRefresh}
|
||||||
key={`carousel-${carousel.uniqueId}`}
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
rowCount={1}
|
rowCount={1}
|
||||||
@@ -140,6 +144,7 @@ const HomeRoute = () => {
|
|||||||
if (carousel.itemType === LibraryItem.SONG) {
|
if (carousel.itemType === LibraryItem.SONG) {
|
||||||
return (
|
return (
|
||||||
<SongInfiniteCarousel
|
<SongInfiniteCarousel
|
||||||
|
containerQuery={containerQuery}
|
||||||
enableRefresh={carousel.enableRefresh}
|
enableRefresh={carousel.enableRefresh}
|
||||||
key={`carousel-${carousel.uniqueId}`}
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
rowCount={1}
|
rowCount={1}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { Suspense, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
import {
|
||||||
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
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 { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
import { DefaultItemControlProps } from '/@/renderer/components/item-list/types';
|
import { DefaultItemControlProps } from '/@/renderer/components/item-list/types';
|
||||||
@@ -21,6 +25,7 @@ import {
|
|||||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface SongCarouselProps {
|
interface SongCarouselProps {
|
||||||
|
containerQuery?: ReturnType<typeof useGridCarouselContainerQuery>;
|
||||||
enableRefresh?: boolean;
|
enableRefresh?: boolean;
|
||||||
excludeIds?: string[];
|
excludeIds?: string[];
|
||||||
query?: Partial<Omit<SongListQuery, 'startIndex'>>;
|
query?: Partial<Omit<SongListQuery, 'startIndex'>>;
|
||||||
@@ -30,21 +35,23 @@ interface SongCarouselProps {
|
|||||||
title: React.ReactNode | string;
|
title: React.ReactNode | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseSongInfiniteCarousel = (props: SongCarouselProps) => {
|
const BaseSongInfiniteCarousel = (props: SongCarouselProps & { rows: DataRow[] }) => {
|
||||||
const {
|
const {
|
||||||
|
containerQuery,
|
||||||
enableRefresh,
|
enableRefresh,
|
||||||
excludeIds,
|
excludeIds,
|
||||||
query: additionalQuery,
|
query: additionalQuery,
|
||||||
rowCount = 1,
|
rowCount = 1,
|
||||||
|
rows,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
title,
|
title,
|
||||||
} = props;
|
} = props;
|
||||||
const rows = useGridRows(LibraryItem.SONG, ItemListKey.SONG);
|
|
||||||
const {
|
const {
|
||||||
data: songs,
|
data: songs,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
refetch,
|
refetch,
|
||||||
} = useSongListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
} = useSongListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
||||||
|
|
||||||
@@ -66,7 +73,7 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => {
|
|||||||
|
|
||||||
const cards = useMemo(() => {
|
const cards = useMemo(() => {
|
||||||
// Flatten all pages and filter excluded IDs
|
// 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
|
const filteredItems = excludeIds
|
||||||
? allItems.filter((song) => !excludeIds.includes(song.id))
|
? allItems.filter((song) => !excludeIds.includes(song.id))
|
||||||
: allItems;
|
: allItems;
|
||||||
@@ -85,7 +92,7 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => {
|
|||||||
),
|
),
|
||||||
id: song.id,
|
id: song.id,
|
||||||
}));
|
}));
|
||||||
}, [songs.pages, controls, excludeIds, rows]);
|
}, [songs, controls, excludeIds, rows]);
|
||||||
|
|
||||||
const handleNextPage = useCallback(() => {}, []);
|
const handleNextPage = useCallback(() => {}, []);
|
||||||
|
|
||||||
@@ -96,8 +103,8 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => {
|
|||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
const firstPageItems = excludeIds
|
const firstPageItems = excludeIds
|
||||||
? songs.pages[0]?.items.filter((song) => !excludeIds.includes(song.id)) || []
|
? songs?.pages[0]?.items.filter((song) => !excludeIds.includes(song.id)) || []
|
||||||
: songs.pages[0]?.items || [];
|
: songs?.pages[0]?.items || [];
|
||||||
|
|
||||||
if (firstPageItems.length === 0) {
|
if (firstPageItems.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -106,12 +113,16 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => {
|
|||||||
return (
|
return (
|
||||||
<GridCarousel
|
<GridCarousel
|
||||||
cards={cards}
|
cards={cards}
|
||||||
|
containerQuery={containerQuery}
|
||||||
enableRefresh={enableRefresh}
|
enableRefresh={enableRefresh}
|
||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
loadNextPage={fetchNextPage}
|
loadNextPage={fetchNextPage}
|
||||||
onNextPage={handleNextPage}
|
onNextPage={handleNextPage}
|
||||||
onPrevPage={handlePrevPage}
|
onPrevPage={handlePrevPage}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
placeholderItemType={LibraryItem.SONG}
|
||||||
|
placeholderRows={rows}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
@@ -119,7 +130,22 @@ const BaseSongInfiniteCarousel = (props: SongCarouselProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SongInfiniteCarousel = (props: SongCarouselProps) => {
|
export const SongInfiniteCarousel = (props: SongCarouselProps) => {
|
||||||
return <BaseSongInfiniteCarousel {...props} />;
|
const rows = useGridRows(LibraryItem.SONG, ItemListKey.SONG);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<GridCarouselSkeletonFallback
|
||||||
|
containerQuery={props.containerQuery}
|
||||||
|
placeholderItemType={LibraryItem.SONG}
|
||||||
|
placeholderRows={rows}
|
||||||
|
title={props.title}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BaseSongInfiniteCarousel {...props} rows={rows} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function useSongListInfinite(
|
function useSongListInfinite(
|
||||||
|
|||||||
Reference in New Issue
Block a user