Files
qbz/crates/qbz-qt/qml/views/PlayHistoryView.qml
T
vicrodh 3b03682e09 fix(qt): pane corners stay round under the dynamic background
AppShell hides its BezelCorner nubs while the field is meant to show
through the pane's corners, and Qt's clip is a rectangular scissor that
ignores radius — so any full-bleed child at y=0 squares the pane. The
doctrine was already written in AppShell.qml ("a full-bleed pane child
must round itself; there is no mask that can do it for it here"); these
eight surfaces predated it or, in the two thin bars added with the
ambient port, ignored it.

- The seven fixed 56px headers (Home, Search, DiscoverBrowse,
  ArtistReleases, PlaylistBrowse, PlaylistManager, PlayHistory) take
  topLeftRadius/topRightRadius. Invisible with the background off, where
  the view root paints the same colour beneath. PlaylistBrowse's SECOND
  bar is the category strip at mid-page and keeps square corners;
  PlayHistory keeps its unconditional opaque fill, which is reference
  parity (:43/:34) and a separate question from the corner.
- LocalAlbumView's neutral header band is now gated on the background,
  1:1 with the Qobuz page's atmosphere (AlbumView headerAtmoOn,
  AlbumPageView.slint:168): the two dark backdrops clash and the field
  already provides one. That band had been square since it was written;
  the nubs had been hiding it, and they only stand down when the field
  is showing. Header text there is theme.textPrimary, not the Qobuz
  page's light-on-dark hdrStrong, so it does not depend on the band to
  stay readable — checked before removing the backdrop from under it.

Gate: cargo build -p qbz-qt, both QML audits, offscreen smoke 0
complaints.

cc
2026-08-13 14:33:35 -06:00

227 lines
9.0 KiB
QML

// Recently Played Albums + Most Played Albums — ONE view for the two
// local-history "View all" pages, because discover/RecentAlbumsView.slint
// and discover/MostPlayedAlbumsView.slint are the same page with five
// differences (the .slint keeps them apart only because Slint has no
// runtime-armed component). `mode` on the published document arms them:
//
// | recent | mostPlayed
// title | "Recently Played Albums" | "Most Played Albums"
// card height | 266 | 286 (the "{} plays" line)
// search box | none | 240x34, right-anchored
// root background | ambient-aware (:31) | surface-main (:26)
// header backgrnd | surface-main (:43) | surface-main (:34)
//
// Everything else is byte-identical, INCLUDING the loading block (spinner 36
// + @tr("Loading…")) and the empty msgid.
//
// NO OFFLINE GATE, deliberately: AppShell.slint mounts both routes while
// offline (:456-470) — neither is in `qobuz-view-blocked` (:112-136) — because
// both read a LOCAL store. Rendering QbzOfflinePlaceholder here would hide
// data that is perfectly available.
//
// NAV BUTTONS: back/forward live in the shell header, so the title starts at
// x: 48 (see DiscoverBrowseView.qml's note).
import QtQuick
import QtQuick.Window
import com.blitzfc.qbz
import "../controls"
import "../theme"
Rectangle {
id: root
QbzTheme { id: theme }
readonly property var doc: {
try {
return JSON.parse(QbzHome.playHistoryJson)
} catch (e) {
return {}
}
}
// The document is the source of truth; `currentView` only covers the
// frame before the first publish lands.
readonly property string mode: doc.mode
|| (QbzShell.currentView === "mostplayedalbums" ? "mostPlayed" : "recent")
readonly property bool mostPlayed: mode === "mostPlayed"
readonly property var items: doc.items || []
readonly property bool ambientOn: theme.ambientOn
// RecentAlbumsView.slint:31 is ambient-aware; MostPlayedAlbumsView
// .slint:26 is plain surface-main.
color: (!mostPlayed && ambientOn) ? "transparent" : theme.surfaceMain
radius: 12
Column {
anchors.fill: parent
spacing: 0
// --- Fixed 56px header --------------------------------------------
Item {
width: parent.width
height: 56
// Unconditional surface-main on BOTH pages (:43 / :34) — these
// two never take the ambient bar alpha the browse pages do.
Rectangle {
anchors.fill: parent
// Top-rounded: full-bleed at y=0 of the content pane. The fill
// stays UNCONDITIONAL per the note above (that is reference
// parity), but the corner is not a colour question — in Slint
// the pane's `clip` follows its radius and cuts this band;
// Qt's clip is a rectangular scissor, so the band squares the
// pane unless it rounds itself. Invisible with the background
// off, where the view root paints the same colour beneath.
topLeftRadius: theme.radiusMd
topRightRadius: theme.radiusMd
color: theme.surfaceMain
}
Text {
x: 48
y: 25 - height / 2
width: Math.max(0, parent.width - 48 - 32 - (root.mostPlayed ? 240 + 16 : 0))
text: root.mostPlayed
? QbzSession.tr("Most Played Albums", QbzSession.trRev)
: QbzSession.tr("Recently Played Albums", QbzSession.trRev)
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
}
// Most Played only: a 240x34 title/artist filter. No clear-X in
// the .slint (:59-104), so the shared search arm is not used here.
Rectangle {
visible: root.mostPlayed
x: parent.width - width - 32
y: 25 - height / 2
width: 240
height: 34
radius: 6
border.width: 1
border.color: theme.borderSubtle
color: theme.surfaceElevated
Row {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
QbzIcon {
name: "search"
width: 14
height: 14
anchors.verticalCenter: parent.verticalCenter
tintName: "muted"
}
Item {
width: parent.width - 14 - 8
height: parent.height
clip: true
TextInput {
id: mpSearch
anchors.fill: parent
color: theme.textPrimary
font.pixelSize: theme.fontBody
verticalAlignment: Text.AlignVCenter
selectByMouse: true
onTextEdited: QbzHome.mostPlayedFilter(text)
// The opener resets the box in the same hop that
// raises `loading`; re-seed while not editing.
Binding {
target: mpSearch
property: "text"
value: root.doc.search || ""
when: !mpSearch.activeFocus
}
}
Text {
visible: mpSearch.text === ""
anchors.fill: parent
text: QbzSession.tr("Search…", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: theme.fontBody
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
// --- Scrolling grid ------------------------------------------------
Item {
width: parent.width
height: parent.height - 56
Flickable {
id: flick
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: page.implicitHeight
boundsBehavior: Flickable.StopAtBounds
Column {
id: page
width: parent.width
leftPadding: 32
rightPadding: 32
topPadding: 8
bottomPadding: 100
spacing: 0
// Spinner + "Loading…" — identical on both pages.
Column {
visible: QbzHome.playHistoryLoading
anchors.horizontalCenter: parent.horizontalCenter
spacing: 12
QbzSpinner {
size: 36
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: QbzSession.tr("Loading…", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 14
horizontalAlignment: Text.AlignHCenter
}
}
Text {
visible: !QbzHome.playHistoryLoading && root.items.length === 0
anchors.horizontalCenter: parent.horizontalCenter
text: QbzSession.tr("Albums you play will appear here.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 14
}
AlbumCollection {
visible: !QbzHome.playHistoryLoading
width: parent.width - 64
albums: root.items
viewMode: "grid"
cardWidth: 200
// The 20px difference IS the "{} plays" line.
cardHeight: root.mostPlayed ? 286 : 266
cardGap: 24
showPlays: root.mostPlayed
flick: flick
contentOffset: 8
}
}
}
QbzScrollBar {
anchors.right: parent.right
anchors.rightMargin: 4
anchors.top: parent.top
anchors.bottom: parent.bottom
target: flick
}
}
}
}