feat(qt): queue rows slide into place instead of the list being rebuilt

The Listen list and the sidebar queue now reach their views through
QbzKeyedModel. In the Listen list a removed track fades out while the
rows below slide up, "play next" slides the queue open, a history row
trimmed above the window leaves the rows on screen where they are, and a
committed drag lands the row where it was dropped while its neighbours
slide (the chevrons keep the animated swap). Identity is the track
(source + id, repeats by occurrence); phase, section and phaseIndex are
data, so a track ending updates rows in place and moves nothing. The
follow-playing position and the cover window now run after the model
holds the new rows. A search is a different list and is replaced at once.

The sidebar's Up Next and Recently played are Repeaters in Columns, which
have no remove transition: a leaving row disappears and the rows below
slide up, a joining row fades in, and a page or search change is laid out
synchronously without animation. Drop slots count the Repeater's own
items.

tst_keyed_model gains the Repeater-in-a-Column case (silent first page and
page change, sliding rows that keep their items, fading insert).

cc
This commit is contained in:
vicrodh committed 2026-09-14 05:56:58 -06:00
1 parent ce625e7e28
commit 408a7af0c9
3 files changed
+168 -17

No files matched your search

+42 -5
View File
@@ -38,6 +38,24 @@ Rectangle {
readonly property var currentRow: doc.current || null
readonly property var upcoming: doc.upcoming || []
readonly property var historyRows: doc.history || []
// Keyed models for both lists (2026-09-14): a track leaving Up Next (it
// started playing, it was removed) disappears and the rows below slide up;
// a track joining slides the list open; nothing is rebuilt. The rows are
// Repeaters in a Column, so the motion is the Column's own add / move
// transitions — a positioner has no remove transition. Identity is the
// track (source + id, repeats by occurrence); another page or search
// replaces the list at once.
QbzKeyedModel {
id: upcomingModel
rows: root.upcoming
keyOf: function (row) { return (row.source || "") + ":" + row.id }
scope: JSON.stringify([root.doc.searchQuery || "", root.doc.page || 0])
}
QbzKeyedModel {
id: historyModel
rows: root.historyRows
keyOf: function (row) { return (row.source || "") + ":" + row.id }
}
readonly property var settingsDoc: {
try { return JSON.parse(QbzBridge.settingsJson) }
catch (e) { return ({}) }
@@ -138,7 +156,8 @@ Rectangle {
// arithmetic model to keep in sync. Top half of row k -> slot k, bottom
// half -> slot k+1, past the last row -> N (append). <=40 rows per page.
function slotFromPointer(winY) {
var n = root.upcoming.length
// The Repeater's own count: its items are what the pointer is over.
var n = upcomingRepeater.count
for (var i = 0; i < n; i++) {
var d = upcomingRepeater.itemAt(i)
if (d && winY < d.rowCenterY())
@@ -223,7 +242,13 @@ Rectangle {
root.coverMap = Object.assign({}, m)
}
}
Component.onCompleted: dispatchCovers()
Component.onCompleted: {
// The lists' Columns, laid out synchronously around a reset so a new
// page or search appears at once (QbzKeyedModel).
upcomingModel.views = [upNextSection]
historyModel.views = [historyBody]
dispatchCovers()
}
onDocChanged: dispatchCovers()
onQueueTrackArtworkChanged: dispatchCovers()
// Collect the covers of the document CURRENTLY on the property and ask the
@@ -1026,13 +1051,18 @@ Rectangle {
font.letterSpacing: 0.5
}
add: QbzRowAdd { enabled: upcomingModel.animate }
move: QbzRowDisplaced { enabled: upcomingModel.animate }
Repeater {
id: upcomingRepeater
model: root.upcoming
model: upcomingModel
delegate: Column {
id: upDelegate
required property var modelData
required property string rowKey
required property int rowRev
required property int index
readonly property var modelData: upcomingModel.row(rowKey, rowRev)
width: parent ? parent.width : 0
// Row center in window space — slotFromPointer()
// reads it off the Repeater's items.
@@ -1167,9 +1197,16 @@ Rectangle {
font.weight: theme.weightSemibold
font.letterSpacing: 0.5
}
add: QbzRowAdd { enabled: historyModel.animate }
move: QbzRowDisplaced { enabled: historyModel.animate }
Repeater {
model: root.historyRows
model: historyModel
delegate: QueueRow {
required property string rowKey
required property int rowRev
required property int index
readonly property var modelData: historyModel.row(rowKey, rowRev)
width: parent.width
row: modelData
rowIndex: index