Clicking an artist (from the album page's artist line) opens the artist detail view: a circular portrait, name, biography, action buttons, a Popular Tracks list, and a Discography carousel. - artist.rs: fetches the artist page via QbzCore::get_artist_page and maps name, biography (HTML-stripped), portrait URL, top tracks and releases; the portrait loads through the shared image cache. - ArtistPageView.slint reuses AlbumCard for the discography (through the Carousel) and AlbumTrackItem for the track list. - NavState becomes a three-way ContentView (home / album / artist) and AppShell routes the content region accordingly. The album page's artist line and the artist page's release cards are navigable. cc
329 lines
11 KiB
Plaintext
329 lines
11 KiB
Plaintext
// Album detail page.
|
|
//
|
|
// Mirrors the Svelte AlbumDetailView: back button, an album header
|
|
// (artwork + title / artist / info line / action buttons), a divider,
|
|
// and the track list with a column header. Data comes from the
|
|
// AlbumState global; the action buttons are visual for now.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { Spacing } from "../foundation/spacing.slint";
|
|
import { AlbumState, AlbumTrackItem, NavState, ContentView } from "../state.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { ScrollView } from "std-widgets.slint";
|
|
|
|
component CircleAction inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <bool> primary: false;
|
|
in property <length> diameter: 40px;
|
|
callback clicked();
|
|
|
|
width: root.diameter;
|
|
height: root.diameter;
|
|
border-radius: root.diameter / 2;
|
|
background: root.primary
|
|
? (ta.has-hover ? Theme.accent-hover : Theme.accent)
|
|
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 17px;
|
|
height: 17px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: root.primary ? Theme.accent-text : Theme.text-secondary;
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
component TrackRow inherits Rectangle {
|
|
in property <AlbumTrackItem> track;
|
|
in property <int> index;
|
|
|
|
height: 56px;
|
|
border-radius: 8px;
|
|
background: ta.has-hover
|
|
? Theme.surface-hover
|
|
: (Math.mod(root.index, 2) == 1 ? Theme.surface-card : transparent);
|
|
|
|
HorizontalLayout {
|
|
padding-left: 12px;
|
|
padding-right: 12px;
|
|
spacing: 16px;
|
|
|
|
Text {
|
|
text: root.track.number;
|
|
width: 32px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 1;
|
|
spacing: 2px;
|
|
|
|
HorizontalLayout {
|
|
spacing: 6px;
|
|
Text {
|
|
text: root.track.title;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
if root.track.explicit: Rectangle {
|
|
width: 16px;
|
|
height: 16px;
|
|
y: (parent.height - self.height) / 2;
|
|
border-radius: 3px;
|
|
background: Theme.surface-elevated;
|
|
Text {
|
|
text: "E";
|
|
color: Theme.text-muted;
|
|
font-size: 9px;
|
|
font-weight: Typography.semibold;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
text: root.track.artist;
|
|
visible: root.track.artist != "";
|
|
color: Theme.text-muted;
|
|
font-size: Typography.link;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: root.track.duration;
|
|
width: 80px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
text: root.track.quality-tier == "hires"
|
|
? "Hi-Res"
|
|
: (root.track.quality-tier == "cd" ? "CD" : "");
|
|
width: 80px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
}
|
|
}
|
|
|
|
export component AlbumPageView inherits Rectangle {
|
|
callback open-artist(string);
|
|
|
|
background: Theme.surface-main;
|
|
|
|
ScrollView {
|
|
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
|
|
VerticalLayout {
|
|
padding-left: 24px;
|
|
padding-right: 24px;
|
|
padding-top: 14px;
|
|
padding-bottom: 100px;
|
|
spacing: 0px;
|
|
alignment: start;
|
|
|
|
// --- Back button -------------------------------------------------
|
|
back := TouchArea {
|
|
width: back-row.preferred-width;
|
|
height: 28px;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
NavState.view = ContentView.home;
|
|
}
|
|
back-row := HorizontalLayout {
|
|
spacing: 6px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/arrow-left.svg");
|
|
width: 16px;
|
|
height: 16px;
|
|
tint: back.has-hover ? Theme.text-secondary : Theme.text-muted;
|
|
}
|
|
}
|
|
Text {
|
|
text: @tr("Back");
|
|
color: back.has-hover ? Theme.text-secondary : Theme.text-muted;
|
|
font-size: Typography.body;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 22px; }
|
|
|
|
// --- Album header ------------------------------------------------
|
|
HorizontalLayout {
|
|
spacing: 32px;
|
|
alignment: start;
|
|
|
|
Rectangle {
|
|
width: 224px;
|
|
height: 224px;
|
|
border-radius: 12px;
|
|
background: Theme.surface-elevated;
|
|
drop-shadow-blur: 32px;
|
|
drop-shadow-color: #00000080;
|
|
drop-shadow-offset-y: 8px;
|
|
clip: true;
|
|
Image {
|
|
width: 100%;
|
|
height: 100%;
|
|
source: AlbumState.artwork;
|
|
image-fit: cover;
|
|
}
|
|
}
|
|
|
|
VerticalLayout {
|
|
alignment: start;
|
|
padding-top: 4px;
|
|
spacing: 0px;
|
|
horizontal-stretch: 1;
|
|
|
|
Text {
|
|
text: AlbumState.title;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.title;
|
|
font-weight: Typography.bold;
|
|
overflow: elide;
|
|
}
|
|
Rectangle { height: 4px; }
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
artist-area := TouchArea {
|
|
width: artist-text.preferred-width;
|
|
height: artist-text.preferred-height;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.open-artist(AlbumState.artist-id);
|
|
}
|
|
artist-text := Text {
|
|
text: AlbumState.artist;
|
|
color: artist-area.has-hover
|
|
? Theme.accent-hover
|
|
: Theme.accent;
|
|
font-size: Typography.heading;
|
|
font-weight: Typography.medium;
|
|
}
|
|
}
|
|
}
|
|
Rectangle { height: 10px; }
|
|
Text {
|
|
text: AlbumState.info-line;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
overflow: elide;
|
|
}
|
|
Rectangle { height: 20px; }
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
spacing: 12px;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/play.svg");
|
|
primary: true;
|
|
diameter: 44px;
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction { icon: @image-url("../assets/icons/shuffle.svg"); }
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction { icon: @image-url("../assets/icons/heart.svg"); }
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction { icon: @image-url("../assets/icons/ellipsis.svg"); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 20px; }
|
|
Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
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.
|
|
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;
|
|
}
|
|
}
|
|
|
|
for track[index] in AlbumState.tracks: TrackRow {
|
|
track: track;
|
|
index: index;
|
|
}
|
|
}
|
|
}
|
|
}
|