2023-06-02 01:01:50 -07:00
|
|
|
import { isValidElement, ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
|
|
|
|
import { Group, Stack } from '@mantine/core';
|
|
|
|
|
import throttle from 'lodash/throttle';
|
2022-12-19 15:59:14 -08:00
|
|
|
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
2023-06-02 01:01:50 -07:00
|
|
|
import styled from 'styled-components';
|
|
|
|
|
import { SwiperOptions, Virtual } from 'swiper';
|
|
|
|
|
import 'swiper/css';
|
2023-05-17 17:10:30 -07:00
|
|
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
|
|
|
import { Swiper as SwiperCore } from 'swiper/types';
|
|
|
|
|
import { Album, AlbumArtist, Artist, LibraryItem, RelatedArtist } from '/@/renderer/api/types';
|
2022-12-20 04:11:06 -08:00
|
|
|
import { Button } from '/@/renderer/components/button';
|
2023-06-02 01:01:50 -07:00
|
|
|
import { PosterCard } from '/@/renderer/components/card/poster-card';
|
|
|
|
|
import { TextTitle } from '/@/renderer/components/text-title';
|
2023-05-17 17:10:30 -07:00
|
|
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
2023-06-02 01:01:50 -07:00
|
|
|
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
|
|
|
|
import { usePlayButtonBehavior } from '/@/renderer/store';
|
|
|
|
|
import { CardRoute, CardRow } from '/@/renderer/types';
|
|
|
|
|
|
|
|
|
|
const getSlidesPerView = (windowWidth: number) => {
|
|
|
|
|
if (windowWidth < 400) return 2;
|
|
|
|
|
if (windowWidth < 700) return 3;
|
|
|
|
|
if (windowWidth < 900) return 4;
|
|
|
|
|
if (windowWidth < 1100) return 5;
|
|
|
|
|
if (windowWidth < 1300) return 6;
|
|
|
|
|
if (windowWidth < 1500) return 7;
|
|
|
|
|
if (windowWidth < 1920) return 8;
|
|
|
|
|
return 10;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CarouselContainer = styled(Stack)`
|
|
|
|
|
container-type: inline-size;
|
|
|
|
|
`;
|
2023-05-17 17:10:30 -07:00
|
|
|
|
|
|
|
|
interface TitleProps {
|
|
|
|
|
handleNext?: () => void;
|
|
|
|
|
handlePrev?: () => void;
|
|
|
|
|
label?: string | ReactNode;
|
|
|
|
|
pagination: {
|
|
|
|
|
hasNextPage: boolean;
|
|
|
|
|
hasPreviousPage: boolean;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
|
|
|
|
|
return (
|
|
|
|
|
<Group position="apart">
|
|
|
|
|
{isValidElement(label) ? (
|
|
|
|
|
label
|
|
|
|
|
) : (
|
|
|
|
|
<TextTitle
|
|
|
|
|
order={2}
|
|
|
|
|
weight={700}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</TextTitle>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Group spacing="sm">
|
|
|
|
|
<Button
|
|
|
|
|
compact
|
|
|
|
|
disabled={!pagination.hasPreviousPage}
|
|
|
|
|
size="lg"
|
|
|
|
|
variant="default"
|
|
|
|
|
onClick={handlePrev}
|
|
|
|
|
>
|
|
|
|
|
<RiArrowLeftSLine />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
compact
|
|
|
|
|
disabled={!pagination.hasNextPage}
|
|
|
|
|
size="lg"
|
|
|
|
|
variant="default"
|
|
|
|
|
onClick={handleNext}
|
|
|
|
|
>
|
|
|
|
|
<RiArrowRightSLine />
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
</Group>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface SwiperGridCarouselProps {
|
|
|
|
|
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
|
|
|
|
data: Album[] | AlbumArtist[] | Artist[] | RelatedArtist[] | undefined;
|
|
|
|
|
isLoading?: boolean;
|
2023-01-12 13:31:25 -08:00
|
|
|
itemType: LibraryItem;
|
2023-05-17 17:10:30 -07:00
|
|
|
route: CardRoute;
|
|
|
|
|
swiperProps?: SwiperOptions;
|
|
|
|
|
title?: {
|
|
|
|
|
children?: ReactNode;
|
|
|
|
|
hasPagination?: boolean;
|
|
|
|
|
icon?: ReactNode;
|
|
|
|
|
label: string | ReactNode;
|
2022-12-19 15:59:14 -08:00
|
|
|
};
|
|
|
|
|
uniqueId: string;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 17:10:30 -07:00
|
|
|
export const SwiperGridCarousel = ({
|
|
|
|
|
cardRows,
|
|
|
|
|
data,
|
|
|
|
|
itemType,
|
|
|
|
|
route,
|
|
|
|
|
swiperProps,
|
|
|
|
|
title,
|
|
|
|
|
isLoading,
|
|
|
|
|
uniqueId,
|
|
|
|
|
}: SwiperGridCarouselProps) => {
|
|
|
|
|
const swiperRef = useRef<SwiperCore | any>(null);
|
2023-01-12 13:31:25 -08:00
|
|
|
const playButtonBehavior = usePlayButtonBehavior();
|
2022-12-31 19:26:58 -08:00
|
|
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
2022-12-20 04:11:06 -08:00
|
|
|
|
2023-05-17 17:10:30 -07:00
|
|
|
const [pagination, setPagination] = useState({
|
2023-06-02 01:01:50 -07:00
|
|
|
hasNextPage: (data?.length || 0) > Math.round(3),
|
2023-05-17 17:10:30 -07:00
|
|
|
hasPreviousPage: false,
|
|
|
|
|
});
|
2022-12-20 04:11:06 -08:00
|
|
|
|
2023-05-17 17:10:30 -07:00
|
|
|
const createFavoriteMutation = useCreateFavorite({});
|
|
|
|
|
const deleteFavoriteMutation = useDeleteFavorite({});
|
2022-12-20 04:11:06 -08:00
|
|
|
|
2023-05-17 17:10:30 -07:00
|
|
|
const handleFavorite = useCallback(
|
|
|
|
|
(options: { id: string[]; isFavorite: boolean; itemType: LibraryItem; serverId: string }) => {
|
|
|
|
|
const { id, itemType, isFavorite, serverId } = options;
|
|
|
|
|
if (isFavorite) {
|
|
|
|
|
deleteFavoriteMutation.mutate({
|
|
|
|
|
query: {
|
|
|
|
|
id,
|
|
|
|
|
type: itemType,
|
|
|
|
|
},
|
|
|
|
|
serverId,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
createFavoriteMutation.mutate({
|
|
|
|
|
query: {
|
|
|
|
|
id,
|
|
|
|
|
type: itemType,
|
|
|
|
|
},
|
|
|
|
|
serverId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[createFavoriteMutation, deleteFavoriteMutation],
|
2022-12-20 04:11:06 -08:00
|
|
|
);
|
|
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
const slides = useMemo(() => {
|
|
|
|
|
if (!data) return [];
|
|
|
|
|
|
|
|
|
|
return data.map((el) => (
|
|
|
|
|
<PosterCard
|
|
|
|
|
controls={{
|
|
|
|
|
cardRows,
|
|
|
|
|
handleFavorite,
|
|
|
|
|
handlePlayQueueAdd,
|
|
|
|
|
itemType,
|
|
|
|
|
playButtonBehavior,
|
|
|
|
|
route,
|
|
|
|
|
}}
|
|
|
|
|
data={el}
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
uniqueId={uniqueId}
|
|
|
|
|
/>
|
|
|
|
|
));
|
|
|
|
|
}, [
|
|
|
|
|
cardRows,
|
|
|
|
|
data,
|
|
|
|
|
handleFavorite,
|
|
|
|
|
handlePlayQueueAdd,
|
|
|
|
|
isLoading,
|
|
|
|
|
itemType,
|
|
|
|
|
playButtonBehavior,
|
|
|
|
|
route,
|
|
|
|
|
uniqueId,
|
|
|
|
|
]);
|
2023-05-17 17:10:30 -07:00
|
|
|
|
|
|
|
|
const handleNext = useCallback(() => {
|
|
|
|
|
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
2023-06-03 05:45:49 -07:00
|
|
|
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
2023-05-17 17:10:30 -07:00
|
|
|
swiperRef?.current?.slideTo(activeIndex + slidesPerView);
|
|
|
|
|
}, [swiperProps?.slidesPerView]);
|
|
|
|
|
|
|
|
|
|
const handlePrev = useCallback(() => {
|
|
|
|
|
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
2023-06-03 05:45:49 -07:00
|
|
|
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
2023-05-17 17:10:30 -07:00
|
|
|
swiperRef?.current?.slideTo(activeIndex - slidesPerView);
|
|
|
|
|
}, [swiperProps?.slidesPerView]);
|
|
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
const handleOnSlideChange = useCallback((e: SwiperCore) => {
|
|
|
|
|
const { slides, isEnd, isBeginning, params } = e;
|
|
|
|
|
if (isEnd || isBeginning) return;
|
2023-05-17 17:10:30 -07:00
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
setPagination({
|
2023-06-03 05:45:49 -07:00
|
|
|
hasNextPage: (params?.slidesPerView || 4) < slides.length,
|
|
|
|
|
hasPreviousPage: (params?.slidesPerView || 4) < slides.length,
|
2023-06-02 01:01:50 -07:00
|
|
|
});
|
|
|
|
|
}, []);
|
2022-12-19 15:59:14 -08:00
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
const handleOnZoomChange = useCallback((e: SwiperCore) => {
|
|
|
|
|
const { slides, isEnd, isBeginning, params } = e;
|
|
|
|
|
if (isEnd || isBeginning) return;
|
2023-05-17 17:47:05 -07:00
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
setPagination({
|
2023-06-03 05:45:49 -07:00
|
|
|
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
|
|
|
|
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
2023-06-02 01:01:50 -07:00
|
|
|
});
|
|
|
|
|
}, []);
|
2023-05-17 17:47:05 -07:00
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
const handleOnReachEnd = useCallback((e: SwiperCore) => {
|
|
|
|
|
const { slides, params } = e;
|
2022-12-19 15:59:14 -08:00
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
setPagination({
|
|
|
|
|
hasNextPage: false,
|
2023-06-03 05:45:49 -07:00
|
|
|
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
2023-06-02 01:01:50 -07:00
|
|
|
});
|
|
|
|
|
}, []);
|
2022-12-19 15:59:14 -08:00
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
|
|
|
|
|
const { slides, params } = e;
|
2022-12-19 15:59:14 -08:00
|
|
|
|
2023-06-02 01:01:50 -07:00
|
|
|
setPagination({
|
2023-06-03 05:45:49 -07:00
|
|
|
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
2023-06-02 01:01:50 -07:00
|
|
|
hasPreviousPage: false,
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2023-06-02 01:21:52 -07:00
|
|
|
const handleOnResize = useCallback((e: SwiperCore) => {
|
2023-06-03 05:45:49 -07:00
|
|
|
if (!e) return;
|
2023-06-02 01:01:50 -07:00
|
|
|
const { width } = e;
|
|
|
|
|
const slidesPerView = getSlidesPerView(width);
|
2023-06-03 05:45:49 -07:00
|
|
|
if (!e.params) return;
|
2023-06-02 01:01:50 -07:00
|
|
|
e.params.slidesPerView = slidesPerView;
|
2023-06-02 01:21:52 -07:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const throttledOnResize = throttle(handleOnResize, 200);
|
2022-12-19 15:59:14 -08:00
|
|
|
|
|
|
|
|
return (
|
2023-06-02 01:01:50 -07:00
|
|
|
<CarouselContainer
|
|
|
|
|
className="grid-carousel"
|
|
|
|
|
spacing="md"
|
2023-05-17 17:10:30 -07:00
|
|
|
>
|
2023-06-02 01:01:50 -07:00
|
|
|
{title ? (
|
|
|
|
|
<Title
|
|
|
|
|
{...title}
|
|
|
|
|
handleNext={handleNext}
|
|
|
|
|
handlePrev={handlePrev}
|
|
|
|
|
pagination={pagination}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
<Swiper
|
|
|
|
|
ref={swiperRef}
|
|
|
|
|
resizeObserver
|
|
|
|
|
modules={[Virtual]}
|
|
|
|
|
slidesPerView={4}
|
|
|
|
|
spaceBetween={20}
|
|
|
|
|
style={{ height: '100%', width: '100%' }}
|
|
|
|
|
onBeforeInit={(swiper) => {
|
|
|
|
|
swiperRef.current = swiper;
|
|
|
|
|
}}
|
2023-06-02 01:21:52 -07:00
|
|
|
onBeforeResize={handleOnResize}
|
2023-06-02 01:01:50 -07:00
|
|
|
onReachBeginning={handleOnReachBeginning}
|
|
|
|
|
onReachEnd={handleOnReachEnd}
|
2023-06-02 01:21:52 -07:00
|
|
|
onResize={throttledOnResize}
|
2023-06-02 01:01:50 -07:00
|
|
|
onSlideChange={handleOnSlideChange}
|
|
|
|
|
onZoomChange={handleOnZoomChange}
|
|
|
|
|
{...swiperProps}
|
2023-05-17 17:10:30 -07:00
|
|
|
>
|
2023-06-02 01:01:50 -07:00
|
|
|
{slides.map((slideContent, index) => {
|
|
|
|
|
return (
|
|
|
|
|
<SwiperSlide
|
|
|
|
|
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
|
|
|
|
|
virtualIndex={index}
|
2023-05-17 17:10:30 -07:00
|
|
|
>
|
2023-06-02 01:01:50 -07:00
|
|
|
{slideContent}
|
|
|
|
|
</SwiperSlide>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Swiper>
|
|
|
|
|
</CarouselContainer>
|
2022-12-19 15:59:14 -08:00
|
|
|
);
|
|
|
|
|
};
|