mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
fix(tracks): RC3 layout regressions + multi-artist links (#976)
* fix(tracks): RC3 layout regressions + multi-artist links - Restore vertical rhythm on the Tracks hub: the header/hero/rails/browse sections sat two wrapper divs below .tracks-page so its flex gap never reached them and they collapsed together. New .tracks-hub-stack re-applies the gap, fixing the tagline-touching-hero and Random-Pick-overlapping-hero spacing (the rail's nav buttons no longer ride into the box above). - Stop the sticky browse-table header going transparent on hover: its base background became opaque but the :hover rule still forced transparent, so rows bled through. Header now keeps its card background on hover. - Widen the Duration column (56px -> 72px; 56 -> 64 on mobile) so the "DURATION" header no longer clips/overruns. - Split multi-artist tracks into individually clickable artist links in both the "Track of the moment" hero and the browse list rows (OpenSubsonic artists[] with single-artist fallback), matching the album tracklist. * docs(changelog): Tracks RC3 spacing, Duration, header hover, multi-artist (#976)
This commit is contained in:
committed by
GitHub
parent
908c349cfd
commit
88df194808
@@ -855,6 +855,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* Hiding **Mainstage** from the sidebar no longer leaves the app opening on a blank page — it now starts on the first visible library entry instead.
|
* Hiding **Mainstage** from the sidebar no longer leaves the app opening on a blank page — it now starts on the first visible library entry instead.
|
||||||
* When every Mainstage section is turned off, the page shows a short message with a shortcut into **Settings → Personalisation** rather than appearing empty.
|
* When every Mainstage section is turned off, the page shows a short message with a shortcut into **Settings → Personalisation** rather than appearing empty.
|
||||||
|
|
||||||
|
### Tracks — spacing, Duration column, header hover, multi-artist links
|
||||||
|
|
||||||
|
**By [@Psychotoxical](https://github.com/Psychotoxical), reported by zunoz on Discord, PR [#976](https://github.com/Psychotoxical/psysonic/pull/976)**
|
||||||
|
|
||||||
|
* The Tracks hub sections (tagline, **Track of the moment**, **Random Pick** rail, **Browse all tracks**) no longer bunch together — even vertical spacing is restored, so the rail's navigation buttons stop riding up into the card above.
|
||||||
|
* The **Browse all tracks** table no longer clips the **Duration** column header.
|
||||||
|
* The track-list column header keeps its background on hover instead of turning transparent and letting rows show through.
|
||||||
|
* **Track of the moment** and the browse rows split multi-artist tracks into individually clickable artist links, matching the album track list.
|
||||||
|
|
||||||
## [1.46.0] - 2026-05-18
|
## [1.46.0] - 2026-05-18
|
||||||
|
|
||||||
> **🙏 Special thanks to [@zz5zz](https://github.com/zz5zz)** for his tireless quirk-spotting and bug reports on the [Psysonic Discord](https://discord.gg/AMnDRErm4u) — several of the polish fixes in this release landed directly off the back of his messages.
|
> **🙏 Special thanks to [@zz5zz](https://github.com/zz5zz)** for his tireless quirk-spotting and bug reports on the [Psysonic Discord](https://discord.gg/AMnDRErm4u) — several of the polish fixes in this release landed directly off the back of his messages.
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ function SongRow({ song, showBpm }: Props) {
|
|||||||
enqueue([songToTrack(song)]);
|
enqueue([songToTrack(song)]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Split multi-artist tracks into individually clickable links (OpenSubsonic
|
||||||
|
// `artists[]`), falling back to the single flat artist when absent — mirrors
|
||||||
|
// the album tracklist so a "A · B" credit isn't one link to a single artist.
|
||||||
|
const artistRefs = song.artists && song.artists.length > 0
|
||||||
|
? song.artists
|
||||||
|
: [{ id: song.artistId, name: song.artist }];
|
||||||
|
|
||||||
const bpmTooltip =
|
const bpmTooltip =
|
||||||
song.localBpmSource === 'analysis'
|
song.localBpmSource === 'analysis'
|
||||||
? t('search.bpmSourceAnalysis')
|
? t('search.bpmSourceAnalysis')
|
||||||
@@ -93,13 +100,17 @@ function SongRow({ song, showBpm }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="song-list-row-cell song-list-row-title truncate" title={song.title}>{song.title}</div>
|
<div className="song-list-row-cell song-list-row-title truncate" title={song.title}>{song.title}</div>
|
||||||
<div className="song-list-row-cell truncate">
|
<div className="song-list-row-cell truncate" title={song.artist}>
|
||||||
<span
|
{artistRefs.map((a, i) => (
|
||||||
className={song.artistId ? 'track-artist-link' : ''}
|
<React.Fragment key={a.id ?? a.name ?? i}>
|
||||||
style={{ cursor: song.artistId ? 'pointer' : 'default' }}
|
{i > 0 && <span className="track-artist-sep"> · </span>}
|
||||||
onClick={(e) => { if (song.artistId) { e.stopPropagation(); navigateToArtist(song.artistId); } }}
|
<span
|
||||||
title={song.artist}
|
className={a.id ? 'track-artist-link' : ''}
|
||||||
>{song.artist}</span>
|
style={{ cursor: a.id ? 'pointer' : 'default' }}
|
||||||
|
onClick={(e) => { if (a.id) { e.stopPropagation(); navigateToArtist(a.id!); } }}
|
||||||
|
>{a.name ?? song.artist}</span>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="song-list-row-cell truncate">
|
<div className="song-list-row-cell truncate">
|
||||||
{song.albumId ? (
|
{song.albumId ? (
|
||||||
|
|||||||
@@ -105,6 +105,14 @@ export default function TracksPageChrome({
|
|||||||
[random, hero],
|
[random, hero],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Split a multi-artist feature into individually clickable links
|
||||||
|
// (OpenSubsonic `artists[]`), falling back to the single flat artist.
|
||||||
|
const heroArtistRefs = hero
|
||||||
|
? (hero.artists && hero.artists.length > 0
|
||||||
|
? hero.artists
|
||||||
|
: [{ id: hero.artistId, name: hero.artist }])
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!perfFlags.disableMainstageStickyHeader && (
|
{!perfFlags.disableMainstageStickyHeader && (
|
||||||
@@ -140,11 +148,16 @@ export default function TracksPageChrome({
|
|||||||
</span>
|
</span>
|
||||||
<h2 className="tracks-hero-title" title={hero.title}>{hero.title}</h2>
|
<h2 className="tracks-hero-title" title={hero.title}>{hero.title}</h2>
|
||||||
<p className="tracks-hero-meta">
|
<p className="tracks-hero-meta">
|
||||||
<span
|
{heroArtistRefs.map((a, i) => (
|
||||||
className={hero.artistId ? 'track-artist-link' : ''}
|
<React.Fragment key={a.id ?? a.name ?? i}>
|
||||||
style={{ cursor: hero.artistId ? 'pointer' : 'default' }}
|
{i > 0 && <span className="track-artist-sep"> · </span>}
|
||||||
onClick={() => hero.artistId && navigateToArtist(hero.artistId)}
|
<span
|
||||||
>{hero.artist}</span>
|
className={a.id ? 'track-artist-link' : ''}
|
||||||
|
style={{ cursor: a.id ? 'pointer' : 'default' }}
|
||||||
|
onClick={() => a.id && navigateToArtist(a.id)}
|
||||||
|
>{a.name ?? hero.artist}</span>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
{hero.album && (
|
{hero.album && (
|
||||||
<>
|
<>
|
||||||
<span className="tracks-hero-meta-dot">·</span>
|
<span className="tracks-hero-meta-dot">·</span>
|
||||||
|
|||||||
@@ -852,7 +852,7 @@ export default function SearchBrowsePage() {
|
|||||||
data-advanced-search-root
|
data-advanced-search-root
|
||||||
>
|
>
|
||||||
<div style={{ visibility: isLeaveRestorePending ? 'hidden' : 'visible' }}>
|
<div style={{ visibility: isLeaveRestorePending ? 'hidden' : 'visible' }}>
|
||||||
<div>
|
<div className={showTracksChrome ? 'tracks-hub-stack' : undefined}>
|
||||||
{showTracksChrome ? (
|
{showTracksChrome ? (
|
||||||
<>
|
<>
|
||||||
<TracksPageChrome
|
<TracksPageChrome
|
||||||
|
|||||||
@@ -9,6 +9,16 @@
|
|||||||
padding-bottom: var(--space-8, 2rem);
|
padding-bottom: var(--space-8, 2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The hub sections (header, hero, rails, browse list) live two wrapper divs
|
||||||
|
deep inside .tracks-page, so the page-level flex gap never reached them and
|
||||||
|
the sections collapsed together. This stack restores even vertical rhythm
|
||||||
|
between them. */
|
||||||
|
.tracks-hub-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6, 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
.tracks-header {
|
.tracks-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.song-list-row {
|
.song-list-row {
|
||||||
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 56px;
|
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-list-row--with-bpm {
|
.song-list-row--with-bpm {
|
||||||
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 48px 56px;
|
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) 48px 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-list-row-cell:nth-child(4), /* album */
|
.song-list-row-cell:nth-child(4), /* album */
|
||||||
|
|||||||
+6
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.song-list-row {
|
.song-list-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 56px;
|
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 72px;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 52px;
|
height: 52px;
|
||||||
@@ -43,8 +43,11 @@
|
|||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Header is sticky and opaque — keep its card background on hover so the
|
||||||
|
generic .song-list-row:hover rule doesn't turn it transparent (which let
|
||||||
|
rows scrolling underneath bleed through). */
|
||||||
.song-list-row--header:hover {
|
.song-list-row--header:hover {
|
||||||
background: transparent;
|
background: var(--bg-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-list-row-cell {
|
.song-list-row-cell {
|
||||||
@@ -75,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.song-list-row--with-bpm {
|
.song-list-row--with-bpm {
|
||||||
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 48px 56px;
|
grid-template-columns: 64px minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) 110px 48px 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-list-row-bpm {
|
.song-list-row-bpm {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
margin-top: var(--space-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-song-list-title {
|
.virtual-song-list-title {
|
||||||
|
|||||||
Reference in New Issue
Block a user