mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
remove item callbacks from list - move to item component
This commit is contained in:
@@ -138,17 +138,29 @@
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: rgb(255 255 255);
|
||||
stroke: rgb(255 255 255);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-button.favorite {
|
||||
.user-data {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.rating {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: var(--theme-spacing-md);
|
||||
}
|
||||
|
||||
.secondary-button.options {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.secondary-button.expand {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'motion/react';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
import styles from './item-card-controls.module.css';
|
||||
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { animationVariants } from '/@/shared/components/animations/animation-variants';
|
||||
import { AppIcon, Icon } from '/@/shared/components/icon/icon';
|
||||
import { Rating } from '/@/shared/components/rating/rating';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
interface ItemCardControlsProps {
|
||||
controls: ItemControls;
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||
itemType: LibraryItem;
|
||||
type?: 'compact' | 'default' | 'poster';
|
||||
}
|
||||
|
||||
@@ -27,27 +42,79 @@ const containerProps = {
|
||||
animate: 'show',
|
||||
exit: 'hidden',
|
||||
initial: 'hidden',
|
||||
variants: animationVariants.combine(animationVariants.slideInUp, animationVariants.fadeIn),
|
||||
variants: animationVariants.combine(animationVariants.zoomIn, animationVariants.fadeIn),
|
||||
},
|
||||
};
|
||||
|
||||
export const ItemCardControls = ({ type = 'default' }: ItemCardControlsProps) => {
|
||||
export const ItemCardControls = ({
|
||||
controls,
|
||||
item,
|
||||
itemType,
|
||||
type = 'default',
|
||||
}: ItemCardControlsProps) => {
|
||||
return (
|
||||
<motion.div className={clsx(styles.container)} {...containerProps[type]}>
|
||||
<PlayButton />
|
||||
<SecondaryPlayButton className={styles.left} icon="mediaPlayNext" />
|
||||
<SecondaryPlayButton className={styles.right} icon="mediaPlayLast" />
|
||||
<SecondaryButton className={styles.favorite} icon="favorite" />
|
||||
<SecondaryButton className={styles.options} icon="ellipsisHorizontal" />
|
||||
<PlayButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
controls.onPlay?.(item, itemType, Play.NOW, e);
|
||||
}}
|
||||
/>
|
||||
<SecondaryPlayButton
|
||||
className={styles.left}
|
||||
icon="mediaPlayNext"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
controls.onPlay?.(item, itemType, Play.NEXT, e);
|
||||
}}
|
||||
/>
|
||||
<SecondaryPlayButton
|
||||
className={styles.right}
|
||||
icon="mediaPlayLast"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
controls.onPlay?.(item, itemType, Play.LAST, e);
|
||||
}}
|
||||
/>
|
||||
<SecondaryButton
|
||||
className={styles.favorite}
|
||||
icon="favorite"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
controls.onFavorite?.(item, itemType, e);
|
||||
}}
|
||||
/>
|
||||
<Rating className={styles.rating} size="xs" />
|
||||
<SecondaryButton
|
||||
className={styles.options}
|
||||
icon="ellipsisHorizontal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
controls.onMore?.(item, itemType, e);
|
||||
}}
|
||||
/>
|
||||
{controls.onItemExpand && (
|
||||
<SecondaryButton
|
||||
className={styles.expand}
|
||||
icon="arrowDownS"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
controls.onItemExpand?.(item, itemType, e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlayButton = () => {
|
||||
const PlayButton = ({ onClick }: { onClick?: (e: MouseEvent<HTMLButtonElement>) => void }) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(styles.playButton, styles.primary)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
<Icon icon="mediaPlay" size="lg" />
|
||||
</button>
|
||||
@@ -57,12 +124,20 @@ const PlayButton = () => {
|
||||
const SecondaryPlayButton = ({
|
||||
className,
|
||||
icon,
|
||||
onClick,
|
||||
}: {
|
||||
className?: string;
|
||||
icon: keyof typeof AppIcon;
|
||||
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
}) => {
|
||||
return (
|
||||
<button className={clsx(styles.playButton, styles.secondary, className)}>
|
||||
<button
|
||||
className={clsx(styles.playButton, styles.secondary, className)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} size="lg" />
|
||||
</button>
|
||||
);
|
||||
@@ -71,11 +146,18 @@ const SecondaryPlayButton = ({
|
||||
interface SecondaryButtonProps {
|
||||
className?: string;
|
||||
icon: keyof typeof AppIcon;
|
||||
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
const SecondaryButton = ({ className, icon }: SecondaryButtonProps) => {
|
||||
const SecondaryButton = ({ className, icon, onClick }: SecondaryButtonProps) => {
|
||||
return (
|
||||
<button className={clsx(styles.secondaryButton, className)}>
|
||||
<button
|
||||
className={clsx(styles.secondaryButton, className)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} size="lg" />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 0.6;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
Dispatch,
|
||||
Fragment,
|
||||
lazy,
|
||||
memo,
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Dispatch, Fragment, memo, ReactNode, SetStateAction, useState } from 'react';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
|
||||
import styles from './item-card.module.css';
|
||||
|
||||
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Separator } from '/@/shared/components/separator/separator';
|
||||
@@ -28,12 +21,6 @@ import {
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
const ItemCardControls = lazy(() =>
|
||||
import('/@/renderer/components/item-card/item-card-controls').then((module) => ({
|
||||
default: module.ItemCardControls,
|
||||
})),
|
||||
);
|
||||
|
||||
type DataRow = {
|
||||
format: (data: Album | AlbumArtist | Artist | Playlist | Song) => ReactNode | string;
|
||||
id: string;
|
||||
@@ -41,27 +28,20 @@ type DataRow = {
|
||||
};
|
||||
|
||||
interface ItemCardProps {
|
||||
controls: ItemControls;
|
||||
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||
isRound?: boolean;
|
||||
itemType: LibraryItem;
|
||||
onClick?: (
|
||||
e: MouseEvent<HTMLDivElement>,
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
) => void;
|
||||
onItemExpand?: () => void;
|
||||
onItemSelect?: () => void;
|
||||
|
||||
type?: 'compact' | 'default' | 'poster';
|
||||
withControls?: boolean;
|
||||
}
|
||||
|
||||
export const ItemCard = ({
|
||||
controls,
|
||||
data,
|
||||
isRound,
|
||||
itemType,
|
||||
onClick,
|
||||
onItemExpand,
|
||||
onItemSelect,
|
||||
type = 'poster',
|
||||
withControls,
|
||||
}: ItemCardProps) => {
|
||||
@@ -74,13 +54,11 @@ export const ItemCard = ({
|
||||
case 'compact':
|
||||
return (
|
||||
<CompactItemCard
|
||||
controls={controls}
|
||||
data={data}
|
||||
imageUrl={imageUrl}
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
onClick={onClick}
|
||||
onItemExpand={onItemExpand}
|
||||
onItemSelect={onItemSelect}
|
||||
rows={rows}
|
||||
setShowControls={setShowControls}
|
||||
showControls={showControls}
|
||||
@@ -90,13 +68,11 @@ export const ItemCard = ({
|
||||
case 'poster':
|
||||
return (
|
||||
<PosterItemCard
|
||||
controls={controls}
|
||||
data={data}
|
||||
imageUrl={imageUrl}
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
onClick={onClick}
|
||||
onItemExpand={onItemExpand}
|
||||
onItemSelect={onItemSelect}
|
||||
rows={rows}
|
||||
setShowControls={setShowControls}
|
||||
showControls={showControls}
|
||||
@@ -107,13 +83,11 @@ export const ItemCard = ({
|
||||
default:
|
||||
return (
|
||||
<DefaultItemCard
|
||||
controls={controls}
|
||||
data={data}
|
||||
imageUrl={imageUrl}
|
||||
isRound={isRound}
|
||||
itemType={itemType}
|
||||
onClick={onClick}
|
||||
onItemExpand={onItemExpand}
|
||||
onItemSelect={onItemSelect}
|
||||
rows={rows}
|
||||
setShowControls={setShowControls}
|
||||
showControls={showControls}
|
||||
@@ -124,6 +98,7 @@ export const ItemCard = ({
|
||||
};
|
||||
|
||||
export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
|
||||
controls: ItemControls;
|
||||
imageUrl: string | undefined;
|
||||
rows: DataRow[];
|
||||
setShowControls: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -131,13 +106,11 @@ export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
|
||||
}
|
||||
|
||||
const CompactItemCard = ({
|
||||
controls,
|
||||
data,
|
||||
imageUrl,
|
||||
isRound,
|
||||
itemType,
|
||||
onClick,
|
||||
onItemExpand,
|
||||
onItemSelect,
|
||||
rows,
|
||||
setShowControls,
|
||||
showControls,
|
||||
@@ -148,7 +121,6 @@ const CompactItemCard = ({
|
||||
<div className={clsx(styles.container, styles.compact)}>
|
||||
<div
|
||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||
onClick={(e) => onClick?.(e, data, itemType)}
|
||||
onMouseEnter={() => withControls && setShowControls(true)}
|
||||
onMouseLeave={() => withControls && setShowControls(false)}
|
||||
>
|
||||
@@ -157,7 +129,14 @@ const CompactItemCard = ({
|
||||
src={imageUrl}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{withControls && showControls && <ItemCardControls type="compact" />}
|
||||
{withControls && showControls && (
|
||||
<ItemCardControls
|
||||
controls={controls}
|
||||
item={data}
|
||||
itemType={itemType}
|
||||
type="compact"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className={clsx(styles.detailContainer, styles.compact)}>
|
||||
{rows.map((row) => (
|
||||
@@ -186,13 +165,11 @@ const CompactItemCard = ({
|
||||
};
|
||||
|
||||
const DefaultItemCard = ({
|
||||
controls,
|
||||
data,
|
||||
imageUrl,
|
||||
isRound,
|
||||
itemType,
|
||||
onClick,
|
||||
onItemExpand,
|
||||
onItemSelect,
|
||||
rows,
|
||||
setShowControls,
|
||||
showControls,
|
||||
@@ -203,8 +180,6 @@ const DefaultItemCard = ({
|
||||
<div className={clsx(styles.container)}>
|
||||
<div
|
||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||
onClick={(e) => onClick?.(e, data, itemType)}
|
||||
onDoubleClick={onItemExpand}
|
||||
onMouseEnter={() => withControls && setShowControls(true)}
|
||||
onMouseLeave={() => withControls && setShowControls(false)}
|
||||
>
|
||||
@@ -213,7 +188,14 @@ const DefaultItemCard = ({
|
||||
src={imageUrl}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{withControls && showControls && <ItemCardControls type="default" />}
|
||||
{withControls && showControls && (
|
||||
<ItemCardControls
|
||||
controls={controls}
|
||||
item={data}
|
||||
itemType={itemType}
|
||||
type="default"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className={styles.detailContainer}>
|
||||
@@ -242,13 +224,11 @@ const DefaultItemCard = ({
|
||||
};
|
||||
|
||||
const PosterItemCard = ({
|
||||
controls,
|
||||
data,
|
||||
imageUrl,
|
||||
isRound,
|
||||
itemType,
|
||||
onClick,
|
||||
onItemExpand,
|
||||
onItemSelect,
|
||||
rows,
|
||||
setShowControls,
|
||||
showControls,
|
||||
@@ -259,7 +239,7 @@ const PosterItemCard = ({
|
||||
<div className={clsx(styles.container, styles.poster)}>
|
||||
<div
|
||||
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
|
||||
onClick={(e) => onClick?.(e, data, itemType)}
|
||||
onClick={(e) => controls?.onClick?.(data, itemType, e)}
|
||||
onMouseEnter={() => withControls && setShowControls(true)}
|
||||
onMouseLeave={() => withControls && setShowControls(false)}
|
||||
>
|
||||
@@ -268,7 +248,14 @@ const PosterItemCard = ({
|
||||
src={imageUrl}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{withControls && showControls && <ItemCardControls type="poster" />}
|
||||
{withControls && showControls && (
|
||||
<ItemCardControls
|
||||
controls={controls}
|
||||
item={data}
|
||||
itemType={itemType}
|
||||
type="poster"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className={styles.detailContainer}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { getServerById } from '/@/renderer/store';
|
||||
|
||||
export interface InfiniteListProps<TQuery> {
|
||||
itemsPerPage?: number;
|
||||
query: Omit<TQuery, 'limit' | 'startIndex'>;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
@@ -6,17 +6,6 @@
|
||||
padding: 0 var(--theme-spacing-md);
|
||||
}
|
||||
|
||||
.auto-sizer {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grid-list-container {
|
||||
width: 100%;
|
||||
padding: 0 var(--theme-spacing-md);
|
||||
@@ -35,11 +24,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.full-width-content {
|
||||
grid-column: 1 / -1;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.list-expanded-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AnimatePresence, motion, Variants } from 'motion/react';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import {
|
||||
CSSProperties,
|
||||
MouseEvent,
|
||||
Ref,
|
||||
UIEvent,
|
||||
useCallback,
|
||||
@@ -21,46 +20,42 @@ import styles from './item-grid-list.module.css';
|
||||
import { getDataRowsCount, ItemCard } from '/@/renderer/components/item-card/item-card';
|
||||
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
|
||||
import {
|
||||
ItemListItem,
|
||||
ItemListStateActions,
|
||||
useItemListState,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export interface GridItemProps {
|
||||
columns: number;
|
||||
data: any[];
|
||||
enableExpansion?: boolean;
|
||||
enableSelection?: boolean;
|
||||
internalState: ItemListStateActions;
|
||||
itemType: LibraryItem;
|
||||
}
|
||||
|
||||
export interface ItemGridListProps {
|
||||
data: unknown[];
|
||||
enableExpansion?: boolean;
|
||||
enableSelection?: boolean;
|
||||
initialTopMostItemIndex?:
|
||||
| number
|
||||
| {
|
||||
align: 'center' | 'end' | 'start';
|
||||
behavior: 'auto' | 'smooth';
|
||||
index: number;
|
||||
offset?: number;
|
||||
};
|
||||
initialTop?: {
|
||||
behavior?: 'auto' | 'smooth';
|
||||
to: number;
|
||||
type: 'index' | 'offset';
|
||||
};
|
||||
itemType: LibraryItem;
|
||||
onEndReached?: (index: number) => void;
|
||||
onItemClick?: (item: unknown, index: number) => void;
|
||||
onItemContextMenu?: (item: unknown, index: number) => void;
|
||||
onItemDoubleClick?: (item: unknown, index: number) => void;
|
||||
onEndReached?: (index: number, handle: ItemListHandle) => void;
|
||||
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
|
||||
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
|
||||
onScrollEnd?: () => void;
|
||||
onStartReached?: (index: number) => void;
|
||||
onScrollEnd?: (offset: number, handle: ItemListHandle) => void;
|
||||
onStartReached?: (index: number, handle: ItemListHandle) => void;
|
||||
ref: Ref<ListImperativeAPI>;
|
||||
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 = {
|
||||
hidden: {
|
||||
height: 0,
|
||||
@@ -79,15 +74,10 @@ export const ItemGridList = ({
|
||||
data,
|
||||
enableExpansion = false,
|
||||
enableSelection = false,
|
||||
initialTopMostItemIndex = 0,
|
||||
itemType,
|
||||
onEndReached,
|
||||
onItemClick,
|
||||
onItemContextMenu,
|
||||
onItemDoubleClick,
|
||||
onRangeChanged,
|
||||
onScroll,
|
||||
onScrollEnd,
|
||||
onStartReached,
|
||||
totalItemCount = 0,
|
||||
}: ItemGridListProps) => {
|
||||
@@ -133,19 +123,6 @@ export const ItemGridList = ({
|
||||
|
||||
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);
|
||||
@@ -218,14 +195,21 @@ export const ItemGridList = ({
|
||||
const endRow = visibleRows.stopIndex;
|
||||
|
||||
if (startRow === 0) {
|
||||
onStartReached?.(startRow);
|
||||
onStartReached?.(startRow, itemGridRef.current ?? (undefined as any));
|
||||
}
|
||||
if (endRow >= totalRows) {
|
||||
onEndReached?.(endRow);
|
||||
onEndReached?.(endRow, itemGridRef.current ?? (undefined as any));
|
||||
}
|
||||
}
|
||||
},
|
||||
[onEndReached, onRangeChanged, onStartReached, totalItemCount, tableMeta?.columnCount],
|
||||
[
|
||||
onRangeChanged,
|
||||
tableMeta?.columnCount,
|
||||
onStartReached,
|
||||
onEndReached,
|
||||
totalItemCount,
|
||||
itemGridRef,
|
||||
],
|
||||
);
|
||||
|
||||
const elements = useMemo(() => {
|
||||
@@ -253,6 +237,15 @@ export const ItemGridList = ({
|
||||
);
|
||||
}, [tableMeta, data]);
|
||||
|
||||
const itemProps: GridItemProps = {
|
||||
columns: tableMeta?.columnCount || 0,
|
||||
data: elements,
|
||||
enableExpansion,
|
||||
enableSelection,
|
||||
internalState,
|
||||
itemType,
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{
|
||||
@@ -272,15 +265,10 @@ export const ItemGridList = ({
|
||||
listRef={itemGridRef}
|
||||
onRowsRendered={handleOnRowsRendered}
|
||||
onScroll={handleScroll}
|
||||
rowComponent={RowComponent}
|
||||
rowComponent={ListComponent}
|
||||
rowCount={tableMeta?.rowCount || 0}
|
||||
rowHeight={tableMeta?.itemHeight || 0}
|
||||
rowProps={{
|
||||
columns: tableMeta?.columnCount || 0,
|
||||
data: elements,
|
||||
handleExpand,
|
||||
itemType,
|
||||
}}
|
||||
rowProps={itemProps}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{hasExpanded && (
|
||||
@@ -300,19 +288,16 @@ export const ItemGridList = ({
|
||||
);
|
||||
};
|
||||
|
||||
function RowComponent({
|
||||
const ListComponent = ({
|
||||
columns,
|
||||
data,
|
||||
handleExpand,
|
||||
enableExpansion,
|
||||
enableSelection,
|
||||
index,
|
||||
internalState,
|
||||
itemType,
|
||||
style,
|
||||
}: RowComponentProps<{
|
||||
columns: number;
|
||||
data: any[];
|
||||
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||
itemType: LibraryItem;
|
||||
}>) {
|
||||
}: RowComponentProps<GridItemProps>) => {
|
||||
return (
|
||||
<div className={styles.itemList} style={style}>
|
||||
{data[index].map((d) => (
|
||||
@@ -322,14 +307,104 @@ function RowComponent({
|
||||
style={{ '--columns': columns } as CSSProperties}
|
||||
>
|
||||
<ItemCard
|
||||
controls={{
|
||||
onClick: enableSelection
|
||||
? (item, itemType) => {
|
||||
return handleItemClick(item, itemType, internalState);
|
||||
}
|
||||
: undefined,
|
||||
onDoubleClick: (item, itemType) => {
|
||||
return handleItemDoubleClick(item, itemType, internalState);
|
||||
},
|
||||
onFavorite: (item, itemType) => {
|
||||
return handleItemFavorite(item, itemType, internalState);
|
||||
},
|
||||
onItemExpand: enableExpansion
|
||||
? (item, itemType) => {
|
||||
return handleItemExpand(item, itemType, internalState);
|
||||
}
|
||||
: undefined,
|
||||
onMore: (item, itemType) => {
|
||||
return handleItemMore(item, itemType, internalState);
|
||||
},
|
||||
onPlay: (item, itemType, playType) => {
|
||||
return handleItemPlay(item, itemType, playType, internalState);
|
||||
},
|
||||
onRating: (item, itemType) => {
|
||||
return handleItemRating(item, itemType, internalState);
|
||||
},
|
||||
}}
|
||||
data={d.data}
|
||||
itemType={itemType}
|
||||
onClick={(e, item, itemType) => handleExpand(e, item, itemType)}
|
||||
type="poster"
|
||||
withControls
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemClick = (
|
||||
item: (ItemListItem & object) | undefined,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemClick', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemDoubleClick = (
|
||||
item: (ItemListItem & object) | undefined,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemDoubleClick', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemExpand = (
|
||||
item: (ItemListItem & object) | undefined,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
return internalState.toggleExpanded({
|
||||
id: item.id,
|
||||
itemType,
|
||||
serverId: item.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleItemFavorite = (
|
||||
item: (ItemListItem & object) | undefined,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemFavorite', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemRating = (
|
||||
item: (ItemListItem & object) | undefined,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemRating', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemMore = (
|
||||
item: (ItemListItem & object) | undefined,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemMore', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemPlay = (
|
||||
item: (ItemListItem & object) | undefined,
|
||||
itemType: LibraryItem,
|
||||
playType: Play,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemPlay', item, itemType, playType, internalState);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
interface UseItemListPaginationProps {
|
||||
initialPage?: number;
|
||||
}
|
||||
|
||||
export const useItemListPagination = ({ initialPage }: UseItemListPaginationProps) => {
|
||||
export const useItemListPagination = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const currentPage = initialPage || Number(searchParams.get('currentPage')) || 0;
|
||||
const currentPage = Number(searchParams.get('currentPage')) || 0;
|
||||
|
||||
const onChange = (index: number) => {
|
||||
setSearchParams(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CellComponentProps } from 'react-window-v2';
|
||||
import styles from './item-table-list-column.module.css';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column';
|
||||
import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column';
|
||||
import { CountColumn } from '/@/renderer/components/item-list/item-table-list/columns/count-column';
|
||||
@@ -23,13 +24,13 @@ import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/c
|
||||
import { RowIndexColumn } from '/@/renderer/components/item-list/item-table-list/columns/row-index-column';
|
||||
import { SizeColumn } from '/@/renderer/components/item-list/item-table-list/columns/size-column';
|
||||
import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column';
|
||||
import { CellProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
import { createDoubleClickHandler } from '/@/shared/utils/double-click-handler';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play, TableColumn } from '/@/shared/types/types';
|
||||
|
||||
export interface ItemTableListColumn extends CellComponentProps<CellProps> {}
|
||||
export interface ItemTableListColumn extends CellComponentProps<TableItemProps> {}
|
||||
|
||||
export interface ItemTableListInnerColumn extends ItemTableListColumn {
|
||||
type: TableColumn;
|
||||
@@ -159,18 +160,6 @@ export const TableColumnTextContainer = (
|
||||
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
|
||||
})}
|
||||
data-row-index={isDataRow ? props.rowIndex : undefined}
|
||||
onClick={createDoubleClickHandler<HTMLDivElement>({
|
||||
onDoubleClick: (e) => {
|
||||
props.onItemDoubleClick?.(props.data[props.rowIndex], dataIndex, e);
|
||||
},
|
||||
onSingleClick: (e) => {
|
||||
props.onItemClick?.(props.data[props.rowIndex], dataIndex, e);
|
||||
props.handleExpand(e, props.data[props.rowIndex], props.itemType);
|
||||
},
|
||||
})}
|
||||
onContextMenu={(e) => {
|
||||
props.onItemContextMenu?.(props.data[props.rowIndex], dataIndex, e);
|
||||
}}
|
||||
ref={containerRef}
|
||||
style={props.style}
|
||||
>
|
||||
@@ -237,18 +226,6 @@ export const TableColumnContainer = (
|
||||
props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
|
||||
})}
|
||||
data-row-index={isDataRow ? props.rowIndex : undefined}
|
||||
onClick={createDoubleClickHandler<HTMLDivElement>({
|
||||
onDoubleClick: (e) => {
|
||||
props.onItemDoubleClick?.(props.data[props.rowIndex], dataIndex, e);
|
||||
},
|
||||
onSingleClick: (e) => {
|
||||
props.onItemClick?.(props.data[props.rowIndex], dataIndex, e);
|
||||
props.handleExpand(e, props.data[props.rowIndex], props.itemType);
|
||||
},
|
||||
})}
|
||||
onContextMenu={(e) => {
|
||||
props.onItemContextMenu?.(props.data[props.rowIndex], dataIndex, e);
|
||||
}}
|
||||
ref={containerRef}
|
||||
style={props.style}
|
||||
>
|
||||
@@ -283,8 +260,73 @@ export const TableColumnHeaderContainer = (
|
||||
);
|
||||
};
|
||||
|
||||
const handleItemClick = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemClick', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemExpand = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemExpand', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemSelect = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemSelect', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemDoubleClick = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemDoubleClick', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemFavorite = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemFavorite', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemRating = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemRating', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemMore = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemMore', item, itemType, internalState);
|
||||
};
|
||||
|
||||
const handleItemPlay = (
|
||||
item: unknown,
|
||||
itemType: LibraryItem,
|
||||
playType: Play,
|
||||
internalState: ItemListStateActions,
|
||||
) => {
|
||||
console.log('handleItemPlay', item, itemType, playType, internalState);
|
||||
};
|
||||
|
||||
const columnLabelMap: Record<TableColumn, ReactNode | string> = {
|
||||
[TableColumn.ACTIONS]: '',
|
||||
[TableColumn.ACTIONS]: <Icon fill="default" icon="ellipsisHorizontal" size="md" />,
|
||||
[TableColumn.ALBUM]: i18n.t('table.column.album', { postProcess: 'upperCase' }) as string,
|
||||
[TableColumn.ALBUM_ARTIST]: i18n.t('table.column.albumArtist', {
|
||||
postProcess: 'upperCase',
|
||||
@@ -335,9 +377,7 @@ const columnLabelMap: Record<TableColumn, ReactNode | string> = {
|
||||
[TableColumn.TRACK_NUMBER]: i18n.t('table.column.trackNumber', {
|
||||
postProcess: 'upperCase',
|
||||
}) as string,
|
||||
[TableColumn.USER_FAVORITE]: i18n.t('table.column.favorite', {
|
||||
postProcess: 'upperCase',
|
||||
}) as string,
|
||||
[TableColumn.USER_FAVORITE]: <Icon fill="default" icon="favorite" size="md" />,
|
||||
[TableColumn.USER_RATING]: i18n.t('table.column.rating', {
|
||||
postProcess: 'upperCase',
|
||||
}) as string,
|
||||
|
||||
@@ -6,38 +6,28 @@ import { AnimatePresence, motion, Variants } from 'motion/react';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import {
|
||||
type JSXElementConstructor,
|
||||
MouseEvent,
|
||||
Ref,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { type CellComponentProps, Grid, GridImperativeAPI, type GridProps } from 'react-window-v2';
|
||||
import { type CellComponentProps, Grid, type GridProps } from 'react-window-v2';
|
||||
|
||||
import styles from './item-table-list.module.css';
|
||||
|
||||
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
|
||||
import { useItemListState } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import {
|
||||
ItemListStateActions,
|
||||
useItemListState,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { sortTableColumns } from '/@/renderer/components/item-list/helpers/sort-table-columns';
|
||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
export interface CellProps {
|
||||
columns: ItemTableListColumnConfig[];
|
||||
data: unknown[];
|
||||
enableHeader?: boolean;
|
||||
enableRowBorders?: boolean;
|
||||
enableRowHover?: boolean;
|
||||
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||
itemType: LibraryItem;
|
||||
onItemClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||
onItemContextMenu?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||
onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||
size?: 'compact' | 'default';
|
||||
}
|
||||
|
||||
export interface ItemTableListColumnConfig {
|
||||
align: 'center' | 'end' | 'start';
|
||||
id: TableColumn;
|
||||
@@ -45,8 +35,21 @@ export interface ItemTableListColumnConfig {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface TableItemProps {
|
||||
columns: ItemTableListColumnConfig[];
|
||||
data: unknown[];
|
||||
enableExpansion?: boolean;
|
||||
enableHeader?: boolean;
|
||||
enableRowBorders?: boolean;
|
||||
enableRowHover?: boolean;
|
||||
enableSelection?: boolean;
|
||||
internalState: ItemListStateActions;
|
||||
itemType: LibraryItem;
|
||||
size?: 'compact' | 'default';
|
||||
}
|
||||
|
||||
interface ItemTableListProps {
|
||||
CellComponent: JSXElementConstructor<CellComponentProps<CellProps>>;
|
||||
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
|
||||
columns: ItemTableListColumnConfig[];
|
||||
data: unknown[];
|
||||
enableExpansion?: boolean;
|
||||
@@ -61,18 +64,17 @@ interface ItemTableListProps {
|
||||
type: 'index' | 'offset';
|
||||
};
|
||||
itemType: LibraryItem;
|
||||
onCellsRendered?: GridProps<CellProps>['onCellsRendered'];
|
||||
onEndReached?: (index: number) => void;
|
||||
onItemClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||
onItemContextMenu?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||
onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void;
|
||||
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
|
||||
onScrollEnd?: (offset: number) => void;
|
||||
onStartReached?: (index: number) => void;
|
||||
ref?: Ref<GridImperativeAPI>;
|
||||
rowHeight: ((index: number, cellProps: CellProps) => number) | number;
|
||||
onCellsRendered?: GridProps<TableItemProps>['onCellsRendered'];
|
||||
onEndReached?: (index: number, internalState: ItemListStateActions) => void;
|
||||
onRangeChanged?: (
|
||||
range: { endIndex: number; startIndex: number },
|
||||
internalState: ItemListStateActions,
|
||||
) => void;
|
||||
onScrollEnd?: (offset: number, internalState: ItemListStateActions) => void;
|
||||
onStartReached?: (index: number, internalState: ItemListStateActions) => void;
|
||||
ref?: Ref<ItemListHandle>;
|
||||
rowHeight?: ((index: number, cellProps: TableItemProps) => number) | number;
|
||||
size?: 'compact' | 'default';
|
||||
totalItemCount: number;
|
||||
}
|
||||
|
||||
const expandedAnimationVariants: Variants = {
|
||||
@@ -97,26 +99,24 @@ export const ItemTableList = ({
|
||||
enableHeader = true,
|
||||
enableRowBorders = false,
|
||||
enableRowHover = false,
|
||||
enableSelection = false,
|
||||
headerHeight = 40,
|
||||
initialTop,
|
||||
itemType,
|
||||
onCellsRendered,
|
||||
onEndReached,
|
||||
onItemClick,
|
||||
onItemContextMenu,
|
||||
onItemDoubleClick,
|
||||
onRangeChanged,
|
||||
onScrollEnd,
|
||||
onStartReached,
|
||||
ref,
|
||||
rowHeight,
|
||||
size = 'default',
|
||||
totalItemCount,
|
||||
}: ItemTableListProps) => {
|
||||
const totalItemCount = data.length;
|
||||
const sortedColumns = useMemo(() => sortTableColumns(columns), [columns]);
|
||||
const columnCount = sortedColumns.length;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const columnWidth = (index: number, _cellProps: CellProps) => sortedColumns[index].width;
|
||||
const columnWidth = (index: number, _cellProps: TableItemProps) => sortedColumns[index].width;
|
||||
const pinnedLeftColumnCount = sortedColumns.filter((col) => col.pinned === 'left').length;
|
||||
const pinnedRightColumnCount = sortedColumns.filter((col) => col.pinned === 'right').length;
|
||||
|
||||
@@ -131,8 +131,53 @@ export const ItemTableList = ({
|
||||
const mergedRowRef = useMergedRef(rowRef, scrollContainerRef);
|
||||
const [showLeftShadow, setShowLeftShadow] = useState(false);
|
||||
const [showRightShadow, setShowRightShadow] = useState(false);
|
||||
const handleRef = useRef<ItemListHandle | null>(null);
|
||||
|
||||
const onScrollEndRef = useRef<ItemTableListProps['onScrollEnd']>(onScrollEnd);
|
||||
useEffect(() => {
|
||||
onScrollEndRef.current = onScrollEnd;
|
||||
}, [onScrollEnd]);
|
||||
|
||||
const scrollToTableOffset = useCallback((offset: number) => {
|
||||
const mainContainer = rowRef.current?.childNodes[0] as HTMLDivElement | undefined;
|
||||
const pinnedLeftContainer = pinnedLeftColumnRef.current?.childNodes[0] as
|
||||
| HTMLDivElement
|
||||
| undefined;
|
||||
const pinnedRightContainer = pinnedRightColumnRef.current?.childNodes[0] as
|
||||
| HTMLDivElement
|
||||
| undefined;
|
||||
|
||||
if (mainContainer) {
|
||||
mainContainer.scrollTo({ behavior: 'instant', top: offset });
|
||||
}
|
||||
if (pinnedLeftContainer) {
|
||||
pinnedLeftContainer.scrollTo({ behavior: 'instant', top: offset });
|
||||
}
|
||||
if (pinnedRightContainer) {
|
||||
pinnedRightContainer.scrollTo({ behavior: 'instant', top: offset });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const calculateScrollTopForIndex = useCallback(
|
||||
(index: number) => {
|
||||
const adjustedIndex = enableHeader ? Math.max(0, index - 1) : index;
|
||||
let scrollTop = 0;
|
||||
for (let i = 0; i < adjustedIndex; i++) {
|
||||
const height = rowHeight as number;
|
||||
scrollTop += height;
|
||||
}
|
||||
return scrollTop;
|
||||
},
|
||||
[enableHeader, rowHeight],
|
||||
);
|
||||
|
||||
const scrollToTableIndex = useCallback(
|
||||
(index: number) => {
|
||||
const offset = calculateScrollTopForIndex(index);
|
||||
scrollToTableOffset(offset);
|
||||
},
|
||||
[calculateScrollTopForIndex, scrollToTableOffset],
|
||||
);
|
||||
|
||||
const [initialize] = useOverlayScrollbars({
|
||||
defer: true,
|
||||
@@ -232,7 +277,10 @@ export const ItemTableList = ({
|
||||
scrollTimeouts.delete(element);
|
||||
|
||||
if (element === row && onScrollEndRef.current) {
|
||||
onScrollEndRef.current(row.scrollTop);
|
||||
onScrollEndRef.current(
|
||||
row.scrollTop,
|
||||
handleRef.current ?? (undefined as any),
|
||||
);
|
||||
}
|
||||
}, 150);
|
||||
|
||||
@@ -430,9 +478,11 @@ export const ItemTableList = ({
|
||||
}, [pinnedLeftColumnCount, pinnedRightColumnCount]);
|
||||
|
||||
const getRowHeight = useCallback(
|
||||
(index: number, cellProps: CellProps) => {
|
||||
(index: number, cellProps: TableItemProps) => {
|
||||
const height = size === 'compact' ? 40 : 68;
|
||||
|
||||
const baseHeight =
|
||||
typeof rowHeight === 'number' ? rowHeight : rowHeight(index, cellProps);
|
||||
typeof rowHeight === 'number' ? rowHeight : rowHeight?.(index, cellProps) || height;
|
||||
|
||||
// If enableHeader is true and this is the first sticky row, use fixed header height
|
||||
if (enableHeader && index === 0 && pinnedRowCount > 0) {
|
||||
@@ -441,30 +491,13 @@ export const ItemTableList = ({
|
||||
|
||||
return baseHeight;
|
||||
},
|
||||
[enableHeader, headerHeight, rowHeight, pinnedRowCount],
|
||||
[enableHeader, headerHeight, rowHeight, pinnedRowCount, size],
|
||||
);
|
||||
|
||||
const internalState = useItemListState();
|
||||
|
||||
const hasExpanded = internalState.hasExpanded();
|
||||
|
||||
const handleExpand = useCallback(
|
||||
(_e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => {
|
||||
if (!enableExpansion) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
[enableExpansion, internalState],
|
||||
);
|
||||
|
||||
const handleOnCellsRendered = useCallback(
|
||||
(cells: {
|
||||
columnStartIndex: number;
|
||||
@@ -472,18 +505,21 @@ export const ItemTableList = ({
|
||||
rowStartIndex: number;
|
||||
rowStopIndex: number;
|
||||
}) => {
|
||||
onRangeChanged?.({
|
||||
endIndex: cells.rowStopIndex,
|
||||
startIndex: cells.rowStartIndex,
|
||||
});
|
||||
onRangeChanged?.(
|
||||
{
|
||||
endIndex: cells.rowStopIndex,
|
||||
startIndex: cells.rowStartIndex,
|
||||
},
|
||||
internalState,
|
||||
);
|
||||
|
||||
if (onStartReached || onEndReached) {
|
||||
if (cells.rowStartIndex === 0) {
|
||||
onStartReached?.(0);
|
||||
onStartReached?.(0, handleRef.current ?? (undefined as any));
|
||||
}
|
||||
|
||||
if (cells.rowStopIndex + 10 >= totalItemCount) {
|
||||
onEndReached?.(totalItemCount);
|
||||
onEndReached?.(totalItemCount, handleRef.current ?? (undefined as any));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,11 +547,12 @@ export const ItemTableList = ({
|
||||
pinnedLeftColumnCount,
|
||||
pinnedRowCount,
|
||||
totalItemCount,
|
||||
internalState,
|
||||
],
|
||||
);
|
||||
|
||||
const PinnedRowCell = useCallback(
|
||||
(cellProps: CellComponentProps & CellProps) => {
|
||||
(cellProps: CellComponentProps & TableItemProps) => {
|
||||
return (
|
||||
<CellComponent
|
||||
{...cellProps}
|
||||
@@ -527,14 +564,14 @@ export const ItemTableList = ({
|
||||
);
|
||||
|
||||
const PinnedColumnCell = useCallback(
|
||||
(cellProps: CellComponentProps & CellProps) => {
|
||||
(cellProps: CellComponentProps & TableItemProps) => {
|
||||
return <CellComponent {...cellProps} rowIndex={cellProps.rowIndex + pinnedRowCount} />;
|
||||
},
|
||||
[pinnedRowCount, CellComponent],
|
||||
);
|
||||
|
||||
const PinnedRightColumnCell = useCallback(
|
||||
(cellProps: CellComponentProps & CellProps) => {
|
||||
(cellProps: CellComponentProps & TableItemProps) => {
|
||||
return (
|
||||
<CellComponent
|
||||
{...cellProps}
|
||||
@@ -547,7 +584,7 @@ export const ItemTableList = ({
|
||||
);
|
||||
|
||||
const PinnedRightIntersectionCell = useCallback(
|
||||
(cellProps: CellComponentProps & CellProps) => {
|
||||
(cellProps: CellComponentProps & TableItemProps) => {
|
||||
return (
|
||||
<CellComponent
|
||||
{...cellProps}
|
||||
@@ -559,7 +596,7 @@ export const ItemTableList = ({
|
||||
);
|
||||
|
||||
const RowCell = useCallback(
|
||||
(cellProps: CellComponentProps<CellProps>) => {
|
||||
(cellProps: CellComponentProps<TableItemProps>) => {
|
||||
return (
|
||||
<CellComponent
|
||||
{...cellProps}
|
||||
@@ -571,17 +608,16 @@ export const ItemTableList = ({
|
||||
[pinnedLeftColumnCount, pinnedRowCount, CellComponent],
|
||||
);
|
||||
|
||||
const cellProps = {
|
||||
const itemProps: TableItemProps = {
|
||||
columns: sortedColumns,
|
||||
data,
|
||||
enableExpansion,
|
||||
enableHeader,
|
||||
enableRowBorders,
|
||||
enableRowHover,
|
||||
handleExpand,
|
||||
enableSelection,
|
||||
internalState,
|
||||
itemType,
|
||||
onItemClick,
|
||||
onItemContextMenu,
|
||||
onItemDoubleClick,
|
||||
size,
|
||||
};
|
||||
|
||||
@@ -589,92 +625,41 @@ export const ItemTableList = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialTop || isInitialScrollPositionSet.current) return;
|
||||
isInitialScrollPositionSet.current = true;
|
||||
|
||||
const scrollToIndex = (index: number, behavior: 'auto' | 'smooth' = 'auto') => {
|
||||
isInitialScrollPositionSet.current = true;
|
||||
const adjustedIndex = enableHeader ? Math.max(0, index - 1) : index;
|
||||
|
||||
// Calculate scroll position based on row heights
|
||||
const calculateScrollTop = (rowIndex: number) => {
|
||||
let scrollTop = 0;
|
||||
for (let i = 0; i < rowIndex; i++) {
|
||||
const height = rowHeight as number;
|
||||
scrollTop += height;
|
||||
}
|
||||
return scrollTop;
|
||||
};
|
||||
|
||||
const scrollTop = calculateScrollTop(adjustedIndex);
|
||||
|
||||
// Get the scroll containers and scroll them directly
|
||||
const mainContainer = rowRef.current?.childNodes[0] as HTMLDivElement;
|
||||
const pinnedLeftContainer = pinnedLeftColumnRef.current
|
||||
?.childNodes[0] as HTMLDivElement;
|
||||
const pinnedRightContainer = pinnedRightColumnRef.current
|
||||
?.childNodes[0] as HTMLDivElement;
|
||||
|
||||
if (initialTop.type === 'offset') {
|
||||
if (mainContainer) {
|
||||
mainContainer.scrollTo({
|
||||
behavior,
|
||||
top: initialTop.to,
|
||||
});
|
||||
}
|
||||
|
||||
if (pinnedLeftContainer) {
|
||||
pinnedLeftContainer.scrollTo({
|
||||
behavior,
|
||||
top: initialTop.to,
|
||||
});
|
||||
}
|
||||
|
||||
if (pinnedRightContainer) {
|
||||
pinnedRightContainer.scrollTo({
|
||||
behavior,
|
||||
top: initialTop.to,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (mainContainer) {
|
||||
mainContainer.scrollTo({
|
||||
behavior,
|
||||
top: scrollTop,
|
||||
});
|
||||
}
|
||||
|
||||
if (pinnedLeftContainer) {
|
||||
pinnedLeftContainer.scrollTo({
|
||||
behavior,
|
||||
top: scrollTop,
|
||||
});
|
||||
}
|
||||
|
||||
if (pinnedRightContainer) {
|
||||
pinnedRightContainer.scrollTo({
|
||||
behavior,
|
||||
top: scrollTop,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scrollToIndex(initialTop.to, initialTop.behavior);
|
||||
}, [initialTop, enableHeader, pinnedLeftColumnCount, pinnedRightColumnCount, rowHeight]);
|
||||
|
||||
// Expose grid refs to parent component
|
||||
useEffect(() => {
|
||||
if (ref && typeof ref === 'object') {
|
||||
// Create a simple API that exposes the main container
|
||||
const combinedAPI: GridImperativeAPI = {
|
||||
// We'll create a minimal API that can be extended later
|
||||
// For now, we'll just expose the main container ref
|
||||
} as GridImperativeAPI;
|
||||
|
||||
if ('current' in ref) {
|
||||
(ref as React.MutableRefObject<GridImperativeAPI>).current = combinedAPI;
|
||||
}
|
||||
if (initialTop.type === 'offset') {
|
||||
scrollToTableOffset(initialTop.to);
|
||||
} else {
|
||||
scrollToTableIndex(initialTop.to);
|
||||
}
|
||||
}, [ref]);
|
||||
}, [initialTop, scrollToTableIndex, scrollToTableOffset]);
|
||||
|
||||
const imperativeHandle: ItemListHandle = useMemo(() => {
|
||||
return {
|
||||
clearExpanded: () => {
|
||||
internalState.clearExpanded();
|
||||
},
|
||||
clearSelected: () => {
|
||||
internalState.clearSelected();
|
||||
},
|
||||
getItem: (index: number) => (enableHeader ? data[index + 1] : data[index]),
|
||||
getItemCount: () => (enableHeader ? data.length - 1 : data.length),
|
||||
getItems: () => data,
|
||||
internalState,
|
||||
scrollToIndex: (index: number) => {
|
||||
scrollToTableIndex(enableHeader ? index + 1 : index);
|
||||
},
|
||||
scrollToOffset: (offset: number) => {
|
||||
scrollToTableOffset(enableHeader ? offset + headerHeight : offset);
|
||||
},
|
||||
};
|
||||
}, [data, enableHeader, headerHeight, internalState, scrollToTableIndex, scrollToTableOffset]);
|
||||
|
||||
useImperativeHandle(ref, () => imperativeHandle);
|
||||
|
||||
useEffect(() => {
|
||||
handleRef.current = imperativeHandle;
|
||||
}, [imperativeHandle]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -694,7 +679,7 @@ export const ItemTableList = ({
|
||||
className={styles.itemTablePinnedColumnsGridContainer}
|
||||
style={{
|
||||
minWidth: `${Array.from({ length: pinnedLeftColumnCount }, () => 0).reduce(
|
||||
(a, _, i) => a + columnWidth(i, cellProps),
|
||||
(a, _, i) => a + columnWidth(i, itemProps),
|
||||
0,
|
||||
)}px`,
|
||||
}}
|
||||
@@ -708,12 +693,12 @@ export const ItemTableList = ({
|
||||
minHeight: `${Array.from(
|
||||
{ length: pinnedRowCount },
|
||||
() => 0,
|
||||
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
|
||||
).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
cellComponent={CellComponent as any}
|
||||
cellProps={cellProps}
|
||||
cellProps={itemProps}
|
||||
className={styles.noScrollbar}
|
||||
columnCount={pinnedLeftColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
@@ -730,7 +715,7 @@ export const ItemTableList = ({
|
||||
>
|
||||
<Grid
|
||||
cellComponent={PinnedColumnCell}
|
||||
cellProps={cellProps}
|
||||
cellProps={itemProps}
|
||||
className={clsx(styles.noScrollbar, styles.height100)}
|
||||
columnCount={pinnedLeftColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
@@ -755,13 +740,13 @@ export const ItemTableList = ({
|
||||
minHeight: `${Array.from(
|
||||
{ length: pinnedRowCount },
|
||||
() => 0,
|
||||
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
|
||||
).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<Grid
|
||||
cellComponent={PinnedRowCell}
|
||||
cellProps={cellProps}
|
||||
cellProps={itemProps}
|
||||
className={styles.noScrollbar}
|
||||
columnCount={totalColumnCount}
|
||||
columnWidth={(index, cellProps) => {
|
||||
@@ -776,7 +761,7 @@ export const ItemTableList = ({
|
||||
<div className={styles.itemTableGridContainer} ref={mergedRowRef}>
|
||||
<Grid
|
||||
cellComponent={RowCell}
|
||||
cellProps={cellProps}
|
||||
cellProps={itemProps}
|
||||
className={styles.height100}
|
||||
columnCount={totalColumnCount}
|
||||
columnWidth={(index, cellProps) => {
|
||||
@@ -808,7 +793,7 @@ export const ItemTableList = ({
|
||||
a +
|
||||
columnWidth(
|
||||
i + pinnedLeftColumnCount + totalColumnCount,
|
||||
cellProps,
|
||||
itemProps,
|
||||
),
|
||||
0,
|
||||
)}px`,
|
||||
@@ -823,12 +808,12 @@ export const ItemTableList = ({
|
||||
minHeight: `${Array.from(
|
||||
{ length: pinnedRowCount },
|
||||
() => 0,
|
||||
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
|
||||
).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
cellComponent={PinnedRightIntersectionCell}
|
||||
cellProps={cellProps}
|
||||
cellProps={itemProps}
|
||||
className={styles.noScrollbar}
|
||||
columnCount={pinnedRightColumnCount}
|
||||
columnWidth={(index, cellProps) => {
|
||||
@@ -851,7 +836,7 @@ export const ItemTableList = ({
|
||||
>
|
||||
<Grid
|
||||
cellComponent={PinnedRightColumnCell}
|
||||
cellProps={cellProps}
|
||||
cellProps={itemProps}
|
||||
className={clsx(styles.noScrollbar, styles.height100)}
|
||||
columnCount={pinnedRightColumnCount}
|
||||
columnWidth={(index, cellProps) => {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export interface ItemControls {
|
||||
onClick?: (
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
e: MouseEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
onDoubleClick?: (
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
e: MouseEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
onFavorite?: (
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
e: MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
onItemExpand?: (
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
e: MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
onMore?: (
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
e: MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
onPlay?: (
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
playType: Play,
|
||||
e: MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
onRating?: (
|
||||
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
|
||||
itemType: LibraryItem,
|
||||
e: MouseEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface ItemListComponentProps<TQuery> {
|
||||
itemsPerPage?: number;
|
||||
query: Omit<TQuery, 'limit' | 'startIndex'>;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export interface ItemListHandle {
|
||||
clearExpanded: () => void;
|
||||
clearSelected: () => void;
|
||||
getItem: (index: number) => unknown;
|
||||
getItemCount: () => number;
|
||||
getItems: () => unknown[];
|
||||
internalState: ItemListStateActions;
|
||||
scrollToIndex: (index: number, options?: { behavior?: 'auto' | 'smooth' }) => void;
|
||||
scrollToOffset: (offset: number, options?: { behavior?: 'auto' | 'smooth' }) => void;
|
||||
}
|
||||
Reference in New Issue
Block a user