remove item callbacks from list - move to item component

This commit is contained in:
jeffvli
2025-10-07 11:36:15 -07:00
parent 545ea25e43
commit d9e8625b15
11 changed files with 580 additions and 350 deletions
@@ -138,17 +138,29 @@
} }
svg { svg {
fill: rgb(255 255 255);
stroke: rgb(255 255 255); stroke: rgb(255 255 255);
} }
} }
.secondary-button.favorite { .user-data {
position: absolute;
top: 0; top: 0;
right: 0; right: 0;
} }
.rating {
position: absolute;
top: 0;
right: 0;
padding: var(--theme-spacing-md);
}
.secondary-button.options { .secondary-button.options {
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
.secondary-button.expand {
bottom: 0;
left: 0;
}
@@ -1,12 +1,27 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { MouseEvent } from 'react';
import styles from './item-card-controls.module.css'; 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 { animationVariants } from '/@/shared/components/animations/animation-variants';
import { AppIcon, Icon } from '/@/shared/components/icon/icon'; 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 { interface ItemCardControlsProps {
controls: ItemControls;
item: Album | AlbumArtist | Artist | Playlist | Song | undefined;
itemType: LibraryItem;
type?: 'compact' | 'default' | 'poster'; type?: 'compact' | 'default' | 'poster';
} }
@@ -27,27 +42,79 @@ const containerProps = {
animate: 'show', animate: 'show',
exit: 'hidden', exit: 'hidden',
initial: '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 ( return (
<motion.div className={clsx(styles.container)} {...containerProps[type]}> <motion.div className={clsx(styles.container)} {...containerProps[type]}>
<PlayButton /> <PlayButton
<SecondaryPlayButton className={styles.left} icon="mediaPlayNext" /> onClick={(e) => {
<SecondaryPlayButton className={styles.right} icon="mediaPlayLast" /> e.stopPropagation();
<SecondaryButton className={styles.favorite} icon="favorite" /> controls.onPlay?.(item, itemType, Play.NOW, e);
<SecondaryButton className={styles.options} icon="ellipsisHorizontal" /> }}
/>
<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> </motion.div>
); );
}; };
const PlayButton = () => { const PlayButton = ({ onClick }: { onClick?: (e: MouseEvent<HTMLButtonElement>) => void }) => {
return ( return (
<button <button
className={clsx(styles.playButton, styles.primary)} className={clsx(styles.playButton, styles.primary)}
onClick={(e) => e.stopPropagation()} onClick={(e) => {
e.stopPropagation();
onClick?.(e);
}}
> >
<Icon icon="mediaPlay" size="lg" /> <Icon icon="mediaPlay" size="lg" />
</button> </button>
@@ -57,12 +124,20 @@ const PlayButton = () => {
const SecondaryPlayButton = ({ const SecondaryPlayButton = ({
className, className,
icon, icon,
onClick,
}: { }: {
className?: string; className?: string;
icon: keyof typeof AppIcon; icon: keyof typeof AppIcon;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
}) => { }) => {
return ( 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" /> <Icon icon={icon} size="lg" />
</button> </button>
); );
@@ -71,11 +146,18 @@ const SecondaryPlayButton = ({
interface SecondaryButtonProps { interface SecondaryButtonProps {
className?: string; className?: string;
icon: keyof typeof AppIcon; icon: keyof typeof AppIcon;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
} }
const SecondaryButton = ({ className, icon }: SecondaryButtonProps) => { const SecondaryButton = ({ className, icon, onClick }: SecondaryButtonProps) => {
return ( return (
<button className={clsx(styles.secondaryButton, className)}> <button
className={clsx(styles.secondaryButton, className)}
onClick={(e) => {
e.stopPropagation();
onClick?.(e);
}}
>
<Icon icon={icon} size="lg" /> <Icon icon={icon} size="lg" />
</button> </button>
); );
@@ -28,7 +28,7 @@
&:hover { &:hover {
&::before { &::before {
opacity: 0.6; opacity: 0.7;
} }
} }
} }
+38 -51
View File
@@ -1,19 +1,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence } from 'motion/react'; import { AnimatePresence } from 'motion/react';
import { import { Dispatch, Fragment, memo, ReactNode, SetStateAction, useState } from 'react';
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';
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 { AppRoute } from '/@/renderer/router/routes';
import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
@@ -28,12 +21,6 @@ import {
Song, Song,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
const ItemCardControls = lazy(() =>
import('/@/renderer/components/item-card/item-card-controls').then((module) => ({
default: module.ItemCardControls,
})),
);
type DataRow = { type DataRow = {
format: (data: Album | AlbumArtist | Artist | Playlist | Song) => ReactNode | string; format: (data: Album | AlbumArtist | Artist | Playlist | Song) => ReactNode | string;
id: string; id: string;
@@ -41,27 +28,20 @@ type DataRow = {
}; };
interface ItemCardProps { interface ItemCardProps {
controls: ItemControls;
data: Album | AlbumArtist | Artist | Playlist | Song | undefined; data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
isRound?: boolean; isRound?: boolean;
itemType: LibraryItem; itemType: LibraryItem;
onClick?: (
e: MouseEvent<HTMLDivElement>,
item: Album | AlbumArtist | Artist | Playlist | Song | undefined,
itemType: LibraryItem,
) => void;
onItemExpand?: () => void;
onItemSelect?: () => void;
type?: 'compact' | 'default' | 'poster'; type?: 'compact' | 'default' | 'poster';
withControls?: boolean; withControls?: boolean;
} }
export const ItemCard = ({ export const ItemCard = ({
controls,
data, data,
isRound, isRound,
itemType, itemType,
onClick,
onItemExpand,
onItemSelect,
type = 'poster', type = 'poster',
withControls, withControls,
}: ItemCardProps) => { }: ItemCardProps) => {
@@ -74,13 +54,11 @@ export const ItemCard = ({
case 'compact': case 'compact':
return ( return (
<CompactItemCard <CompactItemCard
controls={controls}
data={data} data={data}
imageUrl={imageUrl} imageUrl={imageUrl}
isRound={isRound} isRound={isRound}
itemType={itemType} itemType={itemType}
onClick={onClick}
onItemExpand={onItemExpand}
onItemSelect={onItemSelect}
rows={rows} rows={rows}
setShowControls={setShowControls} setShowControls={setShowControls}
showControls={showControls} showControls={showControls}
@@ -90,13 +68,11 @@ export const ItemCard = ({
case 'poster': case 'poster':
return ( return (
<PosterItemCard <PosterItemCard
controls={controls}
data={data} data={data}
imageUrl={imageUrl} imageUrl={imageUrl}
isRound={isRound} isRound={isRound}
itemType={itemType} itemType={itemType}
onClick={onClick}
onItemExpand={onItemExpand}
onItemSelect={onItemSelect}
rows={rows} rows={rows}
setShowControls={setShowControls} setShowControls={setShowControls}
showControls={showControls} showControls={showControls}
@@ -107,13 +83,11 @@ export const ItemCard = ({
default: default:
return ( return (
<DefaultItemCard <DefaultItemCard
controls={controls}
data={data} data={data}
imageUrl={imageUrl} imageUrl={imageUrl}
isRound={isRound} isRound={isRound}
itemType={itemType} itemType={itemType}
onClick={onClick}
onItemExpand={onItemExpand}
onItemSelect={onItemSelect}
rows={rows} rows={rows}
setShowControls={setShowControls} setShowControls={setShowControls}
showControls={showControls} showControls={showControls}
@@ -124,6 +98,7 @@ export const ItemCard = ({
}; };
export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> { export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
controls: ItemControls;
imageUrl: string | undefined; imageUrl: string | undefined;
rows: DataRow[]; rows: DataRow[];
setShowControls: Dispatch<SetStateAction<boolean>>; setShowControls: Dispatch<SetStateAction<boolean>>;
@@ -131,13 +106,11 @@ export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
} }
const CompactItemCard = ({ const CompactItemCard = ({
controls,
data, data,
imageUrl, imageUrl,
isRound, isRound,
itemType, itemType,
onClick,
onItemExpand,
onItemSelect,
rows, rows,
setShowControls, setShowControls,
showControls, showControls,
@@ -148,7 +121,6 @@ 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={(e) => onClick?.(e, data, itemType)}
onMouseEnter={() => withControls && setShowControls(true)} onMouseEnter={() => withControls && setShowControls(true)}
onMouseLeave={() => withControls && setShowControls(false)} onMouseLeave={() => withControls && setShowControls(false)}
> >
@@ -157,7 +129,14 @@ const CompactItemCard = ({
src={imageUrl} src={imageUrl}
/> />
<AnimatePresence> <AnimatePresence>
{withControls && showControls && <ItemCardControls type="compact" />} {withControls && showControls && (
<ItemCardControls
controls={controls}
item={data}
itemType={itemType}
type="compact"
/>
)}
</AnimatePresence> </AnimatePresence>
<div className={clsx(styles.detailContainer, styles.compact)}> <div className={clsx(styles.detailContainer, styles.compact)}>
{rows.map((row) => ( {rows.map((row) => (
@@ -186,13 +165,11 @@ const CompactItemCard = ({
}; };
const DefaultItemCard = ({ const DefaultItemCard = ({
controls,
data, data,
imageUrl, imageUrl,
isRound, isRound,
itemType, itemType,
onClick,
onItemExpand,
onItemSelect,
rows, rows,
setShowControls, setShowControls,
showControls, showControls,
@@ -203,8 +180,6 @@ 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={(e) => onClick?.(e, data, itemType)}
onDoubleClick={onItemExpand}
onMouseEnter={() => withControls && setShowControls(true)} onMouseEnter={() => withControls && setShowControls(true)}
onMouseLeave={() => withControls && setShowControls(false)} onMouseLeave={() => withControls && setShowControls(false)}
> >
@@ -213,7 +188,14 @@ const DefaultItemCard = ({
src={imageUrl} src={imageUrl}
/> />
<AnimatePresence> <AnimatePresence>
{withControls && showControls && <ItemCardControls type="default" />} {withControls && showControls && (
<ItemCardControls
controls={controls}
item={data}
itemType={itemType}
type="default"
/>
)}
</AnimatePresence> </AnimatePresence>
</div> </div>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
@@ -242,13 +224,11 @@ const DefaultItemCard = ({
}; };
const PosterItemCard = ({ const PosterItemCard = ({
controls,
data, data,
imageUrl, imageUrl,
isRound, isRound,
itemType, itemType,
onClick,
onItemExpand,
onItemSelect,
rows, rows,
setShowControls, setShowControls,
showControls, showControls,
@@ -259,7 +239,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={(e) => onClick?.(e, data, itemType)} onClick={(e) => controls?.onClick?.(data, itemType, e)}
onMouseEnter={() => withControls && setShowControls(true)} onMouseEnter={() => withControls && setShowControls(true)}
onMouseLeave={() => withControls && setShowControls(false)} onMouseLeave={() => withControls && setShowControls(false)}
> >
@@ -268,7 +248,14 @@ const PosterItemCard = ({
src={imageUrl} src={imageUrl}
/> />
<AnimatePresence> <AnimatePresence>
{withControls && showControls && <ItemCardControls type="poster" />} {withControls && showControls && (
<ItemCardControls
controls={controls}
item={data}
itemType={itemType}
type="poster"
/>
)}
</AnimatePresence> </AnimatePresence>
</div> </div>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
@@ -6,6 +6,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
import { getServerById } from '/@/renderer/store'; import { getServerById } from '/@/renderer/store';
export interface InfiniteListProps<TQuery> { export interface InfiniteListProps<TQuery> {
itemsPerPage?: number;
query: Omit<TQuery, 'limit' | 'startIndex'>; query: Omit<TQuery, 'limit' | 'startIndex'>;
serverId: string; serverId: string;
} }
@@ -6,17 +6,6 @@
padding: 0 var(--theme-spacing-md); 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 { .grid-list-container {
width: 100%; width: 100%;
padding: 0 var(--theme-spacing-md); padding: 0 var(--theme-spacing-md);
@@ -35,11 +24,6 @@
overflow: hidden; overflow: hidden;
} }
.full-width-content {
grid-column: 1 / -1;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 10%);
}
.list-expanded-container { .list-expanded-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -4,7 +4,6 @@ import { AnimatePresence, motion, Variants } from 'motion/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { import {
CSSProperties, CSSProperties,
MouseEvent,
Ref, Ref,
UIEvent, UIEvent,
useCallback, useCallback,
@@ -21,46 +20,42 @@ import styles from './item-grid-list.module.css';
import { getDataRowsCount, 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 { import {
ItemListItem,
ItemListStateActions, ItemListStateActions,
useItemListState, useItemListState,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } 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 { 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 { export interface ItemGridListProps {
data: unknown[]; data: unknown[];
enableExpansion?: boolean; enableExpansion?: boolean;
enableSelection?: boolean; enableSelection?: boolean;
initialTopMostItemIndex?: initialTop?: {
| number behavior?: 'auto' | 'smooth';
| { to: number;
align: 'center' | 'end' | 'start'; type: 'index' | 'offset';
behavior: 'auto' | 'smooth'; };
index: number;
offset?: number;
};
itemType: LibraryItem; itemType: LibraryItem;
onEndReached?: (index: number) => void; onEndReached?: (index: number, handle: ItemListHandle) => void;
onItemClick?: (item: unknown, index: number) => void;
onItemContextMenu?: (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?: (e: UIEvent<HTMLDivElement>) => void; onScroll?: (e: UIEvent<HTMLDivElement>) => void;
onScrollEnd?: () => void; onScrollEnd?: (offset: number, handle: ItemListHandle) => void;
onStartReached?: (index: number) => void; onStartReached?: (index: number, handle: ItemListHandle) => void;
ref: Ref<ListImperativeAPI>; 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,
@@ -79,15 +74,10 @@ export const ItemGridList = ({
data, data,
enableExpansion = false, enableExpansion = false,
enableSelection = false, enableSelection = false,
initialTopMostItemIndex = 0,
itemType, itemType,
onEndReached, onEndReached,
onItemClick,
onItemContextMenu,
onItemDoubleClick,
onRangeChanged, onRangeChanged,
onScroll, onScroll,
onScrollEnd,
onStartReached, onStartReached,
totalItemCount = 0, totalItemCount = 0,
}: ItemGridListProps) => { }: ItemGridListProps) => {
@@ -133,19 +123,6 @@ export const ItemGridList = ({
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( const handleScroll = useCallback(
(e: UIEvent<HTMLDivElement>) => { (e: UIEvent<HTMLDivElement>) => {
onScroll?.(e); onScroll?.(e);
@@ -218,14 +195,21 @@ export const ItemGridList = ({
const endRow = visibleRows.stopIndex; const endRow = visibleRows.stopIndex;
if (startRow === 0) { if (startRow === 0) {
onStartReached?.(startRow); onStartReached?.(startRow, itemGridRef.current ?? (undefined as any));
} }
if (endRow >= totalRows) { 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(() => { const elements = useMemo(() => {
@@ -253,6 +237,15 @@ export const ItemGridList = ({
); );
}, [tableMeta, data]); }, [tableMeta, data]);
const itemProps: GridItemProps = {
columns: tableMeta?.columnCount || 0,
data: elements,
enableExpansion,
enableSelection,
internalState,
itemType,
};
return ( return (
<motion.div <motion.div
animate={{ animate={{
@@ -272,15 +265,10 @@ export const ItemGridList = ({
listRef={itemGridRef} listRef={itemGridRef}
onRowsRendered={handleOnRowsRendered} onRowsRendered={handleOnRowsRendered}
onScroll={handleScroll} onScroll={handleScroll}
rowComponent={RowComponent} rowComponent={ListComponent}
rowCount={tableMeta?.rowCount || 0} rowCount={tableMeta?.rowCount || 0}
rowHeight={tableMeta?.itemHeight || 0} rowHeight={tableMeta?.itemHeight || 0}
rowProps={{ rowProps={itemProps}
columns: tableMeta?.columnCount || 0,
data: elements,
handleExpand,
itemType,
}}
/> />
<AnimatePresence> <AnimatePresence>
{hasExpanded && ( {hasExpanded && (
@@ -300,19 +288,16 @@ export const ItemGridList = ({
); );
}; };
function RowComponent({ const ListComponent = ({
columns, columns,
data, data,
handleExpand, enableExpansion,
enableSelection,
index, index,
internalState,
itemType, itemType,
style, style,
}: RowComponentProps<{ }: RowComponentProps<GridItemProps>) => {
columns: number;
data: any[];
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
itemType: LibraryItem;
}>) {
return ( return (
<div className={styles.itemList} style={style}> <div className={styles.itemList} style={style}>
{data[index].map((d) => ( {data[index].map((d) => (
@@ -322,14 +307,104 @@ function RowComponent({
style={{ '--columns': columns } as CSSProperties} style={{ '--columns': columns } as CSSProperties}
> >
<ItemCard <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} data={d.data}
itemType={itemType} itemType={itemType}
onClick={(e, item, itemType) => handleExpand(e, item, itemType)}
type="poster"
withControls withControls
/> />
</div> </div>
))} ))}
</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'; import { useSearchParams } from 'react-router-dom';
interface UseItemListPaginationProps { export const useItemListPagination = () => {
initialPage?: number;
}
export const useItemListPagination = ({ initialPage }: UseItemListPaginationProps) => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const currentPage = initialPage || Number(searchParams.get('currentPage')) || 0; const currentPage = Number(searchParams.get('currentPage')) || 0;
const onChange = (index: number) => { const onChange = (index: number) => {
setSearchParams( setSearchParams(
@@ -5,6 +5,7 @@ import { CellComponentProps } from 'react-window-v2';
import styles from './item-table-list-column.module.css'; import styles from './item-table-list-column.module.css';
import i18n from '/@/i18n/i18n'; 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 { 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 { 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'; 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 { 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 { 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 { 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 { Icon } from '/@/shared/components/icon/icon';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { TableColumn } from '/@/shared/types/types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { createDoubleClickHandler } from '/@/shared/utils/double-click-handler'; import { Play, TableColumn } from '/@/shared/types/types';
export interface ItemTableListColumn extends CellComponentProps<CellProps> {} export interface ItemTableListColumn extends CellComponentProps<TableItemProps> {}
export interface ItemTableListInnerColumn extends ItemTableListColumn { export interface ItemTableListInnerColumn extends ItemTableListColumn {
type: TableColumn; type: TableColumn;
@@ -159,18 +160,6 @@ export const TableColumnTextContainer = (
props.enableRowBorders && props.enableHeader && props.rowIndex > 0, props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
})} })}
data-row-index={isDataRow ? props.rowIndex : undefined} 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} ref={containerRef}
style={props.style} style={props.style}
> >
@@ -237,18 +226,6 @@ export const TableColumnContainer = (
props.enableRowBorders && props.enableHeader && props.rowIndex > 0, props.enableRowBorders && props.enableHeader && props.rowIndex > 0,
})} })}
data-row-index={isDataRow ? props.rowIndex : undefined} 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} ref={containerRef}
style={props.style} 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> = { 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]: i18n.t('table.column.album', { postProcess: 'upperCase' }) as string,
[TableColumn.ALBUM_ARTIST]: i18n.t('table.column.albumArtist', { [TableColumn.ALBUM_ARTIST]: i18n.t('table.column.albumArtist', {
postProcess: 'upperCase', postProcess: 'upperCase',
@@ -335,9 +377,7 @@ const columnLabelMap: Record<TableColumn, ReactNode | string> = {
[TableColumn.TRACK_NUMBER]: i18n.t('table.column.trackNumber', { [TableColumn.TRACK_NUMBER]: i18n.t('table.column.trackNumber', {
postProcess: 'upperCase', postProcess: 'upperCase',
}) as string, }) as string,
[TableColumn.USER_FAVORITE]: i18n.t('table.column.favorite', { [TableColumn.USER_FAVORITE]: <Icon fill="default" icon="favorite" size="md" />,
postProcess: 'upperCase',
}) as string,
[TableColumn.USER_RATING]: i18n.t('table.column.rating', { [TableColumn.USER_RATING]: i18n.t('table.column.rating', {
postProcess: 'upperCase', postProcess: 'upperCase',
}) as string, }) as string,
@@ -6,38 +6,28 @@ import { AnimatePresence, motion, Variants } from 'motion/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { import {
type JSXElementConstructor, type JSXElementConstructor,
MouseEvent,
Ref, Ref,
useCallback, useCallback,
useEffect, useEffect,
useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from 'react'; } 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 styles from './item-table-list.module.css';
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item'; 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 { 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 { LibraryItem } from '/@/shared/types/domain-types';
import { TableColumn } from '/@/shared/types/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 { export interface ItemTableListColumnConfig {
align: 'center' | 'end' | 'start'; align: 'center' | 'end' | 'start';
id: TableColumn; id: TableColumn;
@@ -45,8 +35,21 @@ export interface ItemTableListColumnConfig {
width: number; 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 { interface ItemTableListProps {
CellComponent: JSXElementConstructor<CellComponentProps<CellProps>>; CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
columns: ItemTableListColumnConfig[]; columns: ItemTableListColumnConfig[];
data: unknown[]; data: unknown[];
enableExpansion?: boolean; enableExpansion?: boolean;
@@ -61,18 +64,17 @@ interface ItemTableListProps {
type: 'index' | 'offset'; type: 'index' | 'offset';
}; };
itemType: LibraryItem; itemType: LibraryItem;
onCellsRendered?: GridProps<CellProps>['onCellsRendered']; onCellsRendered?: GridProps<TableItemProps>['onCellsRendered'];
onEndReached?: (index: number) => void; onEndReached?: (index: number, internalState: ItemListStateActions) => void;
onItemClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void; onRangeChanged?: (
onItemContextMenu?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void; range: { endIndex: number; startIndex: number },
onItemDoubleClick?: (item: unknown, index: number, event: MouseEvent<HTMLDivElement>) => void; internalState: ItemListStateActions,
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void; ) => void;
onScrollEnd?: (offset: number) => void; onScrollEnd?: (offset: number, internalState: ItemListStateActions) => void;
onStartReached?: (index: number) => void; onStartReached?: (index: number, internalState: ItemListStateActions) => void;
ref?: Ref<GridImperativeAPI>; ref?: Ref<ItemListHandle>;
rowHeight: ((index: number, cellProps: CellProps) => number) | number; rowHeight?: ((index: number, cellProps: TableItemProps) => number) | number;
size?: 'compact' | 'default'; size?: 'compact' | 'default';
totalItemCount: number;
} }
const expandedAnimationVariants: Variants = { const expandedAnimationVariants: Variants = {
@@ -97,26 +99,24 @@ export const ItemTableList = ({
enableHeader = true, enableHeader = true,
enableRowBorders = false, enableRowBorders = false,
enableRowHover = false, enableRowHover = false,
enableSelection = false,
headerHeight = 40, headerHeight = 40,
initialTop, initialTop,
itemType, itemType,
onCellsRendered, onCellsRendered,
onEndReached, onEndReached,
onItemClick,
onItemContextMenu,
onItemDoubleClick,
onRangeChanged, onRangeChanged,
onScrollEnd, onScrollEnd,
onStartReached, onStartReached,
ref, ref,
rowHeight, rowHeight,
size = 'default', size = 'default',
totalItemCount,
}: ItemTableListProps) => { }: ItemTableListProps) => {
const totalItemCount = data.length;
const sortedColumns = useMemo(() => sortTableColumns(columns), [columns]); const sortedColumns = useMemo(() => sortTableColumns(columns), [columns]);
const columnCount = sortedColumns.length; const columnCount = sortedColumns.length;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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 pinnedLeftColumnCount = sortedColumns.filter((col) => col.pinned === 'left').length;
const pinnedRightColumnCount = sortedColumns.filter((col) => col.pinned === 'right').length; const pinnedRightColumnCount = sortedColumns.filter((col) => col.pinned === 'right').length;
@@ -131,8 +131,53 @@ export const ItemTableList = ({
const mergedRowRef = useMergedRef(rowRef, scrollContainerRef); const mergedRowRef = useMergedRef(rowRef, scrollContainerRef);
const [showLeftShadow, setShowLeftShadow] = useState(false); const [showLeftShadow, setShowLeftShadow] = useState(false);
const [showRightShadow, setShowRightShadow] = useState(false); const [showRightShadow, setShowRightShadow] = useState(false);
const handleRef = useRef<ItemListHandle | null>(null);
const onScrollEndRef = useRef<ItemTableListProps['onScrollEnd']>(onScrollEnd); 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({ const [initialize] = useOverlayScrollbars({
defer: true, defer: true,
@@ -232,7 +277,10 @@ export const ItemTableList = ({
scrollTimeouts.delete(element); scrollTimeouts.delete(element);
if (element === row && onScrollEndRef.current) { if (element === row && onScrollEndRef.current) {
onScrollEndRef.current(row.scrollTop); onScrollEndRef.current(
row.scrollTop,
handleRef.current ?? (undefined as any),
);
} }
}, 150); }, 150);
@@ -430,9 +478,11 @@ export const ItemTableList = ({
}, [pinnedLeftColumnCount, pinnedRightColumnCount]); }, [pinnedLeftColumnCount, pinnedRightColumnCount]);
const getRowHeight = useCallback( const getRowHeight = useCallback(
(index: number, cellProps: CellProps) => { (index: number, cellProps: TableItemProps) => {
const height = size === 'compact' ? 40 : 68;
const baseHeight = 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 is true and this is the first sticky row, use fixed header height
if (enableHeader && index === 0 && pinnedRowCount > 0) { if (enableHeader && index === 0 && pinnedRowCount > 0) {
@@ -441,30 +491,13 @@ export const ItemTableList = ({
return baseHeight; return baseHeight;
}, },
[enableHeader, headerHeight, rowHeight, pinnedRowCount], [enableHeader, headerHeight, rowHeight, pinnedRowCount, size],
); );
const internalState = useItemListState(); const internalState = useItemListState();
const hasExpanded = internalState.hasExpanded(); 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( const handleOnCellsRendered = useCallback(
(cells: { (cells: {
columnStartIndex: number; columnStartIndex: number;
@@ -472,18 +505,21 @@ export const ItemTableList = ({
rowStartIndex: number; rowStartIndex: number;
rowStopIndex: number; rowStopIndex: number;
}) => { }) => {
onRangeChanged?.({ onRangeChanged?.(
endIndex: cells.rowStopIndex, {
startIndex: cells.rowStartIndex, endIndex: cells.rowStopIndex,
}); startIndex: cells.rowStartIndex,
},
internalState,
);
if (onStartReached || onEndReached) { if (onStartReached || onEndReached) {
if (cells.rowStartIndex === 0) { if (cells.rowStartIndex === 0) {
onStartReached?.(0); onStartReached?.(0, handleRef.current ?? (undefined as any));
} }
if (cells.rowStopIndex + 10 >= totalItemCount) { if (cells.rowStopIndex + 10 >= totalItemCount) {
onEndReached?.(totalItemCount); onEndReached?.(totalItemCount, handleRef.current ?? (undefined as any));
} }
} }
@@ -511,11 +547,12 @@ export const ItemTableList = ({
pinnedLeftColumnCount, pinnedLeftColumnCount,
pinnedRowCount, pinnedRowCount,
totalItemCount, totalItemCount,
internalState,
], ],
); );
const PinnedRowCell = useCallback( const PinnedRowCell = useCallback(
(cellProps: CellComponentProps & CellProps) => { (cellProps: CellComponentProps & TableItemProps) => {
return ( return (
<CellComponent <CellComponent
{...cellProps} {...cellProps}
@@ -527,14 +564,14 @@ export const ItemTableList = ({
); );
const PinnedColumnCell = useCallback( const PinnedColumnCell = useCallback(
(cellProps: CellComponentProps & CellProps) => { (cellProps: CellComponentProps & TableItemProps) => {
return <CellComponent {...cellProps} rowIndex={cellProps.rowIndex + pinnedRowCount} />; return <CellComponent {...cellProps} rowIndex={cellProps.rowIndex + pinnedRowCount} />;
}, },
[pinnedRowCount, CellComponent], [pinnedRowCount, CellComponent],
); );
const PinnedRightColumnCell = useCallback( const PinnedRightColumnCell = useCallback(
(cellProps: CellComponentProps & CellProps) => { (cellProps: CellComponentProps & TableItemProps) => {
return ( return (
<CellComponent <CellComponent
{...cellProps} {...cellProps}
@@ -547,7 +584,7 @@ export const ItemTableList = ({
); );
const PinnedRightIntersectionCell = useCallback( const PinnedRightIntersectionCell = useCallback(
(cellProps: CellComponentProps & CellProps) => { (cellProps: CellComponentProps & TableItemProps) => {
return ( return (
<CellComponent <CellComponent
{...cellProps} {...cellProps}
@@ -559,7 +596,7 @@ export const ItemTableList = ({
); );
const RowCell = useCallback( const RowCell = useCallback(
(cellProps: CellComponentProps<CellProps>) => { (cellProps: CellComponentProps<TableItemProps>) => {
return ( return (
<CellComponent <CellComponent
{...cellProps} {...cellProps}
@@ -571,17 +608,16 @@ export const ItemTableList = ({
[pinnedLeftColumnCount, pinnedRowCount, CellComponent], [pinnedLeftColumnCount, pinnedRowCount, CellComponent],
); );
const cellProps = { const itemProps: TableItemProps = {
columns: sortedColumns, columns: sortedColumns,
data, data,
enableExpansion,
enableHeader, enableHeader,
enableRowBorders, enableRowBorders,
enableRowHover, enableRowHover,
handleExpand, enableSelection,
internalState,
itemType, itemType,
onItemClick,
onItemContextMenu,
onItemDoubleClick,
size, size,
}; };
@@ -589,92 +625,41 @@ export const ItemTableList = ({
useEffect(() => { useEffect(() => {
if (!initialTop || isInitialScrollPositionSet.current) return; if (!initialTop || isInitialScrollPositionSet.current) return;
isInitialScrollPositionSet.current = true;
const scrollToIndex = (index: number, behavior: 'auto' | 'smooth' = 'auto') => { if (initialTop.type === 'offset') {
isInitialScrollPositionSet.current = true; scrollToTableOffset(initialTop.to);
const adjustedIndex = enableHeader ? Math.max(0, index - 1) : index; } else {
scrollToTableIndex(initialTop.to);
// 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;
}
} }
}, [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 ( return (
<motion.div <motion.div
@@ -694,7 +679,7 @@ export const ItemTableList = ({
className={styles.itemTablePinnedColumnsGridContainer} className={styles.itemTablePinnedColumnsGridContainer}
style={{ style={{
minWidth: `${Array.from({ length: pinnedLeftColumnCount }, () => 0).reduce( minWidth: `${Array.from({ length: pinnedLeftColumnCount }, () => 0).reduce(
(a, _, i) => a + columnWidth(i, cellProps), (a, _, i) => a + columnWidth(i, itemProps),
0, 0,
)}px`, )}px`,
}} }}
@@ -708,12 +693,12 @@ export const ItemTableList = ({
minHeight: `${Array.from( minHeight: `${Array.from(
{ length: pinnedRowCount }, { length: pinnedRowCount },
() => 0, () => 0,
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`, ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
}} }}
> >
<Grid <Grid
cellComponent={CellComponent as any} cellComponent={CellComponent as any}
cellProps={cellProps} cellProps={itemProps}
className={styles.noScrollbar} className={styles.noScrollbar}
columnCount={pinnedLeftColumnCount} columnCount={pinnedLeftColumnCount}
columnWidth={columnWidth} columnWidth={columnWidth}
@@ -730,7 +715,7 @@ export const ItemTableList = ({
> >
<Grid <Grid
cellComponent={PinnedColumnCell} cellComponent={PinnedColumnCell}
cellProps={cellProps} cellProps={itemProps}
className={clsx(styles.noScrollbar, styles.height100)} className={clsx(styles.noScrollbar, styles.height100)}
columnCount={pinnedLeftColumnCount} columnCount={pinnedLeftColumnCount}
columnWidth={columnWidth} columnWidth={columnWidth}
@@ -755,13 +740,13 @@ export const ItemTableList = ({
minHeight: `${Array.from( minHeight: `${Array.from(
{ length: pinnedRowCount }, { length: pinnedRowCount },
() => 0, () => 0,
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`, ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
} as React.CSSProperties } as React.CSSProperties
} }
> >
<Grid <Grid
cellComponent={PinnedRowCell} cellComponent={PinnedRowCell}
cellProps={cellProps} cellProps={itemProps}
className={styles.noScrollbar} className={styles.noScrollbar}
columnCount={totalColumnCount} columnCount={totalColumnCount}
columnWidth={(index, cellProps) => { columnWidth={(index, cellProps) => {
@@ -776,7 +761,7 @@ export const ItemTableList = ({
<div className={styles.itemTableGridContainer} ref={mergedRowRef}> <div className={styles.itemTableGridContainer} ref={mergedRowRef}>
<Grid <Grid
cellComponent={RowCell} cellComponent={RowCell}
cellProps={cellProps} cellProps={itemProps}
className={styles.height100} className={styles.height100}
columnCount={totalColumnCount} columnCount={totalColumnCount}
columnWidth={(index, cellProps) => { columnWidth={(index, cellProps) => {
@@ -808,7 +793,7 @@ export const ItemTableList = ({
a + a +
columnWidth( columnWidth(
i + pinnedLeftColumnCount + totalColumnCount, i + pinnedLeftColumnCount + totalColumnCount,
cellProps, itemProps,
), ),
0, 0,
)}px`, )}px`,
@@ -823,12 +808,12 @@ export const ItemTableList = ({
minHeight: `${Array.from( minHeight: `${Array.from(
{ length: pinnedRowCount }, { length: pinnedRowCount },
() => 0, () => 0,
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`, ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
}} }}
> >
<Grid <Grid
cellComponent={PinnedRightIntersectionCell} cellComponent={PinnedRightIntersectionCell}
cellProps={cellProps} cellProps={itemProps}
className={styles.noScrollbar} className={styles.noScrollbar}
columnCount={pinnedRightColumnCount} columnCount={pinnedRightColumnCount}
columnWidth={(index, cellProps) => { columnWidth={(index, cellProps) => {
@@ -851,7 +836,7 @@ export const ItemTableList = ({
> >
<Grid <Grid
cellComponent={PinnedRightColumnCell} cellComponent={PinnedRightColumnCell}
cellProps={cellProps} cellProps={itemProps}
className={clsx(styles.noScrollbar, styles.height100)} className={clsx(styles.noScrollbar, styles.height100)}
columnCount={pinnedRightColumnCount} columnCount={pinnedRightColumnCount}
columnWidth={(index, cellProps) => { 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;
}