Removing an album (or a track, artist or playlist) from the Library made Rust republish the feed, the derive produced a new array, and every body bound to it took a model swap: the grid jumped to the top, every card was rebuilt and every cover faded in again. All four bodies (grid, list, albums list, playlists list) now share one QbzKeyedModel over the same derive: the leaving card shrinks and fades while its neighbours slide into the gap, the page stays where it was, and surviving cards keep their delegates and decoded covers. The Artists sidepanel rail gets its own keyed model with the same page identity. A different page is still a different page: tab, sort, search, source filters, genres and grouping form the model's scope and replace the rows at once, from the top, as before. A heart removed under "Favorites only" is not a scope change, so that card animates out. Supporting changes: the artwork window reports read the model's published rows and count from originY (a removal above the window moves the origin), scroll memory is origin-relative, the playlists and albums lists are height-gated like the grid so hidden bodies build nothing, pooled cells come back opaque, and a background reload with a page already on screen (a bulk removal, a language switch) keeps that page instead of blinking through the loading skeleton. tst_library_keyed mounts the real grid cell over the keyed model and checks that an album leaving through a republished feed keeps the other cards, their covers and the page offset; bound to the plain array it fails. cc
162 lines
7.9 KiB
QML
162 lines
7.9 KiB
QML
// LibraryAlbumsList — the Library > Albums tab in LIST mode
|
|
// (FavoritesView.slint:1762 `AlbumCollectionView { view-mode: … }` with
|
|
// `albums-view-mode == "list"`), plus its multi-select bulk bar (:1734).
|
|
//
|
|
// REUSE, not a fork: the row IS views/AlbumListRow.qml and the column header
|
|
// IS views/AlbumListHeader.qml — the pair built for Discover Browse / Label
|
|
// Releases. Two properties were added to the row rather than copying it
|
|
// (`artSource`, because this feed is id-keyed through LibraryView's artMap
|
|
// instead of carrying a baked `artPath`; and the `selectMode`/`checked`
|
|
// /`toggleSelect` trio the reference's LIST-only multi-select needs). Both
|
|
// default to the previous behaviour, so the two catalog call sites are
|
|
// untouched.
|
|
//
|
|
// The A-Z jump strip is NOT here: one strip serves grid AND list (and the
|
|
// tracks list), so it lives in views/LibraryView.qml next to the surfaces it
|
|
// scrolls.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import ".."
|
|
import "../../controls"
|
|
import "../../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
/// The LibraryView root (artMap, the selection map, the window reporter).
|
|
property var view: null
|
|
/// The Library's keyed model (views/LibraryView.qml `feedRows`), shared by
|
|
/// every body so a row leaving animates instead of rebuilding the list.
|
|
property var rowsModel: null
|
|
/// For the model's synchronous reset layout.
|
|
readonly property alias listView: list
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
readonly property int rowH: 64
|
|
readonly property int rowGap: 4
|
|
|
|
/// A-Z / index jump target for the host's strip.
|
|
function positionAt(index) {
|
|
list.positionViewAtIndex(index, ListView.Beginning)
|
|
}
|
|
/// Re-report the artwork window (the host calls it after a reconcile).
|
|
function report() { list.report() }
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
spacing: 8
|
|
|
|
QbzMultiSelectBar {
|
|
visible: root.view.albumsMultiSelect
|
|
width: parent.width
|
|
selectedCount: root.view.albumsSelectedCount
|
|
// FavoritesView.slint:1737-1743, verbatim. "Make available
|
|
// offline" is NOT in the reference's ALBUM bar (only the tracks
|
|
// one), so nothing is omitted here.
|
|
actions: [
|
|
{ "id": "select-all", "label": QbzSession.tr("Select all", QbzSession.trRev), "icon": "square-check-big", "danger": false, "needsSelection": false },
|
|
{ "id": "play-next", "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "danger": false, "needsSelection": true },
|
|
{ "id": "play-later", "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "danger": false, "needsSelection": true },
|
|
{ "id": "queue", "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "danger": false, "needsSelection": true },
|
|
// The .slint asks for `heart-crack`; this port's icon set has
|
|
// no such glyph (checked: assets/icons/*/heart*.svg is heart
|
|
// and heart-filled only), and a missing name renders NOTHING.
|
|
// "heart" + danger:true is what views/local/LocalTracksTab.qml
|
|
// already ships for the same action, so the two bars match.
|
|
{ "id": "remove-selected", "label": QbzSession.tr("Remove from Library", QbzSession.trRev), "icon": "heart", "danger": true, "needsSelection": true },
|
|
{ "id": "clear", "label": QbzSession.tr("Clear", QbzSession.trRev), "icon": "x", "danger": false, "needsSelection": true },
|
|
]
|
|
onAction: function (id) { root.view.albumsBulkAction(id) }
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: parent.height - (root.view.albumsMultiSelect ? 52 : 0)
|
|
|
|
AlbumListHeader {
|
|
id: header
|
|
width: parent.width
|
|
selectMode: root.view.albumsMultiSelect
|
|
}
|
|
|
|
ListView {
|
|
id: list
|
|
anchors.top: header.bottom
|
|
anchors.topMargin: 4
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
// Height-gated like the host's grid: a hidden ListView with a
|
|
// model keeps refilling its viewport, and this one shares the
|
|
// Library's model with every other body.
|
|
height: root.visible ? parent.height - header.height - 4 : 0
|
|
// No strip inset here: the A-Z strip lives in LibraryView and
|
|
// the HOST already widened this component's right margin for
|
|
// it. Insetting again would double-count it.
|
|
clip: true
|
|
spacing: root.rowGap
|
|
cacheBuffer: 64 * 8
|
|
reuseItems: true
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
model: root.rowsModel
|
|
add: QbzRowAdd { enabled: root.rowsModel !== null && root.rowsModel.animate }
|
|
remove: QbzRowRemove { enabled: root.rowsModel !== null && root.rowsModel.animate }
|
|
move: QbzRowDisplaced { enabled: root.rowsModel !== null && root.rowsModel.animate }
|
|
displaced: QbzRowDisplaced { enabled: root.rowsModel !== null && root.rowsModel.animate }
|
|
|
|
// Windowed artwork, same contract as the grid: never read
|
|
// `list.model` back off the view (views/LibraryView.qml
|
|
// documents the SIGSEGV), always the array from the host.
|
|
//
|
|
// Gated on visibility like the host's own bodies
|
|
// (LibraryView.gridWindowReport / listWindowReport): `artMap`
|
|
// is ONE map and a report PRUNES outside its band, while this
|
|
// body keeps firing hidden (`anchors.fill` turns any window
|
|
// resize into onHeightChanged). Ungated it evicted whichever
|
|
// body is actually on screen, the Artists sidepanel rail above
|
|
// all, whose band is NOT a feed band. Offsets count from
|
|
// `originY`, which rows leaving above the window move.
|
|
function report() {
|
|
if (!list.visible || root.rowsModel === null) return
|
|
var stride = root.rowH + root.rowGap
|
|
var scrollY = list.contentY - list.originY
|
|
var first = Math.max(0, Math.floor(scrollY / stride) - 4)
|
|
var last = Math.ceil((scrollY + list.height) / stride) + 4
|
|
root.view.queueWindowReport(first,
|
|
Math.min(root.rowsModel.published.length - 1, last))
|
|
}
|
|
onContentYChanged: list.report()
|
|
onModelChanged: list.report()
|
|
onHeightChanged: list.report()
|
|
onVisibleChanged: list.report()
|
|
Component.onCompleted: list.report()
|
|
|
|
delegate: AlbumListRow {
|
|
required property string rowKey
|
|
required property int rowRev
|
|
required property int index
|
|
readonly property var modelData: root.rowsModel.row(rowKey, rowRev)
|
|
ListView.onReused: { opacity = 1; scale = 1 }
|
|
width: list.width
|
|
item: modelData
|
|
rowIndex: index
|
|
artSource: root.view.artMap[modelData.artKey] || ""
|
|
selectMode: root.view.albumsMultiSelect
|
|
checked: root.view.albumsSelected[modelData.id] === true
|
|
onToggleSelect: function (mods) { root.view.toggleAlbumSelected(modelData.id, mods) }
|
|
}
|
|
}
|
|
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: list.top
|
|
anchors.bottom: list.bottom
|
|
target: list
|
|
visible: list.contentHeight > list.height
|
|
}
|
|
}
|
|
}
|
|
}
|