feat(album-view): right sidebar with label and full awards stack

Ports the Qobuz web 'label + awards' sidebar pattern to AlbumView.
Track list and sidebar sit side-by-side above 1100px and collapse
vertically below it. Label card navigates to the existing LabelView.
Award cards call onAwardClick when provided (disabled until Phase 3
lands AwardView).

Wiring:
- Adds awards (Vec<PageArtistAward>) to both qbz-models Album and
  src-tauri api::models Album so /album/get payloads hydrate the
  field instead of silently dropping it.
- Adds awards to QobuzAlbum and AlbumDetail frontend types plus the
  convertQobuzAlbum adapter.
- New album.sidebar i18n keys in all five locales.

Card ribbon still shows only the LAST award (Phase 1 behavior);
sidebar renders every award as a tap target — mobile-style.

cc
This commit is contained in:
vicrodh committed 2026-04-14 11:11:24 -06:00
1 parent ef9cab64bc
commit 7bf9972846
10 files changed
+198 -7

No files matched your search

+3
View File
@@ -265,6 +265,9 @@ pub struct Album {
/// Album goodies (booklets, liner notes PDFs)
#[serde(default)]
pub goodies: Option<Vec<Goody>>,
/// Editorial awards (Qobuzissime, Album of the Week, press accolades).
#[serde(default)]
pub awards: Option<Vec<PageArtistAward>>,
}
/// A downloadable extra bundled with an album (e.g. PDF booklet)
+3
View File
@@ -230,6 +230,9 @@ pub struct Album {
pub upc: Option<String>,
/// Editorial description/review of the album
pub description: Option<String>,
/// Editorial awards attached to the album.
#[serde(default)]
pub awards: Option<Vec<PageArtistAward>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+2 -1
View File
@@ -390,7 +390,8 @@ export function convertQobuzAlbum(album: QobuzAlbum): AlbumDetail {
parental_warning: track.parental_warning ?? false
})) || [],
upc: album.upc,
goodies: album.goodies
goodies: album.goodies,
awards: album.awards?.map(a => ({ id: a.id, name: a.name, awardedAt: a.awarded_at }))
};
}
+157 -1
View File
@@ -11,7 +11,7 @@
setCustomAlbumCover,
removeCustomAlbumCover as removeCustomCoverFromStore
} from '$lib/stores/customAlbumCoverStore';
import { ArrowLeft, Play, Shuffle, Heart, Radio, CloudDownload, ChevronLeft, ChevronRight, LoaderCircle, SquareCheckBig, BookOpen } from 'lucide-svelte';
import { ArrowLeft, Play, Shuffle, Heart, Radio, CloudDownload, ChevronLeft, ChevronRight, LoaderCircle, SquareCheckBig, BookOpen, Award, Disc3 } from 'lucide-svelte';
import { cachedSrc } from '$lib/actions/cachedImage';
import AlbumCard from '../AlbumCard.svelte';
import TrackRow from '../TrackRow.svelte';
@@ -57,6 +57,12 @@
releaseDate?: string;
}
interface Award {
id: number;
name: string;
awardedAt?: string;
}
interface Props {
album: {
id: string;
@@ -74,10 +80,12 @@
duration: string;
tracks: Track[];
goodies?: QobuzGoody[];
awards?: Award[];
};
onBack: () => void;
onArtistClick?: () => void;
onLabelClick?: (labelId: number, labelName: string) => void;
onAwardClick?: (awardId: number, awardName: string) => void;
onTrackPlay?: (track: Track) => void;
onTrackPlayNext?: (track: Track) => void;
onTrackPlayLater?: (track: Track) => void;
@@ -127,6 +135,7 @@
onBack,
onArtistClick,
onLabelClick,
onAwardClick,
onTrackPlay,
onTrackPlayNext,
onTrackPlayLater,
@@ -628,6 +637,8 @@
<!-- Divider -->
<div class="divider"></div>
<!-- Track list + right-side metadata sidebar (label + awards) -->
<div class="track-sidebar-layout">
<!-- Track List -->
<div class="track-list">
<!-- Table Header -->
@@ -723,6 +734,51 @@
/>
</div>
<!-- Album metadata sidebar (label + awards stack). Matches Qobuz web
where the label and press accolades sit to the right of the track
list; collapses under the tracks below 1100px. -->
{#if (album.labelId && album.label) || (album.awards && album.awards.length > 0)}
<aside class="album-sidebar">
{#if album.labelId && album.label}
<section class="sidebar-section">
<h3 class="sidebar-section-title">{$t('album.sidebar.label')}</h3>
<button
class="sidebar-entity-card"
type="button"
onclick={() => onLabelClick?.(album.labelId!, album.label)}
disabled={!onLabelClick}
>
<div class="sidebar-entity-avatar label-avatar">
<Disc3 size={24} />
</div>
<div class="sidebar-entity-name">{album.label}</div>
</button>
</section>
{/if}
{#if album.awards && album.awards.length > 0}
<section class="sidebar-section">
<h3 class="sidebar-section-title">{$t('album.sidebar.awards')}</h3>
<div class="sidebar-awards-list">
{#each album.awards as award (award.id + ':' + award.name)}
<button
class="sidebar-entity-card"
type="button"
onclick={() => onAwardClick?.(award.id, award.name)}
disabled={!onAwardClick}
>
<div class="sidebar-entity-avatar award-avatar">
<Award size={22} />
</div>
<div class="sidebar-entity-name">{award.name}</div>
</button>
{/each}
</div>
</section>
{/if}
</aside>
{/if}
</div>
<!-- By the same artist Section -->
{#if filteredArtistAlbums.length > 0 && !isVariousArtists}
<div class="same-artist-section">
@@ -1287,4 +1343,104 @@
background: var(--border-subtle);
margin: 4px 8px;
}
/* ---- Album metadata sidebar (label + awards) ---- */
.track-sidebar-layout {
display: flex;
flex-direction: row;
gap: 32px;
align-items: flex-start;
}
.track-sidebar-layout .track-list {
flex: 1 1 auto;
min-width: 0;
}
.album-sidebar {
flex: 0 0 280px;
width: 280px;
display: flex;
flex-direction: column;
gap: 24px;
padding-top: 4px;
}
.sidebar-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-section-title {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
margin: 0;
}
.sidebar-awards-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.sidebar-entity-card {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
text-align: left;
transition: background-color 150ms ease;
color: var(--text-primary);
width: 100%;
min-width: 0;
}
.sidebar-entity-card:hover:not(:disabled) {
background: var(--bg-tertiary);
}
.sidebar-entity-card:disabled {
cursor: default;
}
.sidebar-entity-avatar {
flex: 0 0 44px;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.sidebar-entity-avatar.label-avatar {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
}
.sidebar-entity-avatar.award-avatar {
background: linear-gradient(135deg, #b45309 0%, #eab308 100%);
}
.sidebar-entity-name {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.3;
min-width: 0;
flex: 1;
}
/* Collapse sidebar under tracks on narrow windows */
@media (max-width: 1100px) {
.track-sidebar-layout {
flex-direction: column;
gap: 24px;
}
.album-sidebar {
flex: 1 1 auto;
width: 100%;
}
}
</style>
+5 -1
View File
@@ -320,7 +320,11 @@
"seeFullDiscography": "Vollständige Diskografie ansehen",
"loadError": "Albumtitel konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
"releasedOn": "am",
"releasedBy": "Erschienen bei"
"releasedBy": "Erschienen bei",
"sidebar": {
"label": "Label",
"awards": "Auszeichnungen"
}
},
"artist": {
"albums": "Alben",
+5 -1
View File
@@ -320,7 +320,11 @@
"bookletRotate": "Rotate",
"bookletDownload": "Save booklet",
"sameArtist": "By the same artist",
"seeFullDiscography": "See full discography"
"seeFullDiscography": "See full discography",
"sidebar": {
"label": "Label",
"awards": "Awards"
}
},
"artist": {
"albums": "Albums",
+5 -1
View File
@@ -320,7 +320,11 @@
"sameArtist": "Del mismo artista...",
"seeFullDiscography": "Ver discografía completa",
"releasedOn": "el",
"releasedBy": "Publicado en"
"releasedBy": "Publicado en",
"sidebar": {
"label": "Sello",
"awards": "Premios"
}
},
"artist": {
"albums": "Álbumes",
+5 -1
View File
@@ -320,7 +320,11 @@
"seeFullDiscography": "Voir la discographie complète",
"releasedOn": "le",
"releasedBy": "Parution chez",
"loadError": "Impossible de charger les pistes de l'album. Veuillez réessayer."
"loadError": "Impossible de charger les pistes de l'album. Veuillez réessayer.",
"sidebar": {
"label": "Label",
"awards": "Récompenses"
}
},
"artist": {
"albums": "Albums",
+5 -1
View File
@@ -320,7 +320,11 @@
"sameArtist": "Do mesmo artista",
"seeFullDiscography": "Ver discografia completa",
"releasedOn": "em",
"releasedBy": "Lançado pela"
"releasedBy": "Lançado pela",
"sidebar": {
"label": "Selo",
"awards": "Prêmios"
}
},
"artist": {
"albums": "Álbuns",
+8
View File
@@ -106,6 +106,7 @@ export interface QobuzAlbum {
parental_warning?: boolean;
upc?: string;
goodies?: QobuzGoody[];
awards?: { id: number; name: string; awarded_at?: string }[];
}
export interface QobuzGoody {
@@ -180,6 +181,12 @@ export interface Track {
parental_warning?: boolean;
}
export interface AlbumAward {
id: number;
name: string;
awardedAt?: string;
}
export interface AlbumDetail {
id: string;
artwork: string;
@@ -197,6 +204,7 @@ export interface AlbumDetail {
tracks: Track[];
upc?: string; // Universal Product Code for album.link sharing
goodies?: QobuzGoody[];
awards?: AlbumAward[];
}
export interface ArtistDetail {