mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
remove item callbacks from list - move to item component
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
endIndex: cells.rowStopIndex,
|
||||||
startIndex: cells.rowStartIndex,
|
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;
|
||||||
|
|
||||||
const scrollToIndex = (index: number, behavior: 'auto' | 'smooth' = 'auto') => {
|
|
||||||
isInitialScrollPositionSet.current = true;
|
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 (initialTop.type === 'offset') {
|
||||||
if (mainContainer) {
|
scrollToTableOffset(initialTop.to);
|
||||||
mainContainer.scrollTo({
|
|
||||||
behavior,
|
|
||||||
top: initialTop.to,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pinnedLeftContainer) {
|
|
||||||
pinnedLeftContainer.scrollTo({
|
|
||||||
behavior,
|
|
||||||
top: initialTop.to,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pinnedRightContainer) {
|
|
||||||
pinnedRightContainer.scrollTo({
|
|
||||||
behavior,
|
|
||||||
top: initialTop.to,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (mainContainer) {
|
scrollToTableIndex(initialTop.to);
|
||||||
mainContainer.scrollTo({
|
|
||||||
behavior,
|
|
||||||
top: scrollTop,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}, [initialTop, scrollToTableIndex, scrollToTableOffset]);
|
||||||
|
|
||||||
if (pinnedLeftContainer) {
|
const imperativeHandle: ItemListHandle = useMemo(() => {
|
||||||
pinnedLeftContainer.scrollTo({
|
return {
|
||||||
behavior,
|
clearExpanded: () => {
|
||||||
top: scrollTop,
|
internalState.clearExpanded();
|
||||||
});
|
},
|
||||||
}
|
clearSelected: () => {
|
||||||
|
internalState.clearSelected();
|
||||||
if (pinnedRightContainer) {
|
},
|
||||||
pinnedRightContainer.scrollTo({
|
getItem: (index: number) => (enableHeader ? data[index + 1] : data[index]),
|
||||||
behavior,
|
getItemCount: () => (enableHeader ? data.length - 1 : data.length),
|
||||||
top: scrollTop,
|
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]);
|
||||||
|
|
||||||
scrollToIndex(initialTop.to, initialTop.behavior);
|
useImperativeHandle(ref, () => imperativeHandle);
|
||||||
}, [initialTop, enableHeader, pinnedLeftColumnCount, pinnedRightColumnCount, rowHeight]);
|
|
||||||
|
|
||||||
// Expose grid refs to parent component
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref && typeof ref === 'object') {
|
handleRef.current = imperativeHandle;
|
||||||
// Create a simple API that exposes the main container
|
}, [imperativeHandle]);
|
||||||
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]);
|
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user