mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-23 12:27:59 +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 {
|
.expand {
|
||||||
position: absolute;
|
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 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 {
|
import {
|
||||||
ItemTableListInnerColumn,
|
ItemTableListInnerColumn,
|
||||||
TableColumnContainer,
|
TableColumnContainer,
|
||||||
TableColumnTextContainer,
|
TableColumnTextContainer,
|
||||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
} 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 { ItemListItem } from '/@/renderer/components/item-list/types';
|
||||||
import { usePlayerStatus } from '/@/renderer/store';
|
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { PlayerStatus } from '/@/shared/types/types';
|
import { TableColumn } from '/@/shared/types/types';
|
||||||
|
|
||||||
const RowIndexColumnBase = (props: ItemTableListInnerColumn) => {
|
const RowIndexColumnBase = (props: ItemTableListInnerColumn) => {
|
||||||
const { itemType } = props;
|
const { itemType } = props;
|
||||||
|
|
||||||
|
if (!isRowPlayControlColumn(TableColumn.ROW_INDEX, props.columns)) {
|
||||||
|
return <DefaultRowIndexColumn {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
case LibraryItem.FOLDER:
|
case LibraryItem.FOLDER:
|
||||||
case LibraryItem.PLAYLIST_SONG:
|
case LibraryItem.PLAYLIST_SONG:
|
||||||
case LibraryItem.QUEUE_SONG:
|
case LibraryItem.QUEUE_SONG:
|
||||||
case LibraryItem.SONG:
|
case LibraryItem.SONG:
|
||||||
return <QueueSongRowIndexColumn {...props} />;
|
return <PlayableRowIndexColumn {...props} />;
|
||||||
default:
|
default:
|
||||||
return <DefaultRowIndexColumn {...props} />;
|
return <DefaultRowIndexColumn {...props} />;
|
||||||
}
|
}
|
||||||
@@ -88,12 +96,8 @@ const DefaultRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
return <TableColumnTextContainer {...props}>{adjustedRowIndex}</TableColumnTextContainer>;
|
return <TableColumnTextContainer {...props}>{adjustedRowIndex}</TableColumnTextContainer>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const QueueSongRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
const PlayableRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
||||||
const status = usePlayerStatus();
|
const { handlePlay, isActive, isPlaying, showPlayControls } = useRowPlayControl(props);
|
||||||
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;
|
|
||||||
|
|
||||||
let adjustedRowIndex =
|
let adjustedRowIndex =
|
||||||
props.getAdjustedRowIndex?.(props.rowIndex) ??
|
props.getAdjustedRowIndex?.(props.rowIndex) ??
|
||||||
@@ -104,38 +108,20 @@ const QueueSongRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
adjustedRowIndex = props.startRowIndex + adjustedRowIndex;
|
adjustedRowIndex = props.startRowIndex + adjustedRowIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const indexContent = isActive ? (
|
||||||
|
<Flex className={styles.indexContent}>
|
||||||
|
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
adjustedRowIndex
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InnerQueueSongRowIndexColumn
|
<RowPlayControlCell
|
||||||
{...props}
|
{...props}
|
||||||
adjustedRowIndex={adjustedRowIndex}
|
indexContent={indexContent}
|
||||||
isActive={isActive}
|
onPlay={handlePlay}
|
||||||
isPlaying={isActiveAndPlaying}
|
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 { 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 { 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 { 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 { 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 { 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';
|
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 { 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 { 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 { 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 { 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 { 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';
|
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.DISC_NUMBER:
|
||||||
case TableColumn.SAMPLE_RATE:
|
case TableColumn.SAMPLE_RATE:
|
||||||
case TableColumn.TRACK_NUMBER:
|
case TableColumn.TRACK_NUMBER:
|
||||||
return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />;
|
return (
|
||||||
|
<TrackNumberColumn {...props} {...dragProps} controls={controls} type={type} />
|
||||||
|
);
|
||||||
|
|
||||||
case TableColumn.COMPOSER:
|
case TableColumn.COMPOSER:
|
||||||
return <ComposerColumn {...props} {...dragProps} controls={controls} type={type} />;
|
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 { useGridCarouselContainerQuery } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
||||||
import { useItemListStateSubscription } from '/@/renderer/components/item-list/helpers/item-list-state';
|
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 { 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 { 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';
|
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||||
@@ -60,7 +61,7 @@ import {
|
|||||||
SongListSort,
|
SongListSort,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
|
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const MetadataPillGroup = ({
|
const MetadataPillGroup = ({
|
||||||
items,
|
items,
|
||||||
@@ -830,13 +831,13 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
playSongFromItemListControl({
|
||||||
|
index,
|
||||||
const items = internalState?.getData() as Song[];
|
internalState,
|
||||||
|
item: item as Song,
|
||||||
if (index !== undefined) {
|
meta,
|
||||||
player.addToQueueByData(items, playType, item.id);
|
player,
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [player]);
|
}, [player]);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import styles from './album-artist-detail-content.module.css';
|
|||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
||||||
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
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 { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
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 { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||||
@@ -365,12 +366,13 @@ const AlbumArtistMetadataTopSongsContent = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
playSongFromItemListControl({
|
||||||
const items = internalState?.getData() as Song[];
|
index,
|
||||||
|
internalState,
|
||||||
if (index !== undefined) {
|
item: item as Song,
|
||||||
player.addToQueueByData(items, playType, item.id);
|
meta,
|
||||||
}
|
player,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [player]);
|
}, [player]);
|
||||||
@@ -657,12 +659,13 @@ const AlbumArtistMetadataFavoriteSongs = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
playSongFromItemListControl({
|
||||||
const items = internalState?.getData() as Song[];
|
index,
|
||||||
|
internalState,
|
||||||
if (index !== undefined) {
|
item: item as Song,
|
||||||
player.addToQueueByData(items, playType, item.id);
|
meta,
|
||||||
}
|
player,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [player]);
|
}, [player]);
|
||||||
|
|||||||
+9
-7
@@ -2,6 +2,7 @@ import { useSuspenseQueries } from '@tanstack/react-query';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router';
|
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 { 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 { 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';
|
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 { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import { sortSongList } from '/@/shared/api/utils';
|
import { sortSongList } from '/@/shared/api/utils';
|
||||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
||||||
const { albumArtistId, artistId } = useParams() as {
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
@@ -96,12 +97,13 @@ const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
playSongFromItemListControl({
|
||||||
const items = internalState?.getData() as Song[];
|
index,
|
||||||
|
internalState,
|
||||||
if (index !== undefined) {
|
item: item as Song,
|
||||||
player.addToQueueByData(items, playType, item.id);
|
meta,
|
||||||
}
|
player,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [player]);
|
}, [player]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router';
|
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 { 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 { 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';
|
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 { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
const AlbumArtistDetailTopSongsListRoute = () => {
|
const AlbumArtistDetailTopSongsListRoute = () => {
|
||||||
const { albumArtistId, artistId } = useParams() as {
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
@@ -78,12 +79,13 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
playSongFromItemListControl({
|
||||||
const items = internalState?.getData() as Song[];
|
index,
|
||||||
|
internalState,
|
||||||
if (index !== undefined) {
|
item: item as Song,
|
||||||
player.addToQueueByData(items, playType, item.id);
|
meta,
|
||||||
}
|
player,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [player]);
|
}, [player]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { forwardRef, useMemo } from 'react';
|
import { forwardRef, useMemo } from 'react';
|
||||||
import { useEffect } 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 { 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 { 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';
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
Song,
|
Song,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, Play, TableColumn } from '/@/shared/types/types';
|
import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListTableProps extends Omit<
|
interface PlaylistDetailSongListTableProps extends Omit<
|
||||||
ItemListTableComponentProps<PlaylistSongListQuery>,
|
ItemListTableComponentProps<PlaylistSongListQuery>,
|
||||||
@@ -103,12 +104,13 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
playSongFromItemListControl({
|
||||||
const items = internalState?.getData() as Song[];
|
index,
|
||||||
|
internalState,
|
||||||
if (index !== undefined) {
|
item: item as Song,
|
||||||
player.addToQueueByData(items, playType, item.id);
|
meta,
|
||||||
}
|
player,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [player]);
|
}, [player]);
|
||||||
@@ -228,12 +230,13 @@ export const PlaylistDetailSongListEditTable = forwardRef<any, PlaylistDetailSon
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
playSongFromItemListControl({
|
||||||
const items = internalState?.getData() as Song[];
|
index,
|
||||||
|
internalState,
|
||||||
if (index !== undefined) {
|
item: item as Song,
|
||||||
player.addToQueueByData(items, playType, item.id);
|
meta,
|
||||||
}
|
player,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [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 { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form';
|
import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form';
|
||||||
import { useIsMutatingSidebarPlaylistFolderMove } from '/@/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation';
|
import { useIsMutatingSidebarPlaylistFolderMove } from '/@/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation';
|
||||||
import {
|
import { ItemRowPlayControls } from '/@/renderer/features/shared/components/item-row-play-controls';
|
||||||
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 {
|
import {
|
||||||
collectFolderPaths,
|
collectFolderPaths,
|
||||||
PlaylistFolderDragExpandProvider,
|
PlaylistFolderDragExpandProvider,
|
||||||
@@ -41,7 +37,7 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { formatDurationString } from '/@/renderer/utils';
|
import { formatDurationString } from '/@/renderer/utils';
|
||||||
import { Accordion } from '/@/shared/components/accordion/accordion';
|
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 { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
import { animationVariants } from '/@/shared/components/animations/animation-variants';
|
import { animationVariants } from '/@/shared/components/animations/animation-variants';
|
||||||
import { ButtonProps } from '/@/shared/components/button/button';
|
import { ButtonProps } from '/@/shared/components/button/button';
|
||||||
@@ -299,7 +295,12 @@ export const PlaylistRowButton = memo(
|
|||||||
<Text className={styles.compactName} fw={500} size="md">
|
<Text className={styles.compactName} fw={500} size="md">
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isHovered && <RowControls id={to} onPlay={handlePlay} />}
|
{isHovered && (
|
||||||
|
<ItemRowPlayControls
|
||||||
|
className={styles.controls}
|
||||||
|
onPlay={(playType) => handlePlay(to, playType)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MotionLink>
|
</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 = () => {
|
export const SidebarPlaylistList = () => {
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
Reference in New Issue
Block a user