fix(qt): stop the Weekly rows rebuilding the whole Recommendations tab

Reported as "it loads, then it flashes and loads again", and only on
that tab. Measured: the cached blob paints at t+0 and the two Weekly
rows land at t+0.9s — and that is with BOTH of their per-week caches
hitting, so the delay is ListenBrainz's playlist listing, not the
tracks. Their arrival called publish_snapshot, which republishes the
document, which hands QML a brand-new array; QQuickItemView::setModel()
then tears down every rail's QQmlDelegateModel and rebuilds it. Nine
rails destroyed and rebuilt to deliver two, a second after the tab had
already settled.

Not the artwork, which was the obvious suspect: the same run logged
"0 artwork misses".

Split them out the way HomeView already splits pinned / radio /
spotlight, for the same reason and with the same shape: the document
carries an ordering slot, the rows ride reco_weekly_json, and rebinding
that re-creates only those two delegates. Every other rail's sectionData
resolves to the same modelData reference, so a Weekly landing
re-evaluates nine bindings and rebuilds two.

The slots are emitted unconditionally (a new `slot()` helper) rather
than self-hiding on emptiness like `section()` does — a slot that
vanished while empty and reappeared when filled would change the
document, which is the republish being avoided. QML hides an empty one
instead, on the gate the other out-of-document rails already use.

Weekly covers are attached in the same split, so a late cover
republishes that property rather than the document: two rails, not
nine.

cc
This commit is contained in:
vicrodh committed 2026-08-18 07:25:37 -06:00
1 parent 19836547d9
commit 0ed9db2f3f
3 files changed
+142 -12

No files matched your search

+35 -1
View File
@@ -127,6 +127,31 @@ Rectangle {
return ({})
}
}
// Recommendations' two Weekly rails, same split and same reason as the
// three above: they come from ListenBrainz about a second after the rest
// of that tab has painted, and folding them into the document made their
// arrival republish it — which hands every rail a new `model:` array and
// tears down all nine to deliver two. That was the "it loads, then it
// flashes and loads again" report. Keyed by section id
// ("weeklyExploration" / "weeklyJams"); see src/recommendations_qt.rs.
readonly property var recoWeekly: {
try {
return JSON.parse(QbzHome.recoWeeklyJson)
} catch (e) {
return ({})
}
}
/// The rows for an out-of-document Weekly slot, or [] before they land.
function weeklyRows(id) {
var r = root.recoWeekly[id]
return r ? r : []
}
/// Is this descriptor one of the two slots whose rows live outside the
/// document? Ids are unique per tab, so this cannot collide with the
/// other three tabs' sections.
function isWeeklySlot(id) {
return id === "weeklyExploration" || id === "weeklyJams"
}
// Per-URL cover patches for the two out-of-document For You rails. The
// documents are NOT republished for artwork (src/foryou_qt.rs), because a
// republish hands `model:` a new JS array and `QQuickItemView::setModel()`
@@ -1087,6 +1112,7 @@ Rectangle {
visible: modelData.kind === "pinned" ? root.pinnedItems.length > 0
: modelData.kind === "radio" ? root.radioStations.length > 0
: modelData.kind === "spotlight" ? root.spotlight.visible === true
: root.isWeeklySlot(modelData.id) ? root.weeklyRows(modelData.id).length > 0
: true
sourceComponent: modelData.kind === "album" ? albumRailComp
: modelData.kind === "playlist" ? playlistRailComp
@@ -1098,7 +1124,15 @@ Rectangle {
: modelData.kind === "radio" ? radioRailComp
: modelData.kind === "spotlight" ? spotlightRailComp
: recentComp
property var sectionData: modelData
// A Weekly slot arrives with EMPTY items and is filled from
// `recoWeekly` here. Object.assign builds a new descriptor
// only for those two ids; every other rail returns the very
// same `modelData` reference, so a Weekly landing re-evaluates
// all nine bindings but re-creates only the two delegates
// whose value actually changed.
property var sectionData: root.isWeeklySlot(modelData.id)
? Object.assign({}, modelData, { "items": root.weeklyRows(modelData.id) })
: modelData
Component {
id: pinnedRailComp