From 720413f4ff4ac9fc99d34e6bb242b9ed96bdd2ce Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 12 Jul 2022 23:59:32 -0700 Subject: [PATCH] Add infinite grid --- .../components/virtual-grid/GridCard.tsx | 59 ++++++++ .../virtual-grid/VirtualGridWrapper.tsx | 48 +++++++ .../virtual-grid/VirtualInfiniteGrid.tsx | 131 ++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 src/renderer/components/virtual-grid/GridCard.tsx create mode 100644 src/renderer/components/virtual-grid/VirtualGridWrapper.tsx create mode 100644 src/renderer/components/virtual-grid/VirtualInfiniteGrid.tsx diff --git a/src/renderer/components/virtual-grid/GridCard.tsx b/src/renderer/components/virtual-grid/GridCard.tsx new file mode 100644 index 0000000..7738cb9 --- /dev/null +++ b/src/renderer/components/virtual-grid/GridCard.tsx @@ -0,0 +1,59 @@ +import { Card, Image } from '@mantine/core'; +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +const CardWrapper = styled(motion.div)<{ + itemGap: number; + itemHeight: number; + itemWidth: number; +}>` + display: flex; + flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`}; + width: 100%; + height: ${({ itemHeight }) => itemHeight}px; + margin: ${({ itemGap }) => `0 ${itemGap / 2}px`}; + pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682 +`; + +export const GridCard = ({ data, index, style }: any) => { + const { itemHeight, itemWidth, columnCount, itemGap, itemCount, itemData } = + 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].type === ServerType.Jellyfin) { + // const image = getJellyfinImageUrl() + // } + cards.push( + + + + + + + + ); + } + + return ( +
+ {cards} +
+ ); +}; diff --git a/src/renderer/components/virtual-grid/VirtualGridWrapper.tsx b/src/renderer/components/virtual-grid/VirtualGridWrapper.tsx new file mode 100644 index 0000000..aa7a8b6 --- /dev/null +++ b/src/renderer/components/virtual-grid/VirtualGridWrapper.tsx @@ -0,0 +1,48 @@ +import { Ref, useMemo } from 'react'; +import { FixedSizeList, FixedSizeListProps } from 'react-window'; +import { GridCard } from './GridCard'; + +export const VirtualGridWrapper = ({ + refInstance, + itemGap, + itemWidth, + ...rest +}: Omit & { + itemGap: number; + itemHeight: number; + itemWidth: number; + refInstance: Ref; +}) => { + const itemHeight = itemWidth + 55; + + const columnCount = Math.floor( + (Number(rest.width) - itemGap + 3) / (itemWidth + itemGap + 2) + ); + + const rowCount = Math.ceil(rest.itemCount / columnCount); + + const itemData = useMemo( + () => ({ + columnCount, + itemCount: rest.itemCount, + itemData: rest.itemData, + itemGap, + itemHeight, + itemWidth, + }), + [columnCount, itemGap, itemHeight, itemWidth, rest.itemCount, rest.itemData] + ); + + return ( + + {GridCard} + + ); +}; diff --git a/src/renderer/components/virtual-grid/VirtualInfiniteGrid.tsx b/src/renderer/components/virtual-grid/VirtualInfiniteGrid.tsx new file mode 100644 index 0000000..a7afdf6 --- /dev/null +++ b/src/renderer/components/virtual-grid/VirtualInfiniteGrid.tsx @@ -0,0 +1,131 @@ +import { forwardRef, Ref, useState } from 'react'; +import debounce from 'lodash/debounce'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeListProps } from 'react-window'; +import InfiniteLoader from 'react-window-infinite-loader'; +import { VirtualGridWrapper } from './VirtualGridWrapper'; + +interface VirtualGridProps + extends Omit< + FixedSizeListProps, + 'children' | 'itemSize' | 'height' | 'width' + > { + itemGap?: number; + itemHeight?: number; + itemWidth?: number; + minimumBatchSize?: number; + query: (props: any) => Promise; + queryParams?: Record; +} + +export const VirtualInfiniteGrid = forwardRef( + ( + { + itemCount, + itemGap, + itemWidth, + itemHeight, + minimumBatchSize, + query, + queryParams, + }: VirtualGridProps, + ref: Ref + ) => { + const [itemData, setItemData] = useState([]); + + const isItemLoaded = (index: number, columnCount: number) => { + const itemIndex = index * columnCount; + + return ( + itemIndex < itemData.length * columnCount && + itemData[itemIndex] !== undefined + ); + }; + + const loadMoreItems = async ( + startIndex: number, + stopIndex: number, + limit: number, + columnCount: number + ) => { + const currentPage = Math.ceil(startIndex / minimumBatchSize!); + + const t = await query({ + limit, + page: currentPage, + ...queryParams, + }); + + // Need to multiply by columnCount due to the grid layout + const start = startIndex * columnCount; + const end = (stopIndex + 1) * columnCount; + + return new Promise((resolve) => { + const newData: any[] = [...itemData]; + + let itemIndex = 0; + for (let rowIndex = start; rowIndex < end; rowIndex += 1) { + newData[rowIndex] = t?.data[itemIndex]; + itemIndex += 1; + } + + setItemData(newData); + resolve(); + }); + }; + + const debouncedLoadMoreItems = debounce(loadMoreItems, 300); + + return ( + + {({ height, width }) => { + const columnCount = Math.floor( + (Number(width) - itemGap! + 3) / (itemWidth! + itemGap! + 2) + ); + + const pageItemLimit = columnCount * minimumBatchSize!; + + return ( + isItemLoaded(index, columnCount)} + itemCount={itemCount || 0} + loadMoreItems={(startIndex, stopIndex) => + debouncedLoadMoreItems( + startIndex, + stopIndex, + pageItemLimit, + columnCount + ) + } + minimumBatchSize={minimumBatchSize} + threshold={10} + > + {({ onItemsRendered, ref: infiniteLoaderRef }) => ( + + )} + + ); + }} + + ); + } +); + +VirtualInfiniteGrid.defaultProps = { + itemGap: 10, + itemHeight: 200, + itemWidth: 150, + minimumBatchSize: 20, + queryParams: {}, +};