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
251 lines
8.5 KiB
Plaintext
251 lines
8.5 KiB
Plaintext
// Artist detail page.
|
|
//
|
|
// Mirrors the Svelte ArtistDetailView: back button, an artist header
|
|
// (circular portrait, name, biography, action buttons), a Popular
|
|
// Tracks list, and a Discography carousel. Data comes from the
|
|
// ArtistState global; action buttons are visual for now.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { ArtistState, AlbumTrackItem, DiscoverSection, NavState, ContentView } from "../state.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { Carousel } from "../discover/Carousel.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: 52px;
|
|
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: 28px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
vertical-alignment: center;
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 1;
|
|
Text {
|
|
text: root.track.title;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
Text {
|
|
text: root.track.duration;
|
|
width: 70px;
|
|
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: 70px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
}
|
|
}
|
|
|
|
export component ArtistPageView inherits Rectangle {
|
|
callback open-album(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; }
|
|
|
|
// --- Artist header -----------------------------------------------
|
|
HorizontalLayout {
|
|
spacing: 32px;
|
|
alignment: start;
|
|
|
|
Rectangle {
|
|
width: 200px;
|
|
height: 200px;
|
|
border-radius: 100px;
|
|
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: ArtistState.artwork;
|
|
image-fit: cover;
|
|
}
|
|
}
|
|
|
|
VerticalLayout {
|
|
alignment: start;
|
|
padding-top: 8px;
|
|
spacing: 0px;
|
|
horizontal-stretch: 1;
|
|
|
|
Text {
|
|
text: ArtistState.name;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.title;
|
|
font-weight: Typography.bold;
|
|
overflow: elide;
|
|
}
|
|
Rectangle { height: 12px; }
|
|
Text {
|
|
text: ArtistState.bio;
|
|
visible: ArtistState.bio != "";
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
wrap: word-wrap;
|
|
}
|
|
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/cast.svg"); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 32px; }
|
|
|
|
if ArtistState.loading && ArtistState.top-tracks.length == 0: Text {
|
|
text: @tr("Loading artist...");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
}
|
|
|
|
// --- Popular tracks ----------------------------------------------
|
|
if ArtistState.top-tracks.length > 0: Text {
|
|
text: @tr("Popular Tracks");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.heading;
|
|
font-weight: Typography.semibold;
|
|
}
|
|
if ArtistState.top-tracks.length > 0: Rectangle { height: 10px; }
|
|
for track[index] in ArtistState.top-tracks: TrackRow {
|
|
track: track;
|
|
index: index;
|
|
}
|
|
|
|
// --- Discography -------------------------------------------------
|
|
if ArtistState.releases.length > 0: Rectangle { height: 28px; }
|
|
if ArtistState.releases.length > 0: Carousel {
|
|
section: {
|
|
title: @tr("Discography"),
|
|
albums: ArtistState.releases,
|
|
};
|
|
album-clicked(id) => {
|
|
root.open-album(id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|