The small uppercased "ARTIST" eyebrow above the artist name in the header is redundant — the context (artist page) and the prominent name below already say it. Removed the Text node and the 6px spacer that followed. cc
1167 lines
50 KiB
Plaintext
1167 lines
50 KiB
Plaintext
// Artist detail page.
|
|
//
|
|
// An artwork-tinted gradient header (circular portrait, name, biography,
|
|
// action buttons), a Popular Tracks list, and the discography split into
|
|
// titled release sections (Albums, EPs & Singles, Live, ...). 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 { Radius } from "../foundation/radius.slint";
|
|
import {
|
|
ArtistState,
|
|
AlbumTrackItem,
|
|
NavState,
|
|
ContentView,
|
|
NetworkSidebarState,
|
|
NetworkSidebarActions,
|
|
} from "../state.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { ContextMenu } from "../primitives/ContextMenu.slint";
|
|
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
|
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
|
|
import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
|
|
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
|
import { Carousel } from "../discover/Carousel.slint";
|
|
import { NavButtons } from "../shell/NavButtons.slint";
|
|
|
|
component CircleAction inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <bool> primary: false;
|
|
// Stays "pressed" — same surface as a hover, icon tinted accent.
|
|
// Used by the Network button while the sidebar is open.
|
|
in property <bool> active: false;
|
|
in property <length> diameter: 40px;
|
|
callback clicked();
|
|
|
|
width: root.diameter;
|
|
height: root.diameter;
|
|
border-radius: root.diameter / 2;
|
|
// Ghost circle — translucent fill with a faint ring, never a solid
|
|
// accent fill (mirrors the Tauri transport controls).
|
|
background: (ta.has-hover || root.active) ? Theme.surface-hover : transparent;
|
|
border-width: 1px;
|
|
border-color: (ta.has-hover || root.active) ? Theme.text-disabled : Theme.border-subtle;
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: root.primary ? 19px : 17px;
|
|
height: root.primary ? 19px : 17px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: root.active
|
|
? Theme.accent
|
|
: (ta.has-hover ? Theme.text-primary : Theme.text-secondary);
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
component TrackRow inherits Rectangle {
|
|
in property <AlbumTrackItem> track;
|
|
in property <int> index;
|
|
// When true, the row shows a leading checkbox column and hides the
|
|
// per-row hover affordances — driven by the Popular Tracks "Select"
|
|
// header toggle, which mirrors Tauri's multiSelectMode flow.
|
|
in property <bool> multi-select-mode: false;
|
|
callback media-action(string, string, string);
|
|
|
|
// Any-hover detector — Slint does not propagate has-hover to ancestor
|
|
// TouchAreas, so OR the row TouchArea with the nested ones to drive
|
|
// the play overlay on the cell from anywhere over the row.
|
|
property <bool> row-hovered:
|
|
ta.has-hover || play-cell.hovered || more-ta.has-hover;
|
|
|
|
height: 52px;
|
|
border-radius: 8px;
|
|
// The even-row tint is clearly fainter than the hover tint so the two
|
|
// never read as the same state. Use row-hovered so hovering the cell
|
|
// or the more button keeps the row tint.
|
|
background: root.row-hovered
|
|
? #ffffff16
|
|
: (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent);
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: default;
|
|
}
|
|
|
|
HorizontalLayout {
|
|
padding-left: 12px;
|
|
padding-right: 12px;
|
|
spacing: 16px;
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
play-cell := TrackPlayCell {
|
|
track-id: root.track.id;
|
|
number: root.track.number;
|
|
show-artwork: false;
|
|
number-width: 28px;
|
|
row-hovered: root.row-hovered;
|
|
play => {
|
|
root.media-action("track", root.track.id, "play");
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
// Select checkbox — only visible while the section is in
|
|
// multi-select mode (toggled by the section header's Select
|
|
// button). Mirrors Tauri's #if multiSelectMode gate.
|
|
if root.multi-select-mode: VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 11px;
|
|
background: root.track.selected ? Theme.accent : transparent;
|
|
border-width: root.track.selected ? 0px : 1.5px;
|
|
border-color: select-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
animate background { duration: 120ms; }
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/check.svg");
|
|
width: 14px;
|
|
height: 14px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: #ffffff;
|
|
visible: root.track.selected;
|
|
}
|
|
select-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.media-action("track", root.track.id, "toggle-select");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// More / context-menu button — always visible on Popular Tracks.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: Radius.sm;
|
|
background: more-ta.has-hover ? Theme.surface-elevated : transparent;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/ellipsis.svg");
|
|
width: 16px;
|
|
height: 16px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: more-ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
|
}
|
|
more-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
track-menu.show();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
track-menu := TrackContextMenu {
|
|
x: parent.width - self.width - 12px;
|
|
y: 44px;
|
|
track-id: root.track.id;
|
|
media-action(k, i, a) => {
|
|
root.media-action(k, i, a);
|
|
}
|
|
}
|
|
}
|
|
|
|
component SectionTitle inherits Text {
|
|
color: Theme.text-primary;
|
|
font-size: Typography.heading;
|
|
font-weight: Typography.semibold;
|
|
}
|
|
|
|
// One row in the network sidebar — small leading icon + name. Used by
|
|
// Labels (Disc icon), Similar Artists / Members / Discovery (User
|
|
// icon), and Member-Of groups (Music icon). Mirrors Tauri's
|
|
// `.sidebar-artist-link` rendering.
|
|
component SidebarLink inherits Rectangle {
|
|
in property <string> label;
|
|
in property <image> icon;
|
|
in property <string> tooltip;
|
|
callback clicked();
|
|
|
|
height: 28px;
|
|
border-radius: 4px;
|
|
background: link-ta.has-hover ? Theme.surface-elevated : transparent;
|
|
|
|
HorizontalLayout {
|
|
padding-left: 8px;
|
|
padding-right: 8px;
|
|
spacing: 8px;
|
|
alignment: stretch;
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 0;
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 12px;
|
|
height: 12px;
|
|
tint: link-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: root.label;
|
|
color: link-ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
|
font-size: 13px;
|
|
overflow: elide;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
|
|
link-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
export component ArtistPageView inherits Rectangle {
|
|
callback open-album(string);
|
|
callback media-action(string, string, string);
|
|
|
|
// Whether the full-biography modal is open. Mirrors the album
|
|
// description Read-more pattern in AlbumPageView.
|
|
property <bool> show-bio: false;
|
|
// Width of the main content column — shrinks when the network
|
|
// sidebar opens, so the bio text wraps inside the visible band
|
|
// instead of being clipped by the sidebar.
|
|
property <length> content-width:
|
|
NetworkSidebarState.open ? root.width - 300px : root.width;
|
|
// Popular Tracks shows the first 5; Load more expands to all.
|
|
property <bool> top-tracks-expanded: false;
|
|
property <int> top-tracks-preview: 5;
|
|
// Popular Tracks multi-select mode — toggled by the section header's
|
|
// Select button. When true, each row reveals a leading checkbox column
|
|
// (TrackRow's `multi-select-mode` prop). Mirrors Tauri's flow.
|
|
property <bool> multi-select-mode: false;
|
|
|
|
background: Theme.surface-main;
|
|
|
|
// Main content + optional network sidebar — same flow as Tauri's
|
|
// .artist-body. The Flickable shrinks when the sidebar opens; the
|
|
// sidebar itself is a fixed 300px column on the right.
|
|
HorizontalLayout {
|
|
spacing: 0px;
|
|
|
|
Flickable {
|
|
horizontal-stretch: 1;
|
|
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,
|
|
ArtistState.header-color 0%,
|
|
ArtistState.header-color 16%,
|
|
transparent 100%);
|
|
}
|
|
|
|
page := VerticalLayout {
|
|
width: parent.width;
|
|
// padding matches Discover so the [<] [>] pair sits at the same
|
|
// top/left margin across every view.
|
|
padding-left: 32px;
|
|
padding-right: 32px;
|
|
padding-top: 11px;
|
|
padding-bottom: 100px;
|
|
spacing: 0px;
|
|
alignment: start;
|
|
|
|
// --- History navigation ------------------------------------------
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
NavButtons { }
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Biography — truncated body fills the horizontal
|
|
// band to the right of the artwork (subtracts page
|
|
// padding 32+32 + artwork 200 + header gap 32 =
|
|
// 296). Read more opens the full-bio modal, same
|
|
// pattern as the album description in
|
|
// AlbumPageView.
|
|
if ArtistState.bio != "": Rectangle { height: 12px; }
|
|
if ArtistState.bio != "": Text {
|
|
width: root.content-width - 296px;
|
|
text: ArtistState.bio-short;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.legal;
|
|
wrap: word-wrap;
|
|
}
|
|
if ArtistState.bio-truncated: Rectangle { height: 4px; }
|
|
if ArtistState.bio-truncated: read-more := TouchArea {
|
|
width: read-more-row.preferred-width;
|
|
height: 20px;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.show-bio = true;
|
|
}
|
|
read-more-row := HorizontalLayout {
|
|
Text {
|
|
text: @tr("Read more");
|
|
color: read-more.has-hover
|
|
? Theme.accent-hover
|
|
: Theme.accent;
|
|
font-size: Typography.legal;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 18px; }
|
|
|
|
// Action buttons — Heart, Build collection, Radio
|
|
// (with QBZ / Qobuz dropdown), Network. Mirrors
|
|
// Tauri's artist-actions: NO Play here (top tracks
|
|
// has its own play). Hide (blacklist) is still
|
|
// deferred until the blacklist module migrates out
|
|
// of src-tauri.
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
spacing: 12px;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/heart.svg");
|
|
clicked => {
|
|
root.media-action("artist", "", "favorite");
|
|
}
|
|
}
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/library-big.svg");
|
|
clicked => {
|
|
root.media-action("artist", "", "build-collection");
|
|
}
|
|
}
|
|
// Radio with dropdown — QBZ radio (local engine)
|
|
// vs Qobuz radio (qobuz-curated). Both call
|
|
// distinct media-actions; main.rs wires them.
|
|
Rectangle {
|
|
width: 40px;
|
|
height: 40px;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/radio.svg");
|
|
clicked => {
|
|
radio-popup.show();
|
|
}
|
|
}
|
|
radio-popup := PopupWindow {
|
|
x: 0px;
|
|
y: 50px;
|
|
width: 180px;
|
|
close-policy: close-on-click;
|
|
ContextMenu {
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/radio.svg");
|
|
label: "QBZ Radio";
|
|
clicked => {
|
|
root.media-action("artist", "", "radio-qbz");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/radio.svg");
|
|
label: "Qobuz Radio";
|
|
clicked => {
|
|
root.media-action("artist", "", "radio-qobuz");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Network — toggles the artist network sidebar.
|
|
// Stays active (accent-tinted) while the sidebar
|
|
// is open, matching Tauri's `.network-btn.active`.
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/element-connect.svg");
|
|
active: NetworkSidebarState.open;
|
|
clicked => {
|
|
NetworkSidebarState.open = !NetworkSidebarState.open;
|
|
NetworkSidebarActions.toggle();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 36px; }
|
|
|
|
if ArtistState.loading && ArtistState.top-tracks.length == 0: Rectangle {
|
|
height: 280px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
spacing: 18px;
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
LoadingSpinner { size: 36px; }
|
|
}
|
|
Text {
|
|
text: @tr("Loading artist…");
|
|
color: Theme.text-muted;
|
|
font-size: 13px;
|
|
horizontal-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Popular tracks ----------------------------------------------
|
|
// Section header: title on the left, action cluster on the right
|
|
// (Play all / Select-mode toggle / More menu). Mirrors Tauri's
|
|
// popular-tracks section-header.
|
|
if ArtistState.top-tracks.length > 0: HorizontalLayout {
|
|
alignment: space-between;
|
|
spacing: 12px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
SectionTitle {
|
|
text: @tr("Popular Tracks");
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
alignment: end;
|
|
spacing: 12px;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/play-fill.svg");
|
|
primary: true;
|
|
diameter: 40px;
|
|
clicked => {
|
|
root.media-action("artist", "", "play-top");
|
|
}
|
|
}
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/square-check-big.svg");
|
|
clicked => {
|
|
root.multi-select-mode = !root.multi-select-mode;
|
|
}
|
|
}
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/ellipsis.svg");
|
|
clicked => {
|
|
root.media-action("artist", "", "top-tracks-menu");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ArtistState.top-tracks.length > 0: Rectangle { height: 10px; }
|
|
for track[index] in ArtistState.top-tracks: TrackRow {
|
|
visible: root.top-tracks-expanded || index < root.top-tracks-preview;
|
|
height: self.visible ? 52px : 0px;
|
|
track: track;
|
|
index: index;
|
|
multi-select-mode: root.multi-select-mode;
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
// Load more — visible when there are more than the preview cap.
|
|
if !root.top-tracks-expanded
|
|
&& ArtistState.top-tracks.length > root.top-tracks-preview: HorizontalLayout {
|
|
alignment: start;
|
|
Rectangle {
|
|
width: load-more-text.preferred-width + 32px;
|
|
height: 32px;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
background: load-more-ta.has-hover
|
|
? Theme.surface-elevated
|
|
: Theme.surface-card;
|
|
load-more-text := Text {
|
|
text: @tr("Load more");
|
|
color: Theme.text-secondary;
|
|
font-size: 13px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
}
|
|
load-more-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.top-tracks-expanded = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Release sections (Albums, EPs & Singles, Live, ...) ---------
|
|
for section in ArtistState.release-sections: VerticalLayout {
|
|
spacing: 0px;
|
|
Rectangle { height: 32px; }
|
|
Carousel {
|
|
section: section;
|
|
album-clicked(id) => {
|
|
root.open-album(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Network sidebar — a 300px column on the right, vertically
|
|
// inset so it does not occupy the full body height (matches
|
|
// Tauri's `height: calc(100vh - 180px)`, which leaves room for
|
|
// the jump-nav at the top and the now-playing bar at the
|
|
// bottom). Sections (Origin / Labels / Similar / Relationships /
|
|
// Discovery) are populated in the following phases; the shell
|
|
// only renders the header and an empty body.
|
|
if NetworkSidebarState.open: VerticalLayout {
|
|
width: 300px;
|
|
spacing: 0px;
|
|
|
|
// Top inset clears the entire artist header — NavButtons
|
|
// (11 padding + ~32 row + 22 spacer = 65) + the artist
|
|
// header itself (~214px: ARTIST label + name + truncated
|
|
// bio + Read more + action buttons) + the 36px spacer
|
|
// before Popular Tracks. Total ~315px so the sidebar's
|
|
// top edge sits inline with where Popular Tracks starts,
|
|
// matching Tauri's `.network-sidebar-column` natural flow
|
|
// position (inside the body, alongside the sections —
|
|
// never overlapping the artist header).
|
|
Rectangle { height: 320px; }
|
|
|
|
sidebar-panel := Rectangle {
|
|
vertical-stretch: 1;
|
|
background: Theme.surface-card;
|
|
|
|
// Left border line — 1px Theme.border-subtle, matching the
|
|
// Tauri `.network-sidebar-column { border-left: 1px solid
|
|
// var(--bg-tertiary); }`.
|
|
Rectangle {
|
|
x: 0px;
|
|
y: 0px;
|
|
width: 1px;
|
|
height: parent.height;
|
|
background: Theme.border-subtle;
|
|
}
|
|
|
|
VerticalLayout {
|
|
spacing: 0px;
|
|
|
|
// Header — uppercased title on the left, close button
|
|
// on the right.
|
|
Rectangle {
|
|
height: 44px;
|
|
background: Theme.surface-main;
|
|
|
|
HorizontalLayout {
|
|
padding-left: 12px;
|
|
padding-right: 8px;
|
|
spacing: 8px;
|
|
|
|
Text {
|
|
text: @tr("ARTIST NETWORK");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 1.2px;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 6px;
|
|
background: close-ta.has-hover
|
|
? Theme.surface-elevated
|
|
: transparent;
|
|
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/panel-right-close.svg");
|
|
width: 18px;
|
|
height: 18px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: close-ta.has-hover
|
|
? Theme.text-primary
|
|
: Theme.text-muted;
|
|
}
|
|
|
|
close-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
NetworkSidebarState.open = false;
|
|
NetworkSidebarActions.toggle();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Header bottom separator.
|
|
Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
|
|
// Body — sections: Origin (MB), Labels, Similar, MB
|
|
// Relationships, Discovery. Wrapped in a Flickable so a
|
|
// long sidebar (Labels + Relationships + Discovery) can
|
|
// scroll. Phase 5 adds Origin only; phases 6-8 fill the
|
|
// rest.
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: sidebar-body.preferred-height;
|
|
|
|
sidebar-body := VerticalLayout {
|
|
spacing: 0px;
|
|
padding-top: 4px;
|
|
padding-bottom: 12px;
|
|
alignment: start;
|
|
|
|
// --- Origin (MB metadata) ----------------------
|
|
// Mirrors Tauri's .sidebar-section.sidebar-origin
|
|
// and the gate `mbAvailable && (loading || data)`.
|
|
if NetworkSidebarState.mb-available
|
|
&& (NetworkSidebarState.origin-loading
|
|
|| NetworkSidebarState.origin.begin-date != ""
|
|
|| NetworkSidebarState.origin.end-date != ""
|
|
|| NetworkSidebarState.origin.location-display != ""):
|
|
VerticalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
padding-top: 12px;
|
|
padding-bottom: 12px;
|
|
spacing: 6px;
|
|
|
|
if NetworkSidebarState.origin-loading: Text {
|
|
text: @tr("Loading origin…");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
|
|
// Born / Founded
|
|
if !NetworkSidebarState.origin-loading
|
|
&& NetworkSidebarState.origin.begin-date != "":
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
alignment: stretch;
|
|
Text {
|
|
text: NetworkSidebarState.origin.is-person
|
|
? @tr("BORN")
|
|
: @tr("FOUNDED");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 0.5px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 0;
|
|
}
|
|
Text {
|
|
text: NetworkSidebarState.origin.begin-date;
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
horizontal-alignment: right;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
|
|
// Born in / Founded in — optionally clickable
|
|
if !NetworkSidebarState.origin-loading
|
|
&& NetworkSidebarState.origin.location-display != "":
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
alignment: stretch;
|
|
Text {
|
|
text: NetworkSidebarState.origin.is-person
|
|
? @tr("BORN IN")
|
|
: @tr("FOUNDED IN");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 0.5px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 0;
|
|
}
|
|
if NetworkSidebarState.origin.location-clickable: Rectangle {
|
|
horizontal-stretch: 1;
|
|
location-text := Text {
|
|
text: NetworkSidebarState.origin.location-display;
|
|
color: location-ta.has-hover
|
|
? Theme.accent
|
|
: Theme.text-primary;
|
|
font-size: 13px;
|
|
horizontal-alignment: right;
|
|
vertical-alignment: center;
|
|
width: 100%;
|
|
}
|
|
location-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
NetworkSidebarActions.location-clicked(
|
|
NetworkSidebarState.mb-mbid);
|
|
}
|
|
}
|
|
}
|
|
if !NetworkSidebarState.origin.location-clickable: Text {
|
|
text: NetworkSidebarState.origin.location-display;
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
horizontal-alignment: right;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
|
|
// Died / Disbanded
|
|
if !NetworkSidebarState.origin-loading
|
|
&& NetworkSidebarState.origin.end-date != "":
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
alignment: stretch;
|
|
Text {
|
|
text: NetworkSidebarState.origin.is-person
|
|
? @tr("DIED")
|
|
: @tr("DISBANDED");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 0.5px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 0;
|
|
}
|
|
Text {
|
|
text: NetworkSidebarState.origin.end-date;
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
horizontal-alignment: right;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Labels (Qobuz, deduped from own albums) ---
|
|
VerticalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
padding-top: 12px;
|
|
padding-bottom: 6px;
|
|
spacing: 4px;
|
|
|
|
Text {
|
|
text: @tr("LABELS");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
if ArtistState.labels.length == 0: Text {
|
|
text: @tr("No label info");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
for label in ArtistState.labels: SidebarLink {
|
|
label: label.name;
|
|
icon: @image-url("../assets/icons/disc.svg");
|
|
tooltip: label.name;
|
|
clicked => {
|
|
NetworkSidebarActions.label-clicked(
|
|
label.id, label.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Similar Artists (Qobuz) ---
|
|
VerticalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
padding-top: 12px;
|
|
padding-bottom: 6px;
|
|
spacing: 4px;
|
|
|
|
Text {
|
|
text: @tr("SIMILAR ARTISTS");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
if ArtistState.similar-artists.length == 0
|
|
&& !ArtistState.loading: Text {
|
|
text: @tr("No similar artists found");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
if ArtistState.similar-artists.length == 0
|
|
&& ArtistState.loading: Text {
|
|
text: @tr("Loading…");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
for similar in ArtistState.similar-artists: SidebarLink {
|
|
label: similar.name;
|
|
icon: @image-url("../assets/icons/user.svg");
|
|
tooltip: similar.name;
|
|
clicked => {
|
|
NetworkSidebarActions.artist-clicked(similar.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Relationships (MB) --------------------------
|
|
// Section is gated by mb-available so a
|
|
// disabled MB or unmatched artist hides it.
|
|
if NetworkSidebarState.mb-available
|
|
&& (NetworkSidebarState.relationships-loading
|
|
|| NetworkSidebarState.relationships.has-data):
|
|
VerticalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
padding-top: 12px;
|
|
padding-bottom: 6px;
|
|
spacing: 6px;
|
|
|
|
Text {
|
|
text: @tr("RELATIONSHIPS");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
if NetworkSidebarState.relationships-loading: Text {
|
|
text: @tr("Loading…");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
|
|
if !NetworkSidebarState.relationships-loading
|
|
&& NetworkSidebarState.relationships.members.length > 0:
|
|
VerticalLayout {
|
|
spacing: 2px;
|
|
padding-top: 2px;
|
|
Text {
|
|
text: @tr("Members & Former");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
}
|
|
for member in NetworkSidebarState.relationships.members:
|
|
SidebarLink {
|
|
label: member.name;
|
|
icon: @image-url("../assets/icons/user.svg");
|
|
tooltip: member.tooltip;
|
|
clicked => {
|
|
NetworkSidebarActions.musician-clicked(
|
|
member.name, member.role);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !NetworkSidebarState.relationships-loading
|
|
&& NetworkSidebarState.relationships.groups.length > 0:
|
|
VerticalLayout {
|
|
spacing: 2px;
|
|
padding-top: 2px;
|
|
Text {
|
|
text: @tr("Member Of");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
}
|
|
for group in NetworkSidebarState.relationships.groups:
|
|
SidebarLink {
|
|
label: group.name;
|
|
icon: @image-url("../assets/icons/music.svg");
|
|
tooltip: group.tooltip;
|
|
clicked => {
|
|
NetworkSidebarActions.musician-clicked(
|
|
group.name, group.role);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !NetworkSidebarState.relationships-loading
|
|
&& NetworkSidebarState.relationships.collaborators.length > 0:
|
|
VerticalLayout {
|
|
spacing: 2px;
|
|
padding-top: 2px;
|
|
Text {
|
|
text: @tr("Collaborators");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
}
|
|
for collab in NetworkSidebarState.relationships.collaborators:
|
|
SidebarLink {
|
|
label: collab.name;
|
|
icon: @image-url("../assets/icons/user.svg");
|
|
tooltip: collab.tooltip;
|
|
clicked => {
|
|
NetworkSidebarActions.musician-clicked(
|
|
collab.name, collab.role);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Discovery: "You may also like" --------------
|
|
if NetworkSidebarState.mb-available
|
|
&& (NetworkSidebarState.discovery-loading
|
|
|| NetworkSidebarState.discovery-artists.length > 0):
|
|
VerticalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
padding-top: 12px;
|
|
padding-bottom: 12px;
|
|
spacing: 4px;
|
|
|
|
Text {
|
|
text: @tr("YOU MAY ALSO LIKE");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
if NetworkSidebarState.discovery-loading
|
|
&& NetworkSidebarState.discovery-artists.length == 0: Text {
|
|
text: @tr("Loading…");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
|
|
// Each row: SidebarLink filling the row plus a
|
|
// small thumbs-down button anchored to the
|
|
// right. The link click navigates to the
|
|
// Qobuz artist; the thumbs-down emits the
|
|
// dismiss callback so the row drops out and
|
|
// never reappears for this tag.
|
|
for disc in NetworkSidebarState.discovery-artists:
|
|
HorizontalLayout {
|
|
spacing: 2px;
|
|
|
|
SidebarLink {
|
|
horizontal-stretch: 1;
|
|
label: disc.name;
|
|
icon: @image-url("../assets/icons/user.svg");
|
|
tooltip: disc.name;
|
|
clicked => {
|
|
if (disc.qobuz-id != "") {
|
|
NetworkSidebarActions.artist-clicked(
|
|
disc.qobuz-id);
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 0;
|
|
Rectangle {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 4px;
|
|
background: dismiss-ta.has-hover
|
|
? Theme.surface-elevated
|
|
: transparent;
|
|
QbzIcon {
|
|
source: @image-url(
|
|
"../assets/icons/thumbs-down.svg");
|
|
width: 12px;
|
|
height: 12px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: dismiss-ta.has-hover
|
|
? Theme.text-primary
|
|
: Theme.text-muted;
|
|
}
|
|
dismiss-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
NetworkSidebarActions
|
|
.discovery-dismissed(
|
|
disc.mbid, disc.name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 32px gap above the now-playing bar — mirrors Tauri's
|
|
// height = calc(100vh - titlebar - jump-nav - nowplaying)
|
|
// which leaves a small margin above the bar.
|
|
Rectangle { height: 32px; }
|
|
}
|
|
}
|
|
|
|
// --- Full-biography modal ----------------------------------------------
|
|
// Same pattern as AlbumPageView's description modal: scrim that
|
|
// dismisses on click, centered panel with a Flickable so a long
|
|
// bio actually scrolls. Bio source (TiVo etc) appears as a small
|
|
// attribution line below the body.
|
|
if root.show-bio: Rectangle {
|
|
background: #000000bf;
|
|
|
|
TouchArea {
|
|
mouse-cursor: default;
|
|
clicked => {
|
|
root.show-bio = false;
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: Math.min(root.width - 80px, 560px);
|
|
height: Math.min(root.height - 120px, 460px);
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
border-radius: Radius.md;
|
|
background: Theme.surface-card;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
drop-shadow-blur: 32px;
|
|
drop-shadow-color: #00000080;
|
|
|
|
TouchArea { }
|
|
|
|
VerticalLayout {
|
|
padding: 24px;
|
|
spacing: 14px;
|
|
|
|
HorizontalLayout {
|
|
Text {
|
|
text: @tr("About this artist");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.heading;
|
|
font-weight: Typography.semibold;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
close-bio := TouchArea {
|
|
width: 28px;
|
|
height: 28px;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.show-bio = false;
|
|
}
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/x.svg");
|
|
width: 18px;
|
|
height: 18px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: close-bio.has-hover
|
|
? Theme.text-primary
|
|
: Theme.text-muted;
|
|
}
|
|
}
|
|
}
|
|
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: bio-content.preferred-height;
|
|
bio-content := VerticalLayout {
|
|
width: parent.width;
|
|
spacing: 12px;
|
|
Text {
|
|
text: ArtistState.bio;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
wrap: word-wrap;
|
|
}
|
|
if ArtistState.bio-source != "": Text {
|
|
text: @tr("Source") + ": " + ArtistState.bio-source;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|