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 d8ca80ba..8d0b0bd1 100644 --- a/src/renderer/components/virtual-grid/grid-card/default-card.tsx +++ b/src/renderer/components/virtual-grid/grid-card/default-card.tsx @@ -1,37 +1,56 @@ -import { Center } from '@mantine/core'; -import { RiAlbumFill } from 'react-icons/ri'; -import { generatePath, useNavigate } from 'react-router'; -import type { ListChildComponentProps } from 'react-window'; +import { generatePath, Link } from 'react-router-dom'; +import { ListChildComponentProps } from 'react-window'; import styled from 'styled-components'; -import type { 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'; import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types'; import { CardRows } from '/@/renderer/components/card'; +import { Skeleton } from '/@/renderer/components/skeleton'; +import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls'; +import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types'; -const CardWrapper = styled.div<{ - itemGap: number; - itemHeight: number; - itemWidth: number; - link?: boolean; -}>` - flex: ${({ itemWidth }) => `0 0 ${itemWidth - 12}px`}; - width: ${({ itemWidth }) => `${itemWidth}px`}; - height: ${({ itemHeight, itemGap }) => `${itemHeight - 12 - itemGap}px`}; - margin: ${({ itemGap }) => `0 ${itemGap / 2}px`}; - padding: 12px 12px 0; +interface BaseGridCardProps { + columnIndex: number; + controls: { + cardRows: CardRow[]; + handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void; + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; + itemType: LibraryItem; + playButtonBehavior: Play; + route: CardRoute; + }; + data: any; + isHidden?: boolean; + listChildProps: Omit; +} + +const DefaultCardContainer = styled.div<{ $isHidden?: boolean }>` + display: flex; + flex-direction: column; + width: 100%; + height: calc(100% - 2rem); + margin: 1rem; + overflow: hidden; background: var(--card-default-bg); border-radius: var(--card-default-radius); - cursor: ${({ link }) => link && 'pointer'}; - transition: border 0.2s ease-in-out, background 0.2s ease-in-out; - user-select: none; - pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682 + opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)}; &:hover { background: var(--card-default-bg-hover); } +`; - &:hover div { +const InnerCardContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: 1rem; + overflow: hidden; + + .card-controls { + opacity: 0; + } + + &:hover .card-controls { opacity: 1; } @@ -40,26 +59,16 @@ const CardWrapper = styled.div<{ 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: var(--card-default-radius); -`; - -const ImageSection = styled.div<{ size?: number }>` +const ImageContainer = styled.div` position: relative; - width: ${({ size }) => size && `${size - 24}px`}; - height: ${({ size }) => size && `${size - 24}px`}; + display: flex; + align-items: center; + height: 100%; + aspect-ratio: 1/1; + overflow: hidden; + background: var(--placeholder-bg); border-radius: var(--card-default-radius); &::before { @@ -78,151 +87,77 @@ const ImageSection = styled.div<{ size?: number }>` `; const Image = styled.img` - object-fit: cover; - border-radius: var(--card-default-radius); - box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 20%); -`; - -const ControlsContainer = styled.div` - position: absolute; - bottom: 0; - z-index: 50; width: 100%; - opacity: 0; - transition: all 0.2s ease-in-out; + max-width: 100%; + height: auto; + object-fit: contain; + border: 0; `; -const DetailSection = styled.div` - display: flex; - flex-direction: column; +const DetailContainer = styled.div` + margin-top: 0.5rem; `; -interface BaseGridCardProps { - columnIndex: number; - controls: { - cardRows: CardRow[]; - handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void; - handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; - itemType: LibraryItem; - playButtonBehavior: Play; - route: CardRoute; - }; - data: any; - listChildProps: Omit; - sizes: { - itemGap: number; - itemHeight: number; - itemWidth: number; - }; -} - export const DefaultCard = ({ listChildProps, data, columnIndex, controls, - sizes, + isHidden, }: BaseGridCardProps) => { - const navigate = useNavigate(); - const { index } = listChildProps; - const { itemGap, itemHeight, itemWidth } = sizes; - const { itemType, cardRows, route, handlePlayQueueAdd } = controls; - - const cardSize = itemWidth - 24; - if (data) { return ( - - navigate( - generatePath( - route.route, - route.slugs?.reduce((acc, slug) => { - return { - ...acc, - [slug.slugProperty]: data[slug.idProperty], - }; - }, {}), - ), - ) - } - > - - - {data?.imageUrl ? ( + + { + return { + ...acc, + [slug.slugProperty]: data[slug.idProperty], + }; + }, {}), + )} + > + + - ) : ( -
- -
- )} - - -
- - - -
-
+ + + + + + + ); } return ( - - - - - - - {cardRows.map((row: CardRow, index: number) => ( - 0 ? '50%' : '90%') : '100%'} - /> - ))} - - - + + + + + + ); }; 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 d9ef82cc..208a9aa2 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 @@ -1,8 +1,7 @@ import type { MouseEvent } from 'react'; import React from 'react'; import type { UnstyledButtonProps } from '@mantine/core'; -import { Group } from '@mantine/core'; -import { RiPlayFill, RiMore2Fill, RiHeartFill, RiHeartLine } from 'react-icons/ri'; +import { RiPlayFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri'; import styled from 'styled-components'; import { _Button } from '/@/renderer/components/button'; import type { PlayQueueAddOptions } from '/@/renderer/types'; @@ -18,6 +17,7 @@ import { type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>; const PlayButton = styled.button` + position: absolute; display: flex; align-items: center; justify-content: center; @@ -28,7 +28,7 @@ const PlayButton = styled.button` border-radius: 50%; opacity: 0.8; transition: opacity 0.2s ease-in-out; - transition: scale 0.2s linear; + transition: scale 0.1s ease-in-out; &:hover { opacity: 1; @@ -63,6 +63,8 @@ const SecondaryButton = styled(_Button)` `; const GridCardControlsContainer = styled.div` + position: absolute; + z-index: 100; display: flex; flex-direction: column; align-items: center; @@ -76,24 +78,13 @@ const ControlsRow = styled.div` height: calc(100% / 3); `; -// const TopControls = styled(ControlsRow)` -// display: flex; -// align-items: flex-start; -// justify-content: space-between; -// padding: 0.5rem; -// `; - -// const CenterControls = styled(ControlsRow)` -// display: flex; -// align-items: center; -// justify-content: center; -// padding: 0.5rem; -// `; - const BottomControls = styled(ControlsRow)` + position: absolute; + bottom: 0; display: flex; + gap: 0.5rem; align-items: flex-end; - justify-content: space-between; + justify-content: flex-end; padding: 1rem 0.5rem; `; @@ -146,45 +137,43 @@ export const GridCardControls = ({ ); return ( - + + + + - - - - - - - {itemData?.userFavorite ? ( - - ) : ( - - )} - - - { - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(e, [itemData]); - }} - > - - - + + + {itemData?.userFavorite ? ( + + ) : ( + + )} + + + { + e.preventDefault(); + e.stopPropagation(); + handleContextMenu(e, [itemData]); + }} + > + + ); diff --git a/src/renderer/components/virtual-grid/grid-card/index.tsx b/src/renderer/components/virtual-grid/grid-card/index.tsx index b1a48e7e..11c2e9de 100644 --- a/src/renderer/components/virtual-grid/grid-card/index.tsx +++ b/src/renderer/components/virtual-grid/grid-card/index.tsx @@ -3,15 +3,11 @@ import type { ListChildComponentProps } from 'react-window'; import { areEqual } from 'react-window'; import { DefaultCard } from '/@/renderer/components/virtual-grid/grid-card/default-card'; import { PosterCard } from '/@/renderer/components/virtual-grid/grid-card/poster-card'; -import type { GridCardData } from '/@/renderer/types'; -import { ListDisplayType } from '/@/renderer/types'; +import { GridCardData, ListDisplayType } from '/@/renderer/types'; export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => { const { - itemHeight, - itemWidth, columnCount, - itemGap, itemCount, cardRows, itemData, @@ -27,9 +23,14 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) = const startIndex = index * columnCount; const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1); + const columnCountInRow = stopIndex - startIndex + 1; + let columnCountToAdd = 0; + if (columnCountInRow !== columnCount) { + columnCountToAdd = columnCount - columnCountInRow; + } const View = display === ListDisplayType.CARD ? DefaultCard : PosterCard; - for (let i = startIndex; i <= stopIndex; i += 1) { + for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) { cards.push( stopIndex} listChildProps={{ index }} - sizes={{ itemGap, itemHeight, itemWidth }} />, ); } return ( - <> -
- {cards} -
- +
+ {cards} +
); }, areEqual); 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 07582c27..65c4a093 100644 --- a/src/renderer/components/virtual-grid/grid-card/poster-card.tsx +++ b/src/renderer/components/virtual-grid/grid-card/poster-card.tsx @@ -1,28 +1,41 @@ -import { Center } from '@mantine/core'; -import { RiAlbumFill } from 'react-icons/ri'; -import { generatePath } from 'react-router'; -import { Link } from 'react-router-dom'; -import type { ListChildComponentProps } from 'react-window'; +import { generatePath, Link } from 'react-router-dom'; +import { ListChildComponentProps } from 'react-window'; import styled from 'styled-components'; -import { Skeleton } from '/@/renderer/components/skeleton'; -import type { CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types'; -import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls'; -import { Album, Artist, AlbumArtist, LibraryItem } from '/@/renderer/api/types'; +import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types'; import { CardRows } from '/@/renderer/components/card'; +import { Skeleton } from '/@/renderer/components/skeleton'; +import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls'; +import { CardRow, PlayQueueAddOptions, Play, 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, itemGap }) => `${itemHeight - itemGap}px`}; - margin: ${({ itemGap }) => `0 ${itemGap / 2}px`}; - user-select: none; - pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682 +interface BaseGridCardProps { + columnIndex: number; + controls: { + cardRows: CardRow[]; + handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void; + handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; + itemType: LibraryItem; + playButtonBehavior: Play; + route: CardRoute; + }; + data: any; + isHidden?: boolean; + listChildProps: Omit; +} - &:hover div { +const PosterCardContainer = styled.div<{ $isHidden?: boolean }>` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: 1rem; + overflow: hidden; + opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)}; + + .card-controls { + opacity: 0; + } + + &:hover .card-controls { opacity: 1; } @@ -31,30 +44,16 @@ const CardWrapper = styled.div<{ 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; - background: var(--card-poster-bg); - border-radius: var(--card-poster-radius); - - &:hover { - background: var(--card-poster-bg-hover); - } -`; - -const ImageSection = styled.div` +const ImageContainer = styled.div` position: relative; - width: 100%; + display: flex; + align-items: center; + height: 100%; + aspect-ratio: 1/1; + overflow: hidden; + background: var(--card-default-bg); border-radius: var(--card-poster-radius); &::before { @@ -72,148 +71,74 @@ const ImageSection = styled.div` } `; -interface ImageProps { - height: number; - isLoading?: boolean; -} - -const Image = styled.img` +const Image = styled.img` + width: 100%; + max-width: 100%; + height: auto; object-fit: cover; border: 0; - border-radius: var(--card-poster-radius); `; -const ControlsContainer = styled.div` - position: absolute; - bottom: 0; - z-index: 50; - width: 100%; - opacity: 0; - transition: all 0.2s ease-in-out; +const DetailContainer = styled.div` + margin-top: 0.5rem; `; -const DetailSection = styled.div` - display: flex; - flex-direction: column; -`; - -interface BaseGridCardProps { - columnIndex: number; - controls: { - cardRows: CardRow[]; - handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void; - handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; - itemType: LibraryItem; - playButtonBehavior: Play; - route: CardRoute; - }; - data: any; - listChildProps: Omit; - sizes: { - itemGap: number; - itemHeight: number; - itemWidth: number; - }; -} - export const PosterCard = ({ listChildProps, data, columnIndex, controls, - sizes, + isHidden, }: BaseGridCardProps) => { if (data) { return ( - - - { - return { - ...acc, - [slug.slugProperty]: data[slug.idProperty], - }; - }, {}), - )} - > - - {data?.imageUrl ? ( - - ) : ( -
- -
- )} - - - -
- - - + { + return { + ...acc, + [slug.slugProperty]: data[slug.idProperty], + }; + }, {}), + )} + > + + - -
-
+ + + + + + + ); } return ( - - + - - - - {controls.cardRows.map((row: CardRow, index: number) => ( - 0 ? '50%' : '90%') : '100%'} - /> - ))} - - - + /> + + ); }; diff --git a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx index 6402dc95..d9c2d430 100644 --- a/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx +++ b/src/renderer/components/virtual-grid/virtual-infinite-grid.tsx @@ -29,14 +29,6 @@ interface VirtualGridProps extends Omit void; } -// const constrainWidth = (width: number) => { -// if (width < 1920) { -// return width; -// } - -// return 1920; -// }; - export const VirtualInfiniteGrid = forwardRef( ( { @@ -64,16 +56,20 @@ export const VirtualInfiniteGrid = forwardRef( const listRef = useRef(null); const loader = useRef(null); + const sz = itemSize / 2; + const { itemHeight, rowCount, columnCount } = useMemo(() => { - const itemsPerRow = Math.floor((Number(width) - itemGap + 3) / (itemSize! + itemGap + 2)); + const itemsPerRow = Math.floor(Number(width) / sz!) - 1; + const widthPerItem = Number(width) / itemsPerRow - 10; + const itemHeight = widthPerItem + cardRows.length * 26; return { columnCount: itemsPerRow, - itemHeight: itemSize! + cardRows.length * 22 + itemGap, - itemWidth: itemSize! + itemGap, + itemHeight, + itemWidth: sz, rowCount: Math.ceil(itemCount / itemsPerRow), }; - }, [cardRows.length, itemCount, itemGap, itemSize, width]); + }, [cardRows.length, itemCount, sz, width]); const isItemLoaded = useCallback( (index: number) => { @@ -153,7 +149,7 @@ export const VirtualInfiniteGrid = forwardRef( itemCount={itemCount || 0} itemData={itemData} itemGap={itemGap} - itemHeight={itemHeight + itemGap / 2} + itemHeight={itemHeight} itemType={itemType} itemWidth={itemSize} refInstance={(list) => {