mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-10 14:22:46 +02:00
add table row playback controls
- supports song, album, artist, and album artist tables - hovering over the first row index or track number column will display a hovercard for the playback controls
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const ROW_PLAY_CONTROL_COLUMN_IDS = [TableColumn.TRACK_NUMBER, TableColumn.ROW_INDEX] as const;
|
||||
|
||||
type RowPlayControlColumnId = (typeof ROW_PLAY_CONTROL_COLUMN_IDS)[number];
|
||||
|
||||
const isRowPlayControlColumnId = (columnId: TableColumn): columnId is RowPlayControlColumnId =>
|
||||
ROW_PLAY_CONTROL_COLUMN_IDS.includes(columnId as RowPlayControlColumnId);
|
||||
|
||||
export const getRowPlayControlColumnId = (
|
||||
columns: Array<Pick<ItemTableListColumnConfig, 'id'>>,
|
||||
): null | TableColumn => {
|
||||
for (const column of columns) {
|
||||
if (isRowPlayControlColumnId(column.id)) {
|
||||
return column.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isRowPlayControlColumn = (
|
||||
columnId: TableColumn,
|
||||
columns: Array<Pick<ItemTableListColumnConfig, 'id'>>,
|
||||
): boolean => getRowPlayControlColumnId(columns) === columnId;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { Album, AlbumArtist, Artist, LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
type PlayableArtistItem = AlbumArtist | Artist;
|
||||
|
||||
interface PlayerQueueByDataActions {
|
||||
addToQueueByData: (data: Song[], type: Play, playSongId?: string) => void;
|
||||
}
|
||||
|
||||
interface PlayerQueueByFetchActions {
|
||||
addToQueueByFetch: (
|
||||
serverId: string,
|
||||
ids: string[],
|
||||
itemType: LibraryItem,
|
||||
playType: Play,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const playSongFromItemListControl = ({
|
||||
index,
|
||||
internalState,
|
||||
item,
|
||||
meta,
|
||||
player,
|
||||
}: {
|
||||
index?: number;
|
||||
internalState?: ItemListStateActions;
|
||||
item: Song;
|
||||
meta?: Record<string, unknown>;
|
||||
player: PlayerQueueByDataActions;
|
||||
}) => {
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const singleSongOnly = meta?.singleSongOnly === true;
|
||||
|
||||
if (singleSongOnly) {
|
||||
player.addToQueueByData([item], playType, item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined && items) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
};
|
||||
|
||||
export const playAlbumFromItemListControl = ({
|
||||
album,
|
||||
meta,
|
||||
player,
|
||||
}: {
|
||||
album: Album;
|
||||
meta?: Record<string, unknown>;
|
||||
player: PlayerQueueByFetchActions;
|
||||
}) => {
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
player.addToQueueByFetch(album._serverId, [album.id], LibraryItem.ALBUM, playType);
|
||||
};
|
||||
|
||||
export const playArtistFromItemListControl = ({
|
||||
artist,
|
||||
itemType,
|
||||
meta,
|
||||
player,
|
||||
}: {
|
||||
artist: PlayableArtistItem;
|
||||
itemType: LibraryItem.ALBUM_ARTIST | LibraryItem.ARTIST;
|
||||
meta?: Record<string, unknown>;
|
||||
player: PlayerQueueByFetchActions;
|
||||
}) => {
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
player.addToQueueByFetch(artist._serverId, [artist.id], itemType, playType);
|
||||
};
|
||||
@@ -1,3 +1,45 @@
|
||||
.full-size-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
-webkit-line-clamp: unset;
|
||||
line-clamp: unset;
|
||||
}
|
||||
|
||||
.expansion-cell {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.expansion-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.play-target {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.expand {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 4;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.index-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -2,30 +2,38 @@ import clsx from 'clsx';
|
||||
|
||||
import styles from './row-index-column.module.css';
|
||||
|
||||
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
|
||||
import { RowPlayControlCell } from '/@/renderer/components/item-list/item-table-list/columns/row-play-control-cell';
|
||||
import { useRowPlayControl } from '/@/renderer/components/item-list/item-table-list/columns/use-row-play-control';
|
||||
import {
|
||||
ItemTableListInnerColumn,
|
||||
TableColumnContainer,
|
||||
TableColumnTextContainer,
|
||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||
import { ItemListItem } from '/@/renderer/components/item-list/types';
|
||||
import { usePlayerStatus } from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const RowIndexColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
const { itemType } = props;
|
||||
|
||||
if (!isRowPlayControlColumn(TableColumn.ROW_INDEX, props.columns)) {
|
||||
return <DefaultRowIndexColumn {...props} />;
|
||||
}
|
||||
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
case LibraryItem.ARTIST:
|
||||
case LibraryItem.FOLDER:
|
||||
case LibraryItem.PLAYLIST_SONG:
|
||||
case LibraryItem.QUEUE_SONG:
|
||||
case LibraryItem.SONG:
|
||||
return <QueueSongRowIndexColumn {...props} />;
|
||||
return <PlayableRowIndexColumn {...props} />;
|
||||
default:
|
||||
return <DefaultRowIndexColumn {...props} />;
|
||||
}
|
||||
@@ -88,12 +96,8 @@ const DefaultRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
||||
return <TableColumnTextContainer {...props}>{adjustedRowIndex}</TableColumnTextContainer>;
|
||||
};
|
||||
|
||||
const QueueSongRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
||||
const status = usePlayerStatus();
|
||||
const song = (props.getRowItem?.(props.rowIndex) ?? props.data[props.rowIndex]) as QueueSong;
|
||||
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
|
||||
|
||||
const isActiveAndPlaying = isActive && status === PlayerStatus.PLAYING;
|
||||
const PlayableRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
||||
const { handlePlay, isActive, isPlaying, showPlayControls } = useRowPlayControl(props);
|
||||
|
||||
let adjustedRowIndex =
|
||||
props.getAdjustedRowIndex?.(props.rowIndex) ??
|
||||
@@ -104,38 +108,20 @@ const QueueSongRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
||||
adjustedRowIndex = props.startRowIndex + adjustedRowIndex;
|
||||
}
|
||||
|
||||
const indexContent = isActive ? (
|
||||
<Flex className={styles.indexContent}>
|
||||
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
||||
</Flex>
|
||||
) : (
|
||||
adjustedRowIndex
|
||||
);
|
||||
|
||||
return (
|
||||
<InnerQueueSongRowIndexColumn
|
||||
<RowPlayControlCell
|
||||
{...props}
|
||||
adjustedRowIndex={adjustedRowIndex}
|
||||
isActive={isActive}
|
||||
isPlaying={isActiveAndPlaying}
|
||||
indexContent={indexContent}
|
||||
onPlay={handlePlay}
|
||||
showPlayControls={showPlayControls}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const InnerQueueSongRowIndexColumn = (
|
||||
props: ItemTableListInnerColumn & {
|
||||
adjustedRowIndex: number;
|
||||
isActive: boolean;
|
||||
isPlaying: boolean;
|
||||
},
|
||||
) => {
|
||||
return (
|
||||
<TableColumnTextContainer {...props}>
|
||||
{props.isActive ? (
|
||||
props.isPlaying ? (
|
||||
<Flex>
|
||||
<Icon fill="primary" icon="mediaPlay" />
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex>
|
||||
<Icon fill="primary" icon="mediaPause" />
|
||||
</Flex>
|
||||
)
|
||||
) : (
|
||||
props.adjustedRowIndex
|
||||
)}
|
||||
</TableColumnTextContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import clsx from 'clsx';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
|
||||
import styles from './row-index-column.module.css';
|
||||
|
||||
import {
|
||||
ItemTableListInnerColumn,
|
||||
TableColumnContainer,
|
||||
TableColumnTextContainer,
|
||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { ItemListItem } from '/@/renderer/components/item-list/types';
|
||||
import { ItemRowPlayControls } from '/@/renderer/features/shared/components/item-row-play-controls';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { HoverCard } from '/@/shared/components/hover-card/hover-card';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const RowPlayControlCell = (
|
||||
props: ItemTableListInnerColumn & {
|
||||
indexContent: ReactNode;
|
||||
onPlay: (playType: Play) => void;
|
||||
showPlayControls: boolean;
|
||||
},
|
||||
) => {
|
||||
const {
|
||||
controls,
|
||||
data,
|
||||
enableExpansion,
|
||||
getRowItem,
|
||||
indexContent,
|
||||
internalState,
|
||||
itemType,
|
||||
onPlay,
|
||||
rowIndex,
|
||||
showPlayControls,
|
||||
} = props;
|
||||
|
||||
const handleExpand = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
const item = (getRowItem?.(rowIndex) ?? data[rowIndex]) as ItemListItem;
|
||||
const rowId = internalState.extractRowId(item);
|
||||
const index = rowId ? internalState.findItemIndex(rowId) : -1;
|
||||
controls.onExpand?.({
|
||||
event: e,
|
||||
index,
|
||||
internalState,
|
||||
item,
|
||||
itemType,
|
||||
});
|
||||
},
|
||||
[controls, data, getRowItem, internalState, itemType, rowIndex],
|
||||
);
|
||||
|
||||
const getIndexDisplay = (useMutedText: boolean) => {
|
||||
const hideOnHoverClass = enableExpansion ? 'hide-on-hover' : undefined;
|
||||
|
||||
if (typeof indexContent === 'number') {
|
||||
return useMutedText ? (
|
||||
<Text className={hideOnHoverClass} isMuted isNoSelect>
|
||||
{indexContent}
|
||||
</Text>
|
||||
) : (
|
||||
indexContent
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={hideOnHoverClass}>{indexContent}</span>;
|
||||
};
|
||||
|
||||
const expansionTarget = (
|
||||
<div className={styles.playTarget}>
|
||||
{getIndexDisplay(true)}
|
||||
<ActionIcon
|
||||
className={clsx(styles.expand, 'hover-only')}
|
||||
icon="arrowDownS"
|
||||
iconProps={{ color: 'muted', size: 'md' }}
|
||||
onClick={handleExpand}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (enableExpansion) {
|
||||
return (
|
||||
<TableColumnContainer {...props} className={styles.expansionCell}>
|
||||
<div className={styles.expansionInner}>
|
||||
{showPlayControls ? (
|
||||
<HoverCard openDelay={300} position="top" withArrow>
|
||||
<HoverCard.Target>{expansionTarget}</HoverCard.Target>
|
||||
<HoverCard.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||
<ItemRowPlayControls onPlay={onPlay} />
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
) : (
|
||||
expansionTarget
|
||||
)}
|
||||
</div>
|
||||
</TableColumnContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showPlayControls) {
|
||||
return (
|
||||
<TableColumnTextContainer {...props}>{getIndexDisplay(false)}</TableColumnTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableColumnTextContainer {...props} className={styles.fullSizeContent}>
|
||||
<HoverCard openDelay={300} position="top" withArrow>
|
||||
<HoverCard.Target>
|
||||
<Flex className={styles.indexContent} justify="center" w="100%">
|
||||
{getIndexDisplay(false)}
|
||||
</Flex>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||
<ItemRowPlayControls onPlay={onPlay} />
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</TableColumnTextContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import styles from './row-index-column.module.css';
|
||||
|
||||
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
|
||||
import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/columns/numeric-column';
|
||||
import { RowPlayControlCell } from '/@/renderer/components/item-list/item-table-list/columns/row-play-control-cell';
|
||||
import {
|
||||
supportsTrackNumberRowPlayControls,
|
||||
useRowPlayControl,
|
||||
} from '/@/renderer/components/item-list/item-table-list/columns/use-row-play-control';
|
||||
import { ItemTableListInnerColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
export const TrackNumberColumn = (props: ItemTableListInnerColumn) => {
|
||||
if (
|
||||
isRowPlayControlColumn(TableColumn.TRACK_NUMBER, props.columns) &&
|
||||
supportsTrackNumberRowPlayControls(props.itemType)
|
||||
) {
|
||||
return <PlayableTrackNumberColumn {...props} />;
|
||||
}
|
||||
|
||||
return <NumericColumn {...props} />;
|
||||
};
|
||||
|
||||
const PlayableTrackNumberColumn = (props: ItemTableListInnerColumn) => {
|
||||
const { handlePlay, isActive, isPlaying, showPlayControls } = useRowPlayControl(props);
|
||||
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as unknown[])[props.rowIndex];
|
||||
const trackNumber = (rowItem as undefined | { trackNumber?: number })?.trackNumber;
|
||||
|
||||
if (typeof trackNumber !== 'number') {
|
||||
return <NumericColumn {...props} />;
|
||||
}
|
||||
|
||||
const indexContent = isActive ? (
|
||||
<Flex className={styles.indexContent}>
|
||||
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
||||
</Flex>
|
||||
) : (
|
||||
trackNumber
|
||||
);
|
||||
|
||||
return (
|
||||
<RowPlayControlCell
|
||||
{...props}
|
||||
indexContent={indexContent}
|
||||
onPlay={handlePlay}
|
||||
showPlayControls={showPlayControls}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
playAlbumFromItemListControl,
|
||||
playArtistFromItemListControl,
|
||||
playSongFromItemListControl,
|
||||
} from '/@/renderer/components/item-list/helpers/play-row-from-list';
|
||||
import { ItemTableListInnerColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { usePlayerSong, usePlayerStatus } from '/@/renderer/store';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
LibraryItem,
|
||||
QueueSong,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Play, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export const supportsRowPlayControls = (itemType: LibraryItem) =>
|
||||
itemType === LibraryItem.ALBUM ||
|
||||
itemType === LibraryItem.ALBUM_ARTIST ||
|
||||
itemType === LibraryItem.ARTIST ||
|
||||
itemType === LibraryItem.PLAYLIST_SONG ||
|
||||
itemType === LibraryItem.SONG;
|
||||
|
||||
export const supportsTrackNumberRowPlayControls = (itemType: LibraryItem) =>
|
||||
itemType === LibraryItem.PLAYLIST_SONG ||
|
||||
itemType === LibraryItem.QUEUE_SONG ||
|
||||
itemType === LibraryItem.SONG;
|
||||
|
||||
export const hasPlayableRowItem = (
|
||||
itemType: LibraryItem,
|
||||
items: { album: Album; artist: AlbumArtist | Artist; song: QueueSong },
|
||||
) => {
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return !!items.album?.id;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
case LibraryItem.ARTIST:
|
||||
return !!items.artist?.id;
|
||||
default:
|
||||
return !!items.song;
|
||||
}
|
||||
};
|
||||
|
||||
export const useRowPlayControl = (props: ItemTableListInnerColumn) => {
|
||||
const status = usePlayerStatus();
|
||||
const currentSong = usePlayerSong();
|
||||
const player = usePlayer();
|
||||
const rowItem = props.getRowItem?.(props.rowIndex) ?? props.data[props.rowIndex];
|
||||
const song = rowItem as QueueSong;
|
||||
const album = rowItem as Album;
|
||||
const artist = rowItem as AlbumArtist | Artist;
|
||||
|
||||
const isActiveFromRow = useIsActiveRow(song?.id, song?._uniqueId);
|
||||
const isActive = (() => {
|
||||
switch (props.itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return !!album?.id && currentSong?.albumId === album.id;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return (
|
||||
!!artist?.id &&
|
||||
!!currentSong?.albumArtists?.some(
|
||||
(relatedArtist) => relatedArtist.id === artist.id,
|
||||
)
|
||||
);
|
||||
case LibraryItem.ARTIST:
|
||||
return (
|
||||
!!artist?.id &&
|
||||
!!currentSong?.artists?.some((relatedArtist) => relatedArtist.id === artist.id)
|
||||
);
|
||||
default:
|
||||
return isActiveFromRow;
|
||||
}
|
||||
})();
|
||||
|
||||
const isPlaying = isActive && status === PlayerStatus.PLAYING;
|
||||
|
||||
const showPlayControls =
|
||||
supportsRowPlayControls(props.itemType) &&
|
||||
hasPlayableRowItem(props.itemType, { album, artist, song });
|
||||
|
||||
const handlePlay = useCallback(
|
||||
(playType: Play) => {
|
||||
if (props.itemType === LibraryItem.ALBUM) {
|
||||
if (!album?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
playAlbumFromItemListControl({
|
||||
album,
|
||||
meta: { playType },
|
||||
player,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
props.itemType === LibraryItem.ALBUM_ARTIST ||
|
||||
props.itemType === LibraryItem.ARTIST
|
||||
) {
|
||||
if (!artist?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
playArtistFromItemListControl({
|
||||
artist,
|
||||
itemType: props.itemType,
|
||||
meta: { playType },
|
||||
player,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!song) {
|
||||
return;
|
||||
}
|
||||
|
||||
playSongFromItemListControl({
|
||||
item: song as Song,
|
||||
meta: { playType, singleSongOnly: true },
|
||||
player,
|
||||
});
|
||||
},
|
||||
[album, artist, player, props.itemType, song],
|
||||
);
|
||||
|
||||
return {
|
||||
album,
|
||||
artist,
|
||||
handlePlay,
|
||||
isActive,
|
||||
isPlaying,
|
||||
showPlayControls,
|
||||
song,
|
||||
};
|
||||
};
|
||||
@@ -44,7 +44,6 @@ import { FavoriteColumn } from '/@/renderer/components/item-list/item-table-list
|
||||
import { GenreBadgeColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-badge-column';
|
||||
import { GenreColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-column';
|
||||
import { ImageColumn } from '/@/renderer/components/item-list/item-table-list/columns/image-column';
|
||||
import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/columns/numeric-column';
|
||||
import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column';
|
||||
import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column';
|
||||
import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column';
|
||||
@@ -54,6 +53,7 @@ import { TextColumn } from '/@/renderer/components/item-list/item-table-list/col
|
||||
import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-artist-column';
|
||||
import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column';
|
||||
import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column';
|
||||
import { TrackNumberColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-number-column';
|
||||
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
|
||||
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
|
||||
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
@@ -240,7 +240,9 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
|
||||
case TableColumn.DISC_NUMBER:
|
||||
case TableColumn.SAMPLE_RATE:
|
||||
case TableColumn.TRACK_NUMBER:
|
||||
return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
return (
|
||||
<TrackNumberColumn {...props} {...dragProps} controls={controls} type={type} />
|
||||
);
|
||||
|
||||
case TableColumn.COMPOSER:
|
||||
return <ComposerColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||
|
||||
@@ -12,6 +12,7 @@ import styles from './album-detail-content.module.css';
|
||||
|
||||
import { useGridCarouselContainerQuery } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
||||
import { useItemListStateSubscription } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
@@ -60,7 +61,7 @@ import {
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
|
||||
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
const MetadataPillGroup = ({
|
||||
items,
|
||||
@@ -830,13 +831,13 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
playSongFromItemListControl({
|
||||
index,
|
||||
internalState,
|
||||
item: item as Song,
|
||||
meta,
|
||||
player,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
@@ -14,6 +14,7 @@ import styles from './album-artist-detail-content.module.css';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
||||
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
|
||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
@@ -365,12 +366,13 @@ const AlbumArtistMetadataTopSongsContent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
playSongFromItemListControl({
|
||||
index,
|
||||
internalState,
|
||||
item: item as Song,
|
||||
meta,
|
||||
player,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
@@ -657,12 +659,13 @@ const AlbumArtistMetadataFavoriteSongs = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
playSongFromItemListControl({
|
||||
index,
|
||||
internalState,
|
||||
item: item as Song,
|
||||
meta,
|
||||
player,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
+9
-7
@@ -2,6 +2,7 @@ import { useSuspenseQueries } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
@@ -24,7 +25,7 @@ import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { sortSongList } from '/@/shared/api/utils';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
||||
const { albumArtistId, artistId } = useParams() as {
|
||||
@@ -96,12 +97,13 @@ const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
playSongFromItemListControl({
|
||||
index,
|
||||
internalState,
|
||||
item: item as Song,
|
||||
meta,
|
||||
player,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
@@ -18,7 +19,7 @@ import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
const AlbumArtistDetailTopSongsListRoute = () => {
|
||||
const { albumArtistId, artistId } = useParams() as {
|
||||
@@ -78,12 +79,13 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
playSongFromItemListControl({
|
||||
index,
|
||||
internalState,
|
||||
item: item as Song,
|
||||
meta,
|
||||
player,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
PlaylistSongListResponse,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play, TableColumn } from '/@/shared/types/types';
|
||||
import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
||||
|
||||
interface PlaylistDetailSongListTableProps extends Omit<
|
||||
ItemListTableComponentProps<PlaylistSongListQuery>,
|
||||
@@ -103,12 +104,13 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
playSongFromItemListControl({
|
||||
index,
|
||||
internalState,
|
||||
item: item as Song,
|
||||
meta,
|
||||
player,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
@@ -228,12 +230,13 @@ export const PlaylistDetailSongListEditTable = forwardRef<any, PlaylistDetailSon
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
playSongFromItemListControl({
|
||||
index,
|
||||
internalState,
|
||||
item: item as Song,
|
||||
meta,
|
||||
player,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 0;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './item-row-play-controls.module.css';
|
||||
|
||||
import {
|
||||
LONG_PRESS_PLAY_BEHAVIOR,
|
||||
PlayTooltip,
|
||||
} from '/@/renderer/features/shared/components/play-button-group';
|
||||
import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
|
||||
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
interface ItemRowPlayControlsProps {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
onPlay: (playType: Play) => void;
|
||||
}
|
||||
|
||||
export const ItemRowPlayControls = ({ className, disabled, onPlay }: ItemRowPlayControlsProps) => {
|
||||
const handlePlayNext = usePlayButtonClick({
|
||||
onClick: () => {
|
||||
onPlay(Play.NEXT);
|
||||
},
|
||||
onLongPress: () => {
|
||||
onPlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]);
|
||||
},
|
||||
});
|
||||
|
||||
const handlePlayNow = usePlayButtonClick({
|
||||
onClick: () => {
|
||||
onPlay(Play.NOW);
|
||||
},
|
||||
onLongPress: () => {
|
||||
onPlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]);
|
||||
},
|
||||
});
|
||||
|
||||
const handlePlayLast = usePlayButtonClick({
|
||||
onClick: () => {
|
||||
onPlay(Play.LAST);
|
||||
},
|
||||
onLongPress: () => {
|
||||
onPlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ActionIconGroup className={clsx(styles.controls, className)}>
|
||||
<PlayTooltip disabled={disabled} type={Play.NOW}>
|
||||
<ActionIcon
|
||||
icon="mediaPlay"
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
{...handlePlayNow.handlers}
|
||||
{...handlePlayNow.props}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</PlayTooltip>
|
||||
<PlayTooltip disabled={disabled} type={Play.NEXT}>
|
||||
<ActionIcon
|
||||
icon="mediaPlayNext"
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
{...handlePlayNext.handlers}
|
||||
{...handlePlayNext.props}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</PlayTooltip>
|
||||
<PlayTooltip disabled={disabled} type={Play.LAST}>
|
||||
<ActionIcon
|
||||
icon="mediaPlayLast"
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
{...handlePlayLast.handlers}
|
||||
{...handlePlayLast.props}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</PlayTooltip>
|
||||
</ActionIconGroup>
|
||||
);
|
||||
};
|
||||
@@ -14,11 +14,7 @@ import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form';
|
||||
import { useIsMutatingSidebarPlaylistFolderMove } from '/@/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation';
|
||||
import {
|
||||
LONG_PRESS_PLAY_BEHAVIOR,
|
||||
PlayTooltip,
|
||||
} from '/@/renderer/features/shared/components/play-button-group';
|
||||
import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
|
||||
import { ItemRowPlayControls } from '/@/renderer/features/shared/components/item-row-play-controls';
|
||||
import {
|
||||
collectFolderPaths,
|
||||
PlaylistFolderDragExpandProvider,
|
||||
@@ -41,7 +37,7 @@ import {
|
||||
} from '/@/renderer/store';
|
||||
import { formatDurationString } from '/@/renderer/utils';
|
||||
import { Accordion } from '/@/shared/components/accordion/accordion';
|
||||
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||
import { animationVariants } from '/@/shared/components/animations/animation-variants';
|
||||
import { ButtonProps } from '/@/shared/components/button/button';
|
||||
@@ -299,7 +295,12 @@ export const PlaylistRowButton = memo(
|
||||
<Text className={styles.compactName} fw={500} size="md">
|
||||
{name}
|
||||
</Text>
|
||||
{isHovered && <RowControls id={to} onPlay={handlePlay} variant="compact" />}
|
||||
{isHovered && (
|
||||
<ItemRowPlayControls
|
||||
className={clsx(styles.controls, styles.controlsCompact)}
|
||||
onPlay={(playType) => handlePlay(to, playType)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -347,7 +348,12 @@ export const PlaylistRowButton = memo(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isHovered && <RowControls id={to} onPlay={handlePlay} />}
|
||||
{isHovered && (
|
||||
<ItemRowPlayControls
|
||||
className={styles.controls}
|
||||
onPlay={(playType) => handlePlay(to, playType)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MotionLink>
|
||||
@@ -355,88 +361,6 @@ export const PlaylistRowButton = memo(
|
||||
},
|
||||
);
|
||||
|
||||
const RowControls = ({
|
||||
id,
|
||||
onPlay,
|
||||
variant = 'expanded',
|
||||
}: {
|
||||
id: string;
|
||||
onPlay: (id: string, playType: Play) => void;
|
||||
variant?: 'compact' | 'expanded';
|
||||
}) => {
|
||||
const handlePlayNext = usePlayButtonClick({
|
||||
onClick: () => {
|
||||
onPlay(id, Play.NEXT);
|
||||
},
|
||||
onLongPress: () => {
|
||||
onPlay(id, LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]);
|
||||
},
|
||||
});
|
||||
|
||||
const handlePlayNow = usePlayButtonClick({
|
||||
onClick: () => {
|
||||
onPlay(id, Play.NOW);
|
||||
},
|
||||
onLongPress: () => {
|
||||
onPlay(id, LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]);
|
||||
},
|
||||
});
|
||||
|
||||
const handlePlayLast = usePlayButtonClick({
|
||||
onClick: () => {
|
||||
onPlay(id, Play.LAST);
|
||||
},
|
||||
onLongPress: () => {
|
||||
onPlay(id, LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ActionIconGroup
|
||||
className={clsx(styles.controls, {
|
||||
[styles.controlsCompact]: variant === 'compact',
|
||||
})}
|
||||
>
|
||||
<PlayTooltip type={Play.NOW}>
|
||||
<ActionIcon
|
||||
icon="mediaPlay"
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
{...handlePlayNow.handlers}
|
||||
{...handlePlayNow.props}
|
||||
/>
|
||||
</PlayTooltip>
|
||||
<PlayTooltip type={Play.NEXT}>
|
||||
<ActionIcon
|
||||
icon="mediaPlayNext"
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
{...handlePlayNext.handlers}
|
||||
{...handlePlayNext.props}
|
||||
/>
|
||||
</PlayTooltip>
|
||||
<PlayTooltip type={Play.LAST}>
|
||||
<ActionIcon
|
||||
icon="mediaPlayLast"
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
{...handlePlayLast.handlers}
|
||||
{...handlePlayLast.props}
|
||||
/>
|
||||
</PlayTooltip>
|
||||
</ActionIconGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarPlaylistList = () => {
|
||||
const player = usePlayer();
|
||||
const { t } = useTranslation();
|
||||
|
||||
Reference in New Issue
Block a user