refactor(qbz-qt): split QbzAlbum and QbzArtist out of the God-object
Two singletons rather than one merged object, per the owner: each maps to the view it backs. Registering them and deleting the duplicates from bridge.rs is one change, because wiring one without removing the other compiles fine and leaves two independent states with the view frozen and nothing in the log. view_param_id goes with them. It had no consumer in QML, verified, and none in Rust once the two opens stopped writing it — so it is deleted rather than carried into a new bridge for symmetry's sake. bridge.rs is down to three domains: settings, search and playlist. cc
This commit is contained in:
14 files changed
+273
-104
No files matched your search
@@ -35,7 +35,7 @@ fn main() {
|
||||
.qt_module("QuickControls2")
|
||||
.qml_module(QmlModule {
|
||||
uri: "com.blitzfc.qbz",
|
||||
rust_files: &["src/bridge.rs", "src/session_bridge.rs", "src/shell_bridge.rs", "src/player_bridge.rs", "src/queue_bridge.rs", "src/home_bridge.rs", "src/viz_bridge.rs", "src/local_bridge.rs", "src/library_bridge.rs"],
|
||||
rust_files: &["src/bridge.rs", "src/session_bridge.rs", "src/shell_bridge.rs", "src/player_bridge.rs", "src/queue_bridge.rs", "src/home_bridge.rs", "src/viz_bridge.rs", "src/local_bridge.rs", "src/library_bridge.rs", "src/album_bridge.rs", "src/artist_bridge.rs"],
|
||||
qml_files: &[
|
||||
"qml/LoginScreen.qml",
|
||||
"qml/Main.qml",
|
||||
|
||||
@@ -53,6 +53,8 @@ ApplicationWindow {
|
||||
QbzViz.boot()
|
||||
QbzLocal.boot()
|
||||
QbzLibrary.boot()
|
||||
QbzAlbum.boot()
|
||||
QbzArtist.boot()
|
||||
QbzBridge.boot()
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ Rectangle {
|
||||
// --- Local Library mode (additive; default = the Qobuz behaviour) ----
|
||||
// The Local Library mounts THIS card, but its `albumId` is a group key
|
||||
// (folder or metadata identity), not a Qobuz catalog id — routing it
|
||||
// through QbzBridge.openAlbum / QbzPlayer.playAlbum would fire a
|
||||
// through QbzAlbum.openAlbum / QbzPlayer.playAlbum would fire a
|
||||
// catalog fetch for a folder path. In localMode every action is emitted
|
||||
// to the host instead, and the catalog-only affordances (heart, pin,
|
||||
// "Block this album") are hidden. Nothing else about the card changes,
|
||||
@@ -131,7 +131,7 @@ Rectangle {
|
||||
// Phase 8: the card opens the album view (the overlay play
|
||||
// button carries the play affordance).
|
||||
onClicked: root.localMode ? root.openRequested()
|
||||
: QbzBridge.openAlbum(root.albumId)
|
||||
: QbzAlbum.openAlbum(root.albumId)
|
||||
}
|
||||
|
||||
// Pin badge — top-right. Hover-revealed like the overlay
|
||||
@@ -267,7 +267,7 @@ Rectangle {
|
||||
else if (a === "queue") root.enqueueRequested("later")
|
||||
return
|
||||
}
|
||||
if (a === "open") QbzBridge.openAlbum(root.albumId)
|
||||
if (a === "open") QbzAlbum.openAlbum(root.albumId)
|
||||
else if (a === "play") QbzPlayer.playAlbum(root.albumId)
|
||||
else if (a === "next") QbzPlayer.enqueueAlbum(root.albumId, "next")
|
||||
else if (a === "queue") QbzPlayer.enqueueAlbum(root.albumId, "later")
|
||||
@@ -362,7 +362,7 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.localMode ? root.openRequested()
|
||||
: QbzBridge.openAlbum(root.albumId)
|
||||
: QbzAlbum.openAlbum(root.albumId)
|
||||
}
|
||||
}
|
||||
Text {
|
||||
@@ -379,7 +379,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.artistId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (root.artistId !== "") QbzBridge.openArtist(root.artistId)
|
||||
onClicked: if (root.artistId !== "") QbzArtist.openArtist(root.artistId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzBridge.openArtist(root.item.id)
|
||||
onClicked: QbzArtist.openArtist(root.item.id)
|
||||
}
|
||||
// Hover overlay — follow? / play / more (y=113).
|
||||
CardOverlayRow {
|
||||
@@ -148,7 +148,7 @@ Rectangle {
|
||||
return m
|
||||
}
|
||||
onPicked: function (a) {
|
||||
if (a === "open") QbzBridge.openArtist(root.item.id)
|
||||
if (a === "open") QbzArtist.openArtist(root.item.id)
|
||||
else if (a === "play") QbzPlayer.playArtistCard(root.item.id)
|
||||
// "follow": no artist-follow API (POC-NOTE — inert).
|
||||
// "not-interested": the reco dismissal store is not
|
||||
@@ -210,7 +210,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzBridge.openArtist(root.item.id)
|
||||
onClicked: QbzArtist.openArtist(root.item.id)
|
||||
}
|
||||
}
|
||||
Text {
|
||||
|
||||
@@ -75,6 +75,6 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// Phase 8: slim rows are albums — click opens the album view.
|
||||
onClicked: QbzBridge.openAlbum(card.id)
|
||||
onClicked: QbzAlbum.openAlbum(card.id)
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.item.artistId ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (root.item.artistId) QbzBridge.openArtist(root.item.artistId)
|
||||
onClicked: if (root.item.artistId) QbzArtist.openArtist(root.item.artistId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,8 +189,8 @@ Rectangle {
|
||||
else if (a === "next") QbzPlayer.enqueueTrack(root.item.id, "next")
|
||||
else if (a === "later") QbzPlayer.enqueueTrack(root.item.id, "later")
|
||||
else if (a === "queue") QbzPlayer.enqueueTrack(root.item.id, "queue")
|
||||
else if (a === "go-artist") QbzBridge.openArtist(root.item.artistId)
|
||||
else if (a === "go-album") QbzBridge.openAlbum(root.item.albumId)
|
||||
else if (a === "go-artist") QbzArtist.openArtist(root.item.artistId)
|
||||
else if (a === "go-album") QbzAlbum.openAlbum(root.item.albumId)
|
||||
else if (a === "favorite") {
|
||||
root.item.isFavorite = !root.item.isFavorite
|
||||
QbzLibrary.libraryToggleFavorite("track", root.item.id)
|
||||
|
||||
@@ -185,7 +185,7 @@ Rectangle {
|
||||
enabled: root.artistLink && !!root.item.artistId
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: QbzBridge.openArtist(root.item.artistId)
|
||||
onClicked: QbzArtist.openArtist(root.item.artistId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.item.albumId ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (root.item.albumId) QbzBridge.openAlbum(root.item.albumId)
|
||||
onClicked: if (root.item.albumId) QbzAlbum.openAlbum(root.item.albumId)
|
||||
}
|
||||
}
|
||||
// Duration.
|
||||
@@ -325,8 +325,8 @@ Rectangle {
|
||||
else if (a === "next") root.enqueueRequested("next")
|
||||
else if (a === "later") root.enqueueRequested("later")
|
||||
else if (a === "queue") root.enqueueRequested("queue")
|
||||
else if (a === "go-artist") QbzBridge.openArtist(root.item.artistId)
|
||||
else if (a === "go-album") QbzBridge.openAlbum(root.item.albumId)
|
||||
else if (a === "go-artist") QbzArtist.openArtist(root.item.artistId)
|
||||
else if (a === "go-album") QbzAlbum.openAlbum(root.item.albumId)
|
||||
else if (a === "favorite") {
|
||||
root.item.isFavorite = !root.item.isFavorite
|
||||
QbzLibrary.libraryToggleFavorite("track", root.item.id)
|
||||
|
||||
@@ -34,7 +34,7 @@ Rectangle {
|
||||
QbzTheme { id: theme }
|
||||
|
||||
// The view's album + url-keyed cover map (artwork pipeline).
|
||||
readonly property var album: JSON.parse(QbzBridge.albumJson)
|
||||
readonly property var album: JSON.parse(QbzAlbum.albumJson)
|
||||
readonly property var header: album.header || ({})
|
||||
readonly property var tracks: album.tracks || []
|
||||
readonly property var awards: album.awards || []
|
||||
@@ -234,7 +234,7 @@ Rectangle {
|
||||
enabled: modelData[1] !== ""
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData[1] !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: QbzBridge.openArtist(modelData[1])
|
||||
onClicked: QbzArtist.openArtist(modelData[1])
|
||||
}
|
||||
}
|
||||
Text {
|
||||
@@ -384,7 +384,7 @@ Rectangle {
|
||||
|
||||
// Loading.
|
||||
Item {
|
||||
visible: QbzBridge.albumLoading && tracks.length === 0
|
||||
visible: QbzAlbum.albumLoading && tracks.length === 0
|
||||
width: parent.width
|
||||
height: 280
|
||||
Column {
|
||||
@@ -402,7 +402,7 @@ Rectangle {
|
||||
|
||||
// Toolbar — quality badge + track search (+ inert select).
|
||||
Row {
|
||||
visible: !QbzBridge.albumLoading
|
||||
visible: !QbzAlbum.albumLoading
|
||||
width: parent.width
|
||||
height: 52
|
||||
spacing: 16
|
||||
@@ -494,7 +494,7 @@ Rectangle {
|
||||
|
||||
// Column header.
|
||||
Row {
|
||||
visible: !QbzBridge.albumLoading
|
||||
visible: !QbzAlbum.albumLoading
|
||||
width: parent.width
|
||||
height: 40
|
||||
leftPadding: 12
|
||||
@@ -567,7 +567,7 @@ Rectangle {
|
||||
enabled: modelData.workComposerId !== ""
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.workComposerId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: QbzBridge.openArtist(modelData.workComposerId)
|
||||
onClicked: QbzArtist.openArtist(modelData.workComposerId)
|
||||
}
|
||||
}
|
||||
Text {
|
||||
|
||||
@@ -35,7 +35,7 @@ Rectangle {
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
readonly property var artist: JSON.parse(QbzBridge.artistJson)
|
||||
readonly property var artist: JSON.parse(QbzArtist.artistJson)
|
||||
readonly property var topTracks: artist.topTracks || []
|
||||
readonly property var appearsOn: artist.appearsOn || []
|
||||
readonly property var releaseSections: artist.releaseSections || []
|
||||
@@ -67,7 +67,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
// Two blocks, not one: artwork is QbzLibrary's signal and the releases
|
||||
// pager is still QbzBridge's. Retargeting a mixed block wholesale would
|
||||
// pager is QbzArtist's. Retargeting a mixed block wholesale would
|
||||
// silently orphan the other half — QML resolves handlers lazily, so the
|
||||
// discography would just stop loading with nothing in the log.
|
||||
Connections {
|
||||
@@ -80,7 +80,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: QbzBridge
|
||||
target: QbzArtist
|
||||
function onReleaseSectionReady(releaseType, cardsJson, hasMore) {
|
||||
var cards = JSON.parse(cardsJson)
|
||||
var sections = root.releaseSections
|
||||
@@ -273,7 +273,7 @@ Rectangle {
|
||||
enabled: row.albumId !== ""
|
||||
hoverEnabled: true
|
||||
cursorShape: row.albumId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: QbzBridge.openAlbum(row.albumId)
|
||||
onClicked: QbzAlbum.openAlbum(row.albumId)
|
||||
}
|
||||
}
|
||||
Text {
|
||||
@@ -501,7 +501,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzBridge.loadReleaseSection(artist.id, section.releaseType, section.cards.length)
|
||||
onClicked: QbzArtist.loadReleaseSection(artist.id, section.releaseType, section.cards.length)
|
||||
}
|
||||
}
|
||||
Item { width: (parent.width - loadMoreBtn.width) / 2; height: 1 }
|
||||
@@ -740,7 +740,7 @@ Rectangle {
|
||||
|
||||
// --- Loading -------------------------------------------------
|
||||
Item {
|
||||
visible: QbzBridge.artistLoading && topTracks.length === 0
|
||||
visible: QbzArtist.artistLoading && topTracks.length === 0
|
||||
width: parent.width - 64
|
||||
height: 280
|
||||
Column {
|
||||
@@ -759,7 +759,7 @@ Rectangle {
|
||||
// ================= Catalog tab ================================
|
||||
Column {
|
||||
id: sectionAnchors
|
||||
visible: root.artistTab === "catalog" && !QbzBridge.artistLoading
|
||||
visible: root.artistTab === "catalog" && !QbzArtist.artistLoading
|
||||
width: parent.width - 64
|
||||
spacing: 0
|
||||
|
||||
@@ -1050,7 +1050,7 @@ Rectangle {
|
||||
// ================= In library tab =============================
|
||||
Column {
|
||||
id: libraryTab
|
||||
visible: root.artistTab === "library" && !QbzBridge.artistLoading
|
||||
visible: root.artistTab === "library" && !QbzArtist.artistLoading
|
||||
width: parent.width - 64
|
||||
spacing: 0
|
||||
readonly property var libItems: {
|
||||
@@ -1265,7 +1265,7 @@ Rectangle {
|
||||
}
|
||||
// SIMILAR ARTISTS.
|
||||
Column {
|
||||
visible: similarArtists.length > 0 || QbzBridge.artistLoading
|
||||
visible: similarArtists.length > 0 || QbzArtist.artistLoading
|
||||
width: parent.width
|
||||
leftPadding: 14
|
||||
rightPadding: 14
|
||||
@@ -1280,7 +1280,7 @@ Rectangle {
|
||||
font.letterSpacing: 0.5
|
||||
}
|
||||
Text {
|
||||
visible: similarArtists.length === 0 && QbzBridge.artistLoading
|
||||
visible: similarArtists.length === 0 && QbzArtist.artistLoading
|
||||
text: QbzSession.tr("Loading…", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
@@ -1290,7 +1290,7 @@ Rectangle {
|
||||
delegate: SidebarLink {
|
||||
label: modelData.name
|
||||
iconName: "user"
|
||||
onClicked: QbzBridge.openArtist(modelData.id)
|
||||
onClicked: QbzArtist.openArtist(modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,8 +311,8 @@ Rectangle {
|
||||
else if (a === "next") QbzPlayer.enqueueTrack(item.id, "next")
|
||||
else if (a === "later") QbzPlayer.enqueueTrack(item.id, "later")
|
||||
else if (a === "queue") QbzPlayer.enqueueTrack(item.id, "queue")
|
||||
else if (a === "go-artist") QbzBridge.openArtist(item.artistId)
|
||||
else if (a === "go-album") QbzBridge.openAlbum(item.albumId)
|
||||
else if (a === "go-artist") QbzArtist.openArtist(item.artistId)
|
||||
else if (a === "go-album") QbzAlbum.openAlbum(item.albumId)
|
||||
else if (a === "favorite") {
|
||||
item.isFavorite = !item.isFavorite
|
||||
QbzLibrary.libraryToggleFavorite("track", item.id)
|
||||
@@ -335,8 +335,8 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (item.kind === "track") QbzPlayer.playTrack(item.id)
|
||||
else if (item.kind === "album") QbzBridge.openAlbum(item.id)
|
||||
else if (item.kind === "artist") QbzBridge.openArtist(item.id)
|
||||
else if (item.kind === "album") QbzAlbum.openAlbum(item.id)
|
||||
else if (item.kind === "artist") QbzArtist.openArtist(item.id)
|
||||
// playlist/label pages: out of scope (POC-NOTE).
|
||||
}
|
||||
}
|
||||
@@ -418,8 +418,8 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (item.kind === "track") QbzPlayer.playTrack(item.id)
|
||||
else if (item.kind === "album") QbzBridge.openAlbum(item.id)
|
||||
else if (item.kind === "artist") QbzBridge.openArtist(item.id)
|
||||
else if (item.kind === "album") QbzAlbum.openAlbum(item.id)
|
||||
else if (item.kind === "artist") QbzArtist.openArtist(item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,7 +439,7 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: (item.artistId !== "" && (item.kind === "track" || item.kind === "album"))
|
||||
? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (item.artistId !== "") QbzBridge.openArtist(item.artistId)
|
||||
onClicked: if (item.artistId !== "") QbzArtist.openArtist(item.artistId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,12 +549,12 @@ Rectangle {
|
||||
onPicked: function (a) {
|
||||
if (item.kind === "track") { root.trackAction(item, a); return }
|
||||
if (a === "open") {
|
||||
if (item.kind === "album") QbzBridge.openAlbum(item.id)
|
||||
if (item.kind === "album") QbzAlbum.openAlbum(item.id)
|
||||
// playlist/label pages: out of scope (POC-NOTE).
|
||||
} else if (a === "play") {
|
||||
if (item.kind === "album") QbzPlayer.playAlbum(item.id)
|
||||
} else if (a === "go-artist") {
|
||||
QbzBridge.openArtist(item.id)
|
||||
QbzArtist.openArtist(item.id)
|
||||
} else if (a === "favorite") {
|
||||
item.isFavorite = !item.isFavorite
|
||||
QbzLibrary.libraryToggleFavorite(item.kind, item.id)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! QbzAlbum — Album detail view domain bridge (phase 23 split of the
|
||||
//! QbzBridge God-object; the pattern is documented in main.rs).
|
||||
//!
|
||||
//! The module is `qbz_album_bridge`, not `qbz_album`: bridge modules are
|
||||
//! never named after a workspace crate (the `qbz_library` collision is the
|
||||
//! documented precedent). The QML type name comes from the QObject
|
||||
//! (`QbzAlbum`), not from the module, so QML is unaffected.
|
||||
//!
|
||||
//! Props: the one album-view JSON document + its loading flag.
|
||||
//! Invokable: openAlbum (nav push + fetch + publish).
|
||||
//!
|
||||
//! The album/artist domains are TWO singletons on purpose (owner decision):
|
||||
//! one per view, so a view's state can never be perturbed by the other's
|
||||
//! fetch. `view_param_id` did NOT come along — see artist_bridge.rs for the
|
||||
//! deletion rationale; the id QML needs is already inside `albumJson`
|
||||
//! (`AlbumHeader.id`).
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use cxx_qt::CxxQtThread;
|
||||
use cxx_qt::Threading as _;
|
||||
use cxx_qt_lib::QString;
|
||||
|
||||
#[cxx_qt::bridge]
|
||||
pub mod qbz_album_bridge {
|
||||
extern "C++" {
|
||||
include!("cxx-qt-lib/qstring.h");
|
||||
type QString = cxx_qt_lib::QString;
|
||||
}
|
||||
|
||||
#[auto_cxx_name]
|
||||
extern "RustQt" {
|
||||
#[qobject]
|
||||
#[qml_element]
|
||||
#[qml_singleton]
|
||||
// --- Album detail view ---------------------------------------------
|
||||
#[qproperty(bool, album_loading)]
|
||||
// ONE JSON document (album_qt.rs AlbumViewData: header + track rows
|
||||
// + works/goodies/related buckets); QML JSON.parse()s it once.
|
||||
#[qproperty(QString, album_json)]
|
||||
|
||||
type QbzAlbum = super::QbzAlbumRust;
|
||||
|
||||
/// Registers this object's Qt-thread hop (Main.qml boots EVERY
|
||||
/// domain singleton; only QbzSession.boot also fires crate::on_boot).
|
||||
#[qinvokable]
|
||||
fn boot(self: Pin<&mut QbzAlbum>);
|
||||
|
||||
/// Open the album detail view (pushes "album" on the nav stack).
|
||||
#[qinvokable]
|
||||
fn open_album(self: Pin<&mut QbzAlbum>, album_id: QString);
|
||||
}
|
||||
|
||||
impl cxx_qt::Threading for QbzAlbum {}
|
||||
}
|
||||
|
||||
use qbz_album_bridge::QbzAlbum;
|
||||
|
||||
/// Rust side of the album bridge (plain storage, phase-1 pattern).
|
||||
pub struct QbzAlbumRust {
|
||||
album_loading: bool,
|
||||
album_json: QString,
|
||||
}
|
||||
|
||||
impl Default for QbzAlbumRust {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
album_loading: false,
|
||||
album_json: QString::from("{}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The UI hop (bridges/mod.rs pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static QT_THREAD: OnceLock<CxxQtThread<QbzAlbum>> = OnceLock::new();
|
||||
|
||||
/// Queue an album-bridge mutation onto the Qt event loop (no-op before
|
||||
/// boot registers the thread).
|
||||
pub(crate) fn ui(f: impl FnOnce(Pin<&mut QbzAlbum>) + Send + 'static) {
|
||||
if let Some(thread) = QT_THREAD.get() {
|
||||
let _ = thread.queue(f);
|
||||
}
|
||||
}
|
||||
|
||||
impl qbz_album_bridge::QbzAlbum {
|
||||
pub fn boot(self: Pin<&mut Self>) {
|
||||
if QT_THREAD.set(self.qt_thread()).is_err() {
|
||||
log::warn!("[qbz-qt] album Qt thread already registered");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_album(self: Pin<&mut Self>, album_id: QString) {
|
||||
crate::open_album(album_id.to_string());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! QbzArtist — Artist detail view domain bridge (phase 23 split of the
|
||||
//! QbzBridge God-object; the pattern is documented in main.rs).
|
||||
//!
|
||||
//! The module is `qbz_artist_bridge`, not `qbz_artist`: bridge modules are
|
||||
//! never named after a workspace crate (the `qbz_library` collision is the
|
||||
//! documented precedent). The QML type name comes from the QObject
|
||||
//! (`QbzArtist`), not from the module, so QML is unaffected.
|
||||
//!
|
||||
//! Props: the one artist-view JSON document + its loading flag.
|
||||
//! Invokables: openArtist, loadReleaseSection (per-bucket "Load more").
|
||||
//! Signal: releaseSectionReady (the next page of one releases bucket).
|
||||
//!
|
||||
//! `view_param_id` is NOT here and NOT in album_bridge.rs: it was a
|
||||
//! write-only property (three setters in main.rs — album, artist, playlist —
|
||||
//! and zero readers in QML or Rust, verified by grep). Splitting it would
|
||||
//! have duplicated dead state into two singletons; the id each view actually
|
||||
//! needs already travels inside its own document (`ArtistViewData.id` /
|
||||
//! `AlbumHeader.id`), so it was deleted outright instead.
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use cxx_qt::CxxQtThread;
|
||||
use cxx_qt::Threading as _;
|
||||
use cxx_qt_lib::QString;
|
||||
|
||||
#[cxx_qt::bridge]
|
||||
pub mod qbz_artist_bridge {
|
||||
extern "C++" {
|
||||
include!("cxx-qt-lib/qstring.h");
|
||||
type QString = cxx_qt_lib::QString;
|
||||
}
|
||||
|
||||
#[auto_cxx_name]
|
||||
extern "RustQt" {
|
||||
#[qobject]
|
||||
#[qml_element]
|
||||
#[qml_singleton]
|
||||
// --- Artist detail view --------------------------------------------
|
||||
#[qproperty(bool, artist_loading)]
|
||||
// ONE JSON document (artist_qt.rs ArtistViewData: header + top
|
||||
// tracks + releases buckets + labels/similar/playlists).
|
||||
#[qproperty(QString, artist_json)]
|
||||
|
||||
type QbzArtist = super::QbzArtistRust;
|
||||
|
||||
/// Registers this object's Qt-thread hop (Main.qml boots EVERY
|
||||
/// domain singleton; only QbzSession.boot also fires crate::on_boot).
|
||||
#[qinvokable]
|
||||
fn boot(self: Pin<&mut QbzArtist>);
|
||||
|
||||
/// Open the artist detail view (pushes "artist" on the nav stack).
|
||||
#[qinvokable]
|
||||
fn open_artist(self: Pin<&mut QbzArtist>, artist_id: QString);
|
||||
|
||||
/// ArtistView per-section "Load more" — the next releases page.
|
||||
#[qinvokable]
|
||||
fn load_release_section(self: Pin<&mut QbzArtist>, artist_id: QString, release_type: QString, offset: i32);
|
||||
|
||||
/// Emitted with the next page of a releases bucket.
|
||||
#[qsignal]
|
||||
fn release_section_ready(self: Pin<&mut QbzArtist>, release_type: QString, cards_json: QString, has_more: bool);
|
||||
}
|
||||
|
||||
impl cxx_qt::Threading for QbzArtist {}
|
||||
}
|
||||
|
||||
use qbz_artist_bridge::QbzArtist;
|
||||
|
||||
/// Rust side of the artist bridge (plain storage, phase-1 pattern).
|
||||
pub struct QbzArtistRust {
|
||||
artist_loading: bool,
|
||||
artist_json: QString,
|
||||
}
|
||||
|
||||
impl Default for QbzArtistRust {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
artist_loading: false,
|
||||
artist_json: QString::from("{}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The UI hop (bridges/mod.rs pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static QT_THREAD: OnceLock<CxxQtThread<QbzArtist>> = OnceLock::new();
|
||||
|
||||
/// Queue an artist-bridge mutation onto the Qt event loop (no-op before
|
||||
/// boot registers the thread).
|
||||
pub(crate) fn ui(f: impl FnOnce(Pin<&mut QbzArtist>) + Send + 'static) {
|
||||
if let Some(thread) = QT_THREAD.get() {
|
||||
let _ = thread.queue(f);
|
||||
}
|
||||
}
|
||||
|
||||
impl qbz_artist_bridge::QbzArtist {
|
||||
pub fn boot(self: Pin<&mut Self>) {
|
||||
if QT_THREAD.set(self.qt_thread()).is_err() {
|
||||
log::warn!("[qbz-qt] artist Qt thread already registered");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_artist(self: Pin<&mut Self>, artist_id: QString) {
|
||||
crate::open_artist(artist_id.to_string());
|
||||
}
|
||||
|
||||
pub fn load_release_section(self: Pin<&mut Self>, artist_id: QString, release_type: QString, offset: i32) {
|
||||
crate::load_release_section(artist_id.to_string(), release_type.to_string(), offset);
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,11 @@ pub mod qbz_bridge {
|
||||
#[qobject]
|
||||
#[qml_element]
|
||||
#[qml_singleton]
|
||||
// --- Album / Artist detail views (phase 8) -------------------------
|
||||
#[qproperty(bool, album_loading)]
|
||||
#[qproperty(QString, album_json)]
|
||||
#[qproperty(bool, artist_loading)]
|
||||
#[qproperty(QString, artist_json)]
|
||||
// The id backing the current "album"/"artist" view ("" = none).
|
||||
#[qproperty(QString, view_param_id)]
|
||||
// --- Album / Artist detail views: MOVED (phase 23) ----------------
|
||||
// album_* -> QbzAlbum (album_bridge.rs), artist_* + the releases
|
||||
// "Load more" signal -> QbzArtist (artist_bridge.rs). `view_param_id`
|
||||
// was deleted, not moved: write-only, zero readers — the rationale
|
||||
// lives in artist_bridge.rs's header.
|
||||
|
||||
// --- Settings (phase 10) -----------------------------------------
|
||||
// One JSON document (settings_qt.rs SettingsDoc: audio + playback
|
||||
@@ -65,19 +63,6 @@ pub mod qbz_bridge {
|
||||
#[qinvokable]
|
||||
fn boot(self: Pin<&mut QbzBridge>);
|
||||
|
||||
/// Open the album detail view (pushes "album" on the nav stack).
|
||||
#[qinvokable]
|
||||
fn open_album(self: Pin<&mut QbzBridge>, album_id: QString);
|
||||
/// Open the artist detail view (pushes "artist" on the nav stack).
|
||||
#[qinvokable]
|
||||
fn open_artist(self: Pin<&mut QbzBridge>, artist_id: QString);
|
||||
/// ArtistView per-section "Load more" — the next releases page.
|
||||
#[qinvokable]
|
||||
fn load_release_section(self: Pin<&mut QbzBridge>, artist_id: QString, release_type: QString, offset: i32);
|
||||
/// Emitted with the next page of a releases bucket.
|
||||
#[qsignal]
|
||||
fn release_section_ready(self: Pin<&mut QbzBridge>, release_type: QString, cards_json: QString, has_more: bool);
|
||||
|
||||
// --- Settings (phase 10) ------------------------------------------
|
||||
/// Toggle rows (settings_qt.rs key dispatch; persists + applies +
|
||||
/// republishes `settingsJson`).
|
||||
@@ -194,11 +179,6 @@ use cxx_qt_lib::QString;
|
||||
/// generated `set_*` methods on the Qt thread; the struct itself is plain
|
||||
/// storage (as required by cxx-qt's Default-constructed qobjects).
|
||||
pub struct QbzBridgeRust {
|
||||
album_loading: bool,
|
||||
album_json: QString,
|
||||
artist_loading: bool,
|
||||
artist_json: QString,
|
||||
view_param_id: QString,
|
||||
settings_json: QString,
|
||||
cortinilla_open: bool,
|
||||
cortinilla_loading: bool,
|
||||
@@ -213,11 +193,6 @@ pub struct QbzBridgeRust {
|
||||
impl Default for QbzBridgeRust {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
album_loading: false,
|
||||
album_json: QString::from("{}"),
|
||||
artist_loading: false,
|
||||
artist_json: QString::from("{}"),
|
||||
view_param_id: QString::default(),
|
||||
settings_json: QString::from("{}"),
|
||||
cortinilla_open: false,
|
||||
cortinilla_loading: false,
|
||||
@@ -236,18 +211,6 @@ impl qbz_bridge::QbzBridge {
|
||||
crate::register_qt_thread(self.qt_thread());
|
||||
}
|
||||
|
||||
pub fn open_album(self: Pin<&mut Self>, album_id: QString) {
|
||||
crate::open_album(album_id.to_string());
|
||||
}
|
||||
|
||||
pub fn open_artist(self: Pin<&mut Self>, artist_id: QString) {
|
||||
crate::open_artist(artist_id.to_string());
|
||||
}
|
||||
|
||||
pub fn load_release_section(self: Pin<&mut Self>, artist_id: QString, release_type: QString, offset: i32) {
|
||||
crate::load_release_section(artist_id.to_string(), release_type.to_string(), offset);
|
||||
}
|
||||
|
||||
pub fn integrations_action(self: Pin<&mut Self>, action: QString) {
|
||||
crate::integrations_action(action.to_string());
|
||||
}
|
||||
|
||||
+11
-19
@@ -28,6 +28,8 @@ mod home_bridge;
|
||||
mod viz_bridge;
|
||||
mod local_bridge;
|
||||
mod library_bridge;
|
||||
mod album_bridge;
|
||||
mod artist_bridge;
|
||||
mod artwork_qt;
|
||||
mod bridge;
|
||||
mod home_qt;
|
||||
@@ -410,22 +412,18 @@ pub(crate) fn open_album(album_id: String) {
|
||||
nav_qt::record("album");
|
||||
*LAST_DETAIL.lock().unwrap() = ("album".to_string(), album_id.clone());
|
||||
let runtime = app();
|
||||
let param = album_id.clone();
|
||||
ui(move |mut b| {
|
||||
b.as_mut().set_album_loading(true);
|
||||
b.as_mut().set_view_param_id(QString::from(param.as_str()));
|
||||
});
|
||||
album_bridge::ui(move |mut b| b.as_mut().set_album_loading(true));
|
||||
spawn(async move {
|
||||
match album_qt::load_album_view(&runtime, &album_id).await {
|
||||
Ok(json) => {
|
||||
ui(move |mut b| {
|
||||
album_bridge::ui(move |mut b| {
|
||||
b.as_mut().set_album_json(QString::from(json.as_str()));
|
||||
b.as_mut().set_album_loading(false);
|
||||
})
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] album view load failed: {e}");
|
||||
ui(move |mut b| b.as_mut().set_album_loading(false));
|
||||
album_bridge::ui(move |mut b| b.as_mut().set_album_loading(false));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -439,20 +437,16 @@ pub(crate) fn open_artist(artist_id: String) {
|
||||
nav_qt::record("artist");
|
||||
*LAST_DETAIL.lock().unwrap() = ("artist".to_string(), artist_id.clone());
|
||||
let runtime = app();
|
||||
let param = artist_id.clone();
|
||||
ui(move |mut b| {
|
||||
b.as_mut().set_artist_loading(true);
|
||||
b.as_mut().set_view_param_id(QString::from(param.as_str()));
|
||||
});
|
||||
artist_bridge::ui(move |mut b| b.as_mut().set_artist_loading(true));
|
||||
spawn(async move {
|
||||
match artist_qt::load_artist_view(&runtime, &artist_id).await {
|
||||
Ok(json) => ui(move |mut b| {
|
||||
Ok(json) => artist_bridge::ui(move |mut b| {
|
||||
b.as_mut().set_artist_json(QString::from(json.as_str()));
|
||||
b.as_mut().set_artist_loading(false);
|
||||
}),
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] artist view load failed: {e}");
|
||||
ui(move |mut b| b.as_mut().set_artist_loading(false));
|
||||
artist_bridge::ui(move |mut b| b.as_mut().set_artist_loading(false));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -467,7 +461,7 @@ pub(crate) fn load_release_section(artist_id: String, release_type: String, offs
|
||||
{
|
||||
Ok((cards, has_more)) => {
|
||||
let json = serde_json::to_string(&cards).unwrap_or_else(|_| "[]".into());
|
||||
ui(move |mut b| {
|
||||
artist_bridge::ui(move |mut b| {
|
||||
b.as_mut().release_section_ready(
|
||||
QString::from(release_type.as_str()),
|
||||
QString::from(json.as_str()),
|
||||
@@ -682,8 +676,6 @@ pub(crate) fn open_playlist(playlist_id: String) {
|
||||
return;
|
||||
};
|
||||
nav_qt::record("playlist");
|
||||
let param = playlist_id.clone();
|
||||
ui(move |mut b| b.as_mut().set_view_param_id(QString::from(param.as_str())));
|
||||
let runtime = app();
|
||||
spawn(async move {
|
||||
if let Err(e) = playlist_qt::load(&runtime, pid).await {
|
||||
@@ -1023,7 +1015,7 @@ fn republish_album(album_id: String) {
|
||||
let runtime = app();
|
||||
spawn(async move {
|
||||
if let Ok(json) = album_qt::load_album_view(&runtime, &album_id).await {
|
||||
ui(move |mut b| b.as_mut().set_album_json(QString::from(json.as_str())));
|
||||
album_bridge::ui(move |mut b| b.as_mut().set_album_json(QString::from(json.as_str())));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1035,7 +1027,7 @@ fn republish_artist(artist_id: String) {
|
||||
let runtime = app();
|
||||
spawn(async move {
|
||||
if let Ok(json) = artist_qt::load_artist_view(&runtime, &artist_id).await {
|
||||
ui(move |mut b| b.as_mut().set_artist_json(QString::from(json.as_str())));
|
||||
artist_bridge::ui(move |mut b| b.as_mut().set_artist_json(QString::from(json.as_str())));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user