mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
add song infinite carousel (#1464)
This commit is contained in:
@@ -364,6 +364,18 @@ export const queryKeys: Record<
|
||||
|
||||
return [serverId, 'songs', 'detail'] as const;
|
||||
},
|
||||
infiniteList: (serverId: string, query?: SongListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
return [serverId, 'songs', 'infiniteList', filter, pagination] as const;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
return [serverId, 'songs', 'infiniteList', filter] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'songs', 'infiniteList'] as const;
|
||||
},
|
||||
list: (serverId: string, query?: SongListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
@@ -383,14 +395,6 @@ export const queryKeys: Record<
|
||||
lyricsByRemoteId: (searchQuery: { remoteSongId: string; remoteSource: LyricSource }) => {
|
||||
return ['song', 'lyrics', 'remote', searchQuery] as const;
|
||||
},
|
||||
remoteLyrics: (serverId: string, query?: LyricsQuery) => {
|
||||
if (query) return [serverId, 'song', 'lyrics', 'remote', query] as const;
|
||||
return [serverId, 'song', 'lyrics', 'remote'] as const;
|
||||
},
|
||||
serverLyrics: (serverId: string, query?: LyricsQuery) => {
|
||||
if (query) return [serverId, 'song', 'lyrics', 'server', query] as const;
|
||||
return [serverId, 'song', 'lyrics', 'server'] as const;
|
||||
},
|
||||
lyricsSearch: (query?: LyricSearchQuery) => {
|
||||
if (query) return ['lyrics', 'search', query] as const;
|
||||
return ['lyrics', 'search'] as const;
|
||||
@@ -399,7 +403,15 @@ export const queryKeys: Record<
|
||||
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
|
||||
return [serverId, 'songs', 'randomSongList'] as const;
|
||||
},
|
||||
remoteLyrics: (serverId: string, query?: LyricsQuery) => {
|
||||
if (query) return [serverId, 'song', 'lyrics', 'remote', query] as const;
|
||||
return [serverId, 'song', 'lyrics', 'remote'] as const;
|
||||
},
|
||||
root: (serverId: string) => [serverId, 'songs'] as const,
|
||||
serverLyrics: (serverId: string, query?: LyricsQuery) => {
|
||||
if (query) return [serverId, 'song', 'lyrics', 'server', query] as const;
|
||||
return [serverId, 'song', 'lyrics', 'server'] as const;
|
||||
},
|
||||
similar: (serverId: string, query?: SimilarSongsQuery) => {
|
||||
if (query) return [serverId, 'song', 'similar', query] as const;
|
||||
return [serverId, 'song', 'similar'] as const;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AnimatedPage } from '/@/renderer/features/shared/components/animated-pa
|
||||
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||
import { SongInfiniteCarousel } from '/@/renderer/features/songs/components/song-infinite-carousel';
|
||||
import {
|
||||
HomeItem,
|
||||
useCurrentServer,
|
||||
@@ -17,7 +18,13 @@ import {
|
||||
} from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
ServerType,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Platform } from '/@/shared/types/types';
|
||||
|
||||
const HomeRoute = () => {
|
||||
@@ -29,11 +36,10 @@ const HomeRoute = () => {
|
||||
|
||||
const isJellyfin = server?.type === ServerType.JELLYFIN;
|
||||
|
||||
// Carousel configuration - queries are now handled inside AlbumInfiniteCarousel
|
||||
const carousels = {
|
||||
[HomeItem.MOST_PLAYED]: {
|
||||
itemType: isJellyfin ? LibraryItem.SONG : LibraryItem.ALBUM,
|
||||
sortBy: AlbumListSort.PLAY_COUNT,
|
||||
sortBy: isJellyfin ? SongListSort.PLAY_COUNT : AlbumListSort.PLAY_COUNT,
|
||||
sortOrder: SortOrder.DESC,
|
||||
title: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
@@ -122,16 +128,24 @@ const HomeRoute = () => {
|
||||
enableRefresh={carousel.enableRefresh}
|
||||
key={`carousel-${carousel.uniqueId}`}
|
||||
rowCount={1}
|
||||
sortBy={carousel.sortBy}
|
||||
sortBy={carousel.sortBy as AlbumListSort}
|
||||
sortOrder={carousel.sortOrder}
|
||||
title={carousel.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ('data' in carousel && 'query' in carousel) {
|
||||
// TODO: Create SongInfiniteCarousel
|
||||
return null;
|
||||
if (carousel.itemType === LibraryItem.SONG) {
|
||||
return (
|
||||
<SongInfiniteCarousel
|
||||
enableRefresh={carousel.enableRefresh}
|
||||
key={`carousel-${carousel.uniqueId}`}
|
||||
rowCount={1}
|
||||
sortBy={carousel.sortBy as SongListSort}
|
||||
sortOrder={carousel.sortOrder}
|
||||
title={carousel.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||
import { 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 { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||
import { DefaultItemControlProps } from '/@/renderer/components/item-list/types';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
import {
|
||||
LibraryItem,
|
||||
Song,
|
||||
SongListQuery,
|
||||
SongListResponse,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
|
||||
interface SongCarouselProps {
|
||||
enableRefresh?: boolean;
|
||||
excludeIds?: string[];
|
||||
query?: Partial<Omit<SongListQuery, 'startIndex'>>;
|
||||
rowCount?: number;
|
||||
sortBy: SongListSort;
|
||||
sortOrder: SortOrder;
|
||||
title: React.ReactNode | string;
|
||||
}
|
||||
|
||||
const BaseSongInfiniteCarousel = (props: SongCarouselProps) => {
|
||||
const {
|
||||
enableRefresh,
|
||||
excludeIds,
|
||||
query: additionalQuery,
|
||||
rowCount = 1,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
title,
|
||||
} = props;
|
||||
const rows = useGridRows(LibraryItem.SONG, ItemListKey.SONG);
|
||||
const {
|
||||
data: songs,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
refetch,
|
||||
} = useSongListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
||||
|
||||
const player = usePlayer();
|
||||
const baseControls = useDefaultItemListControls();
|
||||
|
||||
const controls = useMemo(() => {
|
||||
return {
|
||||
...baseControls,
|
||||
onPlay: ({ item, playType }: DefaultItemControlProps & { playType: Play }) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.addToQueueByData([item as Song], playType);
|
||||
},
|
||||
};
|
||||
}, [baseControls, player]);
|
||||
|
||||
const cards = useMemo(() => {
|
||||
// Flatten all pages and filter excluded IDs
|
||||
const allItems = songs.pages.flatMap((page: SongListResponse) => page.items);
|
||||
const filteredItems = excludeIds
|
||||
? allItems.filter((song) => !excludeIds.includes(song.id))
|
||||
: allItems;
|
||||
|
||||
return filteredItems.map((song: Song) => ({
|
||||
content: (
|
||||
<MemoizedItemCard
|
||||
controls={controls}
|
||||
data={song}
|
||||
enableDrag
|
||||
itemType={LibraryItem.SONG}
|
||||
rows={rows}
|
||||
type="poster"
|
||||
withControls
|
||||
/>
|
||||
),
|
||||
id: song.id,
|
||||
}));
|
||||
}, [songs.pages, controls, excludeIds, rows]);
|
||||
|
||||
const handleNextPage = useCallback(() => {}, []);
|
||||
|
||||
const handlePrevPage = useCallback(() => {}, []);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
const firstPageItems = excludeIds
|
||||
? songs.pages[0]?.items.filter((song) => !excludeIds.includes(song.id)) || []
|
||||
: songs.pages[0]?.items || [];
|
||||
|
||||
if (firstPageItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridCarousel
|
||||
cards={cards}
|
||||
enableRefresh={enableRefresh}
|
||||
hasNextPage={hasNextPage}
|
||||
loadNextPage={fetchNextPage}
|
||||
onNextPage={handleNextPage}
|
||||
onPrevPage={handlePrevPage}
|
||||
onRefresh={handleRefresh}
|
||||
rowCount={rowCount}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SongInfiniteCarousel = (props: SongCarouselProps) => {
|
||||
return <BaseSongInfiniteCarousel {...props} />;
|
||||
};
|
||||
|
||||
function useSongListInfinite(
|
||||
sortBy: SongListSort,
|
||||
sortOrder: SortOrder,
|
||||
itemLimit: number,
|
||||
additionalQuery?: Partial<Omit<SongListQuery, 'startIndex'>>,
|
||||
) {
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const query = useSuspenseInfiniteQuery<SongListResponse>({
|
||||
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
||||
if (lastPage.items.length < itemLimit) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nextPageParam = Number(lastPageParam) + itemLimit;
|
||||
|
||||
return String(nextPageParam);
|
||||
},
|
||||
initialPageParam: '0',
|
||||
queryFn: ({ pageParam, signal }) => {
|
||||
return api.controller.getSongList({
|
||||
apiClientProps: { serverId, signal },
|
||||
query: {
|
||||
limit: itemLimit,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
startIndex: Number(pageParam),
|
||||
...additionalQuery,
|
||||
},
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.songs.infiniteList(serverId, {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
...additionalQuery,
|
||||
}),
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
Reference in New Issue
Block a user