fix(header): gradient height = header container (ends at divider), artist-page parity, action buttons readable with gradient off
The album header gradient was a fixed px that overshot the header/track-list divider on tall (description-bearing) headers; bind its height to the divider's y so it ends exactly at the header container's bottom at any size. Apply the same readable-text + gradient-to-boundary treatment to the artist page. And CircleAction gains an on-surface variant (theme-colored fill/border/icon) driven by the gradient toggle, so when the header gradient is off the action buttons are visible on light themes instead of white-on-white. Below-divider content untouched. Extends #491/#519. cc
This commit is contained in:
3 files changed
+100
-19
No files matched your search
@@ -170,6 +170,22 @@ export component AlbumPageView inherits Rectangle {
|
||||
property <color> hdr-muted: AppearanceState.album-header-gradient
|
||||
? #ffffff.with-alpha(0.62) : Theme.text-muted;
|
||||
|
||||
// With the gradient OFF the header's CircleActions sit on the plain
|
||||
// theme surface — switch them to the theme-aware on-surface variant so
|
||||
// they stay visible on light themes (their default palette is white-on-
|
||||
// dark, tuned for the atmosphere).
|
||||
property <bool> hdr-on-surface: !AppearanceState.album-header-gradient;
|
||||
|
||||
// Atmosphere/gradient extent: the background blocks must end EXACTLY at
|
||||
// the header/track-list divider — the header height varies (long
|
||||
// editorial descriptions), so a fixed px either overshoots into the
|
||||
// track list or fades early. `header-divider.y` is the divider's offset
|
||||
// within `page`, and the backgrounds share the page's top origin inside
|
||||
// the Flickable viewport (page.y = its own offset there, normally 0).
|
||||
// The backgrounds are absolutely positioned and don't participate in
|
||||
// the page layout, so this cannot form a binding loop.
|
||||
property <length> atmo-height: page.y + header-divider.y;
|
||||
|
||||
background: Theme.surface-main;
|
||||
|
||||
flick := Flickable {
|
||||
@@ -206,7 +222,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: parent.width;
|
||||
height: 440px;
|
||||
height: root.atmo-height;
|
||||
source-image: AlbumState.header-atmosphere;
|
||||
fallback-image: AlbumState.artwork;
|
||||
animated: false;
|
||||
@@ -218,7 +234,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: parent.width;
|
||||
height: 440px;
|
||||
height: root.atmo-height;
|
||||
background: @linear-gradient(180deg,
|
||||
AlbumState.header-color 0%,
|
||||
AlbumState.header-color 16%,
|
||||
@@ -228,10 +244,13 @@ export component AlbumPageView inherits Rectangle {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: parent.width;
|
||||
height: 440px;
|
||||
height: root.atmo-height;
|
||||
// Fade start sits proportionally near the bottom so the blend
|
||||
// to surface-main always COMPLETES right at the divider,
|
||||
// whatever height the header resolved to.
|
||||
background: @linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
transparent 74%,
|
||||
transparent 82%,
|
||||
Theme.surface-main 100%);
|
||||
}
|
||||
|
||||
@@ -486,6 +505,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/play-fill.svg");
|
||||
primary: true;
|
||||
on-surface: root.hdr-on-surface;
|
||||
// Spin only while THIS album's first track is fetching.
|
||||
loading: NowPlayingState.loading
|
||||
&& NowPlayingState.album-id == AlbumState.id;
|
||||
@@ -497,6 +517,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
alignment: center;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/shuffle.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
root.media-action("album", AlbumState.id, "shuffle");
|
||||
}
|
||||
@@ -508,6 +529,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
alignment: center;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/pencil.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
root.media-action("album", AlbumState.id, "edit");
|
||||
}
|
||||
@@ -523,6 +545,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
icon: AlbumState.is-favorite
|
||||
? @image-url("../assets/icons/heart-filled.svg")
|
||||
: @image-url("../assets/icons/heart.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
active: AlbumState.is-favorite;
|
||||
loading: AlbumState.favorite-loading;
|
||||
clicked => {
|
||||
@@ -534,6 +557,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
alignment: center;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/radio.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
root.media-action("album", AlbumState.id, "radio");
|
||||
}
|
||||
@@ -543,6 +567,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
alignment: center;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/book-open.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
root.media-action("album", AlbumState.id, "booklet");
|
||||
}
|
||||
@@ -552,6 +577,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
alignment: center;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/cassette-tape.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
root.media-action("album", AlbumState.id, "add-to-mixtape");
|
||||
}
|
||||
@@ -564,6 +590,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
alignment: center;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/info.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
root.media-action("album", AlbumState.id, "info");
|
||||
}
|
||||
@@ -576,6 +603,7 @@ export component AlbumPageView inherits Rectangle {
|
||||
height: 32px;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/ellipsis.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
album-menu.show();
|
||||
}
|
||||
@@ -600,7 +628,9 @@ export component AlbumPageView inherits Rectangle {
|
||||
}
|
||||
|
||||
Rectangle { height: 20px; }
|
||||
Rectangle {
|
||||
// The atmosphere/fade blocks above size themselves to this
|
||||
// divider (root.atmo-height) so the gradient ends exactly here.
|
||||
header-divider := Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,31 @@ export component ArtistPageView inherits Rectangle {
|
||||
// needs to shrink for the sidebar — it always uses the full
|
||||
// window width minus the page padding + portrait + header gap.
|
||||
property <length> content-width: root.width;
|
||||
|
||||
// Header text colors (same rule as AlbumPageView): the
|
||||
// ImmersiveAtmosphere behind the header is always dark, so when the
|
||||
// gradient setting is ON the header text must be light regardless of
|
||||
// theme (light themes' Theme.text-* are dark and vanish on the
|
||||
// atmosphere). With the gradient OFF the header sits on
|
||||
// Theme.surface-main and keeps the normal theme colors.
|
||||
property <color> hdr-strong: AppearanceState.album-header-gradient
|
||||
? #ffffff : Theme.text-primary;
|
||||
property <color> hdr-body: AppearanceState.album-header-gradient
|
||||
? #ffffff.with-alpha(0.88) : Theme.text-secondary;
|
||||
|
||||
// With the gradient OFF the header's CircleActions sit on the plain
|
||||
// theme surface — switch them to the theme-aware on-surface variant so
|
||||
// they stay visible on light themes.
|
||||
property <bool> hdr-on-surface: !AppearanceState.album-header-gradient;
|
||||
|
||||
// Atmosphere/gradient extent: end EXACTLY at the header/content
|
||||
// boundary — `body-row.y` is where the body content starts within
|
||||
// `page` (the header height varies with the bio length, so a fixed px
|
||||
// either overshoots into the content or fades early). The backgrounds
|
||||
// share the page's top origin inside the Flickable viewport and are
|
||||
// absolutely positioned (not part of the page layout), so this cannot
|
||||
// form a binding loop.
|
||||
property <length> atmo-height: page.y + body-row.y;
|
||||
// Popular Tracks shows the first 5; Load more expands to all.
|
||||
property <bool> top-tracks-expanded: false;
|
||||
property <int> top-tracks-preview: 5;
|
||||
@@ -181,7 +206,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: parent.width;
|
||||
height: 360px;
|
||||
height: root.atmo-height;
|
||||
source-image: ArtistState.header-atmosphere;
|
||||
fallback-image: ArtistState.artwork;
|
||||
animated: false;
|
||||
@@ -193,7 +218,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: parent.width;
|
||||
height: 360px;
|
||||
height: root.atmo-height;
|
||||
background: @linear-gradient(180deg,
|
||||
ArtistState.header-color 0%,
|
||||
ArtistState.header-color 16%,
|
||||
@@ -203,10 +228,13 @@ export component ArtistPageView inherits Rectangle {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: parent.width;
|
||||
height: 360px;
|
||||
height: root.atmo-height;
|
||||
// Fade start sits proportionally near the bottom so the blend
|
||||
// to surface-main always COMPLETES right at the header/content
|
||||
// boundary, whatever height the header resolved to.
|
||||
background: @linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
transparent 58%,
|
||||
transparent 82%,
|
||||
Theme.surface-main 100%);
|
||||
}
|
||||
|
||||
@@ -325,7 +353,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
|
||||
Text {
|
||||
text: ArtistState.name;
|
||||
color: Theme.text-primary;
|
||||
color: root.hdr-strong;
|
||||
font-size: Typography.section;
|
||||
font-weight: Typography.bold;
|
||||
overflow: elide;
|
||||
@@ -341,7 +369,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
if ArtistState.bio != "": Text {
|
||||
width: root.content-width - 296px;
|
||||
text: ArtistState.bio-short;
|
||||
color: Theme.text-secondary;
|
||||
color: root.hdr-body;
|
||||
font-size: Typography.legal;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
@@ -379,6 +407,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
spacing: 12px;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/heart.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
active: ArtistState.is-following;
|
||||
clicked => {
|
||||
root.media-action("artist", ArtistState.id, "follow");
|
||||
@@ -392,6 +421,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
height: 32px;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/radio.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
radio-popup.show();
|
||||
}
|
||||
@@ -424,6 +454,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
// is open, matching Tauri's `.network-btn.active`.
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/element-connect.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
active: NetworkSidebarState.open;
|
||||
clicked => {
|
||||
NetworkSidebarState.open = !NetworkSidebarState.open;
|
||||
@@ -439,6 +470,7 @@ export component ArtistPageView inherits Rectangle {
|
||||
height: 32px;
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/ellipsis.svg");
|
||||
on-surface: root.hdr-on-surface;
|
||||
clicked => {
|
||||
artist-overflow-menu.show();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ export component CircleAction inherits Rectangle {
|
||||
in property <bool> enabled: true;
|
||||
// Optional hover tooltip (empty = none), using the global TooltipOverlay.
|
||||
in property <string> tooltip;
|
||||
// Theme-aware variant for header rows rendered WITHOUT the dark
|
||||
// atmosphere (album-header-gradient OFF): the default light-on-dark
|
||||
// palette (white fills/rings/icons) vanishes on light themes' plain
|
||||
// surface. `on-surface: true` swaps to the app's standard on-page
|
||||
// circular look (MixView pattern): accent play disc with a white glyph,
|
||||
// elevated-surface ghost buttons with a themed ring and text-primary
|
||||
// icon. Additive — every existing caller keeps the overlay palette.
|
||||
in property <bool> on-surface: false;
|
||||
callback clicked();
|
||||
|
||||
width: root.diameter;
|
||||
@@ -34,12 +42,18 @@ export component CircleAction inherits Rectangle {
|
||||
border-radius: root.diameter / 2;
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
// Overlay-matched contrast: solid white for the primary play, semi-
|
||||
// transparent white fill + bright 1.5px ring otherwise.
|
||||
background: root.primary
|
||||
? (ta.has-hover ? #ffffffd6 : #ffffff)
|
||||
: ((ta.has-hover || root.active) ? #ffffff3d : #ffffff24);
|
||||
// transparent white fill + bright 1.5px ring otherwise. The on-surface
|
||||
// variant mirrors the theme-aware circular buttons used on plain page
|
||||
// backgrounds instead.
|
||||
background: root.on-surface
|
||||
? (root.primary
|
||||
? (ta.has-hover ? Theme.accent-hover : Theme.accent)
|
||||
: ((ta.has-hover || root.active) ? Theme.surface-hover : Theme.surface-elevated))
|
||||
: (root.primary
|
||||
? (ta.has-hover ? #ffffffd6 : #ffffff)
|
||||
: ((ta.has-hover || root.active) ? #ffffff3d : #ffffff24));
|
||||
border-width: root.primary ? 0px : 1.5px;
|
||||
border-color: #ffffffcc;
|
||||
border-color: root.on-surface ? Theme.border-strong : #ffffffcc;
|
||||
|
||||
if !root.loading: QbzIcon {
|
||||
source: root.icon;
|
||||
@@ -51,13 +65,18 @@ export component CircleAction inherits Rectangle {
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: root.active
|
||||
? Theme.accent
|
||||
: (root.primary ? #000000 : #ffffff);
|
||||
: root.on-surface
|
||||
? (root.primary ? #ffffff : Theme.text-primary)
|
||||
: (root.primary ? #000000 : #ffffff);
|
||||
}
|
||||
|
||||
if root.loading: LoadingSpinner {
|
||||
size: root.diameter * 0.45;
|
||||
// Dark on the solid white primary disc, white on the ghost variant.
|
||||
tint: root.primary ? #000000 : #ffffff;
|
||||
// Dark on the solid white primary disc, white on the ghost variant;
|
||||
// themed tints for the on-surface variant.
|
||||
tint: root.primary
|
||||
? (root.on-surface ? #ffffff : #000000)
|
||||
: (root.on-surface ? Theme.text-primary : #ffffff);
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user