**Deferred external rows.** The album page awaited the Last.fm row before publishing anything, so the tracklist waited on an external service plus MusicBrainz. It publishes in stages now — header and tracks first, then each Qobuz rail as it lands, then Last.fm — with every patch merged under a generation counter so a late reply for album A cannot paint onto album B. The artist page was already staged this way; its views were the part still showing bare "Loading…" text. **Skeletons as the standard.** One control, new variants rather than new components, mounted across the views. Its second job is the one the owner named: cleanup. A cover that never resolves settles the placeholder once and reveals the card's own empty state instead of shimmering forever — local albums with no embedded art produce no artwork entry at all, so a placeholder gated only on "not arrived yet" would never stop. **Local artwork streams.** Fifteen seconds to first cover on a 1274-album grid was one batch emitted after the whole window resolved. Each cover emits as it resolves now, visible band before prefetch margin. **Lyrics migrated.** Smooth sync instead of a 1 Hz tick, word-level karaoke where the document carries it, the surrounding-line dimming, the controls flyout and the translation layer the v10 backend already supported. Exactly one row subscribes to the 30 Hz output; the timer drops to 250ms when paused or hidden. **Navigation by mouse and touchpad.** Side buttons and a two-finger horizontal swipe, matching #653 — including the part that matters, which is that a swipe over a carousel scrolls the carousel. The layer is declared first in the shell so everything else gets first refusal. **Context menus.** Brought to the Slint inventory, Play later included, with right-click opening the same menu as the button everywhere. Entries whose seam does not exist are hidden rather than shipped inert. Two controls that rendered and no-opped are now honestly disabled: the My QBZ nav row, which routes to a view the shell has no case for, and three sidebar menu entries with no backing call. The sidebar caret was never a condition bug. The row is a positioner and the name cell's width ignored the count label, so a folder WITH content pushed the chevron past the clip — which is why it vanished exactly when it mattered. cc
172 lines
6.7 KiB
QML
172 lines
6.7 KiB
QML
// Folders tree-mode LEFT RAIL (LocalLibraryView.slint:1641).
|
|
//
|
|
// Header toolbar: select-mode, collapse-all, open-ephemeral-folder, then the
|
|
// search box (stretches). Below it the compact bulk bar (only in select mode
|
|
// with a selection — icon buttons, because the rail is narrow), then the
|
|
// tree itself.
|
|
//
|
|
// The tree is a FLAT array windowed by a ListView — the recursive component
|
|
// is deliberately not reproduced, and levels are fetched lazily on expand.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../../controls"
|
|
import "../../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var view: null
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 8
|
|
anchors.topMargin: 12
|
|
anchors.bottomMargin: 8
|
|
spacing: 8
|
|
|
|
// ---- Header toolbar ----
|
|
Row {
|
|
width: parent.width
|
|
height: 30
|
|
spacing: 4
|
|
QbzIconButton {
|
|
name: "square-check-big"
|
|
btnSize: 30
|
|
iconSize: 15
|
|
active: root.view.treeSelectMode
|
|
activeBackground: true
|
|
onClicked: root.view.toggleTreeSelectMode()
|
|
}
|
|
QbzIconButton {
|
|
name: "chevron-up"
|
|
btnSize: 30
|
|
iconSize: 15
|
|
onClicked: QbzLocal.treeCollapseAll()
|
|
}
|
|
QbzIconButton {
|
|
name: "folder-open"
|
|
btnSize: 30
|
|
iconSize: 15
|
|
onClicked: QbzLocal.ephemeralOpen()
|
|
}
|
|
// Filler: the search collapses to a 30px magnifier, so the slot
|
|
// stays right-aligned and the field opens LEFT over this gap.
|
|
Item {
|
|
width: Math.max(0, parent.width - 3 * 30 - 30 - 4 * 4)
|
|
height: 1
|
|
}
|
|
LocalSearchBox {
|
|
boxWidth: parent.width - 3 * 34
|
|
placeholder: QbzSession.tr("Search folders", QbzSession.trRev)
|
|
onEdited: function (v) {
|
|
root.view.treeSearch = v
|
|
treeSearchDebounce.restart()
|
|
}
|
|
Timer {
|
|
id: treeSearchDebounce
|
|
interval: 180
|
|
onTriggered: QbzLocal.treeSearch(root.view.treeSearch)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Compact bulk bar (select mode + a selection) ----
|
|
Rectangle {
|
|
visible: root.view.treeSelectMode && QbzLocal.localTreeSelectedCount > 0
|
|
width: parent.width
|
|
height: visible ? 36 : 0
|
|
radius: 8
|
|
color: theme.surfaceElevated
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 6
|
|
spacing: 2
|
|
Text {
|
|
width: parent.width - 7 * 30
|
|
height: parent.height
|
|
text: QbzLocal.localTreeSelectedCount + " "
|
|
+ QbzSession.tr("sel", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontLegal
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
Repeater {
|
|
// ASSET GAP: Slint's "Check all" uses folder-check, which
|
|
// is not baked in the Qt icon set (see GLUE).
|
|
model: [
|
|
{ "icon": "square-check-big", "action": "select-all", "tip": QbzSession.tr("Check all", QbzSession.trRev) },
|
|
{ "icon": "list-start", "action": "play-next", "tip": QbzSession.tr("Play next", QbzSession.trRev) },
|
|
{ "icon": "list-plus", "action": "play-later", "tip": QbzSession.tr("Play later", QbzSession.trRev) },
|
|
{ "icon": "list-end", "action": "queue", "tip": QbzSession.tr("Add to queue", QbzSession.trRev) },
|
|
{ "icon": "list-music", "action": "add-to-playlist", "tip": QbzSession.tr("Add to playlist", QbzSession.trRev) },
|
|
{ "icon": "cassette-tape", "action": "add-to-mixtape", "tip": QbzSession.tr("Add to Mixtape/Collection", QbzSession.trRev) },
|
|
{ "icon": "x", "action": "clear", "tip": QbzSession.tr("Clear", QbzSession.trRev) },
|
|
]
|
|
delegate: QbzIconButton {
|
|
required property var modelData
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
name: modelData.icon
|
|
btnSize: 28
|
|
iconSize: 15
|
|
onClicked: QbzLocal.foldersBulkAction(modelData.action)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- The tree ----
|
|
Item {
|
|
width: parent.width
|
|
height: parent.height - 38
|
|
- (root.view.treeSelectMode && QbzLocal.localTreeSelectedCount > 0 ? 44 : 0)
|
|
|
|
// Lazy level fetch: the rail shows the shape of the tree rows
|
|
// (26px, small leading glyph) instead of a 28px spinner.
|
|
QbzSkeleton {
|
|
variant: "rowList"
|
|
anchors.fill: parent
|
|
visible: QbzLocal.localTreeLoading
|
|
rowH: 26
|
|
rowGap: 0
|
|
rowArtSize: 14
|
|
phase: root.view ? root.view.skelPhase : false
|
|
}
|
|
ListView {
|
|
id: treeList
|
|
anchors.fill: parent
|
|
visible: !QbzLocal.localTreeLoading
|
|
clip: true
|
|
cacheBuffer: 26 * 20
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
model: root.view.tree
|
|
delegate: TreeRow {
|
|
required property var modelData
|
|
width: treeList.width
|
|
node: modelData
|
|
selected: modelData.path === root.view.selectedFolder
|
|
selectMode: root.view.treeSelectMode
|
|
onToggled: QbzLocal.treeToggle(modelData.path, !modelData.expanded)
|
|
onActivated: root.view.selectFolder(modelData.path)
|
|
onToggleSelect: {
|
|
if (modelData.isFolder) QbzLocal.treeToggleFolderSelect(modelData.path)
|
|
else QbzLocal.treeToggleTrackSelect(modelData.path)
|
|
}
|
|
}
|
|
}
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
target: treeList
|
|
visible: treeList.contentHeight > treeList.height
|
|
}
|
|
}
|
|
}
|
|
}
|