mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-13 20:10:07 +02:00
add new grid carousels
This commit is contained in:
@@ -1,75 +1,33 @@
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
|
||||
import { useSetState } from '@mantine/hooks';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import { Suspense, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath, useParams } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
import { generatePath, Link, useParams } from 'react-router';
|
||||
|
||||
import styles from './album-detail-content.module.css';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
||||
import {
|
||||
getColumnDefs,
|
||||
TableConfigDropdown,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components/virtual-table';
|
||||
import { FullWidthDiscCell } from '/@/renderer/components/virtual-table/cells/full-width-disc-cell';
|
||||
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import {
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
SONG_CONTEXT_MENU_ITEMS,
|
||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import {
|
||||
useHandleGeneralContextMenu,
|
||||
useHandleTableContextMenu,
|
||||
} from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
|
||||
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
||||
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||
import { useAppFocus, useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, usePlayerSong, usePlayerStatus } from '/@/renderer/store';
|
||||
import {
|
||||
PersistedTableColumn,
|
||||
useGeneralSettings,
|
||||
usePlayButtonBehavior,
|
||||
useSettingsStoreActions,
|
||||
useTableSettings,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import {
|
||||
AlbumListQuery,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
QueueSong,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
const isFullWidthRow = (node: RowNode) => {
|
||||
return node.id?.startsWith('disc-');
|
||||
};
|
||||
|
||||
interface AlbumDetailContentProps {
|
||||
background?: string;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentProps) => {
|
||||
export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { albumId } = useParams() as { albumId: string };
|
||||
const server = useCurrentServer();
|
||||
@@ -82,267 +40,59 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
);
|
||||
|
||||
const cq = useContainerQuery();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const tableConfig = useTableSettings('albumDetail');
|
||||
const { setTable } = useSettingsStoreActions();
|
||||
const status = usePlayerStatus();
|
||||
const isFocused = useAppFocus();
|
||||
const currentSong = usePlayerSong();
|
||||
const { externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
||||
const genreRoute = useGenreRoute();
|
||||
|
||||
const columnDefs = useMemo(
|
||||
() => getColumnDefs(tableConfig.columns, false, 'albumDetail'),
|
||||
[tableConfig.columns],
|
||||
);
|
||||
|
||||
const getRowHeight = useCallback(
|
||||
(params: RowHeightParams) => {
|
||||
if (isFullWidthRow(params.node)) {
|
||||
return 45;
|
||||
}
|
||||
|
||||
return tableConfig.rowHeight;
|
||||
},
|
||||
[tableConfig.rowHeight],
|
||||
);
|
||||
|
||||
const songsRowData = useMemo(() => {
|
||||
if (!detail?.songs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let discNumber = -1;
|
||||
let discSubtitle: null | string = null;
|
||||
|
||||
const rowData: (QueueSong | { id: string; name: string })[] = [];
|
||||
const discTranslated = t('common.disc', { postProcess: 'upperCase' });
|
||||
|
||||
for (const song of detail.songs) {
|
||||
if (song.discNumber !== discNumber || song.discSubtitle !== discSubtitle) {
|
||||
discNumber = song.discNumber;
|
||||
discSubtitle = song.discSubtitle;
|
||||
|
||||
let id = `disc-${discNumber}`;
|
||||
let name = `${discTranslated} ${discNumber}`;
|
||||
|
||||
if (discSubtitle) {
|
||||
id += `-${discSubtitle}`;
|
||||
name += `: ${discSubtitle}`;
|
||||
}
|
||||
|
||||
rowData.push({ id, name });
|
||||
}
|
||||
rowData.push(song);
|
||||
}
|
||||
|
||||
return rowData;
|
||||
}, [detail?.songs, t]);
|
||||
|
||||
const [pagination, setPagination] = useSetState({
|
||||
artist: 0,
|
||||
});
|
||||
|
||||
const handleNextPage = useCallback(
|
||||
(key: 'artist') => {
|
||||
setPagination({
|
||||
[key]: pagination[key as keyof typeof pagination] + 1,
|
||||
});
|
||||
},
|
||||
[pagination, setPagination],
|
||||
);
|
||||
|
||||
const handlePreviousPage = useCallback(
|
||||
(key: 'artist') => {
|
||||
setPagination({
|
||||
[key]: pagination[key as keyof typeof pagination] - 1,
|
||||
});
|
||||
},
|
||||
[pagination, setPagination],
|
||||
);
|
||||
|
||||
const artistQuery = useQuery(
|
||||
albumQueries.list({
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
ExcludeItemIds: detail?.id,
|
||||
const carousels = useMemo(
|
||||
() => [
|
||||
{
|
||||
excludeIds: detail?.id ? [detail.id] : undefined,
|
||||
isHidden: !detail?.albumArtists?.[0]?.id,
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
ExcludeItemIds: detail?.id,
|
||||
},
|
||||
},
|
||||
artistIds: detail?.albumArtists.length
|
||||
? [detail.albumArtists[0].id]
|
||||
: undefined,
|
||||
},
|
||||
artistIds: detail?.albumArtists.length ? [detail?.albumArtists[0].id] : undefined,
|
||||
limit: 15,
|
||||
sortBy: AlbumListSort.YEAR,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }),
|
||||
uniqueId: 'moreFromArtist',
|
||||
},
|
||||
serverId: server.id,
|
||||
}),
|
||||
{
|
||||
excludeIds: detail?.id ? [detail.id] : undefined,
|
||||
isHidden: !detailQuery?.data?.genres?.[0],
|
||||
query: {
|
||||
genres: detailQuery.data?.genres.length
|
||||
? [detailQuery.data.genres[0].id]
|
||||
: undefined,
|
||||
},
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.ASC,
|
||||
title: `${t('page.albumDetail.moreFromGeneric', {
|
||||
item: '',
|
||||
postProcess: 'sentenceCase',
|
||||
})} ${detailQuery?.data?.genres?.[0]?.name}`,
|
||||
uniqueId: 'relatedGenres',
|
||||
},
|
||||
],
|
||||
[detail?.id, detail?.albumArtists, detailQuery?.data?.genres, t],
|
||||
);
|
||||
|
||||
// const artistQuery = useAlbumList({
|
||||
// options: {
|
||||
// enabled: detail?.albumArtists[0]?.id !== undefined,
|
||||
// gcTime: 1000 * 60,
|
||||
// placeholderData: true,
|
||||
// },
|
||||
// query: {
|
||||
// _custom: {
|
||||
// jellyfin: {
|
||||
// ExcludeItemIds: detailQuery?.data?.id,
|
||||
// },
|
||||
// },
|
||||
// artistIds: detailQuery?.data?.albumArtists.length
|
||||
// ? [detailQuery?.data?.albumArtists[0].id]
|
||||
// : undefined,
|
||||
// limit: 15,
|
||||
// sortBy: AlbumListSort.YEAR,
|
||||
// sortOrder: SortOrder.DESC,
|
||||
// startIndex: 0,
|
||||
// },
|
||||
// serverId: server?.id,
|
||||
// });
|
||||
|
||||
const relatedAlbumGenresRequest: AlbumListQuery = {
|
||||
genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined,
|
||||
limit: 15,
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
};
|
||||
|
||||
const relatedAlbumGenresQuery = useQuery(
|
||||
albumQueries.list({
|
||||
options: {
|
||||
enabled: !!detailQuery?.data?.genres?.[0],
|
||||
gcTime: 1000 * 60,
|
||||
queryKey: queryKeys.albums.related(
|
||||
server?.id || '',
|
||||
albumId,
|
||||
relatedAlbumGenresRequest,
|
||||
),
|
||||
},
|
||||
query: relatedAlbumGenresRequest,
|
||||
serverId: server?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const carousels = [
|
||||
{
|
||||
data: artistQuery?.data?.items.filter((a) => a.id !== detailQuery?.data?.id),
|
||||
isHidden: !artistQuery?.data?.items.filter((a) => a.id !== detailQuery?.data?.id)
|
||||
.length,
|
||||
loading: artistQuery?.isLoading || artistQuery.isFetching,
|
||||
pagination: {
|
||||
handleNextPage: () => handleNextPage('artist'),
|
||||
handlePreviousPage: () => handlePreviousPage('artist'),
|
||||
hasPreviousPage: pagination.artist > 0,
|
||||
},
|
||||
title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }),
|
||||
uniqueId: 'mostPlayed',
|
||||
},
|
||||
{
|
||||
data: relatedAlbumGenresQuery?.data?.items.filter(
|
||||
(a) => a.id !== detailQuery?.data?.id,
|
||||
),
|
||||
isHidden: !relatedAlbumGenresQuery?.data?.items.filter(
|
||||
(a) => a.id !== detailQuery?.data?.id,
|
||||
).length,
|
||||
loading: relatedAlbumGenresQuery?.isLoading || relatedAlbumGenresQuery.isFetching,
|
||||
title: `${t('page.albumDetail.moreFromGeneric', {
|
||||
item: '',
|
||||
postProcess: 'sentenceCase',
|
||||
})} ${detailQuery?.data?.genres?.[0]?.name}`,
|
||||
uniqueId: 'relatedGenres',
|
||||
},
|
||||
];
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const handlePlay = async (playType?: Play) => {
|
||||
handlePlayQueueAdd?.({
|
||||
byData: detailQuery?.data?.songs,
|
||||
playType: playType || playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
const onCellContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
|
||||
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
||||
if (!e.data || e.node.isFullWidthCell()) return;
|
||||
|
||||
const rowData: QueueSong[] = [];
|
||||
e.api.forEachNode((node) => {
|
||||
if (!node.data || node.isFullWidthCell()) return;
|
||||
rowData.push(node.data);
|
||||
});
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byData: rowData,
|
||||
initialSongId: e.data.id,
|
||||
playType: playButtonBehavior,
|
||||
});
|
||||
};
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite({});
|
||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||
const handlePlay = async (playType?: Play) => {};
|
||||
|
||||
const handleFavorite = () => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
if (detailQuery.data.userFavorite) {
|
||||
deleteFavoriteMutation.mutate({
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
createFavoriteMutation.mutate({
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||
const comment = detailQuery?.data?.comment;
|
||||
|
||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
||||
LibraryItem.ALBUM,
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
);
|
||||
|
||||
const onColumnMoved = useCallback(() => {
|
||||
const { columnApi } = tableRef?.current || {};
|
||||
const columnsOrder = columnApi?.getAllGridColumns();
|
||||
|
||||
if (!columnsOrder) return;
|
||||
|
||||
const columnsInSettings = tableConfig.columns;
|
||||
const updatedColumns: PersistedTableColumn[] = [];
|
||||
for (const column of columnsOrder) {
|
||||
const columnInSettings = columnsInSettings.find(
|
||||
(c) => c.column === column.getColDef().colId,
|
||||
);
|
||||
|
||||
if (columnInSettings) {
|
||||
updatedColumns.push({
|
||||
...columnInSettings,
|
||||
...(!tableConfig.autoFit && {
|
||||
width: column.getActualWidth(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTable('albumDetail', { ...tableConfig, columns: updatedColumns });
|
||||
}, [setTable, tableConfig, tableRef]);
|
||||
|
||||
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
||||
|
||||
const mbzId = detailQuery?.data?.mbzId;
|
||||
|
||||
return (
|
||||
@@ -361,10 +111,6 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
? 'primary'
|
||||
: undefined,
|
||||
}}
|
||||
loading={
|
||||
createFavoriteMutation.isPending ||
|
||||
deleteFavoriteMutation.isPending
|
||||
}
|
||||
onClick={handleFavorite}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
@@ -373,29 +119,12 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
icon="ellipsisHorizontal"
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<Popover position="bottom-end">
|
||||
<Popover.Target>
|
||||
<ActionIcon
|
||||
icon="settings"
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
/>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<TableConfigDropdown type="albumDetail" />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Group>
|
||||
</section>
|
||||
{showGenres && (
|
||||
@@ -468,91 +197,26 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>
|
||||
</section>
|
||||
)}
|
||||
<div style={{ minHeight: '300px' }}>
|
||||
<VirtualTable
|
||||
autoFitColumns={tableConfig.autoFit}
|
||||
autoHeight
|
||||
columnDefs={columnDefs}
|
||||
context={{
|
||||
currentSong,
|
||||
isFocused,
|
||||
itemType: LibraryItem.SONG,
|
||||
onCellContextMenu,
|
||||
status,
|
||||
}}
|
||||
enableCellChangeFlash={false}
|
||||
fullWidthCellRenderer={FullWidthDiscCell}
|
||||
getRowHeight={getRowHeight}
|
||||
getRowId={(data) => data.data.id}
|
||||
isFullWidthRow={(data) => {
|
||||
return isFullWidthRow(data.rowNode) || false;
|
||||
}}
|
||||
isRowSelectable={(data) => {
|
||||
if (isFullWidthRow(data.data)) return false;
|
||||
return true;
|
||||
}}
|
||||
key={`table-${tableConfig.rowHeight}`}
|
||||
onCellContextMenu={onCellContextMenu}
|
||||
onColumnMoved={onColumnMoved}
|
||||
onRowDoubleClicked={handleRowDoubleClick}
|
||||
ref={tableRef}
|
||||
rowClassRules={rowClassRules}
|
||||
rowData={songsRowData}
|
||||
rowSelection="multiple"
|
||||
shouldUpdateSong
|
||||
stickyHeader
|
||||
suppressCellFocus
|
||||
suppressLoadingOverlay
|
||||
suppressRowDrag
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Stack gap="lg" mt="3rem" ref={cq.ref}>
|
||||
{cq.height || cq.width ? (
|
||||
<>
|
||||
{carousels
|
||||
.filter((c) => !c.isHidden)
|
||||
.map((carousel, index) => (
|
||||
<MemoizedSwiperGridCarousel
|
||||
cardRows={[
|
||||
{
|
||||
property: 'name',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [
|
||||
{
|
||||
idProperty: 'id',
|
||||
slugProperty: 'albumId',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [
|
||||
{
|
||||
idProperty: 'id',
|
||||
slugProperty: 'albumArtistId',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]}
|
||||
data={carousel.data}
|
||||
isLoading={carousel.loading}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
key={`carousel-${carousel.uniqueId}-${index}`}
|
||||
route={{
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
}}
|
||||
title={{
|
||||
label: carousel.title,
|
||||
}}
|
||||
uniqueId={carousel.uniqueId}
|
||||
/>
|
||||
.map((carousel) => (
|
||||
<Suspense
|
||||
fallback={<Spinner container />}
|
||||
key={`carousel-${carousel.uniqueId}`}
|
||||
>
|
||||
<AlbumInfiniteCarousel
|
||||
excludeIds={carousel.excludeIds}
|
||||
query={carousel.query}
|
||||
rowCount={1}
|
||||
sortBy={carousel.sortBy}
|
||||
sortOrder={carousel.sortOrder}
|
||||
title={carousel.title}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 { Album, LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface AlbumGridCarouselProps {
|
||||
data: Album[];
|
||||
excludeIds?: string[];
|
||||
rowCount?: number;
|
||||
title: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export function AlbumGridCarousel(props: AlbumGridCarouselProps) {
|
||||
const { data, excludeIds, rowCount = 1, title } = props;
|
||||
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
||||
const controls = useDefaultItemListControls();
|
||||
|
||||
const cards = useMemo(() => {
|
||||
// Filter out excluded IDs if provided
|
||||
const filteredItems = excludeIds
|
||||
? data.filter((album) => !excludeIds.includes(album.id))
|
||||
: data;
|
||||
|
||||
return filteredItems.map((album: Album) => ({
|
||||
content: (
|
||||
<MemoizedItemCard
|
||||
controls={controls}
|
||||
data={album}
|
||||
enableDrag
|
||||
itemType={LibraryItem.ALBUM}
|
||||
rows={rows}
|
||||
type="poster"
|
||||
withControls
|
||||
/>
|
||||
),
|
||||
id: album.id,
|
||||
}));
|
||||
}, [data, excludeIds, controls, rows]);
|
||||
|
||||
const handleNextPage = () => {};
|
||||
const handlePrevPage = () => {};
|
||||
|
||||
if (cards.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridCarousel
|
||||
cards={cards}
|
||||
onNextPage={handleNextPage}
|
||||
onPrevPage={handlePrevPage}
|
||||
rowCount={rowCount}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
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 { useCurrentServerId } from '/@/renderer/store';
|
||||
import {
|
||||
Album,
|
||||
AlbumListQuery,
|
||||
AlbumListResponse,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface AlbumCarouselProps {
|
||||
excludeIds?: string[];
|
||||
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
|
||||
rowCount?: number;
|
||||
sortBy: AlbumListSort;
|
||||
sortOrder: SortOrder;
|
||||
title: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export function AlbumInfiniteCarousel(props: AlbumCarouselProps) {
|
||||
const { excludeIds, query: additionalQuery, rowCount = 1, sortBy, sortOrder, title } = props;
|
||||
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
||||
const {
|
||||
data: albums,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useAlbumListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
||||
|
||||
const controls = useDefaultItemListControls();
|
||||
|
||||
const cards = useMemo(() => {
|
||||
// Flatten all pages and filter excluded IDs
|
||||
const allItems = albums.pages.flatMap((page: AlbumListResponse) => page.items);
|
||||
const filteredItems = excludeIds
|
||||
? allItems.filter((album) => !excludeIds.includes(album.id))
|
||||
: allItems;
|
||||
|
||||
return filteredItems.map((album: Album) => ({
|
||||
content: (
|
||||
<MemoizedItemCard
|
||||
controls={controls}
|
||||
data={album}
|
||||
enableDrag
|
||||
itemType={LibraryItem.ALBUM}
|
||||
rows={rows}
|
||||
type="poster"
|
||||
withControls
|
||||
/>
|
||||
),
|
||||
id: album.id,
|
||||
}));
|
||||
}, [albums.pages, controls, excludeIds, rows]);
|
||||
|
||||
const handleNextPage = useCallback(() => {}, []);
|
||||
|
||||
const handlePrevPage = useCallback(() => {}, []);
|
||||
|
||||
const firstPageItems = excludeIds
|
||||
? albums.pages[0]?.items.filter((album) => !excludeIds.includes(album.id)) || []
|
||||
: albums.pages[0]?.items || [];
|
||||
|
||||
if (firstPageItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridCarousel
|
||||
cards={cards}
|
||||
hasNextPage={hasNextPage}
|
||||
loadNextPage={fetchNextPage}
|
||||
onNextPage={handleNextPage}
|
||||
onPrevPage={handlePrevPage}
|
||||
rowCount={rowCount}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useAlbumListInfinite(
|
||||
sortBy: AlbumListSort,
|
||||
sortOrder: SortOrder,
|
||||
itemLimit: number,
|
||||
additionalQuery?: Partial<Omit<AlbumListQuery, 'startIndex'>>,
|
||||
) {
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const query = useSuspenseInfiniteQuery<AlbumListResponse>({
|
||||
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.getAlbumList({
|
||||
apiClientProps: { serverId, signal },
|
||||
query: {
|
||||
limit: itemLimit,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
startIndex: Number(pageParam),
|
||||
...additionalQuery,
|
||||
},
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albums.list(serverId, {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
...additionalQuery,
|
||||
}),
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import styles from './dummy-album-detail-route.module.css';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { SONG_ALBUM_PAGE } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||
import { LibraryHeader } from '/@/renderer/features/shared/components/library-header';
|
||||
@@ -97,8 +95,6 @@ const DummyAlbumDetailRoute = () => {
|
||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||
const comment = detailQuery?.data?.comment;
|
||||
|
||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(LibraryItem.SONG, SONG_ALBUM_PAGE);
|
||||
|
||||
const handlePlay = () => {
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
@@ -190,7 +186,6 @@ const DummyAlbumDetailRoute = () => {
|
||||
icon="ellipsisHorizontal"
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
variant="subtle"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user