feat(qt): the artist page's radio disc goes live, with a start-up indicator
The disc was btnEnabled: false with a comment saying neither engine had a seam on a Qt bridge. Both do now. ArtistPageView.slint:426-461 opens a two-row dropdown — QBZ Radio (the local qbz-radio pool builder) and Qobuz Radio (/radio/artist) — and that is what this restores. The smart arm already shipped behind Discover's spotlight RADIO card and simply was not wired here; the Qobuz arm is new (get_radio_artist + the same enrich_radio_tracks the album radio uses, because its track objects are just as bare). Both labels already exist in all eight catalogues, so no new msgid. AND THE WAIT IS VISIBLE NOW. A radio is a fetch, an enrich and a queue write; until the first track resolves nothing on screen moves, so the click read as dead. QbzCircleAction gains the `loading` arm the reference already has (CircleAction.slint:18/:77) and QbzHome.radioPending carries the KEY of the radio being built — "album:<id>" or "artist:<id>" — so the Discover rail spins only the station that was clicked instead of all six. The spinner rides QbzShell.pulseMs, NOT a RotationAnimation. Qt Quick has no partial redraws, so a continuous animator presents the whole window at display rate (~1.2% GPU per present on the owner's hybrid laptop); a 60 Hz spin for the seconds a radio takes is exactly the bill the pulse law refuses. 12 degrees per ~30 Hz tick, and the Connections is disabled unless the button is spinning and on screen, so at rest it writes nothing. theme/QbzSpinner.qml still uses a RotationAnimation and is deliberately not reused. On the cards, `loading` also pins the hover disc up: it only exists on hover, so it vanished the moment the pointer left after a click, which is what made the click look swallowed. A RadioGuard disarms the key in Drop, so a bad id, a network error and an empty result all clear it. A disc spinning forever is worse than no indicator. cc
This commit is contained in:
7 files changed
+313
-16
No files matched your search
@@ -46,6 +46,13 @@ Item {
|
||||
/// call sites today, kept separate so a future caller can split them.
|
||||
signal playRequested()
|
||||
|
||||
/// True while THIS card's radio is being built (QbzHome.radioPending).
|
||||
/// The disc stops hiding on pointer-out and its glyph becomes a spinner:
|
||||
/// a station takes a fetch + an enrich + a queue write to start, and the
|
||||
/// tile's own hover affordance vanishing the moment you move the mouse is
|
||||
/// what made the click read as lost.
|
||||
property bool loading: false
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
width: 200
|
||||
@@ -107,22 +114,44 @@ Item {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#000000"
|
||||
opacity: panelArea.containsMouse || playArea.containsMouse ? 0.45 : 0.0
|
||||
opacity: (root.loading || panelArea.containsMouse || playArea.containsMouse)
|
||||
? 0.45 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
Rectangle {
|
||||
id: spinDisc
|
||||
width: 44
|
||||
height: 44
|
||||
radius: 22
|
||||
color: "#ffffff"
|
||||
anchors.centerIn: parent
|
||||
opacity: panelArea.containsMouse || playArea.containsMouse ? 1.0 : 0.0
|
||||
// `root.loading` pins it up: the hover-only rule is right for
|
||||
// an idle tile and wrong for one that is working, because the
|
||||
// pointer leaves the moment the click lands.
|
||||
opacity: (root.loading || panelArea.containsMouse || playArea.containsMouse)
|
||||
? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
// Spinner on the SHARED SHELL PULSE — 12 degrees per ~30 Hz
|
||||
// tick, one turn a second. Never a RotationAnimation: a
|
||||
// continuous animator presents the whole window at display
|
||||
// rate (THE PULSE LAW, qt-frontend/2026-08-11-scenegraph-
|
||||
// batches §9), and a Discover rail can hold several of these.
|
||||
// The Connections is disabled unless this card is the one
|
||||
// loading, so an idle rail writes nothing.
|
||||
property real spinPhase: 0
|
||||
Connections {
|
||||
target: QbzShell
|
||||
enabled: root.loading && root.visible
|
||||
function onPulseMsChanged() {
|
||||
spinDisc.spinPhase = (spinDisc.spinPhase + 12) % 360
|
||||
}
|
||||
}
|
||||
QbzIcon {
|
||||
name: "play-fill"
|
||||
name: root.loading ? "loader-circle" : "play-fill"
|
||||
width: 18
|
||||
height: 18
|
||||
anchors.centerIn: parent
|
||||
rotation: root.loading ? spinDisc.spinPhase : 0
|
||||
tintName: "black"
|
||||
}
|
||||
MouseArea {
|
||||
@@ -130,8 +159,10 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
// The disc is invisible until the panel is hovered, so it
|
||||
// must not swallow the first press before it appears.
|
||||
enabled: parent.opacity > 0
|
||||
// must not swallow the first press before it appears — and
|
||||
// while the station is building there is nothing a second
|
||||
// press could do but start it again.
|
||||
enabled: parent.opacity > 0 && !root.loading
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.playRequested()
|
||||
}
|
||||
@@ -145,6 +176,7 @@ Item {
|
||||
// therefore ON TOP. z pushes it back under.
|
||||
z: -1
|
||||
hoverEnabled: true
|
||||
enabled: !root.loading
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activated()
|
||||
}
|
||||
|
||||
@@ -66,6 +66,13 @@ Rectangle {
|
||||
property bool active: false
|
||||
property bool primary: 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
|
||||
/// port did not, so every slow header action read as a dead click).
|
||||
/// Clicks are gated for the duration: a second click cannot start the
|
||||
/// same work twice, and it cannot re-open a dropdown over a request the
|
||||
/// user already made.
|
||||
property bool loading: false
|
||||
/// Light-on-dark palette for a button painted over artwork/atmosphere
|
||||
/// (CircleAction.slint's default arm; `on-surface: false` there).
|
||||
property bool overlay: false
|
||||
@@ -94,8 +101,55 @@ Rectangle {
|
||||
: (theme.ambientOn ? theme.surfaceElevatedA50 : theme.surfaceElevated)))
|
||||
border.width: primary ? 0 : 1.5
|
||||
border.color: overlay ? "#ccffffff" : theme.borderStrong
|
||||
// A loading button keeps FULL opacity — it is working, not unavailable,
|
||||
// and dimming it would say the opposite of what the spinner says. Only
|
||||
// `btnEnabled` dims.
|
||||
opacity: btnEnabled ? 1.0 : 0.4
|
||||
|
||||
// --- The spinner arm --------------------------------------------------
|
||||
// ON THE SHARED SHELL PULSE, never a RotationAnimation. Qt Quick has no
|
||||
// partial redraws, so any continuous animator presents the WHOLE window at
|
||||
// display rate — ~1.2% GPU per present on the owner's hybrid laptop
|
||||
// (qt-frontend/2026-08-11-scenegraph-batches §9, THE PULSE LAW). A 60 Hz
|
||||
// RotationAnimation for the two-to-five seconds a radio takes to build is
|
||||
// exactly the bill that doctrine exists to refuse; theme/QbzSpinner.qml
|
||||
// still uses one and is NOT reused here for that reason.
|
||||
//
|
||||
// 12 degrees per ~30 Hz pulse = one turn per second, the same rate and the
|
||||
// same arithmetic as the offline manager's downloading glyph
|
||||
// (OfflineManagerView.qml:154) and the transport's loading ring.
|
||||
//
|
||||
// The Connections is DISABLED unless the button is actually spinning and
|
||||
// on screen, so at rest this component writes nothing at all — the other
|
||||
// half of the law.
|
||||
property real spinPhase: 0
|
||||
Connections {
|
||||
target: QbzShell
|
||||
enabled: root.loading && root.visible
|
||||
function onPulseMsChanged() { root.spinPhase = (root.spinPhase + 12) % 360 }
|
||||
}
|
||||
QbzIcon {
|
||||
visible: root.loading
|
||||
name: "loader-circle"
|
||||
// CircleAction.slint:78 `size: diameter * 0.45` — 19.8 on the 44px
|
||||
// primary, 14.4 on the 32px secondary, i.e. deliberately a hair
|
||||
// smaller than the glyph it replaces so the disc does not look fuller
|
||||
// while it waits.
|
||||
width: Math.round(root.width * 0.45)
|
||||
height: Math.round(root.height * 0.45)
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
rotation: root.spinPhase
|
||||
// CircleAction.slint:80-83, the same four-way selector the glyph tint
|
||||
// uses below: dark on the solid white overlay primary, white on the
|
||||
// overlay ghost, themed on-surface.
|
||||
tintName: root.overlay
|
||||
? (root.primary ? "black" : "white")
|
||||
: (root.primary ? theme.accentGlyphTint : root.onSurfaceTint)
|
||||
}
|
||||
|
||||
QbzIcon {
|
||||
visible: !root.loading
|
||||
name: root.name
|
||||
width: root.primary ? 19 : 15
|
||||
height: root.primary ? 19 : 15
|
||||
@@ -140,9 +194,10 @@ Rectangle {
|
||||
MouseArea {
|
||||
id: cbArea
|
||||
anchors.fill: parent
|
||||
enabled: parent.btnEnabled
|
||||
enabled: parent.btnEnabled && !parent.loading
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.btnEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
cursorShape: (parent.btnEnabled && !parent.loading)
|
||||
? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: function (mouse) { parent.clicked(mouse) }
|
||||
}
|
||||
}
|
||||
@@ -860,6 +860,11 @@ Rectangle {
|
||||
QbzCircleAction {
|
||||
name: "radio"
|
||||
overlay: root.hdrOverlay
|
||||
// A radio is a fetch + an enrich + a queue write,
|
||||
// and until the first track resolves nothing on
|
||||
// screen moves. The key is per-album so this disc
|
||||
// spins and the Discover rail's stations do not.
|
||||
loading: QbzHome.radioPending === "album:" + albumHeader.id
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: QbzHome.startAlbumRadio(albumHeader.id)
|
||||
}
|
||||
|
||||
@@ -2258,17 +2258,33 @@ Rectangle {
|
||||
QbzLibrary.libraryToggleFavorite("artist", artist.id)
|
||||
}
|
||||
}
|
||||
// Radio — the .slint opens a QBZ-radio / Qobuz-radio
|
||||
// dropdown; neither engine has a seam on this bridge,
|
||||
// and the dropdown it used to open had two rows that
|
||||
// just closed themselves. DIMMED and inert-by-
|
||||
// declaration until an engine lands.
|
||||
// Radio — ArtistPageView.slint:426-461: the disc opens
|
||||
// a two-row dropdown, QBZ Radio (the local `qbz-radio`
|
||||
// pool builder) vs Qobuz Radio (the curated
|
||||
// `/radio/artist` endpoint).
|
||||
//
|
||||
// IT WAS `btnEnabled: false` — dimmed and inert by
|
||||
// declaration, because at the time neither engine had a
|
||||
// seam on a Qt bridge and the port refuses to render a
|
||||
// control that drives nothing. Both exist now:
|
||||
// `QbzHome.startArtistRadio` (the smart pool, already
|
||||
// shipping behind the Discover spotlight's RADIO card)
|
||||
// and `QbzHome.startQobuzArtistRadio`, added with this
|
||||
// change. So the disc goes live and the dropdown is the
|
||||
// reference's, verbatim.
|
||||
QbzCircleAction {
|
||||
id: radioBtn
|
||||
name: "radio"
|
||||
overlay: root.hdrOverlay
|
||||
btnEnabled: false
|
||||
// BOTH dropdown rows arm the same key: they are
|
||||
// two ways to fill one queue from one disc, and
|
||||
// the disc is what the user is looking at. The
|
||||
// control also gates its own clicks while loading,
|
||||
// so the dropdown cannot be re-opened over a
|
||||
// request that is already running.
|
||||
loading: QbzHome.radioPending === "artist:" + artist.id
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: radioMenu.openBelowLeft(radioBtn)
|
||||
}
|
||||
QbzCircleAction {
|
||||
name: "element-connect"
|
||||
@@ -3371,6 +3387,66 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Radio dropdown (the header disc) ----------------------------------
|
||||
// ArtistPageView.slint:440-460 — 180px, two rows, `close-on-click`. Left
|
||||
// aligned under the trigger (`x: 0px; y: 38px` there), which is what
|
||||
// openBelowLeft does; openBelowRight would hang a 180px panel off to the
|
||||
// LEFT of a 32px disc that sits at the start of the row.
|
||||
QbzContextMenu {
|
||||
id: radioMenu
|
||||
menuWidth: 180
|
||||
Repeater {
|
||||
model: [
|
||||
// Both labels already exist in the catalogue (they are the
|
||||
// TrackContextMenu's), so this adds no new msgid to the eight
|
||||
// locales.
|
||||
{ "label": QbzSession.tr("QBZ Radio", QbzSession.trRev), "action": "qbz" },
|
||||
{ "label": QbzSession.tr("Qobuz Radio", QbzSession.trRev), "action": "qobuz" },
|
||||
]
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
width: parent ? parent.width : 0
|
||||
height: 33
|
||||
radius: 5
|
||||
color: rmiArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
spacing: 8
|
||||
QbzIcon {
|
||||
name: "radio"
|
||||
width: 15
|
||||
height: 15
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: rmiArea.containsMouse ? "textPrimary" : "secondary"
|
||||
}
|
||||
Text {
|
||||
height: parent.height
|
||||
width: parent.width - 23
|
||||
text: modelData.label
|
||||
color: rmiArea.containsMouse ? theme.textPrimary : theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: rmiArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
radioMenu.close()
|
||||
if (modelData.action === "qbz")
|
||||
QbzHome.startArtistRadio(artist.id)
|
||||
else
|
||||
QbzHome.startQobuzArtistRadio(artist.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ⋯ overflow menu ---------------------------------------------------
|
||||
QbzContextMenu {
|
||||
id: overflowMenu
|
||||
|
||||
@@ -964,6 +964,9 @@ Rectangle {
|
||||
// The patch map, not `artPath`: the rail's document is
|
||||
// published ONCE and covers arrive per-URL.
|
||||
artSource: root.forYouArtOf(modelData)
|
||||
// Per-station key, so the tile that was clicked is the
|
||||
// only one in the rail that spins.
|
||||
loading: QbzHome.radioPending === "album:" + modelData.albumId
|
||||
// Both the card body and the hover disc start the
|
||||
// album radio (the .slint wires `clicked` and `play`
|
||||
// to the same callback).
|
||||
@@ -1148,6 +1151,11 @@ Rectangle {
|
||||
seedSubtitle: QbzSession.tr("Qobuz Radio Station", QbzSession.trRev)
|
||||
label: QbzSession.tr("RADIO", QbzSession.trRev)
|
||||
artSource: root.forYouArtOf(spot.doc)
|
||||
// The smart pool builder is the slowest radio in
|
||||
// the app; without this the tile went back to its
|
||||
// resting state the instant the pointer left.
|
||||
loading: QbzHome.radioPending
|
||||
=== "artist:" + (spot.doc.artistId || "")
|
||||
onActivated: QbzHome.startArtistRadio(spot.doc.artistId || "")
|
||||
onPlayRequested: QbzHome.startArtistRadio(spot.doc.artistId || "")
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
//! | Spotlight | `core().get_artist_page()` (+ `artist_qt::map_release` shape) |
|
||||
//! | album radio | `core().get_radio_album()` + `get_tracks_batch()` enrich |
|
||||
//! | artist radio | `core().create_smart_artist_radio()` (the `qbz-radio` pool builder) |
|
||||
//! | qobuz artist radio | `core().get_radio_artist()` + `get_tracks_batch()` enrich |
|
||||
//! | artist top tracks| `playback_qt::play_artist()` |
|
||||
//!
|
||||
//! ## POC-NOTEs (deliberate deltas vs the Slint build)
|
||||
@@ -489,11 +490,52 @@ pub(crate) fn spotlight_play_top_tracks(artist_id: String) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Spotlight RADIO card — `media-action("artist", id, "radio")`, i.e. the
|
||||
/// SMART pool builder (`qbz-radio`), not `/radio/artist`.
|
||||
// ---------------------------------------------------------------------------
|
||||
// Radio start-up indicator (QbzHome.radioPending)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Publish "a radio is being built for this key" / "nothing is".
|
||||
///
|
||||
/// EVERY radio entry point below arms this on the way in and disarms it on
|
||||
/// EVERY way out — the success path, the empty-result path and the error path
|
||||
/// alike. A leaked key is worse than no indicator at all: it is a disc that
|
||||
/// spins forever on a page the user is still looking at, and the only two
|
||||
/// exits from a network call are "it worked" and "it did not".
|
||||
///
|
||||
/// `RadioGuard` is what makes that structural rather than a rule three
|
||||
/// functions have to remember: it disarms in `Drop`, so an early `return` — or
|
||||
/// a future cancelled before it finishes — cannot skip it.
|
||||
fn set_radio_pending(key: &str) {
|
||||
crate::home_bridge::publish_radio_pending(key.to_string());
|
||||
}
|
||||
|
||||
/// Armed for the life of one radio build; disarms itself however the build
|
||||
/// ends. Held across the `.await`s, so it is dropped on the same task.
|
||||
struct RadioGuard;
|
||||
|
||||
impl RadioGuard {
|
||||
fn arm(key: &str) -> Self {
|
||||
set_radio_pending(key);
|
||||
RadioGuard
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RadioGuard {
|
||||
fn drop(&mut self) {
|
||||
set_radio_pending("");
|
||||
}
|
||||
}
|
||||
|
||||
/// Spotlight RADIO card and the artist page's "QBZ Radio" row —
|
||||
/// `media-action("artist", id, "radio")` / `"radio-qbz"`, i.e. the SMART pool
|
||||
/// builder (`qbz-radio`), not `/radio/artist`.
|
||||
pub(crate) fn start_artist_radio(artist_id: String) {
|
||||
let runtime = crate::app();
|
||||
crate::spawn(async move {
|
||||
// Armed BEFORE the parse: a bad id returns immediately and the guard's
|
||||
// Drop clears it in the same turn, so the indicator never flickers,
|
||||
// but the arming does not have to be duplicated after every early exit.
|
||||
let _pending = RadioGuard::arm(&format!("artist:{artist_id}"));
|
||||
let Ok(id) = artist_id.parse::<u64>() else {
|
||||
log::warn!("[qbz-qt] smart radio: bad artist id {artist_id}");
|
||||
return;
|
||||
@@ -506,6 +548,36 @@ pub(crate) fn start_artist_radio(artist_id: String) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The "Qobuz Radio" arm of the ARTIST PAGE's radio dropdown —
|
||||
/// `media-action("artist", id, "radio-qobuz")` in Slint
|
||||
/// (`ArtistPageView.slint:457` -> `main.rs:12906` ->
|
||||
/// `playback::play_artist_radio`). The plain Qobuz `/radio/artist` endpoint,
|
||||
/// as opposed to `start_artist_radio` above, which is the SMART `qbz-radio`
|
||||
/// pool builder wired to the dropdown's other row.
|
||||
///
|
||||
/// Its track objects are MINIMAL, exactly like the album radio's, so they go
|
||||
/// through the same `enrich_radio_tracks` re-fetch — without it the queue rows
|
||||
/// show no artist.
|
||||
pub(crate) fn start_qobuz_artist_radio(artist_id: String) {
|
||||
let runtime = crate::app();
|
||||
crate::spawn(async move {
|
||||
// Same key as the smart arm: they are two rows of ONE dropdown on ONE
|
||||
// disc, and that disc is what spins.
|
||||
let _pending = RadioGuard::arm(&format!("artist:{artist_id}"));
|
||||
match runtime.core().get_radio_artist(&artist_id).await {
|
||||
Ok(resp) => {
|
||||
let tracks = enrich_radio_tracks(&runtime, resp.tracks.items).await;
|
||||
if tracks.is_empty() {
|
||||
log::warn!("[qbz-qt] qobuz artist radio {artist_id} returned no tracks");
|
||||
return;
|
||||
}
|
||||
play_flat(&runtime, tracks, 0, false).await;
|
||||
}
|
||||
Err(e) => log::warn!("[qbz-qt] qobuz artist radio {artist_id} failed: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Radio Stations tile — `media-action("album", id, "radio")`, the Qobuz
|
||||
/// `/radio/album` endpoint. Its track objects are MINIMAL (no performer, no
|
||||
/// album), so they are re-fetched by id exactly like `playback.rs::
|
||||
@@ -513,6 +585,7 @@ pub(crate) fn start_artist_radio(artist_id: String) {
|
||||
pub(crate) fn start_album_radio(album_id: String) {
|
||||
let runtime = crate::app();
|
||||
crate::spawn(async move {
|
||||
let _pending = RadioGuard::arm(&format!("album:{album_id}"));
|
||||
match runtime.core().get_radio_album(&album_id).await {
|
||||
Ok(resp) => {
|
||||
let tracks = enrich_radio_tracks(&runtime, resp.tracks.items).await;
|
||||
|
||||
@@ -113,6 +113,26 @@ pub mod qbz_home {
|
||||
#[qproperty(QString, award_albums_json)]
|
||||
#[qproperty(bool, award_albums_loading)]
|
||||
|
||||
// --- Radio start-up indicator (src/foryou_qt.rs) -------------------
|
||||
// "" while idle, else the KEY of the radio currently being built:
|
||||
// "album:<id>" or "artist:<id>". A radio is not one request — it is a
|
||||
// fetch, then a `get_tracks_batch` enrich (or the whole `qbz-radio`
|
||||
// pool build), then the queue write — and until the first track
|
||||
// resolves the UI has nothing to show, so the disc that was clicked
|
||||
// read as a dead affordance. The button spins on this.
|
||||
//
|
||||
// A KEY, not a bool: the artist page and the album page can both be a
|
||||
// back-step away from each other, and the Discover radio rail draws
|
||||
// one disc per station. A bool would spin every one of them.
|
||||
//
|
||||
// ONE AT A TIME by construction — the key is overwritten, not stacked.
|
||||
// That matches the domain (starting a radio replaces the queue, so a
|
||||
// second one while the first is in flight is not a thing to support)
|
||||
// and it means the LAST click owns the indicator.
|
||||
//
|
||||
// READ + NOTIFY: only `foryou_qt` writes it, always in pairs.
|
||||
#[qproperty(QString, radio_pending, READ, NOTIFY)]
|
||||
|
||||
type QbzHome = super::QbzHomeRust;
|
||||
|
||||
/// Registers this object's Qt-thread hop (Main.qml boots EVERY
|
||||
@@ -238,9 +258,19 @@ pub mod qbz_home {
|
||||
/// Radio Stations tile — Qobuz `/radio/album` off the seed album.
|
||||
#[qinvokable]
|
||||
fn start_album_radio(self: Pin<&mut QbzHome>, album_id: QString);
|
||||
/// Spotlight RADIO card — the smart `qbz-radio` artist pool.
|
||||
/// Spotlight RADIO card — the smart `qbz-radio` artist pool. Also the
|
||||
/// "QBZ Radio" row of the artist page's radio dropdown: Slint routes
|
||||
/// both to the same `play_smart_artist_radio` (main.rs:12900).
|
||||
#[qinvokable]
|
||||
fn start_artist_radio(self: Pin<&mut QbzHome>, artist_id: QString);
|
||||
/// The artist page's "Qobuz Radio" row — the plain `/radio/artist`
|
||||
/// endpoint. It lives on THIS bridge and not on QbzArtist because its
|
||||
/// implementation is `foryou_qt`'s, beside the smart twin and the album
|
||||
/// radio it shares `enrich_radio_tracks` + `play_flat` with; splitting
|
||||
/// the pair across two bridges to match the calling VIEW would put one
|
||||
/// half of one dropdown in each.
|
||||
#[qinvokable]
|
||||
fn start_qobuz_artist_radio(self: Pin<&mut QbzHome>, artist_id: QString);
|
||||
/// Spotlight play / TOP TRACKS card — the artist's popular tracks.
|
||||
#[qinvokable]
|
||||
fn play_artist_top_tracks(self: Pin<&mut QbzHome>, artist_id: QString);
|
||||
@@ -373,6 +403,7 @@ pub struct QbzHomeRust {
|
||||
award_loading: bool,
|
||||
award_albums_json: QString,
|
||||
award_albums_loading: bool,
|
||||
radio_pending: QString,
|
||||
}
|
||||
|
||||
impl Default for QbzHomeRust {
|
||||
@@ -415,6 +446,7 @@ impl Default for QbzHomeRust {
|
||||
award_loading: false,
|
||||
award_albums_json: QString::from("{}"),
|
||||
award_albums_loading: false,
|
||||
radio_pending: QString::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,6 +468,18 @@ static QT_THREAD: OnceLock<CxxQtThread<QbzHome>> = OnceLock::new();
|
||||
/// document that was never built. Three builds went into proving that was NOT
|
||||
/// what was happening during the 2026-08-17 launch freeze; a single warn line
|
||||
/// would have answered it in one.
|
||||
/// Publish `radio_pending` (see the property's note). The property is
|
||||
/// READ + NOTIFY — no generated setter — and its backing field is private to
|
||||
/// this module, so the store + notify lives HERE and `foryou_qt` calls this.
|
||||
/// Same shape as `shader_scene_bridge::publish_pack`.
|
||||
pub(crate) fn publish_radio_pending(key: String) {
|
||||
ui(move |mut b| {
|
||||
use cxx_qt::CxxQtType as _;
|
||||
b.as_mut().rust_mut().radio_pending = QString::from(key.as_str());
|
||||
b.as_mut().radio_pending_changed();
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn ui(f: impl FnOnce(Pin<&mut QbzHome>) + Send + 'static) {
|
||||
match QT_THREAD.get() {
|
||||
Some(thread) => {
|
||||
@@ -644,6 +688,10 @@ impl qbz_home::QbzHome {
|
||||
crate::foryou_qt::start_artist_radio(artist_id.to_string());
|
||||
}
|
||||
|
||||
pub fn start_qobuz_artist_radio(self: Pin<&mut Self>, artist_id: QString) {
|
||||
crate::foryou_qt::start_qobuz_artist_radio(artist_id.to_string());
|
||||
}
|
||||
|
||||
pub fn play_artist_top_tracks(self: Pin<&mut Self>, artist_id: QString) {
|
||||
crate::foryou_qt::spotlight_play_top_tracks(artist_id.to_string());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user