import type { MouseEvent } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { generatePath, Link } from 'react-router'; import styles from './feature-carousel.module.css'; import { ItemImage, useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay'; import { calculateTitleSize } from '/@/renderer/features/shared/components/library-header'; 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 { Group } from '/@/shared/components/group/group'; import { Separator } from '/@/shared/components/separator/separator'; 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: {}, exit: {}, initial: {}, }; const itemVariants = { animate: { opacity: 1, scale: 1, transition: { duration: 0.2, ease: 'easeOut' as const, }, y: 0, }, exit: { opacity: 0, transition: { duration: 0.3, ease: 'easeIn' as const, }, y: 0, }, initial: { opacity: 0, y: 0, }, }; interface CarouselItemProps { album: Album; } interface SingleFeatureCarouselProps { data: Album[] | undefined; onNearEnd?: () => void; } // const CAROUSEL_AUTOPLAY_INTERVAL = 10000; const CarouselItem = ({ album }: CarouselItemProps) => { const imageUrl = useItemImageUrl({ id: album.imageId || undefined, itemType: LibraryItem.ALBUM, type: 'itemCard', }); const { background: backgroundColor } = useFastAverageColor({ algorithm: 'dominant', src: 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); }; const metadataItems = useMemo(() => { return [ ...(album.genres?.slice(0, 2).map((genre) => genre.name) || []), album.releaseYear ? album.releaseYear.toString() : null, ].filter(Boolean); }, [album]); return (
{imageUrl && (
)}
{album.name} {album.albumArtistName && ( {album.albumArtistName} )} {metadataItems.map((item, index) => ( {item} {index < metadataItems.length - 1 && } ))}
); }; export const SingleFeatureCarousel = ({ data, onNearEnd }: SingleFeatureCarouselProps) => { const [currentIndex, setCurrentIndex] = useState(0); const directionRef = useRef<{ isNext: boolean }>({ isNext: true }); const { ref: containerRef } = useContainerQuery({ '2xl': 1920, '3xl': 2560, lg: 1024, md: 768, sm: 640, xl: 1440, }); // Check if we're near the end and trigger loading more useEffect(() => { if (!data || !onNearEnd) return; const remainingItems = data.length - currentIndex; // Trigger when we have less than 3 items remaining if (remainingItems < 3) { onNearEnd(); } }, [data, currentIndex, onNearEnd]); // useEffect(() => { // if (!data || data.length <= 1 || isPaused) { // if (intervalRef.current) { // clearInterval(intervalRef.current); // intervalRef.current = null; // } // return; // } // if (intervalRef.current) { // clearInterval(intervalRef.current); // } // intervalRef.current = setInterval(() => { // setCurrentIndex((prev) => (prev + 1) % data.length); // directionRef.current = { isNext: true }; // }, CAROUSEL_AUTOPLAY_INTERVAL); // return () => { // if (intervalRef.current) { // clearInterval(intervalRef.current); // intervalRef.current = null; // } // }; // }, [data, isPaused, intervalKey]); const handleNext = useCallback( (e?: MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); if (!data) return; directionRef.current = { isNext: true }; setCurrentIndex((prev) => (prev + 1) % data.length); // setIntervalKey((prev) => prev + 1); }, [data], ); const handlePrevious = useCallback( (e?: MouseEvent) => { e?.preventDefault(); e?.stopPropagation(); if (!data) return; directionRef.current = { isNext: false }; setCurrentIndex((prev) => (prev - 1 + data.length) % data.length); // setIntervalKey((prev) => prev + 1); }, [data], ); const canNavigate = data && data.length > 1; const wheelCooldownRef = useRef(0); const wheelThreshold = 10; const wheelCooldownMs = 250; const handleWheel = useCallback( (event: React.WheelEvent) => { if (!canNavigate || !data) { return; } if (!event.shiftKey) { return; } const now = Date.now(); const elapsed = now - wheelCooldownRef.current; const horizontalDelta = Math.abs(event.deltaY); if (horizontalDelta < wheelThreshold || elapsed < wheelCooldownMs) { return; } if (event.deltaY > 0) { wheelCooldownRef.current = now; handleNext(); } else if (event.deltaY < 0) { wheelCooldownRef.current = now; handlePrevious(); } }, [canNavigate, data, handleNext, handlePrevious, wheelCooldownMs, wheelThreshold], ); if (!data || data.length === 0) { return null; } const currentAlbum = data[currentIndex]; return (
setIsPaused(true)} // onMouseLeave={() => setIsPaused(false)} onWheel={handleWheel} ref={containerRef} > {data.length > 1 && ( <> )}
); };