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:
jeffvli
2026-05-19 20:58:34 -07:00
parent 42e9394246
commit 64efbc5210
16 changed files with 656 additions and 180 deletions
@@ -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]);
@@ -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();