fix(qbz-qt): refine compact album header layout

cx
This commit is contained in:
vicrodh committed 2026-08-25 22:04:35 -06:00
1 parent aadad1da54
commit 2d31244a7a
6 files changed
+132 -47

No files matched your search

+14 -6
View File
@@ -5,8 +5,11 @@
//
// Numbers, read off primitives/CircleAction.slint:
// :24 diameter — primary 44, secondary 32 (the hierarchy lives HERE, not
// in the callers; the port had 40 and was uniformly small)
// :66 glyph — primary 19, secondary 15
// in the callers; the port had 40 and was uniformly small). Compact
// headers may deliberately collapse the primary to the secondary
// footprint without giving up its accent palette.
// :66 glyph — primary 19, secondary 15; the compact primary follows the
// secondary footprint here too.
// :48 ON-SURFACE arm (`overlay: false`, the default): primary = accent
// disc, secondary = surface-elevated disc, hover/active surface-hover
// :60 ring — border-strong (the port used border-muted)
@@ -65,6 +68,9 @@ Rectangle {
property string name: ""
property bool active: false
property bool primary: false
/// Preserve the primary action palette while using the secondary action's
/// footprint. Album headers use this when their global compact mode is on.
property bool compactPrimary: false
property bool btnEnabled: true
/// While the action's work is in flight the glyph swaps for a spinning
/// arc (CircleAction.slint:18 / :77 — the reference has this arm and the
@@ -84,8 +90,10 @@ Rectangle {
/// runtime-tinted so it is the real token under every theme.
readonly property string onSurfaceTint: "textPrimary"
width: primary ? 44 : 32
height: primary ? 44 : 32
readonly property bool largePrimary: primary && !compactPrimary
width: largePrimary ? 44 : 32
height: largePrimary ? 44 : 32
radius: width / 2
color: overlay
? (primary
@@ -151,8 +159,8 @@ Rectangle {
QbzIcon {
visible: !root.loading
name: root.name
width: root.primary ? 19 : 15
height: root.primary ? 19 : 15
width: root.largePrimary ? 19 : 15
height: root.largePrimary ? 19 : 15
// Whole-pixel centring, both axes — CircleAction.slint:67-68 rounds
// them explicitly for the same reason: 44-19 and 32-15 are both ODD,
// so `anchors.centerIn` lands the glyph on 12.5px / 8.5px and the
+5 -1
View File
@@ -32,6 +32,9 @@ Rectangle {
property bool active: false
property bool activeBackground: false
property bool btnEnabled: true
/// Optional fixed tint for theme-independent hosts such as the dark album
/// atmosphere. Empty keeps the normal themed idle/hover selector.
property string tintOverride: ""
/// Optional host for the shared QbzTooltip mechanism. Kept opt-in so the
/// many icon-only call sites that intentionally have no bubble stay inert.
property var tooltip: null
@@ -59,7 +62,8 @@ Rectangle {
width: parent.iconSize
height: parent.iconSize
anchors.centerIn: parent
tintName: !parent.btnEnabled ? "muted"
tintName: btn.tintOverride !== "" ? btn.tintOverride
: !parent.btnEnabled ? "muted"
: parent.active ? "accent"
: biArea.containsMouse ? btn.tintStrong : btn.tintWeak
}
+11 -7
View File
@@ -128,6 +128,7 @@ Item {
property real coverSize: 150
property real coverGap: 24
property bool headerRound: false
property bool compactHeader: false
property int actionCount: 0
// "cardGrid" — cell pitch vs the card drawn inside it.
property real cellW: 220
@@ -398,22 +399,24 @@ Item {
width: Math.max(0, root.width - root.coverSize - root.coverGap)
height: root.coverSize
Rectangle {
width: 88
height: 11
y: root.compactHeader ? 4 : 0
width: root.compactHeader ? Math.min(parent.width, 420) : 88
height: root.compactHeader ? 24 : 11
radius: root.blockRadius
color: theme.surfaceElevated
opacity: root.breathe
}
Rectangle {
y: 23
width: Math.min(parent.width, 420)
height: 28
y: root.compactHeader ? 34 : 23
width: root.compactHeader ? Math.min(parent.width, 260)
: Math.min(parent.width, 420)
height: root.compactHeader ? 16 : 28
radius: root.blockRadius
color: theme.surfaceElevated
opacity: root.breathe
}
Rectangle {
y: 63
y: root.compactHeader ? 56 : 63
width: Math.min(parent.width, 300)
height: 14
radius: root.blockRadius
@@ -421,6 +424,7 @@ Item {
opacity: root.breathe
}
Rectangle {
visible: !root.compactHeader
y: 85
width: Math.min(parent.width, 200)
height: 13
@@ -433,7 +437,7 @@ Item {
delegate: Rectangle {
required property int index
x: index * 44
y: 110
y: root.compactHeader ? 80 : 110
width: 32
height: 32
radius: 16
+57 -20
View File
@@ -48,7 +48,8 @@ Rectangle {
/// 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: compactHeaderPref ? 130 : 260
readonly property int coverPx: compactHeaderPref ? 112 : 260
readonly property int headerGapPx: compactHeaderPref ? 20 : 32
/// 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
@@ -121,6 +122,38 @@ Rectangle {
}
return album.compactHeader === true
}
property bool headerLayoutReady: false
property int headerLayoutEpoch: 0
property real headerSavedOffset: 0
property bool headerSnapshotValid: false
function toggleCompactHeader() {
// Capture before settingsJson changes: dependent geometry bindings and
// this property's change handler have no guaranteed observer order.
root.headerSavedOffset = pageFlick.contentY - pageFlick.originY
root.headerSnapshotValid = true
QbzBridge.settingsBool("compact-album-header", !root.compactHeaderPref)
}
onCompactHeaderPrefChanged: {
if (!root.headerLayoutReady)
return
// An inline ListView header changes originY with its height. Keep the
// viewport's offset from that origin so expanding pins the visible top
// edge and grows the header downward instead of behind the viewport.
var savedOffset = root.headerSnapshotValid
? root.headerSavedOffset
: pageFlick.contentY - pageFlick.originY
root.headerSnapshotValid = false
var epoch = ++root.headerLayoutEpoch
Qt.callLater(function () {
if (epoch !== root.headerLayoutEpoch)
return
pageFlick.forceLayout()
var minY = pageFlick.originY
var maxY = minY + Math.max(0, pageFlick.contentHeight - pageFlick.height)
pageFlick.contentY = Math.max(minY,
Math.min(minY + savedOffset, maxY))
})
}
// `header.awards`, NOT `album.awards`. It is a field of AlbumHeader
// (src/album_qt.rs:171), not of AlbumViewData — read one level too high it
// is `undefined`, `|| []` swallows that, and the AWARDS block simply never
@@ -318,6 +351,7 @@ Rectangle {
root.applyAlbumDocument()
syncAlbumState()
dispatchCovers()
root.headerLayoutReady = true
}
Connections {
target: QbzAlbum
@@ -719,7 +753,7 @@ Rectangle {
Row {
visible: root.primaryLoading
width: parent.width - 64
spacing: 32
spacing: root.headerGapPx
QbzSkeleton {
variant: "block"
@@ -729,13 +763,13 @@ Rectangle {
phase: skeletonPhase.on
}
Column {
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 }
QbzSkeleton { variant: "block"; width: Math.min(260, parent.width); height: 18; cellIndex: 1; phase: skeletonPhase.on }
width: parent.width - root.coverPx - root.headerGapPx
spacing: root.compactHeaderPref ? 4 : 12
Item { width: 1; height: root.compactHeaderPref ? 0 : 6 }
QbzSkeleton { variant: "block"; width: Math.min(420, parent.width); height: root.compactHeaderPref ? 24 : 30; cellIndex: 0; phase: skeletonPhase.on }
QbzSkeleton { variant: "block"; width: Math.min(260, parent.width); height: root.compactHeaderPref ? 16 : 18; cellIndex: 1; phase: skeletonPhase.on }
QbzSkeleton { variant: "block"; width: Math.min(340, parent.width); height: 14; cellIndex: 2; phase: skeletonPhase.on }
Item { width: 1; height: 14 }
Item { width: 1; height: root.compactHeaderPref ? 4 : 14 }
Row {
spacing: 12
Repeater {
@@ -743,8 +777,8 @@ Rectangle {
delegate: QbzSkeleton {
required property int index
variant: "circle"
width: 44
height: 44
width: root.compactHeaderPref ? 32 : 44
height: root.compactHeaderPref ? 32 : 44
cellIndex: index
phase: skeletonPhase.on
}
@@ -757,7 +791,7 @@ Rectangle {
Row {
visible: !root.primaryLoading
width: parent.width - 64
spacing: 32
spacing: root.headerGapPx
Rectangle {
width: root.coverPx
@@ -795,7 +829,7 @@ Rectangle {
}
Column {
width: parent.width - root.coverPx - 32
width: parent.width - root.coverPx - root.headerGapPx
anchors.top: parent.top
anchors.topMargin: 4
spacing: 0
@@ -815,14 +849,16 @@ Rectangle {
font.weight: theme.weightBold
elide: Text.ElideRight
}
QbzCircleAction {
QbzIconButton {
id: headerModeButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
anchors.topMargin: -4
btnSize: 26
iconSize: 13
name: root.compactHeaderPref ? "maximize-2" : "minimize-2"
overlay: root.hdrOverlay
onClicked: QbzBridge.settingsBool(
"compact-album-header", !root.compactHeaderPref)
tintOverride: root.hdrOverlay ? "white" : ""
onClicked: root.toggleCompactHeader()
HoverHandler { id: headerModeHover }
ToolTip.visible: headerModeHover.hovered
ToolTip.text: root.compactHeaderPref
@@ -831,7 +867,7 @@ Rectangle {
ToolTip.delay: 350
}
}
Item { width: 1; height: 4 }
Item { width: 1; height: root.compactHeaderPref ? 2 : 4 }
// Credited-artist line (links + role suffixes).
Flow {
width: parent.width
@@ -874,7 +910,7 @@ Rectangle {
}
}
}
Item { width: 1; height: root.compactHeaderPref ? 4 : 10 }
Item { width: 1; height: root.compactHeaderPref ? 2 : 10 }
// Meta line (label as a clickable link when navigable).
Row {
spacing: 0
@@ -968,7 +1004,7 @@ Rectangle {
}
}
Item { width: 1; height: root.compactHeaderPref ? 8 : 20 }
Item { width: 1; height: root.compactHeaderPref ? 4 : 20 }
// Action row — AlbumPageView.slint:504-640. One shared
// CircleAction for every button including Play (the
// hand-rolled 44px disc it used to be drifted from the
@@ -979,6 +1015,7 @@ Rectangle {
spacing: 12
QbzCircleAction {
primary: true
compactPrimary: root.compactHeaderPref
overlay: root.hdrOverlay
name: "play-fill"
anchors.verticalCenter: parent.verticalCenter
+35 -7
View File
@@ -58,6 +58,32 @@ Rectangle {
}
return doc && doc.compactHeader === true
}
property bool headerLayoutReady: false
property int headerLayoutEpoch: 0
property real headerSavedOffset: 0
property bool headerSnapshotValid: false
function toggleCompactHeader() {
root.headerSavedOffset = flick.contentY - flick.originY
root.headerSnapshotValid = true
QbzBridge.settingsBool("compact-album-header", !root.compactHeaderPref)
}
onCompactHeaderPrefChanged: {
if (!root.headerLayoutReady)
return
var savedOffset = root.headerSnapshotValid
? root.headerSavedOffset
: flick.contentY - flick.originY
root.headerSnapshotValid = false
var epoch = ++root.headerLayoutEpoch
Qt.callLater(function () {
if (epoch !== root.headerLayoutEpoch)
return
var minY = flick.originY
var maxY = minY + Math.max(0, flick.contentHeight - flick.height)
flick.contentY = Math.max(minY,
Math.min(minY + savedOffset, maxY))
})
}
readonly property var allArtists: {
if (!album) return []
var raw = (album.allArtists || "").split(",")
@@ -97,9 +123,11 @@ Rectangle {
// las filas ni en el derive, tiene que estar aquí (portada, versiones,
// lista de artistas, toolbar).
property double _mountedAt: Date.now()
Component.onCompleted: console.info(albTiming,
"[albtiming] arbol de la vista construido en "
+ (Date.now() - root._mountedAt) + "ms")
Component.onCompleted: {
console.info(albTiming, "[albtiming] arbol de la vista construido en "
+ (Date.now() - root._mountedAt) + "ms")
root.headerLayoutReady = true
}
readonly property var visibleTracks: {
var _dt = Date.now()
var _r = root._deriveImpl()
@@ -400,9 +428,10 @@ Rectangle {
visible: root.pageLoading && !root.album
variant: "header"
width: parent.width
height: root.compactHeaderPref ? 128 : 224
coverSize: root.compactHeaderPref ? 128 : 224
height: root.compactHeaderPref ? 112 : 224
coverSize: root.compactHeaderPref ? 112 : 224
coverGap: root.compactHeaderPref ? 20 : 32
compactHeader: root.compactHeaderPref
actionCount: 5
phase: root.skelPhase
}
@@ -420,8 +449,7 @@ Rectangle {
skelPhase: root.skelPhase
artSettleMs: root.artSettleMs
onOpenArtist: function (name) { root.openArtist(name) }
onCompactToggled: QbzBridge.settingsBool(
"compact-album-header", !root.compactHeaderPref)
onCompactToggled: root.toggleCompactHeader()
}
Item { width: 1; height: 20 }
@@ -43,7 +43,7 @@ Row {
property bool artistsExpanded: false
onAlbumChanged: artistsExpanded = false
readonly property int coverPx: compact ? 128 : 224
readonly property int coverPx: compact ? 112 : 224
spacing: compact ? 20 : 32
Rectangle {
@@ -92,10 +92,13 @@ Row {
font.weight: theme.weightBold
elide: Text.ElideRight
}
QbzCircleAction {
QbzIconButton {
id: localHeaderModeButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
anchors.topMargin: -4
btnSize: 26
iconSize: 13
name: root.compact ? "maximize-2" : "minimize-2"
onClicked: root.compactToggled()
HoverHandler { id: localHeaderModeHover }
@@ -106,7 +109,7 @@ Row {
ToolTip.delay: 350
}
}
Item { width: 1; height: 4 }
Item { width: 1; height: root.compact ? 2 : 4 }
Row {
spacing: 8
Text {
@@ -170,7 +173,7 @@ Row {
}
}
}
Item { width: 1; height: root.compact ? 4 : 10 }
Item { width: 1; height: root.compact ? 2 : 10 }
Text {
width: parent.width
text: root.compact ? root.compactInfoLine : root.infoLine
@@ -178,7 +181,7 @@ Row {
font.pixelSize: theme.fontBody
elide: Text.ElideRight
}
Item { width: 1; height: root.compact ? 8 : 20 }
Item { width: 1; height: root.compact ? 4 : 20 }
// ---- Local action row ----
Row {
@@ -186,6 +189,7 @@ Row {
QbzCircleAction {
name: "play-fill"
primary: true
compactPrimary: root.compact
onClicked: QbzLocal.albumSelectedAction("play", "")
}
QbzCircleAction {