fix(qt): album info modal never shows a previous album's document

Opening album A's info, closing, and opening B's left A's fetch in
flight with nothing stopping it from publishing A's document over B's
loading state — the modal rendered A's credits inside B's page (smoke
catch, 2026-08-10). The carousel rows in album_qt.rs already carry the
"late replies are generation-guarded" pattern; the modal now does too:
a monotonic open counter gates every publish, open() clears the stale
document with the loading flag, and the QML overlay additionally holds
until the published document's albumId matches the requested one.

km
This commit is contained in:
vicrodh committed 2026-08-10 20:51:38 -06:00
1 parent d9b1c26ff3
commit 1fe20e9af0
2 files changed
+28 -2

No files matched your search

+8 -1
View File
@@ -49,6 +49,11 @@ Popup {
readonly property bool loading: QbzAlbum.albumInfoLoading === true
readonly property string errorText: QbzAlbum.albumInfoError || ""
property string activeTab: "credits"
// The album the user ASKED for. The data overlay holds until the
// published document answers for this id — belt-and-braces next to the
// Rust generation guard (album_info_qt.rs): a late doc for a previous
// album must never render inside this one's page.
property string requestedId: ""
// --- Actions ---------------------------------------------------------
@@ -58,6 +63,7 @@ Popup {
if (!albumId || albumId === "")
return
root.activeTab = "credits"
root.requestedId = albumId
QbzAlbum.openAlbumInfo(albumId)
open()
}
@@ -494,10 +500,11 @@ Popup {
Item { width: 1; height: 24 }
}
// ---- Loading / error overlay ----------------------------------
// ---- Loading / error / stale-doc overlay ----------------------
Rectangle {
anchors.fill: parent
visible: root.loading || root.errorText !== ""
|| (root.doc.albumId || "") !== root.requestedId
color: theme.surfaceCard
radius: theme.radiusMd
MouseArea { anchors.fill: parent }
+20 -1
View File
@@ -234,20 +234,36 @@ fn publish(doc: &AlbumInfoDoc, loading: bool) {
/// fresh (the credits/copyright fields are not in the view's document), then
/// publish. Non-catalog ids (local/Plex keys) are rejected — the button is
/// hidden for those, this is the backstop.
///
/// The generation guard is load-bearing: open A → close → open B leaves A's
/// fetch in flight, and without the guard a slow A publishes ITS document
/// over B's loading state — the modal then shows album A's credits inside
/// album B's page (seen in smoke 2026-08-10).
pub fn open(album_id: String) {
if album_id.parse::<u64>().is_err() {
log::debug!("[qbz-qt] album info skipped for non-catalog id '{album_id}'");
return;
}
let generation = INFO_GENERATION.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
album_bridge::ui(|mut b| {
b.as_mut().set_album_info_loading(true);
b.as_mut().set_album_info_error(QString::default());
// Drop the previous document with the loading flag: a stale doc must
// never paint under the spinner.
b.as_mut().set_album_info_json(QString::from("{}"));
});
let runtime: Arc<AppRuntime<LoggingAdapter>> = crate::app();
crate::spawn(async move {
match runtime.core().get_album(&album_id).await {
Ok(album) => publish(&map(album), false),
Ok(album) => {
if generation == INFO_GENERATION.load(std::sync::atomic::Ordering::SeqCst) {
publish(&map(album), false);
}
}
Err(e) => {
if generation != INFO_GENERATION.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
log::warn!("[qbz-qt] album info load failed for {album_id}: {e}");
let msg = e.to_string();
album_bridge::ui(move |mut b| {
@@ -258,3 +274,6 @@ pub fn open(album_id: String) {
}
});
}
/// Monotonic open counter — see `open`.
static INFO_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);