From 3d0500980a89f96c36fc424b394e8a8e5bfc1dee Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 20 May 2026 20:40:30 -0700 Subject: [PATCH] implement row playback controls for the detail list --- .../item-detail-list/columns/index.ts | 2 + .../columns/row-index-column.tsx | 49 +++++++++++++------ .../columns/row-play-control-cell.module.css | 30 ++++++++++++ .../columns/row-play-control-cell.tsx | 34 +++++++++++++ .../columns/track-number-column.tsx | 41 +++++++++++++++- .../item-detail-list/columns/types.ts | 3 +- .../columns/use-detail-row-play-control.ts | 47 ++++++++++++++++++ .../item-detail-list.module.css | 8 +++ .../item-detail-list/item-detail-list.tsx | 4 ++ 9 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.module.css create mode 100644 src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.tsx create mode 100644 src/renderer/components/item-list/item-detail-list/columns/use-detail-row-play-control.ts diff --git a/src/renderer/components/item-list/item-detail-list/columns/index.ts b/src/renderer/components/item-list/item-detail-list/columns/index.ts index db5bb5285..e95b4f613 100644 --- a/src/renderer/components/item-list/item-detail-list/columns/index.ts +++ b/src/renderer/components/item-list/item-detail-list/columns/index.ts @@ -27,6 +27,7 @@ import { PlayCountColumn } from './play-count-column'; import { RatingColumn } from './rating-column'; import { ReleaseDateColumn } from './release-date-column'; import { RowIndexColumn } from './row-index-column'; +import { ItemDetailRowPlayControlCell } from './row-play-control-cell'; import { SampleRateColumn } from './sample-rate-column'; import { SizeColumn } from './size-column'; import { TitleArtistColumn } from './title-artist-column'; @@ -111,6 +112,7 @@ export { GenreBadgeColumn, GenreColumn, ImageColumn, + ItemDetailRowPlayControlCell, LastPlayedColumn, PathColumn, PlayCountColumn, diff --git a/src/renderer/components/item-list/item-detail-list/columns/row-index-column.tsx b/src/renderer/components/item-list/item-detail-list/columns/row-index-column.tsx index 817f0acec..25d1ed6f9 100644 --- a/src/renderer/components/item-list/item-detail-list/columns/row-index-column.tsx +++ b/src/renderer/components/item-list/item-detail-list/columns/row-index-column.tsx @@ -1,23 +1,40 @@ import styles from './row-index-column.module.css'; +import { ItemDetailRowPlayControlCell } from './row-play-control-cell'; import { ItemDetailListCellProps } from './types'; +import { useDetailRowPlayControl } from './use-detail-row-play-control'; -import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song'; -import { usePlayerStatus } from '/@/renderer/store'; +import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column'; import { Icon } from '/@/shared/components/icon/icon'; -import { PlayerStatus } from '/@/shared/types/types'; - -export const RowIndexColumn = ({ rowIndex, song }: ItemDetailListCellProps) => { - const status = usePlayerStatus(); - const { isActive } = useIsCurrentSong(song); - const isPlaying = isActive && status === PlayerStatus.PLAYING; - - if (isActive) { - return ( -
- -
- ); - } +import { TableColumn } from '/@/shared/types/types'; +const DefaultRowIndexColumn = ({ rowIndex }: ItemDetailListCellProps) => { return <>{String((rowIndex ?? 0) + 1)}; }; + +const PlayableRowIndexColumn = (props: ItemDetailListCellProps) => { + const { handlePlay, isActive, isPlaying, showPlayControls } = useDetailRowPlayControl(props); + + const indexContent = isActive ? ( +
+ +
+ ) : ( + String((props.rowIndex ?? 0) + 1) + ); + + return ( + + ); +}; + +export const RowIndexColumn = (props: ItemDetailListCellProps) => { + if (!props.columns || !isRowPlayControlColumn(TableColumn.ROW_INDEX, props.columns)) { + return ; + } + + return ; +}; diff --git a/src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.module.css b/src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.module.css new file mode 100644 index 000000000..daeb253f2 --- /dev/null +++ b/src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.module.css @@ -0,0 +1,30 @@ +.cell-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + min-height: 100%; +} + +.play-target { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.index-content { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +.icon-wrapper { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.tsx b/src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.tsx new file mode 100644 index 000000000..3f87847f1 --- /dev/null +++ b/src/renderer/components/item-list/item-detail-list/columns/row-play-control-cell.tsx @@ -0,0 +1,34 @@ +import { ReactNode } from 'react'; + +import styles from './row-play-control-cell.module.css'; + +import { ItemRowPlayControls } from '/@/renderer/features/shared/components/item-row-play-controls'; +import { HoverCard } from '/@/shared/components/hover-card/hover-card'; +import { Play } from '/@/shared/types/types'; + +export const ItemDetailRowPlayControlCell = ({ + indexContent, + onPlay, + showPlayControls, +}: { + indexContent: ReactNode; + onPlay: (playType: Play) => void; + showPlayControls: boolean; +}) => { + if (!showPlayControls) { + return <>{indexContent}; + } + + return ( +
+ + +
{indexContent}
+
+ e.stopPropagation()}> + + +
+
+ ); +}; diff --git a/src/renderer/components/item-list/item-detail-list/columns/track-number-column.tsx b/src/renderer/components/item-list/item-detail-list/columns/track-number-column.tsx index b2ee3f7f4..2e31e4647 100644 --- a/src/renderer/components/item-list/item-detail-list/columns/track-number-column.tsx +++ b/src/renderer/components/item-list/item-detail-list/columns/track-number-column.tsx @@ -1,7 +1,46 @@ +import { ItemDetailRowPlayControlCell } from './row-play-control-cell'; +import styles from './row-play-control-cell.module.css'; import { ItemDetailListCellProps } from './types'; +import { useDetailRowPlayControl } from './use-detail-row-play-control'; -export const TrackNumberColumn = ({ song }: ItemDetailListCellProps) => { +import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column'; +import { Icon } from '/@/shared/components/icon/icon'; +import { TableColumn } from '/@/shared/types/types'; + +const formatTrackNumber = (song: ItemDetailListCellProps['song']) => { const disc = song.discNumber ?? 1; const track = song.trackNumber.toString().padStart(2, '0'); return `${disc}-${track}`; }; + +const DefaultTrackNumberColumn = ({ song }: ItemDetailListCellProps) => { + return <>{formatTrackNumber(song)}; +}; + +const PlayableTrackNumberColumn = (props: ItemDetailListCellProps) => { + const { handlePlay, isActive, isPlaying, showPlayControls } = useDetailRowPlayControl(props); + + const indexContent = isActive ? ( +
+ +
+ ) : ( + formatTrackNumber(props.song) + ); + + return ( + + ); +}; + +export const TrackNumberColumn = (props: ItemDetailListCellProps) => { + if (!props.columns || !isRowPlayControlColumn(TableColumn.TRACK_NUMBER, props.columns)) { + return ; + } + + return ; +}; diff --git a/src/renderer/components/item-list/item-detail-list/columns/types.ts b/src/renderer/components/item-list/item-detail-list/columns/types.ts index cad0801a0..63080ec3c 100644 --- a/src/renderer/components/item-list/item-detail-list/columns/types.ts +++ b/src/renderer/components/item-list/item-detail-list/columns/types.ts @@ -1,8 +1,9 @@ import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state'; -import { ItemControls } from '/@/renderer/components/item-list/types'; +import { ItemControls, ItemTableListColumnConfig } from '/@/renderer/components/item-list/types'; import { Song } from '/@/shared/types/domain-types'; export interface ItemDetailListCellProps { + columns?: ItemTableListColumnConfig[]; controls?: ItemControls; internalState?: ItemListStateActions; isMutatingFavorite?: boolean; diff --git a/src/renderer/components/item-list/item-detail-list/columns/use-detail-row-play-control.ts b/src/renderer/components/item-list/item-detail-list/columns/use-detail-row-play-control.ts new file mode 100644 index 000000000..97d67ad9c --- /dev/null +++ b/src/renderer/components/item-list/item-detail-list/columns/use-detail-row-play-control.ts @@ -0,0 +1,47 @@ +import { useCallback } from 'react'; + +import { ItemDetailListCellProps } from './types'; + +import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list'; +import { usePlayer } from '/@/renderer/features/player/context/player-context'; +import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song'; +import { usePlayerStatus } from '/@/renderer/store'; +import { Song } from '/@/shared/types/domain-types'; +import { Play, PlayerStatus } from '/@/shared/types/types'; + +export const useDetailRowPlayControl = ({ + internalState, + rowIndex = 0, + song, +}: Pick) => { + const status = usePlayerStatus(); + const player = usePlayer(); + const { isActive } = useIsCurrentSong(song); + const isPlaying = isActive && status === PlayerStatus.PLAYING; + + const showPlayControls = !!song?.id; + + const handlePlay = useCallback( + (playType: Play) => { + if (!song) { + return; + } + + playSongFromItemListControl({ + index: rowIndex, + internalState, + item: song as Song, + meta: { playType, singleSongOnly: true }, + player, + }); + }, + [internalState, player, rowIndex, song], + ); + + return { + handlePlay, + isActive, + isPlaying, + showPlayControls, + }; +}; diff --git a/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css b/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css index adc6e8d79..fa1f84eb4 100644 --- a/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css +++ b/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css @@ -447,6 +447,14 @@ padding-left: 0; } +.row .track-cell-play-control { + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; +} + .track-row-dragging { opacity: 0.5; } diff --git a/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx b/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx index 4f928ed35..3e09fb567 100644 --- a/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx +++ b/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx @@ -33,6 +33,7 @@ import styles from './item-detail-list.module.css'; import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; import { ItemImage } from '/@/renderer/components/item-image/item-image'; import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; +import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { ItemListStateActions, @@ -365,6 +366,7 @@ const TrackRow = memo( const isTitleColumn = col.id === TableColumn.TITLE; const isImageColumn = col.id === TableColumn.IMAGE; const isIconActionColumn = isNoHorizontalPaddingColumn(col.id); + const isPlayControlColumn = isRowPlayControlColumn(col.id, columns); const showHoverContent = shouldShowHoverOnlyColumnContent( col.id, isRowHovered, @@ -374,6 +376,7 @@ const TrackRow = memo( const content = isSongsLoading ? null : showHoverContent ? (