Improve grid

This commit is contained in:
jeffvli
2022-10-28 13:02:55 -07:00
parent 2352c136a1
commit 00710125d3
4 changed files with 193 additions and 129 deletions
@@ -1,9 +1,16 @@
import React from 'react';
import styled from '@emotion/styled';
import { Button, UnstyledButton, UnstyledButtonProps } from '@mantine/core';
import { Box, UnstyledButton, Group, UnstyledButtonProps } from '@mantine/core';
import { motion } from 'framer-motion';
import { RiPlayFill } from 'react-icons/ri';
import { Play } from '../../../types';
import {
RiPlayFill,
RiMore2Fill,
RiHeartFill,
RiHeartLine,
} from 'react-icons/ri';
import { Button } from '@/renderer/components/button';
import { DropdownMenu } from '@/renderer/components/dropdown-menu';
import { Play } from '@/renderer/types';
type PlayButtonType = UnstyledButtonProps &
React.ComponentPropsWithoutRef<'button'>;
@@ -14,23 +21,24 @@ const PlayButton = styled(UnstyledButton)<PlayButtonType>`
justify-content: center;
width: 50px;
height: 50px;
border: 1px solid var(--primary-color);
background-color: rgb(255, 255, 255);
border-radius: 50%;
cursor: default;
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
transition: scale 0.2s ease-in;
&:hover {
opacity: 1;
scale: 1.1;
}
svg {
fill: #000;
stroke: #000;
fill: rgb(0, 0, 0);
stroke: rgb(0, 0, 0);
}
`;
const GridCardControlsContainer = styled.div`
const GridCardControlsContainer = styled(Box)`
display: flex;
flex-direction: column;
align-items: center;
@@ -48,36 +56,45 @@ 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)`
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 1rem 0.5rem;
`;
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
svg {
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
}
`;
export const GridCardControls = ({
itemData,
itemType,
handlePlayQueueAdd,
cardControls,
}: any) => {
return (
<GridCardControlsContainer>
<TopControls />
<CenterControls animate={{ opacity: 1 }} initial={{ opacity: 0 }}>
<CenterControls animate={{ opacity: 1 }} initial={{ opacity: 0 }} />
<BottomControls>
<PlayButton
onClick={() => {
handlePlayQueueAdd({
byItemType: {
endpoint: cardControls.endpoint,
id: itemData[cardControls.idProperty],
type: cardControls.type,
id: itemData.id,
type: itemType,
},
play: Play.NOW,
});
@@ -85,36 +102,32 @@ export const GridCardControls = ({
>
<RiPlayFill size={25} />
</PlayButton>
</CenterControls>
<BottomControls>
<Button
onClick={() => {
handlePlayQueueAdd({
byItemType: {
endpoint: cardControls.endpoint,
id: itemData[cardControls.idProperty],
type: cardControls.type,
},
play: Play.NEXT,
});
}}
>
NEXT
</Button>
<Button
onClick={() => {
handlePlayQueueAdd({
byItemType: {
endpoint: cardControls.endpoint,
id: itemData[cardControls.idProperty],
type: cardControls.type,
},
play: Play.LAST,
});
}}
>
LATER
</Button>
<Group spacing="xs">
<Button disabled p={5} variant="subtle">
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
{itemData?.isFavorite ? (
<RiHeartFill size={20} />
) : (
<RiHeartLine color="white" size={20} />
)}
</FavoriteWrapper>
</Button>
<DropdownMenu withinPortal position="bottom-start">
<DropdownMenu.Target>
<Button p={5} variant="subtle">
<RiMore2Fill color="white" size={20} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item>Play next</DropdownMenu.Item>
<DropdownMenu.Item>Play later</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Item disabled>Add to playlist</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Item disabled>Refresh metadata</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
</BottomControls>
</GridCardControlsContainer>
);
@@ -1,12 +1,11 @@
import React from 'react';
import styled from '@emotion/styled';
import { Skeleton } from '@mantine/core';
import { motion } from 'framer-motion';
import { GridCardControls } from '@/renderer/components/virtual-grid/grid-card-controls';
import { fadeIn } from '@/renderer/styles';
import { CardRow } from '../../types';
import { Text } from '../text';
import { GridCardControls } from './grid-card-controls';
const CardWrapper = styled(motion.div)<{
const CardWrapper = styled.div<{
itemGap: number;
itemHeight: number;
itemWidth: number;
@@ -15,11 +14,20 @@ const CardWrapper = styled(motion.div)<{
width: ${({ itemWidth }) => `${itemWidth}px`};
height: ${({ itemHeight }) => `${itemHeight}px`};
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
filter: drop-shadow(0 4px 4px #000);
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;
}
@@ -37,8 +45,8 @@ const StyledCard = styled.div`
`;
const ImageSection = styled.div`
position: relative;
width: 100%;
height: 100%;
`;
interface ImageProps {
@@ -46,31 +54,42 @@ interface ImageProps {
src: string;
}
// const Image = styled(motion.div).attrs((props: ImageProps) => ({
// style: {
// background: `url(${props.src})`,
// backgroundPosition: 'center',
// backgroundSize: 'cover',
// },
// }))<ImageProps>`
// height: ${({ height }) => `${height}px`};
// background-position: center;
// background-size: cover;
// border: 0;
// `;
const Image = styled(motion.div)<ImageProps>`
const Image = styled.div<ImageProps>`
${fadeIn};
height: ${({ height }) => `${height}px`};
background: ${({ src }) => `url(${src})`};
background-image: ${({ src }) => `url(${src})`};
background-position: center;
background-size: cover;
border: 0;
border-radius: 5px;
transition: background-image 0.5s ease-in-out;
&::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`
display: block;
position: absolute;
bottom: 0;
z-index: 50;
width: 100%;
height: 100%;
opacity: 0;
transition: all 0.2s ease-in-out;
`;
const DetailSection = styled.div`
@@ -94,6 +113,7 @@ export const GridCard = ({ data, index, style }: any) => {
handlePlayQueueAdd,
cardRows,
itemData,
itemType,
} = data;
const startIndex = index * columnCount;
@@ -101,40 +121,67 @@ export const GridCard = ({ data, index, style }: any) => {
const cards = [];
for (let i = startIndex; i <= stopIndex; i += 1) {
cards.push(
<React.Fragment key={`card-${i}-${index}`}>
if (itemData[i]) {
cards.push(
<CardWrapper
key={`card-${i}-${index}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
>
<Skeleton visible={!itemData[i]}>
<StyledCard>
<ImageSection>
<Image height={itemWidth} src={itemData[i]?.imageUrl}>
<ControlsContainer>
<GridCardControls
cardControls={cardControls}
handlePlayQueueAdd={handlePlayQueueAdd}
itemData={itemData[i]}
/>
</ControlsContainer>
</Image>
</ImageSection>
<DetailSection>
{cardRows.map((row: CardRow) => (
<Row key={`row-${row.prop}`}>
<Text overflow="hidden" weight={500}>
{itemData[i] && itemData[i][row.prop]}
</Text>
</Row>
))}
</DetailSection>
</StyledCard>
</Skeleton>
<StyledCard>
<ImageSection style={{ height: `${itemWidth}px` }}>
<Image height={itemWidth} src={itemData[i]?.imageUrl} />
<ControlsContainer>
<GridCardControls
cardControls={cardControls}
handlePlayQueueAdd={handlePlayQueueAdd}
itemData={itemData[i]}
itemType={itemType}
/>
</ControlsContainer>
</ImageSection>
<DetailSection>
{cardRows.map((row: CardRow) => (
<Row>
<Text overflow="hidden" to={row.route?.route} weight={500}>
{itemData[i] && itemData[i][row.prop]}
</Text>
</Row>
))}
</DetailSection>
</StyledCard>
</CardWrapper>
</React.Fragment>
);
);
} else {
cards.push(
<CardWrapper
key={`card-${i}-${index}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
>
<StyledCard>
<Skeleton visible radius="sm">
<ImageSection style={{ height: `${itemWidth}px` }} />
</Skeleton>
<DetailSection>
{cardRows.map((row: CardRow, index: number) => (
<Skeleton
key={`row-${row.prop}`}
my={2}
radius="md"
visible={!itemData[i]}
width={!itemData[i] ? (index > 0 ? '50%' : '90%') : '100%'}
>
<Row />
</Skeleton>
))}
</DetailSection>
</StyledCard>
</CardWrapper>
);
}
}
return (
@@ -1,14 +1,15 @@
import { Ref, useMemo } from 'react';
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 } from '@/renderer/types';
import { CardRow, LibraryItem } from '@/renderer/types';
export const VirtualGridWrapper = ({
refInstance,
cardControls,
cardRows,
itemGap,
itemType,
itemWidth,
itemHeight,
itemCount,
@@ -17,12 +18,12 @@ export const VirtualGridWrapper = ({
itemData,
...rest
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
cardControls: any;
cardRows: CardRow[];
columnCount: number;
itemData: any[];
itemGap: number;
itemHeight: number;
itemType: LibraryItem;
itemWidth: number;
refInstance: Ref<any>;
rowCount: number;
@@ -31,7 +32,6 @@ export const VirtualGridWrapper = ({
const memo = useMemo(
() => ({
cardControls,
cardRows,
columnCount,
handlePlayQueueAdd,
@@ -39,11 +39,12 @@ export const VirtualGridWrapper = ({
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
}),
[
cardControls,
cardRows,
itemType,
columnCount,
handlePlayQueueAdd,
itemCount,
@@ -67,3 +68,13 @@ export const VirtualGridWrapper = ({
</FixedSizeList>
);
};
export const VirtualGridContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
export const VirtualGridAutoSizerContainer = styled.div`
flex: 1;
`;
@@ -3,31 +3,31 @@ 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 } from '@/renderer/types';
import { CardRow, LibraryItem } from '@/renderer/types';
interface VirtualGridProps
extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
cardControls: any;
cardRows: CardRow[];
itemGap?: number;
fetchFn: (options: {
columnCount: number;
skip: number;
take: number;
}) => Promise<any>;
itemGap: number;
itemSize: number;
itemType: LibraryItem;
minimumBatchSize?: number;
query: ({ serverId }: { serverId: string }, props: any) => Promise<any>;
queryParams?: Record<string, any>;
serverId: string;
}
export const VirtualInfiniteGrid = ({
itemCount,
itemGap,
itemSize,
cardControls,
itemType,
cardRows,
minimumBatchSize,
query,
queryParams,
fetchFn,
height,
serverId,
width,
}: VirtualGridProps) => {
const [itemData, setItemData] = useState<any[]>([]);
@@ -36,7 +36,7 @@ export const VirtualInfiniteGrid = ({
const { itemHeight, rowCount, columnCount } = useMemo(() => {
const itemsPerRow = Math.floor(
(Number(width) - itemGap! + 3) / (itemSize! + itemGap! + 2)
(Number(width) - itemGap + 3) / (itemSize! + itemGap + 2)
);
return {
@@ -60,20 +60,17 @@ export const VirtualInfiniteGrid = ({
const start = startIndex * columnCount;
const end = stopIndex * columnCount + columnCount;
const t = await query(
{ serverId },
{
skip: start,
take: end - start,
...queryParams,
}
);
const items = await fetchFn({
columnCount,
skip: start,
take: end - start,
});
const newData: any[] = [...itemData];
let itemIndex = 0;
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
newData[rowIndex] = t.data[itemIndex];
newData[rowIndex] = items.data[itemIndex];
itemIndex += 1;
}
@@ -85,36 +82,34 @@ export const VirtualInfiniteGrid = ({
useEffect(() => {
if (loader.current) {
listRef.current.scrollTo(0);
loader.current.resetloadMoreItemsCache(true);
loader.current.resetloadMoreItemsCache(false);
setItemData(() => []);
loadMoreItems(0, minimumBatchSize! * 2);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minimumBatchSize, queryParams, setItemData]);
}, [minimumBatchSize, fetchFn]);
return (
<InfiniteLoader
ref={loader}
isItemLoaded={(index) => isItemLoaded(index)}
itemCount={itemCount || 0}
loadMoreItems={(startIndex, stopIndex) =>
debouncedLoadMoreItems(startIndex, stopIndex)
}
loadMoreItems={debouncedLoadMoreItems}
minimumBatchSize={minimumBatchSize}
threshold={30}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
<VirtualGridWrapper
useIsScrolling
cardControls={cardControls}
cardRows={cardRows}
columnCount={columnCount}
height={height}
itemCount={itemCount || 0}
itemData={itemData}
itemGap={itemGap!}
itemHeight={itemHeight! + itemGap! / 2}
itemGap={itemGap}
itemHeight={itemHeight + itemGap / 2}
itemType={itemType}
itemWidth={itemSize}
refInstance={(list) => {
infiniteLoaderRef(list);
@@ -130,7 +125,5 @@ export const VirtualInfiniteGrid = ({
};
VirtualInfiniteGrid.defaultProps = {
itemGap: 10,
minimumBatchSize: 20,
queryParams: {},
};