feat(qt): QbzMini bridge, mini queue projection and mini prefs
Block B1 of the miniplayer/tray contract. No UI: this lands the state the five surfaces will bind to, so B2 can mount a window against a bridge that is already correct. src/mini_bridge.rs (new) — the QbzMini singleton. Six properties (open, surface, backgroundBlur, miniQueueJson, miniWidth, miniHeight), boot(), and the two custom-WRITE funnels. `open` mirrors into a static AtomicBool with is_open() beside it, because the hotkeys pipeline runs on another singleton and cannot read this QObject's properties. src/mini_qt.rs (new) — the plain projection module: mini_queue_doc (current track first, then the FULL unfiltered upcoming, no page cap, no art field), the row-to-upcoming-index mapping, the geometry guards, and the pref seeds. Five unit tests, three of them the contract's UT-1/UT-2/UT-3. src/queue_qt.rs — display_title and fmt_duration widened to pub(crate) so mini_qt reuses them instead of growing a third private copy; new publish_mini_queue with its own identical-document guard, published from THE queue funnel beside publish_coverflow. No second queue fetch. src/settings_qt.rs — pref_f32, the reader with a default that the two geometry keys need. Also reattaches three doc lines that sat on read_pref_f32 while describing save_pref, which had none. src/main.rs, build.rs, qml/Main.qml — the registrations: two mod lines (mini_qt is a plain module and is deliberately absent from rust_files), the bridge in rust_files, and QbzMini.boot() after QbzSession.boot(). Two deviations worth naming. Contract row A-9 asks for an invokable set_surface, but row A-3 fixes a property named surface whose generated setter is also set_surface — one QObject cannot carry both, so the funnel sits on the property. QML assigns `QbzMini.surface = n`; there is no setSurface member. And the surface write persists and re-kicks the queue refresh unconditionally, 1:1 with the reference, suppressing only the change notification when nothing moved. cc
This commit is contained in:
7 files changed
+685
-8
No files matched your search
@@ -112,15 +112,17 @@ static VIEW: Mutex<ViewState> = Mutex::new(ViewState {
|
||||
page: 0,
|
||||
});
|
||||
|
||||
/// queue.rs `display_title` (version suffix).
|
||||
fn display_title(track: &QueueTrack) -> String {
|
||||
/// queue.rs `display_title` (version suffix). `pub(crate)` for `mini_qt`'s row
|
||||
/// projection, which must not grow a private copy of it.
|
||||
pub(crate) fn display_title(track: &QueueTrack) -> String {
|
||||
match track.version.as_deref().filter(|v| !v.is_empty()) {
|
||||
Some(version) => format!("{} ({version})", track.title),
|
||||
None => track.title.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_duration(secs: u64) -> String {
|
||||
/// `pub(crate)` for the same reason as `display_title`.
|
||||
pub(crate) fn fmt_duration(secs: u64) -> String {
|
||||
format!("{}:{:02}", secs / 60, secs % 60)
|
||||
}
|
||||
|
||||
@@ -319,6 +321,49 @@ fn publish_coverflow(state: &qbz_models::QueueState) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Last miniplayer queue document handed to Qt — the mini's OWN
|
||||
/// identical-document guard, same discipline as `LAST_COVERFLOW_JSON` above.
|
||||
/// The mini queue surface is scrollable, and a post that carries an unchanged
|
||||
/// document resets its scroll for nothing (miniplayer/tray contract §7-M9).
|
||||
///
|
||||
/// **Correcting §7-M9's stated reason, which does not hold in this tree:** it
|
||||
/// says the mini is fed by "a pump that ticks every second". `publish` is not
|
||||
/// per-tick — its three call sites inside `start_poll_loop` are all edge-gated
|
||||
/// (`playback_qt.rs`: the peer-track-id edge, the track-id edge, and the
|
||||
/// stop-after arm), plus the explicit mutation sites. So the guard earns its
|
||||
/// place on the mutation path (a reorder or a remove that leaves the mini
|
||||
/// document identical), not on a 1 Hz republish that does not exist.
|
||||
///
|
||||
/// **Known asymmetry with its sibling, stated rather than hidden (§8.1):**
|
||||
/// `publish_coverflow` has `COVERFLOW_CACHE` and skips the serialize itself
|
||||
/// when the queue sequence is unchanged; this one serializes first and compares
|
||||
/// after. Accepted here because `publish` is edge-triggered, so the cost is one
|
||||
/// build per real queue change, not per frame. If `publish` ever becomes
|
||||
/// unconditional, this is the function that needs the cache.
|
||||
static LAST_MINI_QUEUE_JSON: Mutex<String> = Mutex::new(String::new());
|
||||
|
||||
/// Build + publish the miniplayer's navigable queue document
|
||||
/// (`{"currentId":"…","rows":[…]}` — `mini_qt::mini_queue_doc`).
|
||||
///
|
||||
/// Its own function rather than an inline block, because the guard's early
|
||||
/// `return` aborts whatever encloses it: inlined into `publish`, an unchanged
|
||||
/// mini document would skip the panel's own post below and silently freeze the
|
||||
/// queue panel. Runs on EVERY publish BEFORE the queueJson dedup, for the same
|
||||
/// reason `publish_coverflow` does — a change past the current page can leave
|
||||
/// queueJson byte-identical while the mini model moved.
|
||||
fn publish_mini_queue(state: &qbz_models::QueueState) {
|
||||
let doc = crate::mini_qt::mini_queue_doc(state);
|
||||
let mut last = LAST_MINI_QUEUE_JSON.lock().unwrap();
|
||||
if *last == doc {
|
||||
return;
|
||||
}
|
||||
last.clear();
|
||||
last.push_str(&doc);
|
||||
crate::mini_bridge::ui(move |mut b| {
|
||||
b.as_mut().set_mini_queue_json(QString::from(doc.as_str()));
|
||||
});
|
||||
}
|
||||
|
||||
/// Queue-wide upcoming indices of the rows the panel is CURRENTLY showing, in
|
||||
/// display order (search filter + pagination applied) — the hoisted port of
|
||||
/// `crates/qbz/src/queue.rs::resolve_upcoming_index`, resolved once per action
|
||||
@@ -502,6 +547,9 @@ pub async fn publish(runtime: &Arc<AppRuntime<LoggingAdapter>>) {
|
||||
// The coverflow doc publishes on EVERY publish, BEFORE the queueJson
|
||||
// dedup below — see publish_coverflow for why.
|
||||
publish_coverflow(&state);
|
||||
// The miniplayer's navigable model rides the same funnel and the same
|
||||
// before-the-dedup placement, with its own guard (see publish_mini_queue).
|
||||
publish_mini_queue(&state);
|
||||
// Identical document -> no Qt hop, no QML re-parse, no artwork re-dispatch.
|
||||
// The compare/store and the Qt post stay under ONE lock so two concurrent
|
||||
// publishes cannot post out of order and strand a stale document behind an
|
||||
|
||||
Reference in new issue
Block a user