fix(qt): the My QBZ detail delegates read the live row, not the model's snapshot

The whole in-place patch path was a silent no-op, and it took the Quality
column and expanded mode down with it.

`model: root.rows` is a JavaScript array. QQmlDelegateModel SNAPSHOTS such a
model, so a delegate's `modelData` is the engine's own copy of the element, not
the object sitting in `root.rows`. `syncRows` patches with `Object.assign` on
purpose — reassigning the array resets contentY and throws the user to the top
of a 200-item collection — and then bumps `patchRev` to re-evaluate the
bindings. Every part of that worked except the last one: the bindings
faithfully re-read the STALE copy.

Measured before the fix, driving the real app over VNC: 34 items resolved
Rust-side with correct tiers (26 get_album, all 200 OK), 34 publishes, 71 QML
refreshes, `syncRows` taking the patch branch every time — and the Quality
column sat on its loading dots forever, byte-identical five minutes apart.
Expanded mode rendered no inline tracks at all, for exactly the same reason:
`inlineTracks` is delivered by that same patch.

The fix is to index the live array: `patchRev` stays the dependency and
`root.rows[index]` is the object Rust actually patched. `modelData` remains the
fallback for the frame where the array and the delegate model are momentarily
out of step. No array reassignment, so the scroll position is still preserved.

After: the badges resolve to "CD 16-bit / 44.1 kHz" and the inline track lists
render with their durations and per-track badges.

cc
This commit is contained in:
vicrodh committed 2026-07-30 15:19:46 -06:00
1 parent b8b493bd59
commit 1c2bdb749a
1 file changed
+38 -6
@@ -568,18 +568,47 @@ Rectangle {
width: itemList.width
spacing: 0
/// THE LIVE ROW, and the reason every read below goes
/// through it instead of `modelData`.
///
/// `model: root.rows` is a JavaScript array, and
/// QQmlDelegateModel SNAPSHOTS such a model — the
/// delegate's `modelData` is the engine's own copy of
/// the element, not the object in `root.rows`. So
/// `syncRows`' in-place `Object.assign` patch (the
/// whole point of which is to update a row WITHOUT
/// reassigning the array and throwing the user's
/// scroll position to the top) mutated objects the
/// delegates could never see. Bumping `patchRev` then
/// dutifully re-evaluated every binding — against the
/// stale copy.
///
/// Measured: 34 items resolved Rust-side with correct
/// tiers and 34 publishes, 71 QML refreshes, and the
/// Quality column stayed on its skeleton forever.
/// Expanded mode rendered nothing for the same reason
/// (`inlineTracks` is patched in exactly this way).
///
/// Indexing the live array closes it: `patchRev` is
/// the dependency, `root.rows[index]` is the object
/// Rust actually patched. `modelData` stays the
/// fallback for the frame where the array and the
/// delegate model are momentarily out of step.
readonly property var live: root.patchRev >= 0
? (root.rows[index] || modelData) : modelData
readonly property bool expanded: root.viewMode === "expanded"
&& (root.patchRev >= 0 && modelData.canExpand === true)
&& (root.patchRev >= 0 && rowCell.live.canExpand === true)
readonly property var inlineTracks: (root.patchRev >= 0)
? (modelData.inlineTracks || []) : []
? (rowCell.live.inlineTracks || []) : []
readonly property bool expandLoading: root.patchRev >= 0
&& modelData.expandLoading === true
&& rowCell.live.expandLoading === true
readonly property bool tracksLoaded: root.patchRev >= 0
&& modelData.tracksLoaded === true
&& rowCell.live.tracksLoaded === true
MyQbzDetailRow {
width: parent.width
item: rowCell.modelData
item: rowCell.live
ordinal: rowCell.index + 1
selectMode: root.selectMode
dotPhase: root.dotPhase
@@ -722,7 +751,10 @@ Rectangle {
delegate: MyQbzDetailCard {
required property var modelData
item: modelData
required property int index
// Live row, not the delegate model's snapshot — see the
// list delegate's `live` note.
item: root.patchRev >= 0 ? (root.rows[index] || modelData) : modelData
cardW: gridHost.cardW
selectMode: root.selectMode
rev: root.patchRev