diff --git a/src/renderer/components/card/album-card.tsx b/src/renderer/components/card/album-card.tsx index 227e16c7..9553914d 100644 --- a/src/renderer/components/card/album-card.tsx +++ b/src/renderer/components/card/album-card.tsx @@ -6,9 +6,9 @@ import { Link } from 'react-router-dom'; import { SimpleImg } from 'react-simple-img'; import styled from 'styled-components'; import { Text } from '/@/renderer/components/text'; -import type { LibraryItem, CardRow, CardRoute, Play } from '/@/renderer/types'; +import type { LibraryItem, CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types'; import { Skeleton } from '/@/renderer/components/skeleton'; -import CardControls from '/@/renderer/components/card/card-controls'; +import { CardControls } from '/@/renderer/components/card/card-controls'; const CardWrapper = styled.div<{ link?: boolean; @@ -108,11 +108,18 @@ interface BaseGridCardProps { route: CardRoute; }; data: any; + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; loading?: boolean; size: number; } -export const AlbumCard = ({ loading, size, data, controls }: BaseGridCardProps) => { +export const AlbumCard = ({ + loading, + size, + handlePlayQueueAdd, + data, + controls, +}: BaseGridCardProps) => { const navigate = useNavigate(); const { itemType, cardRows, route } = controls; @@ -164,6 +171,7 @@ export const AlbumCard = ({ loading, size, data, controls }: BaseGridCardProps) )} @@ -286,7 +294,7 @@ export const AlbumCard = ({ loading, size, data, controls }: BaseGridCardProps) - {cardRows.map((row: CardRow, index: number) => ( + {cardRows.map((_row: CardRow, index: number) => ( { +export const CardControls = ({ + itemData, + itemType, + handlePlayQueueAdd, +}: { + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; + itemData: any; + itemType: LibraryItem; +}) => { const playButtonBehavior = useSettingsStore((state) => state.player.playButtonBehavior); const handlePlay = (e: MouseEvent, playType?: Play) => { e.preventDefault(); e.stopPropagation(); - import('/@/renderer/features/player/utils/handle-playqueue-add').then((fn) => { - fn.handlePlayQueueAdd({ - byItemType: { - id: itemData.id, - type: itemType, - }, - play: playType || playButtonBehavior, - }); + handlePlayQueueAdd({ + byItemType: { + id: itemData.id, + type: itemType, + }, + play: playType || playButtonBehavior, }); }; @@ -192,5 +198,3 @@ export const CardControls = ({ itemData, itemType }: { itemData: any; itemType: ); }; - -export default CardControls; diff --git a/src/renderer/components/grid-carousel/index.tsx b/src/renderer/components/grid-carousel/index.tsx index 84011c70..84eae42e 100644 --- a/src/renderer/components/grid-carousel/index.tsx +++ b/src/renderer/components/grid-carousel/index.tsx @@ -3,11 +3,13 @@ import { Group, Stack } from '@mantine/core'; import type { Variants } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion'; import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri'; -import { AlbumCard, Button } from '/@/renderer/components'; +import { Button } from '/@/renderer/components/button'; import { AppRoute } from '/@/renderer/router/routes'; import type { CardRow } from '/@/renderer/types'; import { LibraryItem, Play } from '/@/renderer/types'; import styled from 'styled-components'; +import { AlbumCard } from '/@/renderer/components/card'; +import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; interface GridCarouselProps { cardRows: CardRow[]; @@ -27,6 +29,99 @@ interface GridCarouselProps { const GridCarouselContext = createContext(null); +const GridContainer = styled(motion.div)<{ height: number; itemsPerPage: number }>` + display: grid; + grid-auto-rows: 0; + grid-gap: 18px; + grid-template-rows: 1fr; + grid-template-columns: repeat(${(props) => props.itemsPerPage || 4}, minmax(0, 1fr)); + height: ${(props) => props.height}px; + overflow: hidden; +`; + +const Wrapper = styled.div` + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +`; + +const variants: Variants = { + animate: (custom: { direction: number; loading: boolean }) => { + return { + opacity: custom.loading ? 0.5 : 1, + scale: custom.loading ? 0.95 : 1, + transition: { + opacity: { duration: 0.2 }, + x: { damping: 30, stiffness: 300, type: 'spring' }, + }, + x: 0, + }; + }, + exit: (custom: { direction: number; loading: boolean }) => { + return { + opacity: 0, + transition: { + opacity: { duration: 0.2 }, + x: { damping: 30, stiffness: 300, type: 'spring' }, + }, + x: custom.direction > 0 ? -1000 : 1000, + }; + }, + initial: (custom: { direction: number; loading: boolean }) => { + return { + opacity: 0, + x: custom.direction > 0 ? 1000 : -1000, + }; + }, +}; + +const Carousel = ({ data, cardRows }: any) => { + const { loading, pagination, gridHeight, imageSize, direction, uniqueId } = + useContext(GridCarouselContext); + + const handlePlayQueueAdd = useHandlePlayQueueAdd(); + + return ( + + + + {data?.map((item: any, index: number) => ( + + ))} + + + + ); +}; + export const GridCarousel = ({ data, loading, @@ -75,96 +170,6 @@ export const GridCarousel = ({ ); }; -const variants: Variants = { - animate: (custom: { direction: number; loading: boolean }) => { - return { - opacity: custom.loading ? 0.5 : 1, - scale: custom.loading ? 0.95 : 1, - transition: { - opacity: { duration: 0.2 }, - x: { damping: 30, stiffness: 300, type: 'spring' }, - }, - x: 0, - }; - }, - exit: (custom: { direction: number; loading: boolean }) => { - return { - opacity: 0, - transition: { - opacity: { duration: 0.2 }, - x: { damping: 30, stiffness: 300, type: 'spring' }, - }, - x: custom.direction > 0 ? -1000 : 1000, - }; - }, - initial: (custom: { direction: number; loading: boolean }) => { - return { - opacity: 0, - x: custom.direction > 0 ? 1000 : -1000, - }; - }, -}; - -const GridContainer = styled(motion.div)<{ height: number; itemsPerPage: number }>` - display: grid; - grid-auto-rows: 0; - grid-gap: 18px; - grid-template-rows: 1fr; - grid-template-columns: repeat(${(props) => props.itemsPerPage || 4}, minmax(0, 1fr)); - height: ${(props) => props.height}px; - overflow: hidden; -`; - -const Wrapper = styled.div` - position: relative; - width: 100%; - height: 100%; - overflow: hidden; -`; - -const Carousel = ({ data, cardRows }: any) => { - const { loading, pagination, gridHeight, imageSize, direction, uniqueId } = - useContext(GridCarouselContext); - - return ( - - - - {data?.map((item: any, index: number) => ( - - ))} - - - - ); -}; - interface TitleProps { children?: React.ReactNode; } diff --git a/src/renderer/components/virtual-grid/grid-card/default-card.tsx b/src/renderer/components/virtual-grid/grid-card/default-card.tsx index 98d6f33f..247273c6 100644 --- a/src/renderer/components/virtual-grid/grid-card/default-card.tsx +++ b/src/renderer/components/virtual-grid/grid-card/default-card.tsx @@ -7,9 +7,9 @@ import { SimpleImg } from 'react-simple-img'; import type { ListChildComponentProps } from 'react-window'; import styled from 'styled-components'; import { Text } from '/@/renderer/components/text'; -import type { LibraryItem, CardRow, CardRoute, Play } from '/@/renderer/types'; -import GridCardControls from './grid-card-controls'; +import type { LibraryItem, CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types'; import { Skeleton } from '/@/renderer/components/skeleton'; +import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls'; const CardWrapper = styled.div<{ itemGap: number; @@ -114,6 +114,7 @@ interface BaseGridCardProps { columnIndex: number; controls: { cardRows: CardRow[]; + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; itemType: LibraryItem; playButtonBehavior: Play; route: CardRoute; @@ -137,7 +138,7 @@ export const DefaultCard = ({ const navigate = useNavigate(); const { index } = listChildProps; const { itemGap, itemHeight, itemWidth } = sizes; - const { itemType, cardRows, route } = controls; + const { itemType, cardRows, route, handlePlayQueueAdd } = controls; const cardSize = itemWidth - 24; @@ -191,6 +192,7 @@ export const DefaultCard = ({ )} diff --git a/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx b/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx index 79fbbcbf..85827475 100644 --- a/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx +++ b/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx @@ -6,7 +6,7 @@ import { RiPlayFill, RiMore2Fill, RiHeartFill, RiHeartLine } from 'react-icons/r import styled from 'styled-components'; import { _Button } from '/@/renderer/components/button'; import { DropdownMenu } from '/@/renderer/components/dropdown-menu'; -import type { LibraryItem } from '/@/renderer/types'; +import type { LibraryItem, PlayQueueAddOptions } from '/@/renderer/types'; import { Play } from '/@/renderer/types'; import { useSettingsStore } from '/@/renderer/store/settings.store'; @@ -116,23 +116,24 @@ const PLAY_TYPES = [ export const GridCardControls = ({ itemData, itemType, + handlePlayQueueAdd, }: { + handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void; itemData: any; itemType: LibraryItem; }) => { const playButtonBehavior = useSettingsStore((state) => state.player.playButtonBehavior); - const handlePlay = (e: MouseEvent, playType?: Play) => { + const handlePlay = async (e: MouseEvent, playType?: Play) => { e.preventDefault(); e.stopPropagation(); - import('/@/renderer/features/player/utils/handle-playqueue-add').then((fn) => { - fn.handlePlayQueueAdd({ - byItemType: { - id: itemData.id, - type: itemType, - }, - play: playType || playButtonBehavior, - }); + + handlePlayQueueAdd?.({ + byItemType: { + id: itemData.id, + type: itemType, + }, + play: playType || playButtonBehavior, }); }; @@ -200,5 +201,3 @@ export const GridCardControls = ({ ); }; - -export default GridCardControls; diff --git a/src/renderer/components/virtual-grid/grid-card/index.tsx b/src/renderer/components/virtual-grid/grid-card/index.tsx index f08e906c..2c3f839b 100644 --- a/src/renderer/components/virtual-grid/grid-card/index.tsx +++ b/src/renderer/components/virtual-grid/grid-card/index.tsx @@ -17,9 +17,11 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) = itemData, itemType, playButtonBehavior, + handlePlayQueueAdd, route, display, } = data as GridCardData; + const cards = []; const startIndex = index * columnCount; const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1); @@ -33,6 +35,7 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) = columnIndex={i} controls={{ cardRows, + handlePlayQueueAdd, itemType, playButtonBehavior, route, diff --git a/src/renderer/components/virtual-grid/grid-card/poster-card.tsx b/src/renderer/components/virtual-grid/grid-card/poster-card.tsx index aa1e7dbe..51dc016f 100644 --- a/src/renderer/components/virtual-grid/grid-card/poster-card.tsx +++ b/src/renderer/components/virtual-grid/grid-card/poster-card.tsx @@ -8,8 +8,8 @@ import type { ListChildComponentProps } from 'react-window'; import styled from 'styled-components'; import { Skeleton } from '/@/renderer/components/skeleton'; import { Text } from '/@/renderer/components/text'; -import type { LibraryItem, CardRow, CardRoute, Play } from '/@/renderer/types'; -import GridCardControls from './grid-card-controls'; +import type { LibraryItem, CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types'; +import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls'; const CardWrapper = styled.div<{ itemGap: number; @@ -117,6 +117,7 @@ interface BaseGridCardProps { columnIndex: number; controls: { cardRows: CardRow[]; + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; itemType: LibraryItem; playButtonBehavior: Play; route: CardRoute; @@ -184,6 +185,7 @@ export const PosterCard = ({ )} diff --git a/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx b/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx index b3c24159..95b544ef 100644 --- a/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx +++ b/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx @@ -5,7 +5,13 @@ import type { FixedSizeListProps } from 'react-window'; import { FixedSizeList } from 'react-window'; import styled from 'styled-components'; import { GridCard } from '/@/renderer/components/virtual-grid/grid-card'; -import type { CardRow, LibraryItem, CardDisplayType, CardRoute } from '/@/renderer/types'; +import type { + CardRow, + LibraryItem, + CardDisplayType, + CardRoute, + PlayQueueAddOptions, +} from '/@/renderer/types'; const createItemData = memoize( ( @@ -19,10 +25,12 @@ const createItemData = memoize( itemType, itemWidth, route, + handlePlayQueueAdd, ) => ({ cardRows, columnCount, display, + handlePlayQueueAdd, itemCount, itemData, itemGap, @@ -47,6 +55,7 @@ export const VirtualGridWrapper = ({ columnCount, rowCount, initialScrollOffset, + handlePlayQueueAdd, itemData, route, onScroll, @@ -55,6 +64,7 @@ export const VirtualGridWrapper = ({ cardRows: CardRow[]; columnCount: number; display: CardDisplayType; + handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void; itemData: any[]; itemGap: number; itemHeight: number; @@ -75,6 +85,7 @@ export const VirtualGridWrapper = ({ itemType, itemWidth, route, + handlePlayQueueAdd, ); const memoizedOnScroll = createScrollHandler(onScroll); diff --git a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx index 8080e8d7..882a020a 100644 --- a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx +++ b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx @@ -3,18 +3,19 @@ import debounce from 'lodash/debounce'; import type { FixedSizeListProps } from 'react-window'; import InfiniteLoader from 'react-window-infinite-loader'; import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper'; -import type { CardRoute, CardRow, LibraryItem } from '/@/renderer/types'; +import type { CardRoute, CardRow, LibraryItem, PlayQueueAddOptions } from '/@/renderer/types'; import { CardDisplayType } from '/@/renderer/types'; interface VirtualGridProps extends Omit { cardRows: CardRow[]; display?: CardDisplayType; fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise; + handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void; itemGap: number; itemSize: number; itemType: LibraryItem; minimumBatchSize?: number; - refresh?: any; // Pass in any value to refresh the grid when changed + refresh?: any; route?: CardRoute; } @@ -35,6 +36,7 @@ export const VirtualInfiniteGrid = ({ route, onScroll, display, + handlePlayQueueAdd, minimumBatchSize, fetchFn, initialScrollOffset, @@ -123,6 +125,7 @@ export const VirtualInfiniteGrid = ({ cardRows={cardRows} columnCount={columnCount} display={display || CardDisplayType.CARD} + handlePlayQueueAdd={handlePlayQueueAdd} height={height} initialScrollOffset={initialScrollOffset} itemCount={itemCount || 0} diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx index 3b2e0250..bbd8a93d 100644 --- a/src/renderer/features/albums/routes/album-list-route.tsx +++ b/src/renderer/features/albums/routes/album-list-route.tsx @@ -17,6 +17,7 @@ import { controller } from '/@/renderer/api/controller'; import { AnimatedPage } from '/@/renderer/features/shared'; import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header'; import { api } from '/@/renderer/api'; +import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; const AlbumListRoute = () => { const queryClient = useQueryClient(); @@ -24,6 +25,7 @@ const AlbumListRoute = () => { const { setPage } = useAppStoreActions(); const page = useAlbumRouteStore(); const filters = page.list.filter; + const handlePlayQueueAdd = useHandlePlayQueueAdd(); const albumListQuery = useAlbumList({ limit: 1, @@ -101,6 +103,7 @@ const AlbumListRoute = () => { ]} display={page.list?.display || CardDisplayType.CARD} fetchFn={fetch} + handlePlayQueueAdd={handlePlayQueueAdd} height={height} initialScrollOffset={page.list?.gridScrollOffset || 0} itemCount={albumListQuery?.data?.totalRecordCount || 0} @@ -108,7 +111,6 @@ const AlbumListRoute = () => { itemSize={150 + page.list?.size} itemType={LibraryItem.ALBUM} minimumBatchSize={40} - // refresh={advancedFilters} route={{ route: AppRoute.LIBRARY_ALBUMS_DETAIL, slugs: [{ idProperty: 'id', slugProperty: 'albumId' }], diff --git a/src/renderer/features/player/hooks/use-handle-playqueue-add.ts b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts new file mode 100644 index 00000000..2a010461 --- /dev/null +++ b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts @@ -0,0 +1,80 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { api } from '/@/renderer/api/index'; +import { jfNormalize } from '/@/renderer/api/jellyfin.api'; +import { JFSong } from '/@/renderer/api/jellyfin.types'; +import { ndNormalize } from '/@/renderer/api/navidrome.api'; +import { NDSong } from '/@/renderer/api/navidrome.types'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { useAuthStore, usePlayerStore } from '/@/renderer/store'; +import { useSettingsStore } from '/@/renderer/store/settings.store'; +import { PlayQueueAddOptions, LibraryItem, Play, PlaybackType } from '/@/renderer/types'; +import { toast } from '/@/renderer/components/toast'; + +const mpvPlayer = window.electron.mpvPlayer; + +export const useHandlePlayQueueAdd = () => { + const queryClient = useQueryClient(); + const playerType = useSettingsStore.getState().player.type; + const deviceId = useAuthStore.getState().deviceId; + const server = useAuthStore.getState().currentServer; + + const handlePlayQueueAdd = async (options: PlayQueueAddOptions) => { + if (!server) return toast.error({ message: 'No server selected', type: 'error' }); + + if (options.byItemType) { + let songs = null; + + if (options.byItemType.type === LibraryItem.ALBUM) { + const albumDetail = await queryClient.fetchQuery( + queryKeys.albums.detail(server?.id, { id: options.byItemType.id }), + async ({ signal }) => + api.controller.getAlbumDetail({ + query: { id: options.byItemType!.id }, + server, + signal, + }), + ); + + if (!albumDetail) return null; + + switch (server?.type) { + case 'jellyfin': + songs = albumDetail.songs?.map((song) => + jfNormalize.song(song as JFSong, server, deviceId), + ); + break; + case 'navidrome': + songs = albumDetail.songs?.map((song) => + ndNormalize.song(song as NDSong, server, deviceId), + ); + break; + case 'subsonic': + break; + } + } + + if (!songs) return toast.warn({ message: 'No songs found' }); + + const playerData = usePlayerStore.getState().actions.addToQueue(songs, options.play); + + if (options.play === Play.NEXT || options.play === Play.LAST) { + if (playerType === PlaybackType.LOCAL) { + mpvPlayer.setQueueNext(playerData); + } + } + + if (options.play === Play.NOW) { + if (playerType === PlaybackType.LOCAL) { + mpvPlayer.setQueue(playerData); + mpvPlayer.play(); + } + + usePlayerStore.getState().actions.play(); + } + } + + return null; + }; + + return handlePlayQueueAdd; +}; diff --git a/src/renderer/features/player/utils/handle-playqueue-add.ts b/src/renderer/features/player/utils/handle-playqueue-add.ts deleted file mode 100644 index 009f7fa8..00000000 --- a/src/renderer/features/player/utils/handle-playqueue-add.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { controller } from '/@/renderer/api/controller'; -import { jfNormalize } from '/@/renderer/api/jellyfin.api'; -import { JFSong } from '/@/renderer/api/jellyfin.types'; -import { ndNormalize } from '/@/renderer/api/navidrome.api'; -import { NDSong } from '/@/renderer/api/navidrome.types'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { toast } from '/@/renderer/components'; -import { queryClient } from '/@/renderer/lib/react-query'; -import { useAuthStore, usePlayerStore } from '/@/renderer/store'; -import { useSettingsStore } from '/@/renderer/store/settings.store'; -import { PlayQueueAddOptions, LibraryItem, Play, PlaybackType } from '/@/renderer/types'; - -const mpvPlayer = window.electron.mpvPlayer; - -export const handlePlayQueueAdd = async (options: PlayQueueAddOptions) => { - const playerType = useSettingsStore.getState().player.type; - const deviceId = useAuthStore.getState().deviceId; - const server = useAuthStore.getState().currentServer; - - if (!server) return toast.error({ message: 'No server selected' }); - - if (options.byItemType) { - let songs = null; - - if (options.byItemType.type === LibraryItem.ALBUM) { - const albumDetail = await queryClient.fetchQuery( - queryKeys.albums.detail(server?.id, { id: options.byItemType.id }), - async ({ signal }) => - controller.getAlbumDetail({ query: { id: options.byItemType!.id }, server, signal }), - ); - - if (!albumDetail) return null; - - switch (server?.type) { - case 'jellyfin': - songs = albumDetail.songs?.map((song) => - jfNormalize.song(song as JFSong, server, deviceId), - ); - break; - case 'navidrome': - songs = albumDetail.songs?.map((song) => - ndNormalize.song(song as NDSong, server, deviceId), - ); - break; - case 'subsonic': - break; - } - } - - if (!songs) return null; - - const playerData = usePlayerStore.getState().actions.addToQueue(songs, options.play); - - if (options.play === Play.NEXT || options.play === Play.LAST) { - if (playerType === PlaybackType.LOCAL) { - mpvPlayer.setQueueNext(playerData); - } - } - - if (options.play === Play.NOW) { - if (playerType === PlaybackType.LOCAL) { - mpvPlayer.setQueue(playerData); - mpvPlayer.play(); - } - - usePlayerStore.getState().actions.play(); - } - } - - return null; -};