diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b4b67e..3f7431b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * When you add a new server from **Settings → Servers**, the new entry now appears in the server picker but **your current active server stays active** — playback, queue and library view are no longer interrupted. * The login screen on `/login` is unchanged: signing in there still selects the chosen server. +### Most Played — quick actions, real context menu, prominent plays badge + +**By [@Psychotoxical](https://github.com/Psychotoxical), suggested by nzxl, PR [#482](https://github.com/Psychotoxical/psysonic/pull/482)** + +* Each album row now shows always-visible **Play** and **Enqueue** quick-action buttons, reusing the same flows as `AlbumCard` (Play kicks the fade-out replace-and-play, Enqueue appends the album's songs to the queue end). +* **Right-click** on an album row now opens the standard album context menu (Play / Add to queue / Play next / Add to playlist / Go to artist) instead of firing a hidden direct-play action; right-click on a Top Artists card opens the artist context menu. +* The **play count** moved from a small right-aligned column to a localized **pill right next to the album title** — `11 plays` (en), `11× gespielt` (de) — since the play count is the central datum on this page. + ## Fixed ### Hot cache, HTTP streaming replay, and queue source indicator diff --git a/src/pages/MostPlayed.tsx b/src/pages/MostPlayed.tsx index 13c9d0fd..20262c96 100644 --- a/src/pages/MostPlayed.tsx +++ b/src/pages/MostPlayed.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound } from 'lucide-react'; -import { getAlbumList, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic'; +import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound, Play, ListPlus } from 'lucide-react'; +import { getAlbumList, getAlbum, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic'; import { useAuthStore } from '../store/authStore'; +import { usePlayerStore, songToTrack } from '../store/playerStore'; import CachedImage from '../components/CachedImage'; import { playAlbum } from '../utils/playAlbum'; import { useTranslation } from 'react-i18next'; @@ -59,6 +60,17 @@ export default function MostPlayed() { const { t } = useTranslation(); const navigate = useNavigate(); const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion); + const openContextMenu = usePlayerStore(s => s.openContextMenu); + const enqueue = usePlayerStore(s => s.enqueue); + + const handleEnqueueAlbum = useCallback(async (albumId: string) => { + try { + const data = await getAlbum(albumId); + enqueue(data.songs.map(songToTrack)); + } catch { + // Network failure — silent (toast would be too noisy for a hover action). + } + }, [enqueue]); const [albums, setAlbums] = useState([]); const [loading, setLoading] = useState(true); @@ -139,6 +151,10 @@ export default function MostPlayed() { key={artist.id} className="mp-artist-card" onClick={() => navigate(`/artist/${artist.id}`)} + onContextMenu={e => { + e.preventDefault(); + openContextMenu(e.clientX, e.clientY, artist, 'artist'); + }} > {i + 1} {artist.coverArt ? ( @@ -172,7 +188,10 @@ export default function MostPlayed() { key={album.id} className="mp-album-row" onClick={() => navigate(`/album/${album.id}`)} - onContextMenu={e => { e.preventDefault(); playAlbum(album.id); }} + onContextMenu={e => { + e.preventDefault(); + openContextMenu(e.clientX, e.clientY, album, 'album'); + }} > {sortAsc ? withPlays.length - i : i + 1} {album.coverArt ? ( @@ -181,7 +200,13 @@ export default function MostPlayed() {
)}
- {album.name} +
+ {album.name} + + + {t('mostPlayed.plays', { n: (album.playCount ?? 0).toLocaleString() })} + +
{ e.stopPropagation(); navigate(`/artist/${album.artistId}`); }} @@ -189,8 +214,27 @@ export default function MostPlayed() { {album.artist}
+
+ + +
{album.year && {album.year}} - {(album.playCount ?? 0).toLocaleString()}
))} diff --git a/src/styles/components.css b/src/styles/components.css index e9993512..527f4641 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -9318,6 +9318,53 @@ html.no-compositing .fs-seekbar-played { transition: background var(--transition-fast); } +.mp-album-name-row { + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; +} + +.mp-album-plays-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + background: var(--bg-card); + color: var(--accent); + font-size: 12px; + font-weight: 600; + white-space: nowrap; + flex-shrink: 0; + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; +} + +.mp-album-actions { + display: flex; + gap: 4px; +} + +.mp-album-action-btn { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); +} + +.mp-album-action-btn:hover { + background: var(--bg-card); + color: var(--text-primary); +} + .mp-album-row:hover { background: var(--bg-hover); } @@ -9367,16 +9414,6 @@ html.no-compositing .fs-seekbar-played { font-variant-numeric: tabular-nums; } -.mp-album-plays { - font-size: 13px; - font-weight: 600; - color: var(--accent); - white-space: nowrap; - font-variant-numeric: tabular-nums; - min-width: 36px; - text-align: right; -} - .mp-load-more { margin-top: 1rem; display: flex;