mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(most-played): quick actions, real context menu, prominent plays badge (#482)
* feat(most-played): quick actions, real context menu, prominent plays badge Three UX refinements on Settings → Most Played, in response to user feedback: * **Quick actions on each album row** — Play and Enqueue buttons that reuse the same logic as AlbumCard (Play kicks the existing `playAlbum` fade-out flow; Enqueue fetches the album and appends its songs to the queue). Always visible, not hover-gated. * **Real context menu** on right-click — replaces a hidden direct `playAlbum` action with the standard `openContextMenu(...)` flow used elsewhere in the app, so right-click on an album row now opens the full album context menu (Play / Add to queue / Play next / Add to playlist / Go to artist), and right-click on a Top Artists card opens the artist context menu. * **Plays badge next to the album title** — replaces the small right-aligned plays count that was easy to miss. Each row now shows a localized pill (`11 plays` / `11× gespielt`) right next to the album title, since the play count is the central datum on this page. CSS: new `.mp-album-name-row`, `.mp-album-plays-pill`, `.mp-album-actions` and `.mp-album-action-btn` rules; the unused `.mp-album-plays` block and its right-most grid column were removed. * docs: changelog entry for PR #482 Logs the Most Played quick-actions / real context menu / prominent plays badge changes under v1.46.0 "## Changed".
This commit is contained in:
committed by
GitHub
parent
ebce53f8a7
commit
6c1deeeb7f
@@ -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
|
||||
|
||||
@@ -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<SubsonicAlbum[]>([]);
|
||||
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');
|
||||
}}
|
||||
>
|
||||
<span className="mp-rank">{i + 1}</span>
|
||||
{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');
|
||||
}}
|
||||
>
|
||||
<span className="mp-album-rank">{sortAsc ? withPlays.length - i : i + 1}</span>
|
||||
{album.coverArt ? (
|
||||
@@ -181,7 +200,13 @@ export default function MostPlayed() {
|
||||
<div className="mp-album-cover mp-album-cover--placeholder" />
|
||||
)}
|
||||
<div className="mp-album-meta">
|
||||
<span className="mp-album-name truncate">{album.name}</span>
|
||||
<div className="mp-album-name-row">
|
||||
<span className="mp-album-name truncate">{album.name}</span>
|
||||
<span className="mp-album-plays-pill">
|
||||
<Play size={11} fill="currentColor" />
|
||||
{t('mostPlayed.plays', { n: (album.playCount ?? 0).toLocaleString() })}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="mp-album-artist truncate track-artist-link"
|
||||
onClick={e => { e.stopPropagation(); navigate(`/artist/${album.artistId}`); }}
|
||||
@@ -189,8 +214,27 @@ export default function MostPlayed() {
|
||||
{album.artist}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mp-album-actions">
|
||||
<button
|
||||
className="mp-album-action-btn"
|
||||
onClick={e => { e.stopPropagation(); playAlbum(album.id); }}
|
||||
data-tooltip={t('hero.playAlbum')}
|
||||
data-tooltip-pos="top"
|
||||
aria-label={t('hero.playAlbum')}
|
||||
>
|
||||
<Play size={14} fill="currentColor" />
|
||||
</button>
|
||||
<button
|
||||
className="mp-album-action-btn"
|
||||
onClick={e => { e.stopPropagation(); void handleEnqueueAlbum(album.id); }}
|
||||
data-tooltip={t('contextMenu.enqueueAlbum')}
|
||||
data-tooltip-pos="top"
|
||||
aria-label={t('contextMenu.enqueueAlbum')}
|
||||
>
|
||||
<ListPlus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{album.year && <span className="mp-album-year">{album.year}</span>}
|
||||
<span className="mp-album-plays">{(album.playCount ?? 0).toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+47
-10
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user