add new grid carousels

This commit is contained in:
jeffvli
2025-11-15 19:24:31 -08:00
parent 60cc564743
commit 2fc14ecd0e
18 changed files with 843 additions and 1130 deletions
+47 -193
View File
@@ -1,28 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import { Suspense, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel';
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { homeQueries } from '/@/renderer/features/home/api/home-api';
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { AppRoute } from '/@/renderer/router/routes';
import {
HomeItem,
useCurrentServer,
useGeneralSettings,
useWindowSettings,
} from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import {
AlbumListSort,
LibraryItem,
@@ -32,12 +26,6 @@ import {
} from '/@/shared/types/domain-types';
import { Platform } from '/@/shared/types/types';
const BASE_QUERY_ARGS = {
limit: 15,
sortOrder: SortOrder.DESC,
startIndex: 0,
};
const HomeRoute = () => {
const { t } = useTranslation();
const scrollAreaRef = useRef<HTMLDivElement>(null);
@@ -45,16 +33,9 @@ const HomeRoute = () => {
const { windowBarStyle } = useWindowSettings();
const { homeFeature, homeItems } = useGeneralSettings();
const queriesEnabled = useMemo(() => {
return homeItems.reduce(
(previous: Record<HomeItem, boolean>, current) => ({
...previous,
[current.id]: !current.disabled,
}),
{} as Record<HomeItem, boolean>,
);
}, [homeItems]);
const isJellyfin = server?.type === ServerType.JELLYFIN;
// Only keep queries for FeatureCarousel and songs carousel (which still uses old carousel)
const feature = useQuery(
albumQueries.list({
options: {
@@ -72,83 +53,15 @@ const HomeRoute = () => {
}),
);
const isJellyfin = server?.type === ServerType.JELLYFIN;
const featureItemsWithImage = useMemo(() => {
return feature.data?.items?.filter((item) => item.imageUrl) ?? [];
}, [feature.data?.items]);
const random = useQuery(
albumQueries.list({
options: {
staleTime: 1000 * 60 * 5,
},
query: {
...BASE_QUERY_ARGS,
sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId: server?.id,
}),
);
const recentlyPlayed = useQuery(
homeQueries.recentlyPlayed({
options: {
staleTime: 0,
},
query: {
...BASE_QUERY_ARGS,
sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: SortOrder.DESC,
startIndex: 0,
},
serverId: server?.id,
}),
);
const recentlyAdded = useQuery(
albumQueries.list({
options: {
staleTime: 1000 * 60 * 5,
},
query: {
...BASE_QUERY_ARGS,
sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC,
startIndex: 0,
},
serverId: server?.id,
}),
);
const mostPlayedAlbums = useQuery(
albumQueries.list({
options: {
enabled:
server?.type === ServerType.SUBSONIC || server?.type === ServerType.NAVIDROME,
staleTime: 1000 * 60 * 5,
},
query: {
...BASE_QUERY_ARGS,
sortBy: AlbumListSort.PLAY_COUNT,
sortOrder: SortOrder.DESC,
startIndex: 0,
},
serverId: server?.id,
}),
);
const mostPlayedSongs = useQuery(
songsQueries.list(
{
options: {
enabled: server?.type === ServerType.JELLYFIN,
enabled: isJellyfin,
staleTime: 1000 * 60 * 5,
},
query: {
...BASE_QUERY_ARGS,
limit: 15,
sortBy: SongListSort.PLAY_COUNT,
sortOrder: SortOrder.DESC,
startIndex: 0,
@@ -159,62 +72,42 @@ const HomeRoute = () => {
),
);
const recentlyReleased = useQuery(
albumQueries.list({
options: {
enabled: queriesEnabled[HomeItem.RECENTLY_RELEASED],
staleTime: 1000 * 60 * 5,
},
query: {
...BASE_QUERY_ARGS,
sortBy: AlbumListSort.RELEASE_DATE,
},
serverId: server?.id,
}),
);
const isLoading =
(random.isLoading && queriesEnabled[HomeItem.RANDOM]) ||
(recentlyPlayed.isLoading && queriesEnabled[HomeItem.RECENTLY_PLAYED] && !isJellyfin) ||
(recentlyAdded.isLoading && queriesEnabled[HomeItem.RECENTLY_ADDED]) ||
(recentlyReleased.isLoading && queriesEnabled[HomeItem.RECENTLY_RELEASED]) ||
(((isJellyfin && mostPlayedSongs.isLoading) ||
(!isJellyfin && mostPlayedAlbums.isLoading)) &&
queriesEnabled[HomeItem.MOST_PLAYED]);
if (isLoading) {
return <Spinner container />;
}
const featureItemsWithImage = useMemo(() => {
return feature.data?.items?.filter((item) => item.imageUrl) ?? [];
}, [feature.data?.items]);
// Carousel configuration - queries are now handled inside AlbumInfiniteCarousel
const carousels = {
[HomeItem.MOST_PLAYED]: {
data: isJellyfin ? mostPlayedSongs?.data?.items : mostPlayedAlbums?.data?.items,
data: mostPlayedSongs?.data?.items,
itemType: isJellyfin ? LibraryItem.SONG : LibraryItem.ALBUM,
query: isJellyfin ? mostPlayedSongs : mostPlayedAlbums,
query: mostPlayedSongs,
sortBy: AlbumListSort.PLAY_COUNT,
sortOrder: SortOrder.DESC,
title: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }),
},
[HomeItem.RANDOM]: {
data: random?.data?.items,
itemType: LibraryItem.ALBUM,
query: random,
sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC,
title: t('page.home.explore', { postProcess: 'sentenceCase' }),
},
[HomeItem.RECENTLY_ADDED]: {
data: recentlyAdded?.data?.items,
itemType: LibraryItem.ALBUM,
query: recentlyAdded,
sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC,
title: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }),
},
[HomeItem.RECENTLY_PLAYED]: {
data: recentlyPlayed?.data?.items,
itemType: LibraryItem.ALBUM,
query: recentlyPlayed,
sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: SortOrder.DESC,
title: t('page.home.recentlyPlayed', { postProcess: 'sentenceCase' }),
},
[HomeItem.RECENTLY_RELEASED]: {
data: recentlyReleased?.data?.items,
itemType: LibraryItem.ALBUM,
query: recentlyReleased,
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
title: t('page.home.recentlyReleased', { postProcess: 'sentenceCase' }),
},
};
@@ -257,70 +150,31 @@ const HomeRoute = () => {
px="2rem"
>
{homeFeature && <FeatureCarousel data={featureItemsWithImage} />}
{sortedCarousel.map((carousel) => (
<MemoizedSwiperGridCarousel
cardRows={[
{
property: 'name',
route: {
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
slugs: [
{
idProperty:
isJellyfin &&
carousel.itemType === LibraryItem.SONG
? 'albumId'
: 'id',
slugProperty: 'albumId',
},
],
},
},
{
arrayProperty: 'name',
property: 'albumArtists',
route: {
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
slugs: [
{
idProperty: 'id',
slugProperty: 'albumArtistId',
},
],
},
},
]}
data={carousel.data}
itemType={carousel.itemType}
key={`carousel-${carousel.uniqueId}`}
route={{
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
slugs: [
{
idProperty:
isJellyfin && carousel.itemType === LibraryItem.SONG
? 'albumId'
: 'id',
slugProperty: 'albumId',
},
],
}}
title={{
label: (
<Group>
<TextTitle order={3}>{carousel.title}</TextTitle>
<ActionIcon
onClick={() => carousel.query.refetch()}
variant="transparent"
>
<Icon icon="refresh" />
</ActionIcon>
</Group>
),
}}
uniqueId={carousel.uniqueId}
/>
))}
{sortedCarousel.map((carousel) => {
if (carousel.itemType === LibraryItem.ALBUM) {
return (
<Suspense
fallback={<Spinner container />}
key={`carousel-${carousel.uniqueId}`}
>
<AlbumInfiniteCarousel
rowCount={1}
sortBy={carousel.sortBy}
sortOrder={carousel.sortOrder}
title={carousel.title}
/>
</Suspense>
);
}
// Songs carousel (only for Jellyfin most played) - keep using old carousel for now
if ('data' in carousel && 'query' in carousel) {
// TODO: Create SongInfiniteCarousel
return null;
}
return null;
})}
</Stack>
</NativeScrollArea>
</AnimatedPage>