fix(qbz-qt): refine compact album header layout
cx
This commit is contained in:
6 files changed
+132
-47
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user