feat(qt): port the Blacklist manager for artists and albums
The port had a 67-line settings row showing two counters and a header saying the Manage chevron was "NOT shipped — this port has no Blacklist Manager view for it to open". The Slint has 1,114 lines of manager plus the consumer surfaces that make the feature mean anything. - src/artist_blacklist.rs is a 1:1 port of the reference's per-user lifecycle, including the fail-open asymmetry (is_enabled reads TRUE when there is no session, everything else false/empty) and the byte-identical NO_SESSION_ERR. src/reco_dismiss_qt.rs carries the reco-dismissal store. Both are bound in ALL THREE session-activation paths — the offline one included, because the reference records that Tauri never bound it offline and blacklisted artists leaked into offline surfaces — and torn down on logout, or the next account inherits them. - The manager view: three tabs (bordered, ADR-008 — no pills), search, sort, the enable toggle, the disabled warning banner, the four body branches per tab in their load-bearing order, windowed lists, and the clear-all confirm. Reached from Settings only, exactly like the Slint; no sidebar entry. - Every one of the ten store mutations publishes TWICE — its own document and publish_settings() — because the Settings counters live in a different document and nav_qt::back() re-runs no per-view load. Without it, Manage -> unblock -> Back showed pre-mutation counters. The Slint gets this free from a shared global. - The search field is driven imperatively, not by a binding: QbzLineEdit's X used to destroy the binding, after which the box went blank while the list stayed filtered. - Consumer surfaces, which is where the feature actually lives: the album card and album list row gained "Block this album", the artist card gained the 3-arg "Not interested", the album header menu gained Block/Unblock swapping on state, and the artist page gained the Show/Blacklist flip plus the amber hidden banner with its "Show artist" button. album_qt and artist_qt now seed isAlbumBlocked / isBlacklisted at document build time — without the seed an already-blocked album offered "Block this album" and the first click UN-blocked it, the same lie the pin glyph told before isPinned existed. - search_qt passed &Default::default() for both blacklist snapshots, so nothing was filtered anywhere and a greyed row still played. It now passes the real snapshots at both call sites. - The 17 toasts the spec had dropped for want of a toast host are wired: that premise stopped being true when toast_qt landed, and a Clear All that failed with NO_SESSION_ERR was completely silent. Their msgids are in all eight catalogues; the five album-axis ones existed only in es and nl. Also fixed in passing, pre-existing on this branch and unrelated to the blacklist: ArtistView's library tab read `libTracks` unqualified from a child of the object that declares it, which QML resolves against the component root, not the enclosing object — so the "Tracks" heading threw a ReferenceError and never rendered. Found by forcing the artist route in the offscreen smoke, which no earlier gate did. cc
This commit is contained in:
26 files changed
+3393
-132
No files matched your search
@@ -34,6 +34,17 @@ pub mod qbz_bridge {
|
||||
// One JSON document (settings_qt.rs SettingsDoc: audio + playback
|
||||
// row states, select option lists, output-device groups).
|
||||
#[qproperty(QString, settings_json)]
|
||||
// The ACTIVE Settings sub-section (0 Audio … 8 Flatpak/Snap — the
|
||||
// display order is documented in settings/SettingsView.qml's header).
|
||||
//
|
||||
// Bridge state rather than QML state, and that is load-bearing: the
|
||||
// content Loader UNMOUNTS SettingsView when the user navigates away,
|
||||
// which destroys a local `property int section` along with it. The
|
||||
// Blacklist panel's "Manage" chevron does exactly that — it opens the
|
||||
// manager view — so on Back the user landed on Audio instead of
|
||||
// Blacklist. This is Slint's `SettingsState.section`, which is a global
|
||||
// for the same reason (SettingsView.slint:121-200).
|
||||
#[qproperty(i32, settings_section)]
|
||||
|
||||
// --- Search (phase 15) ---------------------------------------------
|
||||
// Cortinilla (live dropdown): open/loading flags, ONE JSON payload
|
||||
@@ -91,6 +102,15 @@ pub mod qbz_bridge {
|
||||
/// Free-text rows (Qobuz Connect device name).
|
||||
#[qinvokable]
|
||||
fn settings_string(self: Pin<&mut QbzBridge>, key: QString, value: QString);
|
||||
/// Settings sub-navigation: switch the active section.
|
||||
///
|
||||
/// The rows MUST call this rather than assigning `section` in QML.
|
||||
/// `section` is a BINDING onto `settingsSection`, and in QML an
|
||||
/// imperative assignment silently destroys the binding it lands on — so
|
||||
/// one stray `root.section = n` would work for exactly one click and
|
||||
/// then strand the view on stale local state.
|
||||
#[qinvokable]
|
||||
fn settings_set_section(self: Pin<&mut QbzBridge>, index: i32);
|
||||
/// "Reset to defaults" (audio + playback stores, quality pref).
|
||||
#[qinvokable]
|
||||
fn settings_reset(self: Pin<&mut QbzBridge>);
|
||||
@@ -239,6 +259,7 @@ use cxx_qt_lib::QString;
|
||||
/// storage (as required by cxx-qt's Default-constructed qobjects).
|
||||
pub struct QbzBridgeRust {
|
||||
settings_json: QString,
|
||||
settings_section: i32,
|
||||
cortinilla_open: bool,
|
||||
cortinilla_loading: bool,
|
||||
cortinilla_json: QString,
|
||||
@@ -255,6 +276,8 @@ impl Default for QbzBridgeRust {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
settings_json: QString::from("{}"),
|
||||
// 0 = Audio, the section Settings opens on.
|
||||
settings_section: 0,
|
||||
cortinilla_open: false,
|
||||
cortinilla_loading: false,
|
||||
cortinilla_json: QString::from("{}"),
|
||||
@@ -302,6 +325,13 @@ impl qbz_bridge::QbzBridge {
|
||||
crate::settings_string(key.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
/// Purely bridge-local state — no crate handler, nothing to persist. The
|
||||
/// Slint global is not persisted either: Settings always opens on Audio,
|
||||
/// the section only has to survive a Loader unmount WITHIN a session.
|
||||
pub fn settings_set_section(mut self: Pin<&mut Self>, index: i32) {
|
||||
self.as_mut().set_settings_section(index);
|
||||
}
|
||||
|
||||
pub fn settings_reset(self: Pin<&mut Self>) {
|
||||
crate::settings_reset();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user