feat(slint): album and artist detail view parity pass

Bring the album and artist pages closer to the Tauri originals.

Shared:
- Artwork-tinted gradient header — header_tint() derives an average,
  darkened color from the decoded cover; the page header renders a
  linear-gradient from it. Both pages scroll on a Flickable (no
  scrollbar gutter).

Album view:
- Full header action row: play, shuffle, favorite, radio, credits,
  booklet, mixtape, multi-select, more (visual where playback/feature
  wiring is pending; play and favorite emit media-action)
- Right sidebar with the record label and editorial awards, each a
  round-avatar card; AlbumData now carries id, label and awards

Artist view:
- Gradient header, fuller action row, and an "ARTIST" eyebrow
- Discography split into titled release sections (Albums, EPs &
  Singles, Live Albums, Compilations, ...) instead of one flat list
- Artist track rows gained the more-button context menu

cc
This commit is contained in:
vicrodh committed 2026-05-18 18:23:50 -06:00
1 parent 644fd88dc5
commit 49cd8f6a87
10 files changed
+465 -123

No files matched your search

+199 -60
View File
@@ -13,7 +13,6 @@ import { AlbumState, AlbumTrackItem, NavState, ContentView } from "../state.slin
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ContextMenu } from "../primitives/ContextMenu.slint";
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
import { ScrollView } from "std-widgets.slint";
component CircleAction inherits Rectangle {
in property <image> icon;
@@ -196,15 +195,89 @@ component TrackRow inherits Rectangle {
}
}
// One label/award entry in the album sidebar — a round gradient avatar
// and a name.
component SidebarCard inherits Rectangle {
in property <image> icon;
in property <string> name;
in property <brush> avatar-bg;
callback clicked();
height: 56px;
border-radius: Radius.sm;
background: ta.has-hover ? Theme.surface-hover : Theme.surface-card;
HorizontalLayout {
padding: 8px;
spacing: 10px;
VerticalLayout {
alignment: center;
Rectangle {
width: 36px;
height: 36px;
border-radius: 18px;
background: root.avatar-bg;
QbzIcon {
source: root.icon;
width: 18px;
height: 18px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: #ffffff;
}
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: root.name;
color: Theme.text-secondary;
font-size: Typography.link;
font-weight: Typography.medium;
wrap: word-wrap;
}
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
component SidebarHeading inherits Text {
color: Theme.text-muted;
font-size: 10px;
font-weight: Typography.semibold;
letter-spacing: 1px;
}
export component AlbumPageView inherits Rectangle {
callback open-artist(string);
callback media-action(string, string, string);
background: Theme.surface-main;
ScrollView {
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
Flickable {
viewport-width: self.width;
viewport-height: page.preferred-height;
// Artwork-tinted gradient behind the header; scrolls with content.
Rectangle {
x: 0;
y: 0;
width: parent.width;
height: 340px;
background: @linear-gradient(180deg,
AlbumState.header-color 0%,
transparent 100%);
}
page := VerticalLayout {
width: parent.width;
padding-left: 24px;
padding-right: 24px;
padding-top: 14px;
@@ -309,9 +382,12 @@ export component AlbumPageView inherits Rectangle {
alignment: start;
spacing: 12px;
CircleAction {
icon: @image-url("../assets/icons/play.svg");
icon: @image-url("../assets/icons/play-fill.svg");
primary: true;
diameter: 44px;
clicked => {
root.media-action("album", AlbumState.id, "play");
}
}
VerticalLayout {
alignment: center;
@@ -319,7 +395,32 @@ export component AlbumPageView inherits Rectangle {
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/heart.svg"); }
CircleAction {
icon: @image-url("../assets/icons/heart.svg");
clicked => {
root.media-action("album", AlbumState.id, "favorite");
}
}
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/radio.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/user.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/book-open.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/cassette-tape.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/square-check-big.svg"); }
}
VerticalLayout {
alignment: center;
@@ -336,62 +437,100 @@ export component AlbumPageView inherits Rectangle {
}
Rectangle { height: 8px; }
// --- Track list --------------------------------------------------
if AlbumState.loading && AlbumState.tracks.length == 0: Text {
text: @tr("Loading album...");
color: Theme.text-muted;
font-size: Typography.body;
}
// Column header.
// --- Track list + label/awards sidebar --------------------------
HorizontalLayout {
height: 40px;
padding-left: 12px;
padding-right: 12px;
spacing: 16px;
Text {
text: "#";
width: 32px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
text: @tr("TITLE");
horizontal-stretch: 1;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
text: @tr("DURATION");
width: 80px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
horizontal-alignment: center;
vertical-alignment: center;
}
Text {
text: @tr("QUALITY");
width: 80px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
horizontal-alignment: center;
vertical-alignment: center;
}
// Aligns with the per-row more-button slot.
Rectangle { width: 32px; }
}
spacing: 32px;
alignment: start;
for track[index] in AlbumState.tracks: TrackRow {
track: track;
index: index;
media-action(kind, id, action) => {
root.media-action(kind, id, action);
VerticalLayout {
horizontal-stretch: 1;
spacing: 0px;
alignment: start;
if AlbumState.loading && AlbumState.tracks.length == 0: Text {
text: @tr("Loading album...");
color: Theme.text-muted;
font-size: Typography.body;
}
// Column header.
HorizontalLayout {
height: 40px;
padding-left: 12px;
padding-right: 12px;
spacing: 16px;
Text {
text: "#";
width: 32px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
text: @tr("TITLE");
horizontal-stretch: 1;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
text: @tr("DURATION");
width: 80px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
horizontal-alignment: center;
vertical-alignment: center;
}
Text {
text: @tr("QUALITY");
width: 80px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
horizontal-alignment: center;
vertical-alignment: center;
}
// Aligns with the per-row more-button slot.
Rectangle { width: 32px; }
}
for track[index] in AlbumState.tracks: TrackRow {
track: track;
index: index;
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
}
// Label / awards sidebar.
if AlbumState.label != "" || AlbumState.awards.length > 0: VerticalLayout {
width: 254px;
spacing: 24px;
alignment: start;
if AlbumState.label != "": VerticalLayout {
spacing: 8px;
SidebarHeading { text: @tr("LABEL"); }
SidebarCard {
icon: @image-url("../assets/icons/disc.svg");
name: AlbumState.label;
avatar-bg: @linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
}
}
if AlbumState.awards.length > 0: VerticalLayout {
spacing: 8px;
SidebarHeading { text: @tr("AWARDS"); }
for award in AlbumState.awards: SidebarCard {
icon: @image-url("../assets/icons/award.svg");
name: award;
avatar-bg: @linear-gradient(135deg, #b45309 0%, #eab308 100%);
}
}
}
}
}