From 3984c7a499683171d2410c19e5e32a820746f061 Mon Sep 17 00:00:00 2001 From: Senko-san Date: Sat, 13 Jun 2026 17:44:35 +0300 Subject: [PATCH] feat(track): play-on-hover cover art, replacing double-click 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. --- src/components/track/TrackRow.tsx | 69 ++++++++++++++++++++++--------- src/store/slices/queue.ts | 6 +++ src/styles/shell.css | 25 +++++++++++ 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/src/components/track/TrackRow.tsx b/src/components/track/TrackRow.tsx index df1dbdd..9b58fff 100644 --- a/src/components/track/TrackRow.tsx +++ b/src/components/track/TrackRow.tsx @@ -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 ( dispatch(play(track.id))} style={{ display: 'grid', gridTemplateColumns: '2rem 2.5rem 1fr auto auto', @@ -58,24 +73,38 @@ export function TrackRow({ > {isActive && isPlaying ? '▶' : index !== undefined ? index + 1 : ''} - {artUrl ? ( - - ) : ( -
- )} +
+ {artUrl ? ( + + ) : ( +
+ )} + +
) { state.entries.splice(state.currentIndex + 1, 0, action.payload); }, + playNow(state, action: PayloadAction) { + const insertAt = state.currentIndex + 1; + state.entries.splice(insertAt, 0, action.payload); + state.currentIndex = insertAt; + }, removeFromQueue(state, action: PayloadAction) { 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, diff --git a/src/styles/shell.css b/src/styles/shell.css index f55e3bb..551ea63 100644 --- a/src/styles/shell.css +++ b/src/styles/shell.css @@ -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); +}