Files
qbz/crates/qbz-qt/qml/cards/MixArtwork.qml
T
vicrodh 0c159c141c feat(qt): For You gains Qobuz Mixes pages, Artist Spotlight and Radios
Ports the three sections the tab was missing, against the Slint
implementation rather than invented: the section rails, the cards, and the
mix landing page with its navigation seam so back/forward behave.

The mixes reuse the shared crates for their requests -- the Slint app
already makes them, so nothing new talks to Qobuz here.

Also carries the qt-run.sh portability work needed for the Mac mini:
`free`, `nproc`, `pgrep` and `timeout` all differ or are simply absent on
BSD/macOS. Two of those bit during the first Mac build. `pgrep` exits 1
when it matches nothing -- the normal case -- and under `pipefail` that
killed the script before a single line of output, which reads exactly like
a build that did nothing. And `timeout` does not exist on macOS at all, so
the smoke produced a one-line log over which the grep cheerfully reported
"0 QML complaints": a test that never ran, rendering as a pass. The smoke
now fails outright on a log that short.

cc
2026-07-29 16:26:11 -06:00

117 lines
4.5 KiB
QML

// The Qobuz-mix gradient square — ONE definition of the four mix identities
// (colours, badge, display name), used by BOTH surfaces that draw them:
//
// views/HomeView.qml MixesRail — 220px tile, badge + 22px name
// views/MixView.qml header — 224px art, no badge, 34px name
//
// In the Slint build those two live in different files and each re-declares
// the same four gradients (discover/QobuzMixesRow.slint:79-104 and
// mix/MixView.slint:63-70). Porting that duplication would mean a colour
// changed on the tile and not on the page it opens, so the port keeps one
// source (track rule 5).
//
// GRADIENT DELTA, inherited from the existing MixesRail: the .slint uses
// `@linear-gradient(135deg, a 0%, b 50%, c 100%)` and QML's Gradient has no
// angle — only Vertical / Horizontal. The vertical 3-stop plus a
// top-left-to-bottom-right darkening overlay is the approximation the POC
// already shipped for the tiles; the page header now uses the same one, so
// the two match each other exactly.
import QtQuick
import com.blitzfc.qbz
import "../theme"
Item {
id: root
/// "daily" | "weekly" | "fav" | "top".
property string kind: "daily"
property int size: 220
property int titleSize: 22
property int cornerRadius: 8
property bool showBadge: true
/// Whether the square reacts to the pointer at all (the page header does
/// not — it is not a link to anywhere).
property bool interactive: true
signal clicked()
QbzTheme { id: theme }
width: root.size
height: root.size
// QobuzMixesRow.slint:79-104 — the badge is the SERVICE the mix comes
// from ("qobuz" = Qobuz's own dynamic mixes, "qbz" = built locally from
// the user's library / playlists), which is why it is not uniform.
readonly property var _spec: ({
"daily": { "badge": "qobuz", "name": "DailyQ",
"c0": "#1e3a8a", "c1": "#6366f1", "c2": "#c084fc" },
"weekly": { "badge": "qobuz", "name": "WeeklyQ",
"c0": "#065f46", "c1": "#10b981", "c2": "#fbbf24" },
"fav": { "badge": "qbz", "name": "FavQ",
"c0": "#7f1d1d", "c1": "#ef4444", "c2": "#fb923c" },
"top": { "badge": "qbz", "name": "TopQ",
"c0": "#1f2937", "c1": "#4b5563", "c2": "#fbbf24" }
})
readonly property var spec: root._spec[root.kind] !== undefined
? root._spec[root.kind] : root._spec["daily"]
readonly property string mixName: root.spec.name
readonly property string badge: root.spec.badge
/// The four tile descriptions (QobuzMixesRow.slint:82-103). NOTE these
/// are NOT the mix PAGE's subtitles — `mix.rs::mix_meta` uses a different
/// string for `weekly` and `top`, and the page reads its own from Rust.
function tileDescription(k) {
return k === "daily"
? QbzSession.tr("Elevate your day with a customized selection of music.", QbzSession.trRev)
: k === "weekly"
? QbzSession.tr("Take a weekly journey with a fresh mix every Friday.", QbzSession.trRev)
: k === "fav"
? QbzSession.tr("A fresh shuffle from your personal library.", QbzSession.trRev)
: QbzSession.tr("Discover new music from your most-played playlists.", QbzSession.trRev)
}
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
gradient: Gradient {
GradientStop { position: 0.0; color: root.spec.c0 }
GradientStop { position: 0.5; color: root.spec.c1 }
GradientStop { position: 1.0; color: root.spec.c2 }
}
// The 135deg sweep, approximated (see the header note).
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
gradient: Gradient {
GradientStop { position: 0.0; color: "#00000000" }
GradientStop { position: 1.0; color: "#33000000" }
}
}
Text {
visible: root.showBadge
x: 12
y: 12
text: root.badge
color: "#ccffffff"
font.pixelSize: 10
font.weight: theme.weightSemibold
font.letterSpacing: 1
}
Text {
anchors.centerIn: parent
text: root.mixName
color: "#ffffff"
font.pixelSize: root.titleSize
font.weight: theme.weightBold
}
MouseArea {
anchors.fill: parent
enabled: root.interactive
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}
}