From 64efbc52107e2d4c13c71159d3d4dcebf1670d85 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 19 May 2026 20:58:34 -0700 Subject: [PATCH] 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 --- .../helpers/get-row-play-control-column.ts | 26 ++++ .../item-list/helpers/play-row-from-list.ts | 74 +++++++++ .../columns/row-index-column.module.css | 42 ++++++ .../columns/row-index-column.tsx | 68 ++++----- .../columns/row-play-control-cell.tsx | 125 ++++++++++++++++ .../columns/track-number-column.tsx | 51 +++++++ .../columns/use-row-play-control.ts | 140 ++++++++++++++++++ .../item-table-list-column.tsx | 6 +- .../components/album-detail-content.tsx | 17 ++- .../album-artist-detail-content.tsx | 27 ++-- ...rtist-detail-favorite-songs-list-route.tsx | 16 +- ...bum-artist-detail-top-songs-list-route.tsx | 16 +- .../playlist-detail-song-list-table.tsx | 29 ++-- .../item-row-play-controls.module.css | 5 + .../components/item-row-play-controls.tsx | 90 +++++++++++ .../components/sidebar-playlist-list.tsx | 104 ++----------- 16 files changed, 656 insertions(+), 180 deletions(-) create mode 100644 src/renderer/components/item-list/helpers/get-row-play-control-column.ts create mode 100644 src/renderer/components/item-list/helpers/play-row-from-list.ts create mode 100644 src/renderer/components/item-list/item-table-list/columns/row-play-control-cell.tsx create mode 100644 src/renderer/components/item-list/item-table-list/columns/track-number-column.tsx create mode 100644 src/renderer/components/item-list/item-table-list/columns/use-row-play-control.ts create mode 100644 src/renderer/features/shared/components/item-row-play-controls.module.css create mode 100644 src/renderer/features/shared/components/item-row-play-controls.tsx diff --git a/src/renderer/components/item-list/helpers/get-row-play-control-column.ts b/src/renderer/components/item-list/helpers/get-row-play-control-column.ts new file mode 100644 index 000000000..b4985a961 --- /dev/null +++ b/src/renderer/components/item-list/helpers/get-row-play-control-column.ts @@ -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>, +): null | TableColumn => { + for (const column of columns) { + if (isRowPlayControlColumnId(column.id)) { + return column.id; + } + } + + return null; +}; + +export const isRowPlayControlColumn = ( + columnId: TableColumn, + columns: Array>, +): boolean => getRowPlayControlColumnId(columns) === columnId; diff --git a/src/renderer/components/item-list/helpers/play-row-from-list.ts b/src/renderer/components/item-list/helpers/play-row-from-list.ts new file mode 100644 index 000000000..73b04a014 --- /dev/null +++ b/src/renderer/components/item-list/helpers/play-row-from-list.ts @@ -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; + 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; + 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; + player: PlayerQueueByFetchActions; +}) => { + const playType = (meta?.playType as Play) || Play.NOW; + player.addToQueueByFetch(artist._serverId, [artist.id], itemType, playType); +}; diff --git a/src/renderer/components/item-list/item-table-list/columns/row-index-column.module.css b/src/renderer/components/item-list/item-table-list/columns/row-index-column.module.css index b3cd7c080..d1f709f11 100644 --- a/src/renderer/components/item-list/item-table-list/columns/row-index-column.module.css +++ b/src/renderer/components/item-list/item-table-list/columns/row-index-column.module.css @@ -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%; } diff --git a/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx b/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx index 88d15ce41..00bc6971f 100644 --- a/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx @@ -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 ; + } + 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 ; + return ; default: return ; } @@ -88,12 +96,8 @@ const DefaultRowIndexColumn = (props: ItemTableListInnerColumn) => { return {adjustedRowIndex}; }; -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 ? ( + + + + ) : ( + adjustedRowIndex + ); + return ( - ); }; - -const InnerQueueSongRowIndexColumn = ( - props: ItemTableListInnerColumn & { - adjustedRowIndex: number; - isActive: boolean; - isPlaying: boolean; - }, -) => { - return ( - - {props.isActive ? ( - props.isPlaying ? ( - - - - ) : ( - - - - ) - ) : ( - props.adjustedRowIndex - )} - - ); -}; diff --git a/src/renderer/components/item-list/item-table-list/columns/row-play-control-cell.tsx b/src/renderer/components/item-list/item-table-list/columns/row-play-control-cell.tsx new file mode 100644 index 000000000..1ec11e132 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/row-play-control-cell.tsx @@ -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) => { + 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 ? ( + + {indexContent} + + ) : ( + indexContent + ); + } + + return {indexContent}; + }; + + const expansionTarget = ( +
+ {getIndexDisplay(true)} + +
+ ); + + if (enableExpansion) { + return ( + +
+ {showPlayControls ? ( + + {expansionTarget} + e.stopPropagation()}> + + + + ) : ( + expansionTarget + )} +
+
+ ); + } + + if (!showPlayControls) { + return ( + {getIndexDisplay(false)} + ); + } + + return ( + + + + + {getIndexDisplay(false)} + + + e.stopPropagation()}> + + + + + ); +}; diff --git a/src/renderer/components/item-list/item-table-list/columns/track-number-column.tsx b/src/renderer/components/item-list/item-table-list/columns/track-number-column.tsx new file mode 100644 index 000000000..6d980520d --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/track-number-column.tsx @@ -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 ; + } + + return ; +}; + +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 ; + } + + const indexContent = isActive ? ( + + + + ) : ( + trackNumber + ); + + return ( + + ); +}; diff --git a/src/renderer/components/item-list/item-table-list/columns/use-row-play-control.ts b/src/renderer/components/item-list/item-table-list/columns/use-row-play-control.ts new file mode 100644 index 000000000..ade681d24 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/use-row-play-control.ts @@ -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, + }; +}; diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx index 63de98b74..3cc83a97c 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx @@ -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 ; + return ( + + ); case TableColumn.COMPOSER: return ; diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 1d8b4e02d..d4273b63c 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -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]); diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index 1be7fb34b..383085104 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -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]); diff --git a/src/renderer/features/artists/routes/album-artist-detail-favorite-songs-list-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-favorite-songs-list-route.tsx index 9758646f9..7c58950af 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-favorite-songs-list-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-favorite-songs-list-route.tsx @@ -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]); diff --git a/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx index 2c0753089..ee1bfd214 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx @@ -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]); diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx index 7107c66fd..f420d0f24 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx @@ -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, @@ -103,12 +104,13 @@ export const PlaylistDetailSongListTable = forwardRef 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 ( + + + + + + + + + + + + ); +}; diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx index 5a1294275..a31c4153e 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx @@ -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( {name} - {isHovered && } + {isHovered && ( + handlePlay(to, playType)} + /> + )} ) : ( <> @@ -347,7 +348,12 @@ export const PlaylistRowButton = memo( - {isHovered && } + {isHovered && ( + handlePlay(to, playType)} + /> + )} )} @@ -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 ( - - - - - - - - - - - - ); -}; - export const SidebarPlaylistList = () => { const player = usePlayer(); const { t } = useTranslation();