mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
update ItemGrid to use react-window v2
This commit is contained in:
@@ -1,6 +1,15 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { AnimatePresence } from 'motion/react';
|
import { AnimatePresence } from 'motion/react';
|
||||||
import { Dispatch, Fragment, lazy, ReactNode, SetStateAction, useState } from 'react';
|
import {
|
||||||
|
Dispatch,
|
||||||
|
Fragment,
|
||||||
|
lazy,
|
||||||
|
memo,
|
||||||
|
MouseEvent,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
|
|
||||||
import styles from './item-card.module.css';
|
import styles from './item-card.module.css';
|
||||||
@@ -35,7 +44,11 @@ interface ItemCardProps {
|
|||||||
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||||
isRound?: boolean;
|
isRound?: boolean;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
onClick?: () => void;
|
onClick?: (
|
||||||
|
e: MouseEvent<HTMLDivElement>,
|
||||||
|
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||||
|
itemType: LibraryItem,
|
||||||
|
) => void;
|
||||||
onItemExpand?: () => void;
|
onItemExpand?: () => void;
|
||||||
onItemSelect?: () => void;
|
onItemSelect?: () => void;
|
||||||
type?: 'compact' | 'default' | 'poster';
|
type?: 'compact' | 'default' | 'poster';
|
||||||
@@ -121,6 +134,7 @@ const CompactItemCard = ({
|
|||||||
data,
|
data,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
isRound,
|
isRound,
|
||||||
|
itemType,
|
||||||
onClick,
|
onClick,
|
||||||
onItemExpand,
|
onItemExpand,
|
||||||
onItemSelect,
|
onItemSelect,
|
||||||
@@ -134,7 +148,7 @@ const CompactItemCard = ({
|
|||||||
<div className={clsx(styles.container, styles.compact)}>
|
<div className={clsx(styles.container, styles.compact)}>
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||||
onClick={onClick}
|
onClick={(e) => onClick?.(e, data, itemType)}
|
||||||
onMouseEnter={() => withControls && setShowControls(true)}
|
onMouseEnter={() => withControls && setShowControls(true)}
|
||||||
onMouseLeave={() => withControls && setShowControls(false)}
|
onMouseLeave={() => withControls && setShowControls(false)}
|
||||||
>
|
>
|
||||||
@@ -158,7 +172,7 @@ const CompactItemCard = ({
|
|||||||
return (
|
return (
|
||||||
<div className={clsx(styles.container, styles.compact)}>
|
<div className={clsx(styles.container, styles.compact)}>
|
||||||
<div className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}>
|
<div className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}>
|
||||||
<Skeleton className={styles.image} enableAnimation={false} />
|
<Skeleton className={styles.image} />
|
||||||
<div className={clsx(styles.detailContainer, styles.compact)}>
|
<div className={clsx(styles.detailContainer, styles.compact)}>
|
||||||
{rows.map((row) => (
|
{rows.map((row) => (
|
||||||
<div className={styles.row} key={row.id}>
|
<div className={styles.row} key={row.id}>
|
||||||
@@ -175,6 +189,7 @@ const DefaultItemCard = ({
|
|||||||
data,
|
data,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
isRound,
|
isRound,
|
||||||
|
itemType,
|
||||||
onClick,
|
onClick,
|
||||||
onItemExpand,
|
onItemExpand,
|
||||||
onItemSelect,
|
onItemSelect,
|
||||||
@@ -188,7 +203,7 @@ const DefaultItemCard = ({
|
|||||||
<div className={clsx(styles.container)}>
|
<div className={clsx(styles.container)}>
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||||
onClick={onClick}
|
onClick={(e) => onClick?.(e, data, itemType)}
|
||||||
onDoubleClick={onItemExpand}
|
onDoubleClick={onItemExpand}
|
||||||
onMouseEnter={() => withControls && setShowControls(true)}
|
onMouseEnter={() => withControls && setShowControls(true)}
|
||||||
onMouseLeave={() => withControls && setShowControls(false)}
|
onMouseLeave={() => withControls && setShowControls(false)}
|
||||||
@@ -213,7 +228,7 @@ const DefaultItemCard = ({
|
|||||||
return (
|
return (
|
||||||
<div className={clsx(styles.container)}>
|
<div className={clsx(styles.container)}>
|
||||||
<div className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}>
|
<div className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}>
|
||||||
<Skeleton className={styles.image} enableAnimation={false} />
|
<Skeleton className={styles.image} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.detailContainer}>
|
<div className={styles.detailContainer}>
|
||||||
{rows.map((row) => (
|
{rows.map((row) => (
|
||||||
@@ -230,6 +245,7 @@ const PosterItemCard = ({
|
|||||||
data,
|
data,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
isRound,
|
isRound,
|
||||||
|
itemType,
|
||||||
onClick,
|
onClick,
|
||||||
onItemExpand,
|
onItemExpand,
|
||||||
onItemSelect,
|
onItemSelect,
|
||||||
@@ -243,7 +259,7 @@ const PosterItemCard = ({
|
|||||||
<div className={clsx(styles.container, styles.poster)}>
|
<div className={clsx(styles.container, styles.poster)}>
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||||
onClick={onClick}
|
onClick={(e) => onClick?.(e, data, itemType)}
|
||||||
onMouseEnter={() => withControls && setShowControls(true)}
|
onMouseEnter={() => withControls && setShowControls(true)}
|
||||||
onMouseLeave={() => withControls && setShowControls(false)}
|
onMouseLeave={() => withControls && setShowControls(false)}
|
||||||
>
|
>
|
||||||
@@ -330,6 +346,23 @@ const getDataRows = (itemType: LibraryItem): DataRow[] => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDataRowsCount = (itemType: LibraryItem) => {
|
||||||
|
switch (itemType) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
|
return 2;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return 1;
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
|
return 1;
|
||||||
|
case LibraryItem.PLAYLIST:
|
||||||
|
return 2;
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
||||||
if (data && 'imageUrl' in data) {
|
if (data && 'imageUrl' in data) {
|
||||||
return data.imageUrl || undefined;
|
return data.imageUrl || undefined;
|
||||||
@@ -375,3 +408,5 @@ const ItemCardRow = ({
|
|||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MemoizedItemCard = memo(ItemCard);
|
||||||
|
|||||||
@@ -1,95 +1,38 @@
|
|||||||
.item-grid-container {
|
.item-grid-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column !important;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 var(--theme-spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-sizer {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-list-container {
|
.grid-list-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0 var(--theme-spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
flex: 1 1 calc(100% / var(--columns));
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(100% / var(--columns));
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-right: var(--theme-spacing-md);
|
|
||||||
container-name: grid-list;
|
|
||||||
container-type: inline-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-list-component {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
grid-auto-flow: row dense;
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-xs) {
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-sm) {
|
|
||||||
grid-template-columns: repeat(8, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-md) {
|
|
||||||
grid-template-columns: repeat(10, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-lg) {
|
|
||||||
grid-template-columns: repeat(12, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-xl) {
|
|
||||||
grid-template-columns: repeat(14, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-2xl) {
|
|
||||||
grid-template-columns: repeat(16, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-3xl) {
|
|
||||||
grid-template-columns: repeat(18, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* display: flex; */
|
|
||||||
|
|
||||||
/* flex-wrap: wrap; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-item-component {
|
|
||||||
display: flex;
|
|
||||||
grid-column: span 2;
|
|
||||||
padding: var(--theme-spacing-sm);
|
padding: var(--theme-spacing-sm);
|
||||||
|
overflow: hidden;
|
||||||
/* box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
align-content: stretch;
|
|
||||||
width: 50%;
|
|
||||||
padding: var(--theme-spacing-sm);
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-xs) {
|
|
||||||
width: 33.33%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-sm) {
|
|
||||||
width: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-md) {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-lg) {
|
|
||||||
width: 14.28%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-xl) {
|
|
||||||
width: 12.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-2xl) {
|
|
||||||
width: 11.11%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (min-width: $breakpoint-3xl) {
|
|
||||||
width: 10%;
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width-content {
|
.full-width-content {
|
||||||
|
|||||||
@@ -1,83 +1,32 @@
|
|||||||
import clsx from 'clsx';
|
import { useElementSize, useMergedRef } from '@mantine/hooks';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
import { AnimatePresence, motion, Variants } from 'motion/react';
|
import { AnimatePresence, motion, Variants } from 'motion/react';
|
||||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
forwardRef,
|
MouseEvent,
|
||||||
memo,
|
|
||||||
ReactNode,
|
|
||||||
Ref,
|
Ref,
|
||||||
RefObject,
|
UIEvent,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import { List, ListImperativeAPI, RowComponentProps, useListRef } from 'react-window-v2';
|
||||||
GridComponents,
|
|
||||||
VirtuosoGrid,
|
|
||||||
VirtuosoGridHandle,
|
|
||||||
VirtuosoGridProps,
|
|
||||||
} from 'react-virtuoso';
|
|
||||||
|
|
||||||
import { ItemListItem, ItemListStateActions, useItemListState } from '../helpers/item-list-state';
|
|
||||||
import styles from './item-grid.module.css';
|
import styles from './item-grid.module.css';
|
||||||
|
|
||||||
import { ItemCard } from '/@/renderer/components/item-card/item-card';
|
import { getDataRowsCount, ItemCard } from '/@/renderer/components/item-card/item-card';
|
||||||
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
|
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
|
||||||
|
import {
|
||||||
|
ItemListStateActions,
|
||||||
|
useItemListState,
|
||||||
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const gridComponents: GridComponents<any> = {
|
export interface ItemGridProps {
|
||||||
Item: forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
{
|
|
||||||
children?: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
context?: ItemContext;
|
|
||||||
'data-index': number;
|
|
||||||
enableExpanded?: boolean;
|
|
||||||
style?: CSSProperties;
|
|
||||||
virtuosoRef?: RefObject<VirtuosoGridHandle>;
|
|
||||||
}
|
|
||||||
>((props, ref) => {
|
|
||||||
const { children, context, 'data-index': index } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={clsx(styles.gridItemComponent)} ref={ref}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
List: forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
{ children?: ReactNode; className?: string; style?: CSSProperties }
|
|
||||||
>((props, ref) => {
|
|
||||||
const { children, className, style, ...rest } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(styles.gridListComponent, className)}
|
|
||||||
ref={ref}
|
|
||||||
style={{ ...style }}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ItemContext {
|
|
||||||
enableExpansion?: boolean;
|
|
||||||
enableSelection?: boolean;
|
|
||||||
internalState: ItemListStateActions;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
onItemClick?: (item: unknown, index: number) => void;
|
|
||||||
onItemContextMenu?: (item: unknown, index: number) => void;
|
|
||||||
onItemDoubleClick?: (item: unknown, index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ItemGridProps {
|
|
||||||
data: unknown[];
|
data: unknown[];
|
||||||
enableExpansion?: boolean;
|
enableExpansion?: boolean;
|
||||||
enableSelection?: boolean;
|
enableSelection?: boolean;
|
||||||
@@ -91,25 +40,34 @@ interface ItemGridProps {
|
|||||||
};
|
};
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
onEndReached?: (index: number) => void;
|
onEndReached?: (index: number) => void;
|
||||||
onIsScrolling?: VirtuosoGridProps<any, any>['isScrolling'];
|
|
||||||
onItemClick?: (item: unknown, index: number) => void;
|
onItemClick?: (item: unknown, index: number) => void;
|
||||||
onItemContextMenu?: (item: unknown, index: number) => void;
|
onItemContextMenu?: (item: unknown, index: number) => void;
|
||||||
onItemDoubleClick?: (item: unknown, index: number) => void;
|
onItemDoubleClick?: (item: unknown, index: number) => void;
|
||||||
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
|
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
|
||||||
onScroll?: VirtuosoGridProps<any, any>['onScroll'];
|
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
|
||||||
|
onScrollEnd?: () => void;
|
||||||
onStartReached?: (index: number) => void;
|
onStartReached?: (index: number) => void;
|
||||||
ref: Ref<VirtuosoGridHandle>;
|
ref: Ref<ListImperativeAPI>;
|
||||||
totalItemCount?: number;
|
totalItemCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ItemContext {
|
||||||
|
enableExpansion?: boolean;
|
||||||
|
enableSelection?: boolean;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
onItemClick?: (item: unknown, index: number) => void;
|
||||||
|
onItemContextMenu?: (item: unknown, index: number) => void;
|
||||||
|
onItemDoubleClick?: (item: unknown, index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const expandedAnimationVariants: Variants = {
|
const expandedAnimationVariants: Variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
height: 0,
|
height: 0,
|
||||||
maxHeight: 0,
|
minHeight: 0,
|
||||||
},
|
},
|
||||||
show: {
|
show: {
|
||||||
height: '40dvh',
|
minHeight: '300px',
|
||||||
maxHeight: '500px',
|
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: 'easeInOut',
|
ease: 'easeInOut',
|
||||||
@@ -124,24 +82,31 @@ export const ItemGrid = ({
|
|||||||
initialTopMostItemIndex = 0,
|
initialTopMostItemIndex = 0,
|
||||||
itemType,
|
itemType,
|
||||||
onEndReached,
|
onEndReached,
|
||||||
onIsScrolling,
|
|
||||||
onItemClick,
|
onItemClick,
|
||||||
onItemContextMenu,
|
onItemContextMenu,
|
||||||
onItemDoubleClick,
|
onItemDoubleClick,
|
||||||
onRangeChanged,
|
onRangeChanged,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
onScrollEnd,
|
||||||
onStartReached,
|
onStartReached,
|
||||||
ref,
|
totalItemCount = 0,
|
||||||
totalItemCount,
|
|
||||||
}: ItemGridProps) => {
|
}: ItemGridProps) => {
|
||||||
const rootRef = useRef(null);
|
const itemGridRef = useListRef(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
const { ref: containerRef, width: containerWidth } = useElementSize();
|
||||||
|
const mergedContainerRef = useMergedRef(containerRef, scrollContainerRef);
|
||||||
|
|
||||||
const internalState = useItemListState();
|
const internalState = useItemListState();
|
||||||
|
|
||||||
const [initialize, osInstance] = useOverlayScrollbars({
|
const [initialize] = useOverlayScrollbars({
|
||||||
defer: true,
|
defer: true,
|
||||||
|
events: {
|
||||||
|
initialized(osInstance) {
|
||||||
|
const { viewport } = osInstance.elements();
|
||||||
|
viewport.style.overflowX = `var(--os-viewport-overflow-x)`;
|
||||||
|
viewport.style.overflowY = `var(--os-viewport-overflow-y)`;
|
||||||
|
},
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
overflow: { x: 'hidden', y: 'scroll' },
|
overflow: { x: 'hidden', y: 'scroll' },
|
||||||
paddingAbsolute: true,
|
paddingAbsolute: true,
|
||||||
@@ -156,65 +121,169 @@ export const ItemGrid = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { current: root } = rootRef;
|
const { current: root } = scrollContainerRef;
|
||||||
|
|
||||||
if (scroller && root) {
|
if (root) {
|
||||||
initialize({
|
initialize({
|
||||||
elements: { viewport: scroller },
|
elements: { viewport: root.firstElementChild as HTMLElement },
|
||||||
target: root,
|
target: root,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, [itemGridRef, initialize]);
|
||||||
return () => osInstance()?.destroy();
|
|
||||||
}, [scroller, initialize, osInstance]);
|
|
||||||
|
|
||||||
const itemContext = useMemo(
|
|
||||||
() => ({
|
|
||||||
enableExpansion,
|
|
||||||
enableSelection,
|
|
||||||
internalState,
|
|
||||||
itemType,
|
|
||||||
onItemClick,
|
|
||||||
onItemContextMenu,
|
|
||||||
onItemDoubleClick,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
internalState,
|
|
||||||
enableExpansion,
|
|
||||||
enableSelection,
|
|
||||||
itemType,
|
|
||||||
onItemClick,
|
|
||||||
onItemDoubleClick,
|
|
||||||
onItemContextMenu,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasExpanded = internalState.hasExpanded();
|
const hasExpanded = internalState.hasExpanded();
|
||||||
|
|
||||||
|
const handleExpand = useCallback(
|
||||||
|
(_e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => {
|
||||||
|
if (item && typeof item === 'object' && 'id' in item && 'serverId' in item) {
|
||||||
|
internalState.toggleExpanded({
|
||||||
|
id: item.id as string,
|
||||||
|
itemType: itemType,
|
||||||
|
serverId: item.serverId as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[internalState],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
(e: UIEvent<HTMLDivElement>) => {
|
||||||
|
onScroll?.(e);
|
||||||
|
},
|
||||||
|
[onScroll],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tableMeta, setTableMeta] = useState<null | {
|
||||||
|
columnCount: number;
|
||||||
|
itemHeight: number;
|
||||||
|
rowCount: number;
|
||||||
|
}>(null);
|
||||||
|
|
||||||
|
// Throttled function to update table meta
|
||||||
|
const throttledSetTableMeta = useMemo(() => {
|
||||||
|
return throttle((width: number, dataLength: number, type: LibraryItem) => {
|
||||||
|
const isSm = width >= 600;
|
||||||
|
const isMd = width >= 768;
|
||||||
|
const isLg = width >= 1200;
|
||||||
|
const isXl = width >= 1500;
|
||||||
|
const is2xl = width >= 1920;
|
||||||
|
const is3xl = width >= 2560;
|
||||||
|
|
||||||
|
let itemsPerRow = 2;
|
||||||
|
|
||||||
|
if (is3xl) {
|
||||||
|
itemsPerRow = 12;
|
||||||
|
} else if (is2xl) {
|
||||||
|
itemsPerRow = 10;
|
||||||
|
} else if (isXl) {
|
||||||
|
itemsPerRow = 8;
|
||||||
|
} else if (isLg) {
|
||||||
|
itemsPerRow = 6;
|
||||||
|
} else if (isMd) {
|
||||||
|
itemsPerRow = 4;
|
||||||
|
} else if (isSm) {
|
||||||
|
itemsPerRow = 3;
|
||||||
|
} else {
|
||||||
|
itemsPerRow = 2;
|
||||||
|
}
|
||||||
|
const widthPerItem = Number(width) / itemsPerRow;
|
||||||
|
const itemHeight = widthPerItem + getDataRowsCount(type) * 26;
|
||||||
|
|
||||||
|
if (widthPerItem === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableMeta({
|
||||||
|
columnCount: itemsPerRow,
|
||||||
|
itemHeight,
|
||||||
|
rowCount: Math.ceil(dataLength / itemsPerRow),
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
throttledSetTableMeta(containerWidth, data.length, itemType);
|
||||||
|
}, [containerWidth, data.length, itemType, throttledSetTableMeta]);
|
||||||
|
|
||||||
|
const handleOnRowsRendered = useCallback(
|
||||||
|
(visibleRows: { startIndex: number; stopIndex: number }) => {
|
||||||
|
onRangeChanged?.({
|
||||||
|
endIndex: visibleRows.stopIndex * (tableMeta?.columnCount || 0),
|
||||||
|
startIndex: visibleRows.startIndex * (tableMeta?.columnCount || 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onStartReached || onEndReached) {
|
||||||
|
const totalRows = Math.ceil(totalItemCount / (tableMeta?.columnCount || 0));
|
||||||
|
const startRow = visibleRows.startIndex;
|
||||||
|
const endRow = visibleRows.stopIndex;
|
||||||
|
|
||||||
|
if (startRow === 0) {
|
||||||
|
onStartReached?.(startRow);
|
||||||
|
}
|
||||||
|
if (endRow >= totalRows) {
|
||||||
|
onEndReached?.(endRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onEndReached, onRangeChanged, onStartReached, totalItemCount, tableMeta?.columnCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
const elements = useMemo(() => {
|
||||||
|
if (!tableMeta) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('data change');
|
||||||
|
|
||||||
|
return data
|
||||||
|
.map((d, i) => {
|
||||||
|
return {
|
||||||
|
data: d,
|
||||||
|
index: i,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(acc, d) => {
|
||||||
|
if (d.index % (tableMeta?.columnCount || 0) === 0) {
|
||||||
|
acc.push([]);
|
||||||
|
}
|
||||||
|
const prev = acc[acc.length - 1];
|
||||||
|
prev.push(d);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as { data: any; index: number }[][],
|
||||||
|
);
|
||||||
|
}, [tableMeta, data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.itemGridContainer}>
|
<motion.div
|
||||||
<div
|
animate={{
|
||||||
className={styles.gridListContainer}
|
height: '100%',
|
||||||
data-overlayscrollbars-initialize=""
|
opacity: 1,
|
||||||
ref={rootRef}
|
transition: {
|
||||||
>
|
duration: 0.5,
|
||||||
<VirtuosoGrid
|
ease: 'backInOut',
|
||||||
components={gridComponents}
|
},
|
||||||
context={itemContext}
|
}}
|
||||||
data={data}
|
className={styles.itemGridContainer}
|
||||||
endReached={onEndReached}
|
data-overlayscrollbars-initialize=""
|
||||||
increaseViewportBy={200}
|
initial={{ opacity: 0 }}
|
||||||
initialTopMostItemIndex={initialTopMostItemIndex}
|
ref={mergedContainerRef}
|
||||||
isScrolling={onIsScrolling}
|
>
|
||||||
itemContent={itemContent}
|
<List
|
||||||
onScroll={onScroll}
|
listRef={itemGridRef}
|
||||||
rangeChanged={onRangeChanged}
|
onRowsRendered={handleOnRowsRendered}
|
||||||
ref={ref}
|
onScroll={handleScroll}
|
||||||
scrollerRef={setScroller}
|
rowComponent={RowComponent}
|
||||||
startReached={onStartReached}
|
rowCount={tableMeta?.rowCount || 0}
|
||||||
totalCount={totalItemCount || data.length}
|
rowHeight={tableMeta?.itemHeight || 0}
|
||||||
/>
|
rowProps={{
|
||||||
</div>
|
columns: tableMeta?.columnCount || 0,
|
||||||
|
data: elements,
|
||||||
|
handleExpand,
|
||||||
|
itemType,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{hasExpanded && (
|
{hasExpanded && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -222,37 +291,47 @@ export const ItemGrid = ({
|
|||||||
className={styles.gridExpandedContainer}
|
className={styles.gridExpandedContainer}
|
||||||
exit="hidden"
|
exit="hidden"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
|
style={{ height: '500px' }}
|
||||||
variants={expandedAnimationVariants}
|
variants={expandedAnimationVariants}
|
||||||
>
|
>
|
||||||
<ExpandedListItem internalState={internalState} itemType={itemType} />
|
<ExpandedListItem internalState={internalState} itemType={itemType} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemContent = (index: number, item: any, context: ItemContext) => {
|
function RowComponent({
|
||||||
return <InnerItem context={context} index={index} item={item} />;
|
columns,
|
||||||
};
|
data,
|
||||||
|
handleExpand,
|
||||||
const InnerItem = memo(
|
index,
|
||||||
({ context, index, item }: { context: ItemContext; index: number; item: ItemListItem }) => {
|
itemType,
|
||||||
const handleClick = () => {
|
style,
|
||||||
context.internalState.toggleExpanded({
|
}: RowComponentProps<{
|
||||||
id: item.id,
|
columns: number;
|
||||||
itemType: item.itemType,
|
data: any[];
|
||||||
serverId: item.serverId,
|
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||||
});
|
itemType: LibraryItem;
|
||||||
};
|
}>) {
|
||||||
|
return (
|
||||||
return (
|
<div className={styles.itemList} style={style}>
|
||||||
<ItemCard
|
{data[index].map((d) => (
|
||||||
data={item as any}
|
<div
|
||||||
onClick={handleClick}
|
className={styles.itemRow}
|
||||||
onItemExpand={() => context.onItemDoubleClick?.(item, index)}
|
key={d.index}
|
||||||
withControls
|
style={{ '--columns': columns } as CSSProperties}
|
||||||
/>
|
>
|
||||||
);
|
<ItemCard
|
||||||
},
|
data={d.data}
|
||||||
);
|
itemType={itemType}
|
||||||
|
onClick={(e, item, itemType) => handleExpand(e, item, itemType)}
|
||||||
|
type="poster"
|
||||||
|
withControls
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user