mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 04:50:12 +02:00
220 lines
7.7 KiB
TypeScript
220 lines
7.7 KiB
TypeScript
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/renderer/types';
|
|
import type { FixedSizeListProps } from 'react-window';
|
|
|
|
import debounce from 'lodash/debounce';
|
|
import {
|
|
forwardRef,
|
|
Ref,
|
|
useCallback,
|
|
useImperativeHandle,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import InfiniteLoader from 'react-window-infinite-loader';
|
|
|
|
import { AnyLibraryItem, Genre, LibraryItem } from '/@/renderer/api/types';
|
|
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
|
import { ListDisplayType } from '/@/renderer/types';
|
|
|
|
export type VirtualInfiniteGridRef = {
|
|
resetLoadMoreItemsCache: () => void;
|
|
scrollTo: (index: number) => void;
|
|
setItemData: (data: LibraryItemOrGenre[]) => void;
|
|
updateItemData: (rule: (item: LibraryItemOrGenre) => LibraryItemOrGenre) => void;
|
|
};
|
|
|
|
type LibraryItemOrGenre = AnyLibraryItem | Genre;
|
|
|
|
interface VirtualGridProps
|
|
extends Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'width'> {
|
|
cardRows: CardRow<any>[];
|
|
display?: ListDisplayType;
|
|
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
|
|
fetchInitialData?: () => LibraryItemOrGenre[];
|
|
handleFavorite?: (options: {
|
|
id: string[];
|
|
isFavorite: boolean;
|
|
itemType: LibraryItem;
|
|
}) => void;
|
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
|
height?: number;
|
|
itemGap: number;
|
|
itemSize: number;
|
|
itemType: LibraryItem;
|
|
loading?: boolean;
|
|
minimumBatchSize?: number;
|
|
route?: CardRoute;
|
|
width?: number;
|
|
}
|
|
|
|
export const VirtualInfiniteGrid = forwardRef(
|
|
(
|
|
{
|
|
cardRows,
|
|
display,
|
|
fetchFn,
|
|
fetchInitialData,
|
|
handleFavorite,
|
|
handlePlayQueueAdd,
|
|
height,
|
|
initialScrollOffset,
|
|
itemCount,
|
|
itemGap,
|
|
itemSize,
|
|
itemType,
|
|
loading,
|
|
minimumBatchSize,
|
|
onScroll,
|
|
route,
|
|
width,
|
|
}: VirtualGridProps,
|
|
ref: Ref<VirtualInfiniteGridRef>,
|
|
) => {
|
|
const listRef = useRef<any>(null);
|
|
const loader = useRef<InfiniteLoader>(null);
|
|
const minItemCount = useRef(0);
|
|
|
|
// itemData can be a sparse array. Treat the intermediate elements as being undefined
|
|
const [itemData, setItemData] = useState<Array<LibraryItemOrGenre | undefined>>(
|
|
fetchInitialData?.() || [],
|
|
);
|
|
|
|
const { columnCount, itemHeight, rowCount } = useMemo(() => {
|
|
const itemsPerRow = width ? Math.floor(width / (itemSize + itemGap * 2)) : 5;
|
|
const widthPerItem = Number(width) / itemsPerRow;
|
|
const itemHeight = widthPerItem + cardRows.length * 26;
|
|
|
|
return {
|
|
columnCount: itemsPerRow,
|
|
itemHeight,
|
|
rowCount: Math.ceil(itemCount / itemsPerRow),
|
|
};
|
|
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
|
|
|
|
const isItemLoaded = useCallback(
|
|
(index: number) => {
|
|
const itemIndex = index * columnCount;
|
|
|
|
return itemData[itemIndex] !== undefined;
|
|
},
|
|
[columnCount, itemData],
|
|
);
|
|
|
|
const loadMoreItems = useCallback(
|
|
async (startIndex: number, stopIndex: number) => {
|
|
if (
|
|
// Fixes a caching bug(?) when switching between filters and the itemCount increases
|
|
startIndex === 1 ||
|
|
// Fixes a caching bug when refreshing items. Prevents a second
|
|
// refetch from happening if:
|
|
// 1: we are already in a refresh (-1)
|
|
// 2: we just had a refresh, and we are index 0
|
|
minItemCount.current === -1 ||
|
|
(minItemCount.current > 0 && startIndex === 0)
|
|
)
|
|
return;
|
|
|
|
// Need to multiply by columnCount due to the grid layout
|
|
const start = startIndex * columnCount;
|
|
const end = stopIndex * columnCount + columnCount;
|
|
|
|
const data = await fetchFn({
|
|
columnCount,
|
|
skip: start,
|
|
take: end - start,
|
|
});
|
|
|
|
setItemData((itemData) => {
|
|
const newData = [...itemData];
|
|
|
|
let itemIndex = 0;
|
|
for (let rowIndex = start; rowIndex < itemCount; rowIndex += 1) {
|
|
newData[rowIndex] = data.items[itemIndex];
|
|
itemIndex += 1;
|
|
}
|
|
|
|
return newData;
|
|
});
|
|
},
|
|
[columnCount, fetchFn, itemCount],
|
|
);
|
|
|
|
const debouncedLoadMoreItems = debounce(loadMoreItems, 500);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
resetLoadMoreItemsCache: () => {
|
|
if (loader.current) {
|
|
loader.current.resetloadMoreItemsCache(false);
|
|
minItemCount.current = -1;
|
|
setItemData([]);
|
|
}
|
|
},
|
|
scrollTo: (index: number) => {
|
|
listRef?.current?.scrollToItem(index);
|
|
},
|
|
setItemData: (data: LibraryItemOrGenre[]) => {
|
|
setItemData(data);
|
|
minItemCount.current = data.length;
|
|
},
|
|
updateItemData: (rule) => {
|
|
setItemData((data) => data.map((item) => item && rule(item)));
|
|
},
|
|
}));
|
|
|
|
if (loading) return null;
|
|
|
|
return (
|
|
<>
|
|
<InfiniteLoader
|
|
isItemLoaded={(index) => isItemLoaded(index)}
|
|
itemCount={itemCount || 0}
|
|
loadMoreItems={debouncedLoadMoreItems}
|
|
minimumBatchSize={minimumBatchSize}
|
|
ref={loader}
|
|
threshold={30}
|
|
>
|
|
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
|
|
<VirtualGridWrapper
|
|
cardRows={cardRows}
|
|
columnCount={columnCount}
|
|
display={display || ListDisplayType.CARD}
|
|
handleFavorite={handleFavorite}
|
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
|
height={height}
|
|
initialScrollOffset={initialScrollOffset}
|
|
itemCount={itemCount || 0}
|
|
itemData={itemData}
|
|
itemGap={itemGap}
|
|
itemHeight={itemHeight}
|
|
itemType={itemType}
|
|
itemWidth={itemSize}
|
|
onItemsRendered={onItemsRendered}
|
|
onScroll={onScroll}
|
|
refInstance={(list) => {
|
|
infiniteLoaderRef(list);
|
|
listRef.current = list;
|
|
}}
|
|
resetInfiniteLoaderCache={() => {
|
|
if (loader.current) {
|
|
loader.current.resetloadMoreItemsCache(false);
|
|
setItemData([]);
|
|
}
|
|
}}
|
|
route={route}
|
|
rowCount={rowCount}
|
|
width={width}
|
|
/>
|
|
)}
|
|
</InfiniteLoader>
|
|
</>
|
|
);
|
|
},
|
|
);
|
|
|
|
VirtualInfiniteGrid.defaultProps = {
|
|
display: ListDisplayType.CARD,
|
|
minimumBatchSize: 20,
|
|
route: undefined,
|
|
};
|