Extracts the two Qobuz-driven sidebar sections from /artist/page data
the artist controller already receives and wires them into the
NetworkSidebar.
Controller (`crates/qbz-slint/src/artist.rs`):
- Labels collected during the existing release-bucketing loop,
matching Tauri's extractLabelsFromPageReleases: only the "album"
release group, only releases attributed to this artist, dedupe by
label id, sorted by name. Each label exposes {id, name}.
- Similar artists pulled from page.similar_artists.items as
{id, name}. No artwork on the sidebar rows (Tauri only renders a
User icon + name there).
- ArtistData / ArtistState gained `labels` and `similar_artists`;
reset_artist clears them on artist change.
Slint (`ui/state.slint`, `ui/artist/ArtistPageView.slint`):
- New LabelEntry and SimilarEntry structs alongside ArtistState.
- New SidebarLink local component — small leading icon (12px) +
elided name, surface-elevated hover, mirrors Tauri's
`.sidebar-artist-link`. Used by Labels (Disc), Similar (User),
and the upcoming Relationships rows.
- Labels section: title + items or "No label info" placeholder.
- Similar Artists section: title + items, with "Loading…" while
the artist page itself is loading and "No similar artists found"
once loaded with an empty list.
main.rs:
- on_artist_clicked now navigates: records nav history and calls
navigate_artist, same flow as the top-level on_open_artist. The
target view exists in Slint already (unlike LabelReleases /
ArtistsByLocation / MusicianPage, which stay logged-only).
cc
899 lines
37 KiB
Plaintext
899 lines
37 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);
|
|
|
|
// Read-more toggle for the biography.
|
|
property <bool> bio-expanded: false;
|
|
// 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: @tr("ARTIST");
|
|
color: Theme.text-muted;
|
|
font-size: 10px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 1.5px;
|
|
}
|
|
Rectangle { height: 6px; }
|
|
Text {
|
|
text: ArtistState.name;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.title;
|
|
font-weight: Typography.bold;
|
|
overflow: elide;
|
|
}
|
|
|
|
// Biography — single inline block matching Tauri's
|
|
// structure: truncated body + Read more/less toggle +
|
|
// optional "Source: <name>" line. The full text is
|
|
// shown when `bio-expanded` flips.
|
|
if ArtistState.bio != "": Rectangle { height: 12px; }
|
|
if ArtistState.bio != "": Text {
|
|
text: root.bio-expanded ? ArtistState.bio : ArtistState.bio-short;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
wrap: word-wrap;
|
|
}
|
|
if ArtistState.bio-truncated: Rectangle {
|
|
height: 24px;
|
|
width: read-more-text.preferred-width + 16px;
|
|
read-more-text := Text {
|
|
text: root.bio-expanded ? @tr("Read less") : @tr("Read more");
|
|
color: read-more-ta.has-hover
|
|
? Theme.text-primary
|
|
: Theme.accent;
|
|
font-size: 13px;
|
|
font-weight: Typography.medium;
|
|
vertical-alignment: center;
|
|
height: 100%;
|
|
}
|
|
read-more-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.bio-expanded = !root.bio-expanded;
|
|
}
|
|
}
|
|
}
|
|
if ArtistState.bio-source != "": Rectangle { height: 4px; }
|
|
if ArtistState.bio-source != "": Text {
|
|
text: @tr("Source") + ": " + ArtistState.bio-source;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
}
|
|
|
|
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;
|
|
|
|
Rectangle { height: 22px; }
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 22px; }
|
|
}
|
|
}
|
|
}
|