Selection existed but had no keyboard: every click was a single toggle,
so picking 40 tracks meant 40 clicks. Slint and Tauri have had ranges
since 2026-06; this is that port.
NINE surfaces, not the five the track views suggested: Album, Artist,
Playlist, Label, Local Album, plus Library's tracks and albums and Local
Library's tracks and albums. Each carried its own hand-copied `selected`
map and its own `toggleSelected`, so the rule went into
controls/SelectionModel.qml and the maps stayed where they were — the
smallest change that stops it being copied a tenth time.
The rules are Tauri's `applyShiftRange`, unchanged: a plain click
toggles one row without clearing the rest, Shift fills the range from
the anchor ADDITIVELY (so re-dragging a range cannot eat the set) and
leaves the anchor where it is, and Ctrl/Cmd is the same as a plain click
— not a shortcut, but the behaviour both references already have,
because an accumulating click is what "add without disturbing" means.
Qt makes this CHEAPER than Slint, and the difference is worth recording:
Slint had no modifiers at click time and had to read a thread-local set
from a ModifiersChanged handler, with a documented stale-read edge case
when the window lost focus mid-chord. A QML MouseArea delivers
`mouse.modifiers` on the event itself, so the modifier and the click can
never disagree and no such seam exists here.
Three judgement calls, stated because they are not forced:
- Library gets TWO models, one per kind. Its rows are mixed, and one
anchor would let a Shift-click on a track sweep up the albums drawn
between it and the anchor.
- Ranges run over the VISIBLE rows. Local Library's search box is a
view filter, and a range over rows the user cannot see is not the
range they asked for — the same call `select-all` already made.
- The Local Library tree rail keeps plain toggles. Its selection lives
in Rust (QbzLocal.treeToggle*Select), so there is no QML map to hang
an anchor off; teaching the Rust side about an anchor is its own
change, and the file says so where a reader would expect otherwise.
The anchor is an ID, not an index, because every one of these views can
re-sort or filter under a live selection — an index anchor silently
points at a different track the moment the order changes. Slint's
selection.rs made the same call for the same reason.
Verified by driving the app: in Abbey Road, click row 1 then Shift-click
row 5 selects exactly rows 1-5 and leaves 6-8 untouched, with the bar
reading "5 selected"; the same in a playlist, additively over a row that
was already selected.
cc
149 lines
6.5 KiB
QML
149 lines
6.5 KiB
QML
// Albums tab body (LocalLibraryView.slint:1133).
|
|
//
|
|
// States, in the Slint's own order: initial spinner, load error + Retry,
|
|
// "nothing indexed yet", "nothing matches" (search OR the quality/format/
|
|
// source filter emptied the visible set), then the chunked collection with
|
|
// the bulk bar and the A-Z jump strip.
|
|
//
|
|
// Deviation: the Slint scrolls its MultiSelectBar away with the grid (it
|
|
// lives inside the scrolling page); here the bar is pinned above the
|
|
// collection, because the collection IS the scrolling view (one windowing
|
|
// ListView) rather than a page inside a Flickable.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../../controls"
|
|
import "../../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var view: null
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// Loading = the SHAPE of the collection that is coming (the Slint mounts
|
|
// a bare 36px LoadingSpinner here), matching the tab's current view mode:
|
|
// the 220x266 card grid or the 56px list rows. ONE instance for the whole
|
|
// viewport — one animator, not one per cell (QbzSkeleton COST note).
|
|
QbzSkeleton {
|
|
visible: QbzLocal.localAlbumsLoading && root.view.albumsView === "grid"
|
|
variant: "cardGrid"
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 32
|
|
anchors.rightMargin: 32
|
|
anchors.topMargin: 16
|
|
cellW: 220
|
|
cellH: 266
|
|
phase: root.view.skelPhase
|
|
}
|
|
QbzSkeleton {
|
|
visible: QbzLocal.localAlbumsLoading && root.view.albumsView !== "grid"
|
|
variant: "rowList"
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 32
|
|
anchors.rightMargin: 32
|
|
anchors.topMargin: 16
|
|
rowH: 56
|
|
rowGap: 0
|
|
rowArtSize: 40
|
|
phase: root.view.skelPhase
|
|
}
|
|
|
|
// Load error + retry.
|
|
Column {
|
|
visible: !QbzLocal.localAlbumsLoading && QbzLocal.localAlbumsError !== ""
|
|
anchors.centerIn: parent
|
|
spacing: 10
|
|
Text {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: QbzSession.tr("Couldn't load your local albums.", QbzSession.trRev)
|
|
color: theme.textPrimary
|
|
font.pixelSize: theme.fontBody
|
|
font.weight: theme.weightSemibold
|
|
}
|
|
SettingsButton {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: QbzSession.tr("Retry", QbzSession.trRev)
|
|
onClicked: QbzLocal.loadTab("albums")
|
|
}
|
|
}
|
|
|
|
LocalNote {
|
|
visible: !QbzLocal.localAlbumsLoading && QbzLocal.localAlbumsError === ""
|
|
&& root.view.albums.length === 0 && root.view.albumsSearch === ""
|
|
text: QbzSession.tr("No albums in your local library yet.", QbzSession.trRev)
|
|
}
|
|
LocalNote {
|
|
visible: !QbzLocal.localAlbumsLoading && QbzLocal.localAlbumsError === ""
|
|
&& root.view.albums.length > 0 && root.view.albumsVisible.length === 0
|
|
text: QbzSession.tr("No albums match your search.", QbzSession.trRev)
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 32
|
|
anchors.rightMargin: 32
|
|
anchors.topMargin: 16
|
|
spacing: 8
|
|
visible: !QbzLocal.localAlbumsLoading && QbzLocal.localAlbumsError === ""
|
|
&& root.view.albumsVisible.length > 0
|
|
|
|
QbzMultiSelectBar {
|
|
visible: root.view.albumsMultiSelect
|
|
width: parent.width
|
|
selectedCount: root.view.albumsSelectedCount
|
|
actions: [
|
|
{ "id": "select-all", "label": QbzSession.tr("Select all", QbzSession.trRev), "icon": "square-check-big", "danger": false, "needsSelection": false },
|
|
{ "id": "queue", "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "danger": false, "needsSelection": true },
|
|
{ "id": "play-later", "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "danger": false, "needsSelection": true },
|
|
{ "id": "play-next", "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "danger": false, "needsSelection": true },
|
|
{ "id": "add-to-playlist", "label": QbzSession.tr("Add to playlist", QbzSession.trRev), "icon": "list-music", "danger": false, "needsSelection": true },
|
|
{ "id": "add-to-mixtape", "label": QbzSession.tr("Add to Mixtape/Collection", QbzSession.trRev), "icon": "cassette-tape", "danger": false, "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)
|
|
|
|
LocalAlbumCollection {
|
|
id: collection
|
|
anchors.fill: parent
|
|
anchors.rightMargin: root.view.albumsGroup === "alpha" ? 20 : 0
|
|
// 32 of tab padding, plus the 20 above when the strip is up.
|
|
hostRightInset: 32 + (root.view.albumsGroup === "alpha" ? 20 : 0)
|
|
scrollBarInset: root.view.albumsGroup === "alpha" ? 34 : 4
|
|
view: root.view
|
|
surface: "albums"
|
|
scrollScope: "local:albums"
|
|
rows: root.view.albumsVisible
|
|
groups: root.view.albumsGrouped
|
|
grouped: root.view.albumsGroup !== "off"
|
|
viewMode: root.view.albumsView
|
|
showSource: true
|
|
selectMode: root.view.albumsMultiSelect
|
|
selected: root.view.albumsSelected
|
|
onOpenRequested: function (id) { root.view.openAlbum(id) }
|
|
onPlayRequested: function (id) { QbzLocal.playAlbum(id, false) }
|
|
onEnqueueRequested: function (id, m) { QbzLocal.enqueue("album", id, m) }
|
|
onToggleSelect: function (id, mods) { root.view.toggleAlbumSelected(id, mods) }
|
|
}
|
|
QbzAlphaStrip {
|
|
visible: root.view.albumsGroup === "alpha"
|
|
&& collection.alphaJumps.length > 0
|
|
anchors.right: parent.right
|
|
// Out past the tab's 32 padding to 12 from the window edge,
|
|
// which is where LibraryView.qml:1269 puts the same strip.
|
|
anchors.rightMargin: -20
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
jumps: collection.alphaJumps
|
|
onJump: function (ordinal, index) { collection.jumpToEntry(index) }
|
|
}
|
|
}
|
|
}
|
|
}
|