import type { MouseEvent } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { generatePath, Link } from 'react-router'; import styles from './feature-carousel.module.css'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay'; import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group'; import { useContainerQuery, useFastAverageColor } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Badge } from '/@/shared/components/badge/badge'; import { Group } from '/@/shared/components/group/group'; import { Image } from '/@/shared/components/image/image'; import { Stack } from '/@/shared/components/stack/stack'; import { TextTitle } from '/@/shared/components/text-title/text-title'; import { Text } from '/@/shared/components/text/text'; import { Album, LibraryItem } from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; const containerVariants = { animate: (custom: { isNext: boolean }) => ({ transition: { delayChildren: 0.1, staggerChildren: 0.3, staggerDirection: custom.isNext ? 1 : -1, }, }), exit: (custom: { isNext: boolean }) => ({ transition: { staggerChildren: 0.3, staggerDirection: custom.isNext ? 1 : -1, }, }), initial: (custom: { isNext: boolean }) => ({ transition: { staggerChildren: 0.3, staggerDirection: custom.isNext ? -1 : 1, }, }), }; const itemVariants = { animate: { opacity: 1, scale: 1, transition: { duration: 0.4, ease: 'easeOut' as const, }, y: 0, }, exit: { opacity: 0, scale: 0.8, transition: { duration: 0.3, ease: 'easeIn' as const, }, y: -20, }, initial: { opacity: 0, y: 20, }, }; interface FeatureCarouselProps { data: Album[] | undefined; onNearEnd?: () => void; } const getItemsPerRow = (breakpoints: { is2xl: boolean; is3xl: boolean; isLg: boolean; isMd: boolean; isSm: boolean; isXl: boolean; }) => { if (breakpoints.is3xl) return 6; if (breakpoints.is2xl) return 5; if (breakpoints.isXl) return 5; if (breakpoints.isLg) return 4; if (breakpoints.isMd) return 3; if (breakpoints.isSm) return 2; return 2; }; interface CarouselItemProps { album: Album; } const CarouselItem = ({ album }: CarouselItemProps) => { const { background: backgroundColor } = useFastAverageColor({ algorithm: 'dominant', src: album.imageUrl || null, srcLoaded: true, }); const server = useCurrentServer(); const { addToQueueByFetch } = usePlayer(); const handlePlay = (type: Play) => { if (!server?.id) return; addToQueueByFetch(server.id, [album.id], LibraryItem.ALBUM, type); }; return (
{album.name}
{album.albumArtists.slice(0, 1).map((artist) => ( {artist.name} ))} {album.genres?.slice(0, 2).map((genre) => ( {genre.name} ))} {album.releaseYear && ( {album.releaseYear} )}
); }; export const FeatureCarousel = ({ data, onNearEnd }: FeatureCarouselProps) => { const [startIndex, setStartIndex] = useState(0); const directionRef = useRef<{ isNext: boolean }>({ isNext: true }); const { is2xl, is3xl, isLg, isMd, isSm, isXl, ref: containerRef, } = useContainerQuery({ '2xl': 1920, '3xl': 2560, lg: 1024, md: 768, sm: 640, xl: 1440, }); const itemsPerRow = useMemo( () => getItemsPerRow({ is2xl, is3xl, isLg, isMd, isSm, isXl }), [is2xl, is3xl, isLg, isMd, isSm, isXl], ); const visibleItems = useMemo(() => { if (!data) return []; const items: Album[] = []; for (let i = 0; i < itemsPerRow; i++) { const index = (startIndex + i) % data.length; items.push(data[index]); } return items; }, [data, startIndex, itemsPerRow]); // Check if we're near the end and trigger loading more useEffect(() => { if (!data || !onNearEnd) return; const remainingItems = data.length - startIndex; // Trigger when we have less than 2 rows worth of items remaining if (remainingItems < itemsPerRow * 2) { onNearEnd(); } }, [data, startIndex, itemsPerRow, onNearEnd]); const handleNext = (e?: MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); if (!data) return; directionRef.current = { isNext: true }; setStartIndex((prev) => (prev + itemsPerRow) % data.length); }; const handlePrevious = (e?: MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); if (!data) return; directionRef.current = { isNext: false }; setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length); }; if (!data || data.length === 0) { return null; } return (
{visibleItems.map((album, index) => ( ))} {data.length > itemsPerRow && ( <> )}
); };