feat(track): play-on-hover cover art, replacing double-click
Docker Build & Publish / Prune old image versions (push) Has been cancelled
Docker Build & Publish / build (push) Has been cancelled
Docker Build & Publish / push (push) Has been cancelled

Add a play overlay button shown on cover art hover that inserts the
track into the queue right after the current track and jumps to it,
so it takes priority over what's already queued. Replaces the
double-click-to-play interaction with a new playNow queue action.
This commit is contained in:
Senko-san
2026-06-13 17:44:35 +03:00
parent b37fabd936
commit 3984c7a499
3 changed files with 80 additions and 20 deletions
+31 -2
View File
@@ -1,10 +1,12 @@
import { Row } from '@olly/modern-sk';
import { useTranslation } from 'react-i18next';
import { TrackContextMenu } from './TrackContextMenu';
import { AvailabilityBadge } from './AvailabilityBadge';
import { MetadataStatusBadge } from './MetadataStatusBadge';
import { Icon } from '../common/Icon';
import { formatDuration } from '../../lib/format';
import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
import { play } from '../../store/slices/player';
import { playNow } from '../../store/slices/queue';
import type { Track } from '../../api/types';
import { getCoverUrl, getTrackCoverUrl } from '../../api/endpoints/streaming';
@@ -25,6 +27,7 @@ export function TrackRow({
onEditMetadata,
onDelete,
}: Props) {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const currentTrackId = useAppSelector((s) => s.player.currentTrackId);
const isPlaying = useAppSelector((s) => s.player.isPlaying);
@@ -36,10 +39,22 @@ export function TrackRow({
getCoverUrl(track.albumArtUrl) ??
(token ? getTrackCoverUrl(track.id, token, track.hasCover) : undefined);
const handlePlayNow = () => {
dispatch(
playNow({
trackId: track.id,
title: track.title,
artistName: track.artistName,
albumTitle: track.albumTitle,
durationMs: track.durationMs,
albumArtUrl: track.albumArtUrl,
}),
);
};
return (
<Row
selected={isActive}
onDoubleClick={() => dispatch(play(track.id))}
style={{
display: 'grid',
gridTemplateColumns: '2rem 2.5rem 1fr auto auto',
@@ -58,6 +73,10 @@ export function TrackRow({
>
{isActive && isPlaying ? '▶' : index !== undefined ? index + 1 : ''}
</span>
<div
className="track-art"
style={{ position: 'relative', width: 36, height: 36 }}
>
{artUrl ? (
<img
src={artUrl}
@@ -76,6 +95,16 @@ export function TrackRow({
}}
/>
)}
<button
type="button"
className="track-art-play"
onClick={handlePlayNow}
aria-label={t('track.menu.playNow')}
title={t('track.menu.playNow')}
>
<Icon name="play" fill />
</button>
</div>
<div style={{ minWidth: 0 }}>
<div
style={{
+6
View File
@@ -59,6 +59,11 @@ export const queueSlice = createSlice({
addNextInQueue(state, action: PayloadAction<QueueEntry>) {
state.entries.splice(state.currentIndex + 1, 0, action.payload);
},
playNow(state, action: PayloadAction<QueueEntry>) {
const insertAt = state.currentIndex + 1;
state.entries.splice(insertAt, 0, action.payload);
state.currentIndex = insertAt;
},
removeFromQueue(state, action: PayloadAction<number>) {
state.entries.splice(action.payload, 1);
if (action.payload < state.currentIndex) state.currentIndex--;
@@ -93,6 +98,7 @@ export const {
setQueue,
addToQueue,
addNextInQueue,
playNow,
removeFromQueue,
moveInQueue,
goToIndex,
+25
View File
@@ -951,3 +951,28 @@
.sb-sec-link.active {
color: var(--fg-1);
}
/* ============================================================
TRACK ROW — cover art play overlay
============================================================ */
.track-art-play {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 4px;
background: rgba(0, 0, 0, 0.5);
color: var(--fg-1);
font-size: 16px;
cursor: pointer;
opacity: 0;
transition: opacity var(--dur-quick);
}
.track-art:hover .track-art-play {
opacity: 1;
}
.track-art-play:hover {
color: var(--lime);
}