feat(qt): album header — 260px cover, no divider rule, a gradient that lands mid-toolbar
Four owner-requested changes to the Qobuz album page, validated by smoke. 1. THE DIVIDER RULE goes invisible rather than away. The gradient band sizes itself off that item's `y` and the whole column below is positioned after it, so deleting the item would move the page; only the paint was unwanted. 2. COVER 224 -> 260. Now one constant: the literal was repeated at five sites — the real header, its skeleton twin, and the two text columns that measure `parent.width - 224 - 32`. Changing only the header would leave the skeleton at 224 and make the page jump 36px when the real one replaces it. The header Row has no explicit height, so it grows on its own. The URL tier is unchanged (`best()`, per the 2026-08-16 ruling that no existing surface is re-tiered DOWN); what decodes is RoundedImage's scaled derivative at the drawn size, so 260 over 224 is ~35% of a small image. 3. THE ATMOSPHERE REACHES MID-TOOLBAR — the 8px spacer below the divider plus half the 52px toolbar band — so it dies across the quality chip and the search box instead of stopping at the divider. A named constant rather than a `mapToItem` chain: that chain crosses three nested containers and goes stale when an ancestor moves without notifying. 4. THE FALLOFF IS PROGRESSIVE. The tint held FULL strength to 0.82 and died inside the last 18%, drawing a visible seam — and extending the band made it worse, not better, because the falloff is a FRACTION of the height: the solid part grew while the fade stayed as short. The page-surface overlay underneath started at 0.82 too, putting a second seam on the same line. Both now decay over the bottom ~60% on an ease-out curve, stepped through explicit alphas because a GradientStop interpolates linearly between the two colours it is given. cc
This commit is contained in:
2 files changed
+62
-14
No files matched your search
@@ -105,6 +105,18 @@ Item {
|
||||
// logs a warning on every re-evaluation.
|
||||
readonly property color tintColor: root.tint === "" ? "transparent" : root.tint
|
||||
|
||||
/// The tint at a given alpha. A GradientStop needs a concrete colour, and
|
||||
/// `Qt.alpha()` does not exist — the channels have to be rebuilt.
|
||||
function tintAlpha(a) {
|
||||
var c = root.tintColor
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * a)
|
||||
}
|
||||
/// Twin of tintAlpha for the page-surface fade underneath.
|
||||
function surfaceAlpha(a) {
|
||||
var c = theme.surfaceMain
|
||||
return Qt.rgba(c.r, c.g, c.b, a)
|
||||
}
|
||||
|
||||
visible: root.active && (root.tint !== "" || root.hasAtmosphere)
|
||||
// Never eat clicks meant for the header controls painted on top.
|
||||
enabled: false
|
||||
@@ -204,10 +216,23 @@ Item {
|
||||
anchors.fill: parent
|
||||
visible: !root.hasAtmosphere
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: root.tintColor }
|
||||
// The tint used to hold FULL strength to 0.82 and then die inside
|
||||
// the last 18%, which draws a visible horizontal seam across the
|
||||
// page — the taller the band, the worse, because the falloff stays
|
||||
// the same fraction while the solid part grows. The stops below
|
||||
// spread the decay over the bottom ~60% on an ease-out curve, so
|
||||
// there is no single row of pixels where the gradient "starts".
|
||||
// Alpha is stepped explicitly because a GradientStop interpolates
|
||||
// linearly between whatever two colours it is given.
|
||||
GradientStop { position: 0.00; color: root.tintColor }
|
||||
GradientStop { position: 0.16; color: root.tintColor }
|
||||
GradientStop { position: 0.82; color: root.tintColor }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
GradientStop { position: 0.38; color: root.tintAlpha(0.94) }
|
||||
GradientStop { position: 0.52; color: root.tintAlpha(0.80) }
|
||||
GradientStop { position: 0.65; color: root.tintAlpha(0.60) }
|
||||
GradientStop { position: 0.77; color: root.tintAlpha(0.38) }
|
||||
GradientStop { position: 0.88; color: root.tintAlpha(0.18) }
|
||||
GradientStop { position: 0.95; color: root.tintAlpha(0.07) }
|
||||
GradientStop { position: 1.00; color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,9 +256,16 @@ Item {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "transparent" }
|
||||
GradientStop { position: 0.82; color: "transparent" }
|
||||
GradientStop { position: 1.0; color: theme.surfaceMain }
|
||||
// Mirrors the tint's curve. Held at zero to 0.82 this overlay put
|
||||
// its own seam in the same place, doubling the edge the tint was
|
||||
// already drawing.
|
||||
GradientStop { position: 0.00; color: "transparent" }
|
||||
GradientStop { position: 0.45; color: "transparent" }
|
||||
GradientStop { position: 0.62; color: root.surfaceAlpha(0.12) }
|
||||
GradientStop { position: 0.75; color: root.surfaceAlpha(0.34) }
|
||||
GradientStop { position: 0.86; color: root.surfaceAlpha(0.60) }
|
||||
GradientStop { position: 0.94; color: root.surfaceAlpha(0.84) }
|
||||
GradientStop { position: 1.00; color: theme.surfaceMain }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,18 @@ Rectangle {
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
/// Header cover edge. One constant because the real header, its skeleton
|
||||
/// twin and both text columns all measure off it — they drifted apart at
|
||||
/// five separate literals before. The Row has no explicit height, so the
|
||||
/// header grows with this on its own.
|
||||
readonly property int coverPx: 260
|
||||
|
||||
/// How far past the header divider the artwork atmosphere reaches: the 8px
|
||||
/// spacer below the divider plus half of the 52px track toolbar band, so
|
||||
/// the gradient dies across the middle of the quality chip and the search
|
||||
/// box instead of stopping at the divider.
|
||||
readonly property int atmoReach: 1 + 8 + 26
|
||||
|
||||
// The view's album + url-keyed cover map (artwork pipeline).
|
||||
readonly property var album: JSON.parse(QbzAlbum.albumJson)
|
||||
readonly property var header: album.header || ({})
|
||||
@@ -518,7 +530,7 @@ Rectangle {
|
||||
// .slint:189 `atmo-height: page.y + header-divider.y` — the band
|
||||
// ends EXACTLY on the header/track-list divider, whatever height
|
||||
// a long editorial description gave the header.
|
||||
height: page.y + headerDivider.y
|
||||
height: page.y + headerDivider.y + root.atmoReach
|
||||
tint: album.headerColor || ""
|
||||
// Route A: the blurred field. Empty until the cover resolves, and the
|
||||
// flat tint stands in meanwhile (HeaderGradient handles the swap).
|
||||
@@ -549,13 +561,13 @@ Rectangle {
|
||||
|
||||
QbzSkeleton {
|
||||
variant: "block"
|
||||
width: 224
|
||||
height: 224
|
||||
width: root.coverPx
|
||||
height: root.coverPx
|
||||
blockRadius: 12
|
||||
phase: skeletonPhase.on
|
||||
}
|
||||
Column {
|
||||
width: parent.width - 224 - 32
|
||||
width: parent.width - root.coverPx - 32
|
||||
spacing: 12
|
||||
Item { width: 1; height: 6 }
|
||||
QbzSkeleton { variant: "block"; width: Math.min(420, parent.width); height: 30; cellIndex: 0; phase: skeletonPhase.on }
|
||||
@@ -586,8 +598,8 @@ Rectangle {
|
||||
spacing: 32
|
||||
|
||||
Rectangle {
|
||||
width: 224
|
||||
height: 224
|
||||
width: root.coverPx
|
||||
height: root.coverPx
|
||||
radius: 12
|
||||
color: theme.surfaceElevated
|
||||
// No clip: RoundedImage confines itself on both arms; a clip is an
|
||||
@@ -621,7 +633,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 224 - 32
|
||||
width: parent.width - root.coverPx - 32
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 4
|
||||
spacing: 0
|
||||
@@ -824,7 +836,11 @@ Rectangle {
|
||||
Item { width: 1; height: 20 }
|
||||
// Header divider. The gradient band above sizes itself to THIS
|
||||
// item's y (.slint:189 atmo-height), so it keeps its id.
|
||||
Rectangle { id: headerDivider; width: parent.width - 64; height: 1; color: theme.borderSubtle }
|
||||
// Kept, deliberately invisible. The gradient band above sizes itself
|
||||
// off THIS item's y (.slint:189 atmo-height) and the whole column
|
||||
// below is positioned after it, so removing the item would move the
|
||||
// page; only the paint is unwanted.
|
||||
Rectangle { id: headerDivider; width: parent.width - 64; height: 1; color: "transparent" }
|
||||
Item { width: 1; height: 8 }
|
||||
|
||||
// --- Track list + label/awards sidebar ----------------------
|
||||
|
||||
Reference in new issue
Block a user