From e014ac0a4b7058df1e344a9466766bcefed04198 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 5 Nov 2022 03:11:51 -0700 Subject: [PATCH] Update grid card styles and props --- .../components/virtual-grid/grid-card.tsx | 220 ------------ .../{ => grid-card}/grid-card-controls.tsx | 57 ++- .../virtual-grid/grid-card/index.tsx | 61 ++++ .../virtual-grid/grid-card/poster-card.tsx | 338 ++++++++++++++++++ .../virtual-grid/virtual-grid-wrapper.tsx | 19 +- .../virtual-grid/virtual-infinite-grid.tsx | 20 +- .../player/hooks/use-playqueue-handler.ts | 14 +- src/renderer/types.ts | 50 ++- 8 files changed, 524 insertions(+), 255 deletions(-) delete mode 100644 src/renderer/components/virtual-grid/grid-card.tsx rename src/renderer/components/virtual-grid/{ => grid-card}/grid-card-controls.tsx (67%) create mode 100644 src/renderer/components/virtual-grid/grid-card/index.tsx create mode 100644 src/renderer/components/virtual-grid/grid-card/poster-card.tsx diff --git a/src/renderer/components/virtual-grid/grid-card.tsx b/src/renderer/components/virtual-grid/grid-card.tsx deleted file mode 100644 index 7d1c197d5..000000000 --- a/src/renderer/components/virtual-grid/grid-card.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import styled from '@emotion/styled'; -import { Center, Skeleton } from '@mantine/core'; -import { RiAlbumFill } from 'react-icons/ri'; -import { Link, generatePath } from 'react-router-dom'; -import { Text } from '@/renderer/components/text'; -import { GridCardControls } from '@/renderer/components/virtual-grid/grid-card-controls'; -import { AppRoute } from '@/renderer/router/routes'; -import { fadeIn } from '@/renderer/styles'; -import { CardRow } from '@/renderer/types'; - -const CardWrapper = styled.div<{ - itemGap: number; - itemHeight: number; - itemWidth: number; -}>` - flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`}; - width: ${({ itemWidth }) => `${itemWidth}px`}; - height: ${({ itemHeight }) => `${itemHeight}px`}; - margin: ${({ itemGap }) => `0 ${itemGap / 2}px`}; - transition: border 0.2s ease-in-out; - user-select: none; - pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682 - - &:hover div { - opacity: 1; - } - - &:hover * { - &::before { - opacity: 0.5; - } - } - - &:focus-visible { - outline: 1px solid #fff; - } -`; - -const StyledCard = styled.div` - display: flex; - flex-direction: column; - gap: 0.5rem; - width: 100%; - height: 100%; - padding: 0; - border-radius: 3px; -`; - -const ImageSection = styled.div` - position: relative; - width: 100%; -`; - -interface ImageProps { - height: number; - src: string; -} - -const Image = styled.div` - ${fadeIn}; - height: ${({ height }) => `${height}px`}; - background-image: ${({ src }) => `url(${src})`}; - background-position: center; - background-size: cover; - border: 0; - border-radius: 5px; - - &::before { - position: absolute; - top: 0; - left: 0; - z-index: 1; - width: 100%; - height: 100%; - background: linear-gradient( - 0deg, - rgba(0, 0, 0, 100%) 35%, - rgba(0, 0, 0, 0%) 100% - ); - opacity: 0; - transition: all 0.2s ease-in-out; - content: ''; - user-select: none; - } -`; - -const ControlsContainer = styled.div` - position: absolute; - bottom: 0; - z-index: 50; - width: 100%; - opacity: 0; - transition: all 0.2s ease-in-out; -`; - -const DetailSection = styled.div` - display: flex; - flex-direction: column; -`; - -const Row = styled.div` - height: 25px; - padding: 0 0.2rem; -`; - -export const GridCard = ({ data, index, style }: any) => { - const { - itemHeight, - itemWidth, - columnCount, - itemGap, - itemCount, - cardControls, - handlePlayQueueAdd, - cardRows, - itemData, - itemType, - } = data; - - const startIndex = index * columnCount; - const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1); - const cards = []; - - for (let i = startIndex; i <= stopIndex; i += 1) { - if (itemData[i]) { - cards.push( - - - - - {itemData[i]?.imageUrl ? ( - - ) : ( -
- -
- )} - - - - -
- - - {cardRows.map((row: CardRow) => ( - - - {itemData[i] && itemData[i][row.prop]} - - - ))} - -
-
- ); - } else { - cards.push( - - - - - - - {cardRows.map((row: CardRow, index: number) => ( - 0 ? '50%' : '90%') : '100%'} - > - - - ))} - - - - ); - } - } - - return ( -
- {cards} -
- ); -}; diff --git a/src/renderer/components/virtual-grid/grid-card-controls.tsx b/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx similarity index 67% rename from src/renderer/components/virtual-grid/grid-card-controls.tsx rename to src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx index 6eaff11d3..76497e48b 100644 --- a/src/renderer/components/virtual-grid/grid-card-controls.tsx +++ b/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { MouseEvent } from 'react'; import styled from '@emotion/styled'; -import { Box, UnstyledButton, Group, UnstyledButtonProps } from '@mantine/core'; +import { Group, UnstyledButtonProps } from '@mantine/core'; import { motion } from 'framer-motion'; import { RiPlayFill, @@ -15,30 +15,25 @@ import { Play } from '@/renderer/types'; type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>; -const PlayButton = styled(UnstyledButton)` +const PlayButton = styled(motion.button)` display: flex; align-items: center; justify-content: center; width: 50px; height: 50px; background-color: rgb(255, 255, 255); + border: none; border-radius: 50%; - opacity: 0.8; transition: opacity 0.2s ease-in-out; transition: scale 0.2s ease-in; - &:hover { - opacity: 1; - scale: 1.1; - } - svg { fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); } `; -const GridCardControlsContainer = styled(Box)` +const GridCardControlsContainer = styled.div` display: flex; flex-direction: column; align-items: center; @@ -90,7 +85,11 @@ export const GridCardControls = ({ { + animate={{ opacity: 0.6 }} + whileHover={{ opacity: 1, scale: 1.1 }} + whileTap={{ scale: 1 }} + onClick={(e) => { + e.preventDefault(); handlePlayQueueAdd({ byItemType: { id: itemData.id, @@ -114,13 +113,43 @@ export const GridCardControls = ({ - - Play next - Play later + { + e.preventDefault(); + handlePlayQueueAdd({ + byItemType: { + id: itemData.id, + type: itemType, + }, + play: Play.LAST, + }); + }} + > + Play later + + { + e.preventDefault(); + handlePlayQueueAdd({ + byItemType: { + id: itemData.id, + type: itemType, + }, + play: Play.NEXT, + }); + }} + > + Play next + Add to playlist diff --git a/src/renderer/components/virtual-grid/grid-card/index.tsx b/src/renderer/components/virtual-grid/grid-card/index.tsx new file mode 100644 index 000000000..27930ee90 --- /dev/null +++ b/src/renderer/components/virtual-grid/grid-card/index.tsx @@ -0,0 +1,61 @@ +import { ListChildComponentProps } from 'react-window'; +import { PosterCard } from '@/renderer/components/virtual-grid/grid-card/poster-card'; +import { GridCardData } from '@/renderer/types'; + +export const GridCard = ({ + data, + index, + style, + isScrolling, +}: ListChildComponentProps) => { + const { + itemHeight, + itemWidth, + columnCount, + itemGap, + itemCount, + cardControls, + handlePlayQueueAdd, + cardRows, + itemData, + itemType, + route, + display, + } = data as GridCardData; + + const startIndex = index * columnCount; + const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1); + const cards = []; + + for (let i = startIndex; i <= stopIndex; i += 1) { + cards.push( + + ); + } + + return ( +
+ {cards} +
+ ); +}; diff --git a/src/renderer/components/virtual-grid/grid-card/poster-card.tsx b/src/renderer/components/virtual-grid/grid-card/poster-card.tsx new file mode 100644 index 000000000..44a3c3ca3 --- /dev/null +++ b/src/renderer/components/virtual-grid/grid-card/poster-card.tsx @@ -0,0 +1,338 @@ +import styled from '@emotion/styled'; +import { Center, Skeleton } from '@mantine/core'; +import { RiAlbumFill } from 'react-icons/ri'; +import { generatePath } from 'react-router'; +import { Link } from 'react-router-dom'; +import { ListChildComponentProps } from 'react-window'; +import { Text } from '@/renderer/components/text'; +import { GridCardControls } from '@/renderer/components/virtual-grid/grid-card/grid-card-controls'; +import { fadeIn } from '@/renderer/styles'; +import { + PlayQueueAddOptions, + LibraryItem, + CardRow, + CardRoute, +} from '@/renderer/types'; + +const CardWrapper = styled.div<{ + itemGap: number; + itemHeight: number; + itemWidth: number; +}>` + flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`}; + width: ${({ itemWidth }) => `${itemWidth}px`}; + height: ${({ itemHeight }) => `${itemHeight}px`}; + margin: ${({ itemGap }) => `0 ${itemGap / 2}px`}; + transition: border 0.2s ease-in-out; + user-select: none; + pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682 + + &:hover div { + opacity: 1; + } + + &:hover * { + &::before { + opacity: 0.5; + } + } + + &:focus-visible { + outline: 1px solid #fff; + } +`; + +const StyledCard = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + height: 100%; + padding: 0; + border-radius: 5px; +`; + +const ImageSection = styled.div` + position: relative; + width: 100%; + border-radius: 5px; + + &::before { + position: absolute; + top: 0; + left: 0; + z-index: 1; + width: 100%; + height: 100%; + background: linear-gradient( + 0deg, + rgba(0, 0, 0, 100%) 35%, + rgba(0, 0, 0, 0%) 100% + ); + opacity: 0; + transition: all 0.2s ease-in-out; + content: ''; + user-select: none; + } +`; + +interface ImageProps { + height: number; + isLoading?: boolean; +} + +const Image = styled.img` + ${fadeIn}; + width: ${({ height }) => `${height}px`}; + height: ${({ height }) => `${height}px`}; + object-fit: cover; + border: 0; + border-radius: 2px; +`; + +const ControlsContainer = styled.div` + position: absolute; + bottom: 0; + z-index: 50; + width: 100%; + opacity: 0; + transition: all 0.2s ease-in-out; +`; + +const DetailSection = styled.div` + display: flex; + flex-direction: column; +`; + +const Row = styled.div<{ secondary?: boolean }>` + width: 100%; + max-width: 100%; + height: 25px; + padding: 0 0.2rem; + overflow: hidden; + color: ${({ secondary }) => + secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)'}; + white-space: nowrap; + text-overflow: ellipsis; +`; + +interface BaseGridCardProps { + columnIndex: number; + controls: { + cardControls: any[]; + cardRows: CardRow[]; + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; + itemType: LibraryItem; + route?: CardRoute; + }; + data: any; + listChildProps: Omit; + sizes: { + itemGap: number; + itemHeight: number; + itemWidth: number; + }; +} + +export const PosterCard = ({ + listChildProps, + data, + columnIndex, + controls, + sizes, +}: BaseGridCardProps) => { + const { isScrolling, index } = listChildProps; + const { itemGap, itemHeight, itemWidth } = sizes; + const { cardControls, handlePlayQueueAdd, itemType, cardRows, route } = + controls; + + if (data) { + return ( + + + {route ? ( + { + return { + ...acc, + [slug.slugProperty]: data[slug.idProperty], + }; + }, {}) + )} + > + + {data?.imageUrl ? ( + + ) : ( +
+ +
+ )} + + {!isScrolling && ( + + )} + +
+ + ) : ( + + {data?.imageUrl ? ( + + ) : ( +
+ +
+ )} + + {!isScrolling && ( + + )} + +
+ )} + + {cardRows.map((row: CardRow, index: number) => { + if (row.arrayProperty) { + if (row.route) { + return ( + 0}> + {data[row.property].map( + (item: any, itemIndex: number) => ( + <> + {itemIndex > 0 && ( + + , + + )}{' '} + 0} + to={generatePath( + row.route!.route, + row.route!.slugs?.reduce((acc, slug) => { + return { + ...acc, + [slug.slugProperty]: data[slug.idProperty], + }; + }, {}) + )} + > + {row.arrayProperty && item[row.arrayProperty]} + + + ) + )} + + ); + } + + return ( + + {data[row.property].map((item: any) => ( + 0}> + {row.arrayProperty && item[row.arrayProperty]} + + ))} + + ); + } + + return ( + + {row.route ? ( + { + return { + ...acc, + [slug.slugProperty]: data[slug.idProperty], + }; + }, {}) + )} + > + {data && data[row.property]} + + ) : ( + 0}> + {data && data[row.property]} + + )} + + ); + })} + +
+
+ ); + } + + return ( + + + + + + + {cardRows.map((row: CardRow, index: number) => ( + 0 ? '50%' : '90%') : '100%'} + > + + + ))} + + + + ); +}; diff --git a/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx b/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx index 6f09f7fc5..546369dd5 100644 --- a/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx +++ b/src/renderer/components/virtual-grid/virtual-grid-wrapper.tsx @@ -3,7 +3,12 @@ import styled from '@emotion/styled'; import { FixedSizeList, FixedSizeListProps } from 'react-window'; import { GridCard } from '@/renderer/components/virtual-grid/grid-card'; import { usePlayQueueHandler } from '@/renderer/features/player/hooks/use-playqueue-handler'; -import { CardRow, LibraryItem } from '@/renderer/types'; +import { + CardRow, + LibraryItem, + CardDisplayType, + CardRoute, +} from '@/renderer/types'; export const VirtualGridWrapper = ({ refInstance, @@ -16,16 +21,19 @@ export const VirtualGridWrapper = ({ columnCount, rowCount, itemData, + route, ...rest }: Omit & { cardRows: CardRow[]; columnCount: number; + display: CardDisplayType; itemData: any[]; itemGap: number; itemHeight: number; itemType: LibraryItem; itemWidth: number; refInstance: Ref; + route?: CardRoute; rowCount: number; }) => { const handlePlayQueueAdd = usePlayQueueHandler(); @@ -41,6 +49,7 @@ export const VirtualGridWrapper = ({ itemHeight, itemType, itemWidth, + route, }), [ cardRows, @@ -51,6 +60,7 @@ export const VirtualGridWrapper = ({ itemData, itemGap, itemHeight, + route, itemWidth, ] ); @@ -59,16 +69,21 @@ export const VirtualGridWrapper = ({ {GridCard} ); }; +VirtualGridWrapper.defaultProps = { + route: undefined, +}; + export const VirtualGridContainer = styled.div` display: flex; flex-direction: column; diff --git a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx index 2b6456d2e..a990e6d1a 100644 --- a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx +++ b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx @@ -3,11 +3,17 @@ import debounce from 'lodash/debounce'; import { FixedSizeListProps } from 'react-window'; import InfiniteLoader from 'react-window-infinite-loader'; import { VirtualGridWrapper } from '@/renderer/components/virtual-grid/virtual-grid-wrapper'; -import { CardRow, LibraryItem } from '@/renderer/types'; +import { + CardDisplayType, + CardRoute, + CardRow, + LibraryItem, +} from '@/renderer/types'; interface VirtualGridProps extends Omit { cardRows: CardRow[]; + display?: CardDisplayType; fetchFn: (options: { columnCount: number; skip: number; @@ -17,6 +23,8 @@ interface VirtualGridProps itemSize: number; itemType: LibraryItem; minimumBatchSize?: number; + refresh?: any; // Pass in any value to refresh the grid when changed + route?: CardRoute; } export const VirtualInfiniteGrid = ({ @@ -25,10 +33,13 @@ export const VirtualInfiniteGrid = ({ itemSize, itemType, cardRows, + route, + display, minimumBatchSize, fetchFn, height, width, + refresh, }: VirtualGridProps) => { const [itemData, setItemData] = useState([]); const listRef = useRef(null); @@ -88,7 +99,7 @@ export const VirtualInfiniteGrid = ({ loadMoreItems(0, minimumBatchSize! * 2); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [minimumBatchSize, fetchFn]); + }, [minimumBatchSize, fetchFn, refresh]); return ( { const queryClient = useQueryClient(); @@ -12,14 +12,7 @@ export const usePlayQueueHandler = () => { const { serverToken, isImageTokenRequired } = useServerCredential(); const addToQueue = usePlayerStore((state) => state.addToQueue); - const handlePlayQueueAdd = async (options: { - byData?: any[]; - byItemType?: { - id: string; - type: LibraryItem; - }; - play: Play; - }) => { + const handlePlayQueueAdd = async (options: PlayQueueAddOptions) => { if (options.byData) { // dispatchSongsToQueue(options.byData, options.play); } @@ -65,6 +58,7 @@ export const usePlayQueueHandler = () => { mpvPlayer.setQueueNext(playerData); } else { mpvPlayer.setQueue(playerData); + mpvPlayer.play(); } } }; diff --git a/src/renderer/types.ts b/src/renderer/types.ts index bed59c0a6..b75fa01dc 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -1,12 +1,24 @@ import { AppRoute } from './router/routes'; -export interface CardRow { - align?: 'left' | 'center' | 'right'; - prop: string; - route?: { - prop: string; - route: AppRoute | string; - }; +export type RouteSlug = { + idProperty: string; + slugProperty: string; +}; + +export type CardRoute = { + route: AppRoute | string; + slugs?: RouteSlug[]; +}; + +export type CardRow = { + arrayProperty?: string; + property: string; + route?: CardRoute; +}; + +export enum CardDisplayType { + CARD = 'card', + POSTER = 'poster', } export enum LibraryItem { @@ -74,3 +86,27 @@ export enum SortOrder { ASC = 'asc', DESC = 'desc', } + +export type PlayQueueAddOptions = { + byData?: any[]; + byItemType?: { + id: string; + type: LibraryItem; + }; + play: Play; +}; + +export type GridCardData = { + cardControls: any; + cardRows: CardRow[]; + columnCount: number; + display: CardDisplayType; + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; + itemCount: number; + itemData: any[]; + itemGap: number; + itemHeight: number; + itemType: LibraryItem; + itemWidth: number; + route?: CardRoute; +};