fix(library): append Tracks pages without model reset

Keep one compatibility ListModel mounted and publish stable page prefixes through rowsInserted notifications. Preserve semantic anchor restoration only for grouped reorders so ordinary lazy pagination cannot move the viewport.

cx
This commit is contained in:
vicrodh committed 2026-08-23 18:44:13 -06:00
1 parent 1244687ab9
commit fcb9851736
2 files changed
+77 -12

No files matched your search

@@ -9,6 +9,7 @@
// heights instead of a Repeater per group.
import QtQuick
import QtQml.Models
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
@@ -45,6 +46,62 @@ Item {
// replacement wins before Qt's next event-loop turn.
property int entriesEpoch: 0
// Keep one QAbstractItemModel object mounted for the entire compatibility
// session. Binding a fresh JS array to ListView.model for every accumulated
// 500-row document invokes setModel() and resets the view before any anchor
// repair can run. A ListModel append is a real rowsInserted notification:
// Qt keeps contentY and the visible delegate untouched.
ListModel {
id: legacyEntriesModel
}
function sameEntryIdentity(a, b) {
if (!a || !b || a.t !== b.t)
return false
if (a.t === 0)
return String(a.label) === String(b.label)
return a.row && b.row && String(a.row.id) === String(b.row.id)
}
function canAppendLegacyEntries(nextEntries) {
if (legacyEntriesModel.count !== root.entries.length
|| nextEntries.length < root.entries.length)
return false
for (var i = 0; i < root.entries.length; i++) {
if (!root.sameEntryIdentity(root.entries[i], nextEntries[i]))
return false
}
return true
}
function publishLegacyEntries(nextEntries, anchor, epoch) {
var previousCount = root.entries.length
if (root.canAppendLegacyEntries(nextEntries)) {
// The normal ungrouped paging path has an immutable prefix. Append
// only the new tail, preserving the viewport by construction.
// Hold the request guard through the rowsInserted polish so a
// kinetic-scroll signal cannot request a second page recursively.
if (root.pageRequestPending)
root.restoringPageAnchor = true
for (var i = previousCount; i < nextEntries.length; i++)
legacyEntriesModel.append({"modelData": nextEntries[i]})
root.entries = nextEntries
if (root.pageRequestPending)
pageAnchorSettle.restart()
return
}
// Search/sort and grouped compatibility modes can reorder the prefix.
// Rebuild rows inside the SAME ListModel object and then restore the
// semantic anchor. This avoids QQuickItemView::setModel() even when a
// full reconciliation is necessary.
legacyEntriesModel.clear()
for (var j = 0; j < nextEntries.length; j++)
legacyEntriesModel.append({"modelData": nextEntries[j]})
root.entries = nextEntries
root.restorePageAnchor(anchor, nextEntries, epoch)
}
function requestNextPage(force) {
if (root.nativeModelActive
|| root.pageRequestPending
@@ -122,10 +179,10 @@ Item {
if (target < 0)
return
// Assigning a fresh JS array invokes QQuickItemView::setModel(); the
// reset and its content-height polish finish after `entries = out`.
// Restore on the next turn, then use the delegate's viewport position
// to retain the exact partial-row offset.
// A grouped update can clear and refill the stable ListModel because
// newly fetched rows belong before the current viewport. Let those
// remove/insert notifications settle, then retain the exact semantic
// track and partial-row offset on the next event-loop turn.
Qt.callLater(function () {
if (epoch !== root.entriesEpoch || root.nativeModelActive)
return
@@ -162,6 +219,7 @@ Item {
root.entriesEpoch += 1
var epoch = root.entriesEpoch
if (root.nativeModelActive) {
legacyEntriesModel.clear()
entries = []
rowIndex = []
try {
@@ -193,11 +251,10 @@ Item {
out.push({ "t": 1, "row": t, "n": i + 1 })
idx.push(i)
}
entries = out
alphaJumps = jumps
rowIndex = idx
root.publishLegacyEntries(out, anchor, epoch)
report()
root.restorePageAnchor(anchor, out, epoch)
}
Component.onCompleted: { rebuild(); reportSoon() }
Component.onDestruction: if (view) view.releaseWindow("tracks")
@@ -357,7 +414,8 @@ Item {
clip: true
cacheBuffer: 50 * 12
boundsBehavior: Flickable.StopAtBounds
model: root.nativeModelActive ? root.view.nativeTracksModel : root.entries
model: root.nativeModelActive
? root.view.nativeTracksModel : legacyEntriesModel
onContentYChanged: {
root.report()