feat(library): page Albums from local catalog

Materialize album browse fields and FTS in the derived catalog, then expose exact counts and stable keyset queries for the current filters, sort, grouping, and enabled sources.

Bind the existing Albums layout to a bounded native Qt model with asynchronous page misses, anchors, selection ranges, generation cancellation, artwork updates, and per-surface fallback. Resolve Plex native edition keys directly and retain the metadata-mode legacy reader.

cx
This commit is contained in:
vicrodh committed 2026-08-23 10:54:31 -06:00
1 parent 1bb3c41dcf
commit 299dcde5b4
21 files changed
+3092 -47

No files matched your search

+42 -6
View File
@@ -62,6 +62,7 @@ Rectangle {
// hand-written singleton owns the resident LRU; keeping this reference at
// the composition root also gives every Tracks delegate one stable model.
readonly property var nativeTracksModel: QbzLocalTracks
readonly property var nativeAlbumsModel: QbzLocalAlbums
// ============================ state ==================================
// Slint defaults, verbatim (state.slint LocalLibraryState).
@@ -74,7 +75,9 @@ Rectangle {
property string albumsView: "grid" // grid | list
property bool albumsMultiSelect: false
property var albumsSelected: ({})
readonly property int albumsSelectedCount: Object.keys(albumsSelected).length
readonly property int albumsSelectedCount: QbzLocal.localAlbumsNativeActive
? QbzLocal.localAlbumsNativeSelectedCount
: Object.keys(albumsSelected).length
// Folders tab.
property string foldersMode: "tree" // flat | tree
@@ -331,6 +334,7 @@ Rectangle {
target: QbzLocal
function onLocalArtworkReady(key, path) {
root.nativeTracksModel.setArtwork(key, path)
root.nativeAlbumsModel.setArtwork(key, path)
root._artInbox[key] = path
if (!artFlush.running) artFlush.start()
// An arrival is live evidence that the pass is still running, so
@@ -347,6 +351,12 @@ Rectangle {
QbzLocal.tracksNativePageMiss(page, generation)
}
}
Connections {
target: root.nativeAlbumsModel
function onPageMiss(page, generation) {
QbzLocal.albumsNativePageMiss(page, generation)
}
}
// A name route from LocalAlbumView ("go to artist" on a local album:
// local/Plex artists have no catalog id) lands the view on the Artists
@@ -393,11 +403,16 @@ Rectangle {
consumePendingArtist()
consumePendingRoute()
}
Component.onDestruction: {
if (QbzLocal.localAlbumsNativeActive) QbzLocal.albumsNativeClearSelection()
if (QbzLocal.localTracksNativeActive) QbzLocal.tracksNativeClearSelection()
}
onActiveTabChanged: {
QbzLocal.loadTab(activeTab)
// The Artists detail derives from the album set (the DB aggregates
// the contributor list per album), so make sure it is loaded.
if (activeTab === "artists" && albums.length === 0) QbzLocal.loadTab("albums")
if (activeTab === "artists" && albums.length === 0)
QbzLocal.loadTab("albums-legacy")
// The tab that just appeared has covers to ask for and the one that
// left has covers to let go of. Each surface answers for itself.
artworkRefresh()
@@ -551,8 +566,12 @@ Rectangle {
return out
}
readonly property var albumsVisible:
applyFilter(sortRows(filterRows(albums, albumsSearch), albumsSort))
// Once F1 owns the Albums tab, never run the legacy O(n) JS pipeline in
// parallel. The old document may still be resident because Artists uses
// it until F2; it is evaluated only while that legacy surface is active.
readonly property var albumsVisible: QbzLocal.localAlbumsNativeActive
&& activeTab === "albums" ? []
: applyFilter(sortRows(filterRows(albums, albumsSearch), albumsSort))
readonly property var albumsGrouped: groupRows(albumsVisible, albumsGroup)
readonly property var foldersVisible:
@@ -905,7 +924,10 @@ Rectangle {
function toggleAlbumsMultiSelect() {
albumsMultiSelect = !albumsMultiSelect
if (!albumsMultiSelect) { albumsSelected = ({}); albumSel.anchorId = "" }
if (!albumsMultiSelect) {
albumsSelected = ({}); albumSel.anchorId = ""
if (QbzLocal.localAlbumsNativeActive) QbzLocal.albumsNativeClearSelection()
}
}
/// Excel-style selection — controls/SelectionModel.qml holds the anchor
/// and the Shift-range rule; this view keeps owning its maps. Ranges run
@@ -916,7 +938,17 @@ Rectangle {
albumsSelected = albumSel.next(albumsSelected, id, albumsVisible,
mods === undefined ? Qt.NoModifier : mods)
}
function toggleNativeAlbumSelected(index, mods) {
QbzLocal.albumsNativeToggleSelect(
index, (mods & Qt.ShiftModifier) !== 0)
}
function albumsBulkAction(action) {
if (QbzLocal.localAlbumsNativeActive) {
if (action === "clear") QbzLocal.albumsNativeClearSelection()
else if (action === "select-all") QbzLocal.albumsNativeSelectAll()
else QbzLocal.albumsNativeBulkAction(action)
return
}
if (action === "clear") { albumsSelected = ({}); albumSel.anchorId = ""; return }
if (action === "select-all") {
var s = {}
@@ -974,7 +1006,11 @@ Rectangle {
function exitMultiSelectMode() {
// Same "leaving drops the selection" contract the toggle buttons
// carry (toggleAlbumsMultiSelect / toggleTracksMultiSelect).
if (root.albumsMultiSelect) { root.albumsMultiSelect = false; root.albumsSelected = ({}) }
if (root.albumsMultiSelect) {
root.albumsMultiSelect = false
root.albumsSelected = ({})
if (QbzLocal.localAlbumsNativeActive) QbzLocal.albumsNativeClearSelection()
}
if (root.tracksMultiSelect) {
root.tracksMultiSelect = false
root.tracksSelected = ({})