Files
feishin/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx
T
2025-12-28 17:19:32 -08:00

393 lines
14 KiB
TypeScript

import clsx from 'clsx';
import { CSSProperties, useMemo, useState } from 'react';
import { generatePath, Link } from 'react-router';
import styles from './title-combined-column.module.css';
import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import {
ColumnNullFallback,
ColumnSkeletonVariable,
ItemTableListInnerColumn,
TableColumnContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
import {
LONG_PRESS_PLAY_BEHAVIOR,
PlayTooltip,
} from '/@/renderer/features/shared/components/play-button-group';
import { AppRoute } from '/@/renderer/router/routes';
import { usePlayButtonBehavior } from '/@/renderer/store';
import { Icon } from '/@/shared/components/icon/icon';
import { Text } from '/@/shared/components/text/text';
import { Folder, LibraryItem, QueueSong, RelatedAlbumArtist } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
const row: object | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.id;
const item = props.data[props.rowIndex] as any;
const internalState = (props as any).internalState;
const playButtonBehavior = usePlayButtonBehavior();
const [isHovered, setIsHovered] = useState(false);
const handlePlay = (playType: Play, event: React.MouseEvent<HTMLButtonElement>) => {
if (!item) {
return;
}
// For SONG items, use double click behavior
if (
(props.itemType === LibraryItem.SONG ||
props.itemType === LibraryItem.PLAYLIST_SONG ||
item._itemType === LibraryItem.SONG) &&
props.controls?.onDoubleClick
) {
// Calculate the index based on rowIndex, accounting for header if enabled
const isHeaderEnabled = !!props.enableHeader;
const index = isHeaderEnabled ? props.rowIndex - 1 : props.rowIndex;
props.controls.onDoubleClick({
event: null,
index,
internalState,
item,
itemType: props.itemType,
meta: {
playType,
},
});
return;
}
// For other item types, use regular onPlay
if (!props.controls?.onPlay) {
return;
}
props.controls.onPlay({
event,
item,
itemType: props.itemType,
playType,
});
};
const artists = useMemo(() => {
if (row && 'artists' in item && Array.isArray(item.artists)) {
return (item.artists as RelatedAlbumArtist[]).map((artist) => {
const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
artistId: artist.id,
});
return { ...artist, path };
});
}
return [];
}, [item, row]);
if (item && 'name' in item && 'imageUrl' in item && 'artists' in item) {
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
const item = props.data[props.rowIndex] as any;
const titleLinkProps = path
? {
component: Link,
isLink: true,
state: { item },
to: path,
}
: {};
return (
<TableColumnContainer
className={styles.titleCombined}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
containerClassName={styles.image}
id={item?.id}
itemType={item?._itemType}
src={item?.imageUrl}
/>
{isHovered && (
<div
className={clsx(styles.playButtonOverlay, {
[styles.compactPlayButtonOverlay]: props.size === 'compact',
})}
>
<PlayTooltip
disabled={props.itemType === LibraryItem.QUEUE_SONG}
type={playButtonBehavior}
>
<PlayButton
fill
onClick={(e) => handlePlay(playButtonBehavior, e)}
onLongPress={(e) =>
handlePlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior], e)
}
/>
</PlayTooltip>
</div>
)}
</div>
<div
className={clsx(styles.textContainer, {
[styles.compact]: props.size === 'compact',
})}
>
<Text className={styles.title} isNoSelect size="md" {...titleLinkProps}>
{item.name as string}
</Text>
<div className={styles.artists}>
{artists.map((artist, index) => (
<span key={artist.id}>
<Text
component={Link}
isLink
isMuted
isNoSelect
size="sm"
state={{ item: artist }}
to={artist.path}
>
{artist.name}
</Text>
{index < artists.length - 1 && ', '}
</span>
))}
</div>
</div>
</TableColumnContainer>
);
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonVariable {...props} />;
};
export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
const row: object | undefined = (props.data as (any | undefined)[])[props.rowIndex];
const song = props.data[props.rowIndex] as QueueSong;
const item = props.data[props.rowIndex] as any;
const internalState = (props as any).internalState;
const playButtonBehavior = usePlayButtonBehavior();
const [isHovered, setIsHovered] = useState(false);
const isActive =
!!props.activeRowId &&
(props.activeRowId === song?.id || props.activeRowId === song?._uniqueId);
const handlePlay = (playType: Play, event: React.MouseEvent<HTMLButtonElement>) => {
if (!item) {
return;
}
// For SONG items, use double click behavior
if (
(props.itemType === LibraryItem.SONG ||
props.itemType === LibraryItem.PLAYLIST_SONG ||
item._itemType === LibraryItem.SONG) &&
props.controls?.onDoubleClick
) {
// Calculate the index based on rowIndex, accounting for header if enabled
const isHeaderEnabled = !!props.enableHeader;
const index = isHeaderEnabled ? props.rowIndex - 1 : props.rowIndex;
props.controls.onDoubleClick({
event: null,
index,
internalState,
item,
itemType: props.itemType,
meta: {
playType,
},
});
return;
}
// For other item types, use regular onPlay
if (!props.controls?.onPlay) {
return;
}
props.controls.onPlay({
event,
item,
itemType: props.itemType,
playType,
});
};
const artists = useMemo(() => {
if (row && 'artists' in row && Array.isArray(row.artists)) {
return (row.artists as RelatedAlbumArtist[]).map((artist) => {
const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
artistId: artist.id,
});
return { ...artist, path };
});
}
return [];
}, [row]);
if (row && 'name' in row && 'imageUrl' in row && 'artists' in row) {
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
const item = props.data[props.rowIndex] as any;
const textStyles = isActive ? { color: 'var(--theme-colors-primary)' } : {};
const titleLinkProps = path
? {
component: Link,
isLink: true,
state: { item },
to: path,
}
: {};
return (
<TableColumnContainer
className={styles.titleCombined}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
containerClassName={styles.image}
id={item?.id}
itemType={item?._itemType}
src={item?.imageUrl}
/>
{isHovered && (
<div
className={clsx(styles.playButtonOverlay, {
[styles.compactPlayButtonOverlay]: props.size === 'compact',
})}
>
<PlayTooltip
disabled={props.itemType === LibraryItem.QUEUE_SONG}
type={playButtonBehavior}
>
<PlayButton
fill
onClick={(e) => handlePlay(playButtonBehavior, e)}
onLongPress={(e) =>
handlePlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior], e)
}
/>
</PlayTooltip>
</div>
)}
</div>
<div
className={clsx(styles.textContainer, {
[styles.compact]: props.size === 'compact',
})}
>
<Text
className={styles.title}
isNoSelect
size="md"
{...titleLinkProps}
style={textStyles}
>
{row.name as string}
</Text>
<div className={styles.artists}>
{artists.map((artist, index) => (
<span key={artist.id}>
<Text
component={Link}
isLink
isMuted
isNoSelect
size="sm"
state={{ item: artist }}
to={artist.path}
>
{artist.name}
</Text>
{index < artists.length - 1 && ', '}
</span>
))}
</div>
</div>
</TableColumnContainer>
);
}
if ((props.data[props.rowIndex] as unknown as Folder)?._itemType === LibraryItem.FOLDER) {
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
const item = props.data[props.rowIndex] as any;
const textStyles = isActive ? { color: 'var(--theme-colors-primary)' } : {};
const titleLinkProps = path
? {
component: Link,
isLink: true,
state: { item },
to: path,
}
: {};
const title = (props.data[props.rowIndex] as unknown as Folder)?.name;
return (
<TableColumnContainer
className={styles.titleCombined}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<Icon className={styles.folderIcon} icon="folder" size="2xl" />
<Text
className={styles.title}
isNoSelect
size="md"
{...titleLinkProps}
style={textStyles}
>
{title}
</Text>
</TableColumnContainer>
);
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonVariable {...props} />;
};
export const TitleCombinedColumn = (props: ItemTableListInnerColumn) => {
const { itemType } = props;
switch (itemType) {
case LibraryItem.FOLDER:
case LibraryItem.PLAYLIST_SONG:
case LibraryItem.QUEUE_SONG:
case LibraryItem.SONG:
return <QueueSongTitleCombinedColumn {...props} />;
default:
return <DefaultTitleCombinedColumn {...props} />;
}
};