refactor(qbz-qt): split QbzLibrary out of the God-object

library_bridge.rs has sat in the tree unreferenced since the phase-23 work
stopped: no mod declaration, not in build.rs, zero call sites, and duplicating
members that were still live in bridge.rs. It could not have been wired as it
stood — its module was named qbz_library, which is ambiguous with the
qbz-library crate this binary depends on, so it never compiled. The module is
now qbz_library_bridge; the QML type name comes from the QObject, so QML is
unaffected.

Registering it and deleting the duplicates had to happen together. Wiring it
alone would have compiled cleanly and produced two independent Library states —
QML reading one object while Rust published to the other — with the view simply
frozen and nothing in the log.

Moved with it: four properties, five invokables, three signals, the seven
library-only ui() hops in main.rs, and thirty-four QML references. The
Connections blocks in LibraryView, Sidebar, QueuePanel, AlbumView and
PlaylistCollage retarget wholesale because every handler in them is a library
signal. ArtistView's does not: it mixes onLibraryArtworkReady with
onReleaseSectionReady, so it becomes two blocks — retargeting it whole would
have orphaned the releases pager, and QML resolves handlers lazily enough that
the discography would just have stopped loading with nothing in the log.

bridge.rs is down to 389 lines and five domains.

cc
This commit is contained in:
vicrodh committed 2026-07-28 14:07:40 -06:00
1 parent 0799d59e01
commit 5c631000b7
18 files changed
+65 -122

No files matched your search

+1 -1
View File
@@ -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"],
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"],
qml_files: &[
"qml/LoginScreen.qml",
"qml/Main.qml",
+1
View File
@@ -52,6 +52,7 @@ ApplicationWindow {
QbzHome.boot()
QbzViz.boot()
QbzLocal.boot()
QbzLibrary.boot()
QbzBridge.boot()
}
+2 -2
View File
@@ -62,11 +62,11 @@ Rectangle {
function toggleFavorite() {
root.isFavorite = !root.isFavorite
QbzBridge.libraryToggleFavorite("album", root.albumId)
QbzLibrary.libraryToggleFavorite("album", root.albumId)
}
function togglePin() {
root.isPinned = !root.isPinned
QbzBridge.togglePin("album", root.albumId, root.title, root.artist, "")
QbzLibrary.togglePin("album", root.albumId, root.title, root.artist, "")
}
Column {
+1 -1
View File
@@ -181,7 +181,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: {
root.isPinned = !root.isPinned
QbzBridge.togglePin("artist", root.item.id, root.item.title,
QbzLibrary.togglePin("artist", root.item.id, root.item.title,
root.item.subtitle || "", root.artworkUrl)
}
}
+3 -3
View File
@@ -121,7 +121,7 @@ Rectangle {
onClicked: {
if (root.item.playlistOwned) {
root.item.isFavorite = !root.item.isFavorite
QbzBridge.libraryToggleFavorite("playlist", root.item.id)
QbzLibrary.libraryToggleFavorite("playlist", root.item.id)
} else {
root.item.playlistFollowing = !root.item.playlistFollowing
QbzBridge.playlistSetFollowById(root.item.id, root.item.playlistFollowing)
@@ -166,7 +166,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: {
root.isPinned = !root.isPinned
QbzBridge.togglePin("playlist", root.item.id, root.item.title,
QbzLibrary.togglePin("playlist", root.item.id, root.item.title,
root.item.subtitle || "", root.artworkUrl)
}
}
@@ -196,7 +196,7 @@ Rectangle {
else if (a === "favorite") {
if (root.item.playlistOwned) {
root.item.isFavorite = !root.item.isFavorite
QbzBridge.libraryToggleFavorite("playlist", root.item.id)
QbzLibrary.libraryToggleFavorite("playlist", root.item.id)
} else {
root.item.playlistFollowing = !root.item.playlistFollowing
QbzBridge.playlistSetFollowById(root.item.id, root.item.playlistFollowing)
+1 -1
View File
@@ -109,7 +109,7 @@ Item {
}
Connections {
target: QbzBridge
target: QbzLibrary
// Shared with the feed's artKey-keyed emissions — ignore anything
// that is not one of OUR urls.
function onLibraryArtworkReady(key, path) {
+2 -2
View File
@@ -80,7 +80,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
onClicked: {
root.item.isFavorite = !root.item.isFavorite
QbzBridge.libraryToggleFavorite("track", root.item.id)
QbzLibrary.libraryToggleFavorite("track", root.item.id)
}
}
CardOverlayButton {
@@ -193,7 +193,7 @@ Rectangle {
else if (a === "go-album") QbzBridge.openAlbum(root.item.albumId)
else if (a === "favorite") {
root.item.isFavorite = !root.item.isFavorite
QbzBridge.libraryToggleFavorite("track", root.item.id)
QbzLibrary.libraryToggleFavorite("track", root.item.id)
}
}
}
+2 -2
View File
@@ -257,7 +257,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: {
root.item.isFavorite = !root.item.isFavorite
QbzBridge.libraryToggleFavorite("track", root.item.id)
QbzLibrary.libraryToggleFavorite("track", root.item.id)
}
}
}
@@ -329,7 +329,7 @@ Rectangle {
else if (a === "go-album") QbzBridge.openAlbum(root.item.albumId)
else if (a === "favorite") {
root.item.isFavorite = !root.item.isFavorite
QbzBridge.libraryToggleFavorite("track", root.item.id)
QbzLibrary.libraryToggleFavorite("track", root.item.id)
} else if (a === "remove") root.removeRequested()
}
}
@@ -501,7 +501,7 @@ Rectangle {
if (id !== "") QbzPlayer.enqueueTrack(id, a)
} else if (a === "album-favorite") {
if (QbzPlayer.npAlbumId !== "")
QbzBridge.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
QbzLibrary.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
}
// TODO(qt-bridge): "playlist" (add-to-playlist modal) and
// "mixtape" have no invokable in the Qt port yet — the rows are
+1 -1
View File
@@ -755,7 +755,7 @@ Rectangle {
if (id !== "") QbzPlayer.enqueueTrack(id, a)
} else if (a === "album-favorite") {
if (QbzPlayer.npAlbumId !== "")
QbzBridge.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
QbzLibrary.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
}
// TODO(qt-bridge): "playlist" (add-to-playlist modal) and
// "mixtape" have no invokable in the Qt port yet — the rows are
+1 -1
View File
@@ -42,7 +42,7 @@ Rectangle {
QbzTheme { id: theme }
Connections {
target: QbzBridge
target: QbzLibrary
function onLibraryArtworkReady(key, path) {
var m = root.coverMap
m[key] = path
+1 -1
View File
@@ -62,7 +62,7 @@ Rectangle {
}
Connections {
target: QbzBridge
target: QbzLibrary
function onLibraryArtworkReady(key, path) {
var m = root.coverMap
m[key] = path
+4 -4
View File
@@ -67,7 +67,7 @@ Rectangle {
}
Connections {
target: QbzBridge
target: QbzLibrary
function onLibraryArtworkReady(key, path) {
var m = root.coverMap
m[key] = path
@@ -350,7 +350,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
onClicked: {
header.isFavorite = !header.isFavorite
QbzBridge.libraryToggleFavorite("album", header.id)
QbzLibrary.libraryToggleFavorite("album", header.id)
}
}
// Radio / Mixtape / Info — INERT stubs (POC-NOTE:
@@ -710,10 +710,10 @@ Rectangle {
else if (a === "queue") QbzPlayer.enqueueAlbum(header.id, "later")
else if (a === "favorite") {
header.isFavorite = !header.isFavorite
QbzBridge.libraryToggleFavorite("album", header.id)
QbzLibrary.libraryToggleFavorite("album", header.id)
} else if (a === "pin") {
header.isPinned = !header.isPinned
QbzBridge.togglePin("album", header.id, header.title, header.artist, header.artUrl)
QbzLibrary.togglePin("album", header.id, header.title, header.artist, header.artUrl)
}
}
}
+13 -5
View File
@@ -66,13 +66,21 @@ Rectangle {
return tabs
}
// Two blocks, not one: artwork is QbzLibrary's signal and the releases
// pager is still QbzBridge'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 {
target: QbzBridge
target: QbzLibrary
function onLibraryArtworkReady(key, path) {
var m = root.coverMap
m[key] = path
root.coverMap = Object.assign({}, m)
}
}
Connections {
target: QbzBridge
function onReleaseSectionReady(releaseType, cardsJson, hasMore) {
var cards = JSON.parse(cardsJson)
var sections = root.releaseSections
@@ -306,7 +314,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: {
row.isFavorite = !row.isFavorite
QbzBridge.libraryToggleFavorite("track", row.id)
QbzLibrary.libraryToggleFavorite("track", row.id)
}
}
}
@@ -596,7 +604,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
onClicked: {
artist.isFollowing = !artist.isFollowing
QbzBridge.libraryToggleFavorite("artist", artist.id)
QbzLibrary.libraryToggleFavorite("artist", artist.id)
}
}
QbzCircleAction {
@@ -1124,7 +1132,7 @@ Rectangle {
// Library feed access (the phase-5 document, parsed in LibraryView).
function libraryFeed() {
return JSON.parse(QbzBridge.libraryJson)
return JSON.parse(QbzLibrary.libraryJson)
}
// --- Network sidebar (300px, surface-card + 1px left border) ---------
@@ -1385,7 +1393,7 @@ Rectangle {
overflowMenu.close()
if (modelData.action === "pin") {
artist.isPinned = !artist.isPinned
QbzBridge.togglePin("artist", artist.id, artist.name, "", artist.artUrl)
QbzLibrary.togglePin("artist", artist.id, artist.name, "", artist.artUrl)
}
}
}
+13 -13
View File
@@ -1,7 +1,7 @@
// Library view — QML port of crates/qbz-ui/ui/favorites/FavoritesView.slint
// + the Library "All" mixed feed (library_all.rs semantics).
//
// Data: QbzBridge.libraryJson (ONE JSON document — the full merged feed;
// Data: QbzLibrary.libraryJson (ONE JSON document — the full merged feed;
// tabs/search/sort/source-filters derive HERE in JS, measured per the
// phase brief) + libraryCountsJson (tab badges). Artwork is id-keyed
// through the libraryArtworkReady signal into `artMap` (never a
@@ -44,9 +44,9 @@ Rectangle {
// ============================ state ==================================
property string activeTab: "all"
readonly property var counts: JSON.parse(QbzBridge.libraryCountsJson)
readonly property var counts: JSON.parse(QbzLibrary.libraryCountsJson)
// Full merged feed (parsed once per publish — timed for the report).
readonly property var feed: parseFeed(QbzBridge.libraryJson)
readonly property var feed: parseFeed(QbzLibrary.libraryJson)
function parseFeed(json) {
var t = Date.now()
var f = JSON.parse(json)
@@ -162,11 +162,11 @@ Rectangle {
if (!keep[key]) { delete m[key]; changed = true }
}
if (changed) artMap = Object.assign({}, m)
QbzBridge.libraryArtworkWindow(JSON.stringify(keys))
QbzLibrary.libraryArtworkWindow(JSON.stringify(keys))
}
Connections {
target: QbzBridge
target: QbzLibrary
function onLibraryArtworkReady(key, path) {
var m = root.artMap
m[key] = path
@@ -315,7 +315,7 @@ Rectangle {
else if (a === "go-album") QbzBridge.openAlbum(item.albumId)
else if (a === "favorite") {
item.isFavorite = !item.isFavorite
QbzBridge.libraryToggleFavorite("track", item.id)
QbzLibrary.libraryToggleFavorite("track", item.id)
}
}
@@ -557,7 +557,7 @@ Rectangle {
QbzBridge.openArtist(item.id)
} else if (a === "favorite") {
item.isFavorite = !item.isFavorite
QbzBridge.libraryToggleFavorite(item.kind, item.id)
QbzLibrary.libraryToggleFavorite(item.kind, item.id)
}
}
}
@@ -871,12 +871,12 @@ Rectangle {
QbzSpinner {
anchors.centerIn: parent
size: 36
visible: QbzBridge.libraryLoading
visible: QbzLibrary.libraryLoading
}
// Error + retry.
Column {
visible: !QbzBridge.libraryLoading && QbzBridge.libraryError !== ""
visible: !QbzLibrary.libraryLoading && QbzLibrary.libraryError !== ""
anchors.centerIn: parent
spacing: 10
Text {
@@ -886,7 +886,7 @@ Rectangle {
font.weight: theme.weightSemibold
}
Text {
text: QbzBridge.libraryError
text: QbzLibrary.libraryError
color: theme.textMuted
font.pixelSize: theme.fontLegal
}
@@ -910,7 +910,7 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzBridge.reloadLibrary()
onClicked: QbzLibrary.reloadLibrary()
}
}
}
@@ -922,7 +922,7 @@ Rectangle {
anchors.leftMargin: 32
anchors.rightMargin: 32
anchors.topMargin: 16
visible: !QbzBridge.libraryLoading && QbzBridge.libraryError === ""
visible: !QbzLibrary.libraryLoading && QbzLibrary.libraryError === ""
&& (root.activeTab !== "all" || root.viewMode === "grid")
&& root.activeTab !== "tracks"
cellWidth: 220
@@ -1007,7 +1007,7 @@ Rectangle {
anchors.leftMargin: 32
anchors.rightMargin: 32
anchors.topMargin: 10
visible: !QbzBridge.libraryLoading && QbzBridge.libraryError === ""
visible: !QbzLibrary.libraryLoading && QbzLibrary.libraryError === ""
&& ((root.activeTab === "all" && root.viewMode === "list")
|| root.activeTab === "tracks")
cacheBuffer: 44 * 10
-73
View File
@@ -24,16 +24,6 @@ pub mod qbz_bridge {
#[qobject]
#[qml_element]
#[qml_singleton]
// --- Library view --------------------------------------------------
#[qproperty(bool, library_loading)]
#[qproperty(QString, library_error)]
// One JSON document: the FULL merged feed (tabs/search/sort/source
// filters derive QML-side from the parsed array — see library_qt.rs
// for the rationale + the measured parse cost).
#[qproperty(QString, library_json)]
// {tracks, albums, artists, playlists, labels, all} — tab badges.
#[qproperty(QString, library_counts_json)]
// --- Album / Artist detail views (phase 8) -------------------------
#[qproperty(bool, album_loading)]
#[qproperty(QString, album_json)]
@@ -75,39 +65,6 @@ pub mod qbz_bridge {
#[qinvokable]
fn boot(self: Pin<&mut QbzBridge>);
/// Library retry button / manual refresh.
#[qinvokable]
fn reload_library(self: Pin<&mut QbzBridge>);
/// Windowed artwork: the grid/list reports the mounted window as a
/// JSON array of artKeys (tab views filter the feed, so raw indices
/// don't map); Rust dispatches covers for those keys (id-keyed via
/// `libraryArtworkReady`).
#[qinvokable]
fn library_artwork_window(self: Pin<&mut QbzBridge>, keys_json: QString);
/// Card heart: toggle favorite (Qobuz API or the local store,
/// routed by id shape); the result arrives via
/// `libraryFavoriteChanged`.
#[qinvokable]
fn library_toggle_favorite(self: Pin<&mut QbzBridge>, kind: QString, id: QString);
/// Emitted when a dispatched cover lands on disk (id-keyed —
/// `{kind}:{id}`); QML updates its artwork map.
#[qsignal]
fn library_artwork_ready(self: Pin<&mut QbzBridge>, key: QString, path: QString);
/// Emitted after a heart toggle (optimistic flip + rollback).
#[qsignal]
fn library_favorite_changed(self: Pin<&mut QbzBridge>, key: QString, value: bool);
/// AlbumCard pin badge: toggle pin (album/artist/playlist).
#[qinvokable]
fn toggle_pin(self: Pin<&mut QbzBridge>, kind: QString, id: QString, title: QString, subtitle: QString, artwork_url: QString);
/// Emitted after a pin toggle (`{kind}:{id}` key like artKey).
#[qsignal]
fn pin_changed(self: Pin<&mut QbzBridge>, key: QString, value: bool);
/// Open the album detail view (pushes "album" on the nav stack).
#[qinvokable]
fn open_album(self: Pin<&mut QbzBridge>, album_id: QString);
@@ -237,10 +194,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 {
library_loading: bool,
library_error: QString,
library_json: QString,
library_counts_json: QString,
album_loading: bool,
album_json: QString,
artist_loading: bool,
@@ -260,10 +213,6 @@ pub struct QbzBridgeRust {
impl Default for QbzBridgeRust {
fn default() -> Self {
Self {
library_loading: false,
library_error: QString::default(),
library_json: QString::from("[]"),
library_counts_json: QString::from("{}"),
album_loading: false,
album_json: QString::from("{}"),
artist_loading: false,
@@ -287,28 +236,6 @@ impl qbz_bridge::QbzBridge {
crate::register_qt_thread(self.qt_thread());
}
pub fn reload_library(self: Pin<&mut Self>) {
crate::reload_library();
}
pub fn library_artwork_window(self: Pin<&mut Self>, keys_json: QString) {
crate::library_artwork_window(keys_json.to_string());
}
pub fn library_toggle_favorite(self: Pin<&mut Self>, kind: QString, id: QString) {
crate::library_toggle_favorite(kind.to_string(), id.to_string());
}
pub fn toggle_pin(self: Pin<&mut Self>, kind: QString, id: QString, title: QString, subtitle: QString, artwork_url: QString) {
crate::toggle_pin(
kind.to_string(),
id.to_string(),
title.to_string(),
subtitle.to_string(),
artwork_url.to_string(),
);
}
pub fn open_album(self: Pin<&mut Self>, album_id: QString) {
crate::open_album(album_id.to_string());
}
+10 -4
View File
@@ -1,4 +1,10 @@
//! QbzLibrary — Library view domain bridge (phase 23 split of the QbzBridge
//! QbzLibrary — Library view domain bridge.
//!
//! The module is `qbz_library_bridge`, NOT `qbz_library`: this crate depends
//! on the `qbz-library` crate, whose Rust name is `qbz_library`, and a bridge
//! module by that name is ambiguous at every use site. The QML type name comes
//! from the QObject (`QbzLibrary`), not from the module, so QML is unaffected.
//! Original note: Library view domain bridge (phase 23 split of the QbzBridge
//! God-object; the pattern is documented in main.rs). Props: the merged-feed
//! document + counts + loading/error. Invokables: reload, windowed artwork
//! dispatch, favorite/pin toggles. Signals: artwork-ready / favorite-changed
@@ -12,7 +18,7 @@ use cxx_qt::Threading as _;
use cxx_qt_lib::QString;
#[cxx_qt::bridge]
pub mod qbz_library {
pub mod qbz_library_bridge {
extern "C++" {
include!("cxx-qt-lib/qstring.h");
type QString = cxx_qt_lib::QString;
@@ -77,7 +83,7 @@ pub mod qbz_library {
impl cxx_qt::Threading for QbzLibrary {}
}
use qbz_library::QbzLibrary;
use qbz_library_bridge::QbzLibrary;
/// Rust side of the library bridge (plain storage, phase-1 pattern).
pub struct QbzLibraryRust {
@@ -112,7 +118,7 @@ pub(crate) fn ui(f: impl FnOnce(Pin<&mut QbzLibrary>) + Send + 'static) {
}
}
impl qbz_library::QbzLibrary {
impl qbz_library_bridge::QbzLibrary {
pub fn boot(self: Pin<&mut Self>) {
if QT_THREAD.set(self.qt_thread()).is_err() {
log::warn!("[qbz-qt] library Qt thread already registered");
+8 -7
View File
@@ -27,6 +27,7 @@ mod queue_bridge;
mod home_bridge;
mod viz_bridge;
mod local_bridge;
mod library_bridge;
mod artwork_qt;
mod bridge;
mod home_qt;
@@ -282,7 +283,7 @@ pub(crate) fn do_logout() {
b.as_mut().set_home_error(QString::from(""));
b.as_mut().set_home_loading(false);
});
ui(|mut b| {
library_bridge::ui(|mut b| {
b.as_mut().set_library_json(QString::from("[]"));
b.as_mut().set_library_counts_json(QString::from("{}"));
b.as_mut().set_library_error(QString::from(""));
@@ -516,7 +517,7 @@ pub(crate) fn enqueue_album(album_id: String, mode: String) {
pub(crate) fn toggle_pin(kind: String, id: String, title: String, subtitle: String, artwork_url: String) {
if let Some(value) = sidebar_qt::toggle_pin(&kind, &id, &title, &subtitle, &artwork_url) {
let key = format!("{kind}:{id}");
ui(move |mut b| {
library_bridge::ui(move |mut b| {
b.as_mut().pin_changed(QString::from(key.as_str()), value);
});
}
@@ -1183,7 +1184,7 @@ pub(crate) fn reload_library() {
return;
}
log_rss("library load start");
ui(|mut b| {
library_bridge::ui(|mut b| {
b.as_mut().set_library_loading(true);
b.as_mut().set_library_error(QString::from(""));
});
@@ -1209,7 +1210,7 @@ pub(crate) fn reload_library() {
"[qbz-qt][perf] library load total: {:?} ({total} items)",
t.elapsed(),
);
ui(move |mut b| {
library_bridge::ui(move |mut b| {
b.as_mut().set_library_json(QString::from(feed_json.as_str()));
b.as_mut()
.set_library_counts_json(QString::from(counts_json.as_str()));
@@ -1219,7 +1220,7 @@ pub(crate) fn reload_library() {
}
Err(e) => {
log::warn!("[qbz-qt] library load failed: {e}");
ui(move |mut b| {
library_bridge::ui(move |mut b| {
b.as_mut().set_library_error(QString::from(e.as_str()));
b.as_mut().set_library_loading(false);
});
@@ -1285,7 +1286,7 @@ pub(crate) fn library_artwork_window(keys_json: String) {
}
fn emit_library_artwork(key: String, path: String) {
ui(move |mut b| {
library_bridge::ui(move |mut b| {
b.as_mut()
.library_artwork_ready(QString::from(key.as_str()), QString::from(path.as_str()));
});
@@ -1298,7 +1299,7 @@ pub(crate) fn library_toggle_favorite(kind: String, id: String) {
let runtime = app();
spawn(async move {
if let Some(value) = library_qt::toggle_favorite(&runtime, &kind, &id).await {
ui(move |mut b| {
library_bridge::ui(move |mut b| {
b.as_mut()
.library_favorite_changed(QString::from(key.as_str()), value);
});