Per the user's 2nd visual review: - Local/Plex album art now renders in the detail rows: resolve_items backfills empty-art rows from the first resolved track (source-aware dispatch -> spawn_local_or_plex_loads with live Plex creds); to_item re-hydrates from cache. - Type/source FILTER homologated: a LABELED filter button (FavGenreButton idiom, accent-fill active) + a checkbox popup; the "Select" toggle is now ICON-ONLY (square-check-big, no text) matching Favorites/LocalLibrary. - New primitives/SelectionCheckbox.slint (the canonical CIRCULAR 13px selection checkbox, accent fill, check / minus for indeterminate). Replaced all 5 square MyQBZ checkboxes (detail list+grid, filter popup, builder per-row + tri-state select-all). KindRadio dots stay round radios (single-select, correct). - Quality column shows a LoadingDots skeleton while resolving (not empty). - Album view header buttons: deleted AlbumPageView's private divergent CircleAction copy -> the shared primitive, so it matches every other view's thickness. CollectionsView kind-filter stays QbzSelect (single-select group-by = the app's group-by idiom). Backend crates untouched. cargo check -p qbz-slint green. cc
715 lines
31 KiB
Plaintext
715 lines
31 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, AlbumActions, ArtworkActions, NavState, ContentView, DragState, NowPlayingState } from "../state.slint";
|
|
import { TrackRow } from "../primitives/TrackRow.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { ContextMenu } from "../primitives/ContextMenu.slint";
|
|
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
|
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
|
|
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
|
|
import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
|
|
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
|
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
|
import { CircleAction } from "../primitives/CircleAction.slint";
|
|
import { NavButtons } from "../shell/NavButtons.slint";
|
|
|
|
// 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 : transparent;
|
|
|
|
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: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
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);
|
|
|
|
// Whether the full-description modal is open.
|
|
property <bool> show-description: false;
|
|
|
|
background: Theme.surface-main;
|
|
|
|
flick := Flickable {
|
|
// Freeze scroll during a track drag so the ghost doesn't drift.
|
|
interactive: !DragState.active;
|
|
viewport-width: self.width;
|
|
viewport-height: page.preferred-height;
|
|
// Scroll-position restore on back/forward (see nav::scope_for).
|
|
property <string> sr-armed: NavState.restore-scope;
|
|
function sr-restore() {
|
|
if (NavState.restore-scope == "album" && self.viewport-height > self.height) {
|
|
self.viewport-y = NavState.scroll-restore;
|
|
NavState.restore-scope = "";
|
|
}
|
|
}
|
|
init => { self.sr-restore(); }
|
|
changed sr-armed => { self.sr-restore(); }
|
|
changed viewport-height => { self.sr-restore(); }
|
|
changed viewport-y => { NavState.report-scroll(self.viewport-y); }
|
|
|
|
// 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%,
|
|
AlbumState.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; }
|
|
|
|
// --- Album header ------------------------------------------------
|
|
HorizontalLayout {
|
|
spacing: 32px;
|
|
|
|
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;
|
|
}
|
|
// Right-click → cover context menu (same pattern
|
|
// as the artist portrait): Add/Change/Remove
|
|
// custom cover, Open in browser, Save as.
|
|
cover-ta := TouchArea {
|
|
mouse-cursor: default;
|
|
pointer-event(e) => {
|
|
if (e.button == PointerEventButton.right
|
|
&& e.kind == PointerEventKind.up) {
|
|
cover-menu.show();
|
|
}
|
|
}
|
|
}
|
|
cover-menu := PopupWindow {
|
|
x: cover-ta.mouse-x;
|
|
y: cover-ta.mouse-y;
|
|
width: 220px;
|
|
close-policy: close-on-click;
|
|
ContextMenu {
|
|
if !AlbumState.has-custom-cover: ContextMenuItem {
|
|
icon: @image-url("../assets/icons/image-plus.svg");
|
|
label: @tr("Add cover");
|
|
clicked => {
|
|
ArtworkActions.add-custom(
|
|
"album", AlbumState.id);
|
|
}
|
|
}
|
|
if AlbumState.has-custom-cover: ContextMenuItem {
|
|
icon: @image-url("../assets/icons/image-plus.svg");
|
|
label: @tr("Change cover");
|
|
clicked => {
|
|
ArtworkActions.add-custom(
|
|
"album", AlbumState.id);
|
|
}
|
|
}
|
|
if AlbumState.has-custom-cover: ContextMenuItem {
|
|
icon: @image-url("../assets/icons/trash-2.svg");
|
|
label: @tr("Remove cover");
|
|
clicked => {
|
|
ArtworkActions.remove-custom(
|
|
"album", AlbumState.id);
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/external-link.svg");
|
|
label: @tr("Open in browser");
|
|
clicked => {
|
|
ArtworkActions.open-in-browser(
|
|
AlbumState.artwork-url);
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/cloud-download.svg");
|
|
label: @tr("Save as…");
|
|
clicked => {
|
|
ArtworkActions.save-as(
|
|
AlbumState.artwork-url,
|
|
AlbumState.title + " - Cover.jpg");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
VerticalLayout {
|
|
alignment: start;
|
|
padding-top: 4px;
|
|
spacing: 0px;
|
|
horizontal-stretch: 1;
|
|
|
|
Text {
|
|
text: AlbumState.title;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.section;
|
|
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;
|
|
// Same color as the surrounding metadata,
|
|
// bold to read as a link.
|
|
color: artist-area.has-hover
|
|
? Theme.text-primary
|
|
: Theme.text-secondary;
|
|
font-size: Typography.heading;
|
|
font-weight: Typography.bold;
|
|
}
|
|
}
|
|
}
|
|
Rectangle { height: 10px; }
|
|
Text {
|
|
text: AlbumState.info-line;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
overflow: elide;
|
|
}
|
|
|
|
// Editorial description — lives with the metadata and
|
|
// fills the width to the right of the artwork; truncated,
|
|
// with a Read more link to the full modal.
|
|
if AlbumState.description != "": Rectangle { height: 12px; }
|
|
if AlbumState.description != "": Text {
|
|
// Explicit width so the text fills the whole band
|
|
// to the right of the artwork instead of wrapping
|
|
// narrow and leaving dead space. Bound to root.width
|
|
// (not the parent layout) to avoid a binding loop:
|
|
// page padding 32+32, artwork 224, header gap 32.
|
|
width: root.width - 320px;
|
|
text: AlbumState.description-short;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.legal;
|
|
wrap: word-wrap;
|
|
}
|
|
if AlbumState.description != AlbumState.description-short: Rectangle { height: 4px; }
|
|
if AlbumState.description != AlbumState.description-short: read-more := TouchArea {
|
|
width: read-more-row.preferred-width;
|
|
height: 20px;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.show-description = 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: 20px; }
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
spacing: 12px;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/play-fill.svg");
|
|
primary: true;
|
|
diameter: 44px;
|
|
// Spin only while THIS album's first track is fetching.
|
|
loading: NowPlayingState.loading
|
|
&& NowPlayingState.album-id == AlbumState.id;
|
|
clicked => {
|
|
root.media-action("album", AlbumState.id, "play");
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/shuffle.svg");
|
|
clicked => {
|
|
root.media-action("album", AlbumState.id, "shuffle");
|
|
}
|
|
}
|
|
}
|
|
// Edit metadata (tag editor) — local albums only; hidden
|
|
// for Plex (tag write isn't supported there).
|
|
if AlbumState.is-local && AlbumState.source != "plex": VerticalLayout {
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/pencil.svg");
|
|
clicked => {
|
|
root.media-action("album", AlbumState.id, "edit");
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
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/cloud-download.svg");
|
|
clicked => {
|
|
root.media-action("album", AlbumState.id, "cache");
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/radio.svg");
|
|
clicked => {
|
|
root.media-action("album", AlbumState.id, "radio");
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/user.svg");
|
|
clicked => {
|
|
root.open-artist(AlbumState.artist-id);
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction { icon: @image-url("../assets/icons/book-open.svg"); }
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/cassette-tape.svg");
|
|
clicked => {
|
|
root.media-action("album", AlbumState.id, "add-to-mixtape");
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CircleAction { icon: @image-url("../assets/icons/square-check-big.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 + label/awards sidebar --------------------------
|
|
// No `alignment: start` here: it would defeat the track list's
|
|
// horizontal-stretch and leave the sidebar floating mid-row
|
|
// with dead space to its right. The track list stretches and
|
|
// the 254px sidebar is pushed flush to the right edge; when a
|
|
// main right sidebar opens, the whole content region narrows
|
|
// and the sidebar rides along with it.
|
|
HorizontalLayout {
|
|
spacing: 32px;
|
|
|
|
VerticalLayout {
|
|
horizontal-stretch: 1;
|
|
spacing: 0px;
|
|
alignment: start;
|
|
|
|
if AlbumState.loading && AlbumState.tracks.length == 0: Rectangle {
|
|
height: 280px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
spacing: 18px;
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
LoadingSpinner { size: 36px; }
|
|
}
|
|
Text {
|
|
text: @tr("Loading album…");
|
|
color: Theme.text-muted;
|
|
font-size: 13px;
|
|
horizontal-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Toolbar — album quality badge and track search.
|
|
HorizontalLayout {
|
|
height: 52px;
|
|
spacing: 16px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QualityBadgeFull {
|
|
tier: AlbumState.quality-tier;
|
|
detail: AlbumState.quality-detail;
|
|
}
|
|
}
|
|
Rectangle { horizontal-stretch: 1; }
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 280px;
|
|
height: 34px;
|
|
border-radius: 6px;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
background: Theme.surface-elevated;
|
|
HorizontalLayout {
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
spacing: 7px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/search.svg");
|
|
width: 14px;
|
|
height: 14px;
|
|
tint: Theme.text-muted;
|
|
}
|
|
}
|
|
// Functional client-side filter.
|
|
Rectangle {
|
|
horizontal-stretch: 1;
|
|
track-search := TextInput {
|
|
width: 100%;
|
|
height: 100%;
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
vertical-alignment: center;
|
|
single-line: true;
|
|
edited => {
|
|
AlbumActions.search(self.text);
|
|
}
|
|
}
|
|
Text {
|
|
visible: track-search.text == "";
|
|
text: @tr("Search tracks...");
|
|
// Span the box so the placeholder
|
|
// centers like the TextInput text.
|
|
width: 100%;
|
|
height: 100%;
|
|
color: Theme.text-muted;
|
|
font-size: 13px;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
// Favorite / offline-cache column headers (Qobuz). For
|
|
// a local album the rows show a source indicator
|
|
// instead, so one unlabeled column keeps alignment.
|
|
if !AlbumState.is-local: Rectangle {
|
|
width: 28px;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/heart.svg");
|
|
width: 14px;
|
|
height: 14px;
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
tint: Theme.text-muted;
|
|
}
|
|
}
|
|
if !AlbumState.is-local: Rectangle {
|
|
width: 28px;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/cloud-download.svg");
|
|
width: 14px;
|
|
height: 14px;
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
tint: Theme.text-muted;
|
|
}
|
|
}
|
|
if AlbumState.is-local: Rectangle { width: 28px; }
|
|
// Aligns with the per-row more-button slot.
|
|
Rectangle { width: 32px; }
|
|
}
|
|
|
|
for track[index] in AlbumState.tracks: TrackRow {
|
|
track: track;
|
|
index: index;
|
|
// Number cell (no cover). Qobuz albums carry the
|
|
// favorite heart (matching the column header) + the
|
|
// offline-cache button; local albums show the source
|
|
// indicator instead.
|
|
show-artwork: false;
|
|
show-favorite: !AlbumState.is-local;
|
|
show-download: !AlbumState.is-local;
|
|
show-source: AlbumState.is-local;
|
|
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%);
|
|
clicked => {
|
|
if (AlbumState.label-id != "") {
|
|
root.media-action("label", AlbumState.label-id, "open");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if AlbumState.awards.length > 0: VerticalLayout {
|
|
spacing: 8px;
|
|
SidebarHeading { text: @tr("AWARDS"); }
|
|
for award in AlbumState.awards: SidebarCard {
|
|
icon: @image-url("../assets/laurels.svg");
|
|
name: award;
|
|
avatar-bg: @linear-gradient(135deg, #b45309 0%, #eab308 100%);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shared scrollbar in the right gutter (homologated cross-view).
|
|
ListScrollbar {
|
|
x: parent.width - self.width - 4px;
|
|
width: 14px;
|
|
height: 100%;
|
|
viewport-height: flick.viewport-height;
|
|
visible-height: flick.height;
|
|
viewport-y <=> flick.viewport-y;
|
|
}
|
|
|
|
// --- Full-description modal --------------------------------------------
|
|
if root.show-description: Rectangle {
|
|
background: #000000bf;
|
|
|
|
// Scrim — a click anywhere outside the panel dismisses.
|
|
TouchArea {
|
|
mouse-cursor: default;
|
|
clicked => {
|
|
root.show-description = false;
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: Math.min(root.width - 80px, 560px);
|
|
height: Math.min(root.height - 120px, 460px);
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
border-radius: Radius.md;
|
|
background: Theme.surface-card;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
drop-shadow-blur: 32px;
|
|
drop-shadow-color: Theme.card-shadow;
|
|
|
|
// Swallow clicks so they do not reach the scrim.
|
|
TouchArea { }
|
|
|
|
VerticalLayout {
|
|
padding: 24px;
|
|
spacing: 14px;
|
|
|
|
HorizontalLayout {
|
|
Text {
|
|
text: @tr("About this album");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.heading;
|
|
font-weight: Typography.semibold;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
close-x := TouchArea {
|
|
width: 28px;
|
|
height: 28px;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.show-description = false;
|
|
}
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/x.svg");
|
|
width: 18px;
|
|
height: 18px;
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
tint: close-x.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
}
|
|
}
|
|
|
|
// vertical-stretch bounds the Flickable to the leftover
|
|
// panel height so a long description actually scrolls.
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: desc-text.preferred-height;
|
|
desc-text := Text {
|
|
width: parent.width;
|
|
text: AlbumState.description;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|