mirror of
https://github.com/jeffvli/sonixd.git
synced 2026-04-29 02:32:37 -04:00
Add infinite grid
This commit is contained in:
59
src/renderer/components/virtual-grid/GridCard.tsx
Normal file
59
src/renderer/components/virtual-grid/GridCard.tsx
Normal file
@@ -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(
|
||||
<CardWrapper
|
||||
key={`card-${i}`}
|
||||
itemGap={itemGap}
|
||||
itemHeight={itemHeight}
|
||||
itemWidth={itemWidth}
|
||||
whileHover={{ rotateX: 15, scale: 1.05 }}
|
||||
>
|
||||
<Card style={{ height: '100%', width: '100%' }}>
|
||||
<Card.Section>
|
||||
<Image src={itemData && itemData[i]?.image} />
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...style,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
{cards}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
src/renderer/components/virtual-grid/VirtualGridWrapper.tsx
Normal file
48
src/renderer/components/virtual-grid/VirtualGridWrapper.tsx
Normal file
@@ -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<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
|
||||
itemGap: number;
|
||||
itemHeight: number;
|
||||
itemWidth: number;
|
||||
refInstance: Ref<any>;
|
||||
}) => {
|
||||
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 (
|
||||
<FixedSizeList
|
||||
{...rest}
|
||||
ref={refInstance}
|
||||
initialScrollOffset={0}
|
||||
itemCount={rowCount}
|
||||
itemData={itemData}
|
||||
itemSize={itemHeight + itemGap}
|
||||
>
|
||||
{GridCard}
|
||||
</FixedSizeList>
|
||||
);
|
||||
};
|
||||
131
src/renderer/components/virtual-grid/VirtualInfiniteGrid.tsx
Normal file
131
src/renderer/components/virtual-grid/VirtualInfiniteGrid.tsx
Normal file
@@ -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<any>;
|
||||
queryParams?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const VirtualInfiniteGrid = forwardRef(
|
||||
(
|
||||
{
|
||||
itemCount,
|
||||
itemGap,
|
||||
itemWidth,
|
||||
itemHeight,
|
||||
minimumBatchSize,
|
||||
query,
|
||||
queryParams,
|
||||
}: VirtualGridProps,
|
||||
ref: Ref<InfiniteLoader>
|
||||
) => {
|
||||
const [itemData, setItemData] = useState<any[]>([]);
|
||||
|
||||
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<void>((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 (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
const columnCount = Math.floor(
|
||||
(Number(width) - itemGap! + 3) / (itemWidth! + itemGap! + 2)
|
||||
);
|
||||
|
||||
const pageItemLimit = columnCount * minimumBatchSize!;
|
||||
|
||||
return (
|
||||
<InfiniteLoader
|
||||
ref={ref}
|
||||
isItemLoaded={(index) => isItemLoaded(index, columnCount)}
|
||||
itemCount={itemCount || 0}
|
||||
loadMoreItems={(startIndex, stopIndex) =>
|
||||
debouncedLoadMoreItems(
|
||||
startIndex,
|
||||
stopIndex,
|
||||
pageItemLimit,
|
||||
columnCount
|
||||
)
|
||||
}
|
||||
minimumBatchSize={minimumBatchSize}
|
||||
threshold={10}
|
||||
>
|
||||
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
|
||||
<VirtualGridWrapper
|
||||
height={height}
|
||||
itemCount={itemCount || 0}
|
||||
itemData={itemData}
|
||||
itemGap={itemGap!}
|
||||
itemHeight={itemHeight!}
|
||||
itemWidth={itemWidth!}
|
||||
refInstance={infiniteLoaderRef}
|
||||
width={width}
|
||||
onItemsRendered={onItemsRendered}
|
||||
/>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
VirtualInfiniteGrid.defaultProps = {
|
||||
itemGap: 10,
|
||||
itemHeight: 200,
|
||||
itemWidth: 150,
|
||||
minimumBatchSize: 20,
|
||||
queryParams: {},
|
||||
};
|
||||
Reference in New Issue
Block a user