Un-following a Qobuz playlist left it in the sidebar and in My Library. toggle_follow called unsubscribe_playlist and settled the ownership snapshot and stopped there, so the row only left at the next full refetch. Both surfaces are patched directly now, in both directions: following from a card buys the metadata with one get_playlist rather than re-reading getUserPlaylists, which lags the write. The un-follow collapses a hearted-and-followed playlist to the one row the next load will actually produce instead of dropping it, because the local heart is a library.db flag that survives the unsubscribe — removing every row made it vanish and come back, which is the original complaint wearing the other shoe. The Plex mark on album cards was a hard-drive glyph tinted accent — a blue hard drive — and the three Qobuz kinds drew no badge at all. The badge now ports discover/AlbumCard.slint:326-354 exactly: 24x24 at radius 4 pinned bottom-right with a 6px inset, plex-logo untinted at 16, qobuz-logo at 18 (22 for a download), the purchase arm gold on its amber chip, hard-drive white at 14 for local only. The brand marks are plain Images: routing them through QbzIcon floods every pixel with the tint and flattens a logo to a silhouette. SourceIcon moves to controls/ since it is no longer local-only, and every other surface drawing a hard-drive was audited against its .slint. The local album detail loses the two nav chevrons above the art. They are a Tauri-era leftover from before the app had global navigation. My QBZ used a bespoke card in the grid. MyQbzDetailCard is deleted and the standard cards/AlbumCard is mounted with its overlay. localMode bundled four separate things — action routing, the heart, the pin and Block this album — so a multi-source container had to switch all four off together; it is split, and the catalog affordances now follow the ITEM. On a Qobuz cell the heart works through the shared favourite seam; on a track, playlist, local or Plex cell it is absent rather than dead. Row covers carry two rungs: the list keeps _50, the grid gets _600. Not the _230 that would merely cover a 200px card — the complaint was that the cover reads softer than a normal AlbumCard, and a normal AlbumCard is fed image.large, which IS _600. _200 is not a rung the CDN serves at all (it answers 404). The My QBZ grid sets reuseItems, and AlbumCard assigns to its own isFavorite and isPinned when toggled, which destroys the binding that fed them. A recycled delegate therefore showed the previous row's heart and the click added the new album while the glyph said remove. Restored with the Qt.binding idiom this port already uses on TrackCard, PlaylistCard and TrackRow. The details view no longer force-opens every row on entry, and the per-row open state persists. Library > Artists showed placeholders for nearly every portrait. The side panel never reported its artwork window — the one caller of libraryArtworkWindow is LibraryView.reportWindow, and the panel had no report call at all, so no key ever reached the resolver and artMap stayed empty for its rows. The rail now reports through that same path, with the section-header sentinel reportWindow already skips. Note the reference has no portrait fallback either: an artist Qobuz serves no image for is a placeholder on both sides. cc
538 lines
25 KiB
QML
538 lines
25 KiB
QML
// Local album detail — QML port of album/LocalAlbumView.slint, a ROUTED
|
|
// PAGE (not a pane inside the Local Library view, which is how this port
|
|
// used to render it).
|
|
//
|
|
// Homologated 1:1 with the Qobuz AlbumView: whole-page Flickable, the same
|
|
// header proportions and action row, the toolbar (quality badge + track
|
|
// search), the column header, and source-aware track rows. The intentional
|
|
// differences vs the Qobuz page are the Slint's own: no label/awards
|
|
// sidebar, no Qobuz context menus, and the local-only version picker.
|
|
//
|
|
// Local actions ONLY: play all / shuffle / edit tags / add to playlist /
|
|
// add to Mixtape. A multi-artist album gets the "+N more artists" expander;
|
|
// a multi-disc album gets the disc dividers with their per-disc ⋯ menu.
|
|
//
|
|
// ONE deliberate ADDITION over the reference (owner, 2026-07-31): the
|
|
// toolbar's multi-select toggle and its bulk bar. The Slint puts those on the
|
|
// QOBUZ album page (AlbumPageView.slint:752-808) and not on the local one,
|
|
// which is an asymmetry, not a design — the local page is where the owner
|
|
// asked for them. See the `multiSelect` block below for the wiring.
|
|
|
|
import QtQuick
|
|
import QtQuick.Window
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../rows"
|
|
import "../theme"
|
|
import "local"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
color: ambientOn ? "transparent" : theme.surfaceMain
|
|
readonly property bool ambientOn: QbzShell.ambientMode > 0 && QbzPlayer.npHasTrack
|
|
radius: 12
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// ---------------------------- document -------------------------------
|
|
function parseDoc(json, fallback) {
|
|
if (json === "") return fallback
|
|
try { return JSON.parse(json) } catch (e) { return fallback }
|
|
}
|
|
readonly property var doc: parseDoc(QbzLocal.localAlbumJson, null)
|
|
readonly property var album: doc ? doc.album : null
|
|
readonly property var tracks: doc ? doc.tracks : []
|
|
readonly property var versions: album && album.versions ? album.versions : []
|
|
readonly property var allArtists: {
|
|
if (!album) return []
|
|
var raw = (album.allArtists || "").split(",")
|
|
var out = []
|
|
for (var i = 0; i < raw.length; i++) {
|
|
var n = raw[i].trim()
|
|
if (n !== "" && out.indexOf(n) < 0) out.push(n)
|
|
}
|
|
return out
|
|
}
|
|
// The Slint's `info-line` — built in Rust there, derived here from the
|
|
// fields the album row already carries.
|
|
readonly property string infoLine: {
|
|
if (!album) return ""
|
|
var parts = []
|
|
if ((album.year || "") !== "") parts.push(album.year)
|
|
parts.push((album.trackCount || 0) + " "
|
|
+ QbzSession.tr("tracks", QbzSession.trRev))
|
|
if ((album.duration || "") !== "") parts.push(album.duration)
|
|
if ((album.format || "") !== "") parts.push(album.format.toUpperCase())
|
|
return parts.join(" • ")
|
|
}
|
|
|
|
property string trackQuery: ""
|
|
// Client-side track search: an album's track list is bounded, so the
|
|
// Slint's LocalAlbumActions.search is a pure view filter here.
|
|
readonly property var visibleTracks: {
|
|
if (trackQuery === "") return tracks
|
|
var q = trackQuery.toLowerCase()
|
|
var out = []
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
if ((tracks[i].title || "").toLowerCase().indexOf(q) >= 0) out.push(tracks[i])
|
|
}
|
|
return out
|
|
}
|
|
// ------------------------- multi-select ------------------------------
|
|
// The Slint's LOCAL album page has no bulk bar; its QOBUZ one does
|
|
// (album/AlbumPageView.slint:752-808 — the 30x30 square toggle in the
|
|
// toolbar beside the search box, then MultiSelectBar over the rows).
|
|
// Owner call 2026-07-31: close that asymmetry here, in the local page.
|
|
//
|
|
// Selection lives in QML, exactly as the two Local Library tabs keep
|
|
// theirs (LocalLibraryView.qml:684-720) — the ids go down per action, so
|
|
// `select-all` / `clear` never reach Rust. Scope is "track": the album
|
|
// detail's rows ARE `LocalState.detail_raw` (local_album_actions.rs:154
|
|
// caches every version's rows there), which is one of the two caches
|
|
// `local_bulk::resolve_blocking` reads for that scope, so a Plex row with
|
|
// no DB id resolves too.
|
|
property bool multiSelect: false
|
|
property var selected: ({})
|
|
readonly property int selectedCount: Object.keys(selected).length
|
|
// A republish means the track list changed underneath the selection — a
|
|
// different album, or the version picker swapping every row for another
|
|
// physical copy's. Both invalidate the ids, so the selection goes and
|
|
// select mode with it (the same "leaving drops the selection" contract
|
|
// `local_bulk::set_select_mode` documents for the tree rail).
|
|
onDocChanged: { multiSelect = false; selected = ({}) }
|
|
function toggleSelected(id) {
|
|
var s = Object.assign({}, selected)
|
|
if (s[id]) delete s[id]; else s[id] = true
|
|
selected = s
|
|
}
|
|
/// Selected ids in the order they are ON SCREEN, so a bulk enqueue lands
|
|
/// in disc/track order. `Object.keys` on the selection map would sort the
|
|
/// numeric-looking keys ascending by row id, which is the INSERT order of
|
|
/// the scan, not the album's. Iterates `tracks`, not `visibleTracks`, so a
|
|
/// selection made before typing in the search box still enqueues whole.
|
|
function selectedIdsInOrder() {
|
|
var out = []
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
if (selected[tracks[i].id] === true) out.push(tracks[i].id)
|
|
}
|
|
return out
|
|
}
|
|
function bulkAction(action) {
|
|
if (action === "clear") { selected = ({}); return }
|
|
if (action === "select-all") {
|
|
// What the user can SEE: the search box is a view filter, so
|
|
// "select all" means the filtered set (LocalLibraryView.qml:715).
|
|
var s = {}
|
|
for (var i = 0; i < visibleTracks.length; i++) s[visibleTracks[i].id] = true
|
|
selected = s
|
|
return
|
|
}
|
|
QbzLocal.bulkAction("track", JSON.stringify(selectedIdsInOrder()), action)
|
|
}
|
|
|
|
// Disc divider before the first row of each disc on a multi-disc album
|
|
// (0 = flat list, as the Slint's disc-header-number).
|
|
function discHeader(i) {
|
|
var t = visibleTracks[i]
|
|
if (!t) return 0
|
|
var multi = false
|
|
for (var j = 0; j < visibleTracks.length; j++) {
|
|
if ((visibleTracks[j].disc || 1) > 1) { multi = true; break }
|
|
}
|
|
if (!multi) return 0
|
|
if (i === 0) return t.disc || 1
|
|
return (visibleTracks[i - 1].disc || 1) !== (t.disc || 1) ? (t.disc || 1) : 0
|
|
}
|
|
|
|
// Cover — the same id-keyed artwork channel every local surface uses.
|
|
property var artMap: ({})
|
|
Connections {
|
|
target: QbzLocal
|
|
function onLocalArtworkReady(key, path) {
|
|
var m = root.artMap
|
|
m[key] = path
|
|
root.artMap = Object.assign({}, m)
|
|
}
|
|
}
|
|
onAlbumChanged: {
|
|
if (album && album.artKey) {
|
|
QbzLocal.artworkWindow(JSON.stringify([album.artKey]))
|
|
}
|
|
}
|
|
|
|
// ------------------------- skeleton pulse ----------------------------
|
|
// ONE 900ms Timer for the whole page. GATING RULE: freeze on NOT VISIBLE
|
|
// (view hidden / window minimized), NEVER on lost focus — a tiling
|
|
// desktop keeps windows visible and unfocused.
|
|
property bool skelPhase: false
|
|
readonly property bool windowShowing: root.Window.window
|
|
? (root.Window.window.visibility !== Window.Minimized
|
|
&& root.Window.window.visibility !== Window.Hidden)
|
|
: true
|
|
readonly property bool pageLoading: QbzLocal.localAlbumLoading
|
|
&& root.tracks.length === 0
|
|
// Local artwork resolution DROPS keys with no cover (local_artwork.rs),
|
|
// so the cover placeholder MUST be bounded or an album with no embedded
|
|
// art shimmers forever. Same constant as LocalLibraryView.artSettleMs.
|
|
readonly property int artSettleMs: 2500
|
|
readonly property bool coverPending: root.album
|
|
? ((root.album.artKey || "") !== ""
|
|
&& (root.artMap[root.album.artKey] || "") === "")
|
|
: false
|
|
Timer {
|
|
interval: 900
|
|
repeat: true
|
|
running: (root.pageLoading || root.coverPending)
|
|
&& root.visible && root.windowShowing
|
|
onTriggered: root.skelPhase = !root.skelPhase
|
|
}
|
|
|
|
// ============================ page ===================================
|
|
// Neutral header band (local albums have no artwork-derived tint yet).
|
|
Rectangle {
|
|
x: 0
|
|
y: 0
|
|
width: parent.width
|
|
height: 340
|
|
gradient: Gradient {
|
|
GradientStop { position: 0.0; color: "#181820" }
|
|
GradientStop { position: 0.16; color: "#181820" }
|
|
GradientStop { position: 1.0; color: "#00181820" }
|
|
}
|
|
}
|
|
|
|
Flickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: page.height + 100
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
Column {
|
|
id: page
|
|
x: 32
|
|
y: 11
|
|
width: parent.width - 64
|
|
spacing: 0
|
|
|
|
// ---- History navigation ----
|
|
// NOTHING here, by design. LocalAlbumView.slint:207 does mount
|
|
// `NavButtons { }`, but NavButtons.slint:5-8 is a ZERO-SIZE empty
|
|
// placeholder: "the back / forward / collapse buttons moved to the
|
|
// app header (HeaderBar, floating right) … the real buttons live
|
|
// once, in the header". Drawing real chevrons here was a Tauri-era
|
|
// leftover — the page-level nav from before the app had global
|
|
// history — and views/AlbumView.qml already gets this right.
|
|
// The 22px spacer stays: it is the .slint's own
|
|
// `Rectangle { height: 22px; }` (:211), which follows a zero-high
|
|
// NavButtons, so the header lands exactly where it does today.
|
|
Item { width: 1; height: 22 }
|
|
|
|
// ---- Album header ----
|
|
// Placeholder in the header's own proportions (224px cover, 32px
|
|
// gap — LocalAlbumHeader.qml:32/46) until the document lands.
|
|
QbzSkeleton {
|
|
visible: root.pageLoading && !root.album
|
|
variant: "header"
|
|
width: parent.width
|
|
height: 224
|
|
coverSize: 224
|
|
coverGap: 32
|
|
actionCount: 5
|
|
phase: root.skelPhase
|
|
}
|
|
LocalAlbumHeader {
|
|
visible: !(root.pageLoading && !root.album)
|
|
width: parent.width
|
|
album: root.album
|
|
allArtists: root.allArtists
|
|
infoLine: root.infoLine
|
|
versions: root.versions
|
|
coverSource: root.album ? (root.artMap[root.album.artKey] || "") : ""
|
|
coverPending: root.coverPending
|
|
skelPhase: root.skelPhase
|
|
artSettleMs: root.artSettleMs
|
|
onOpenArtist: function (name) { root.openArtist(name) }
|
|
}
|
|
|
|
Item { width: 1; height: 20 }
|
|
Rectangle { width: parent.width; height: 1; color: theme.borderSubtle }
|
|
Item { width: 1; height: 8 }
|
|
|
|
// ---- Loading ----
|
|
// The track list in the shape it will arrive in (50px rows, no
|
|
// artwork column on this page), replacing the 36px spinner the
|
|
// Slint mounts here. ONE instance, ONE animator.
|
|
QbzSkeleton {
|
|
visible: root.pageLoading
|
|
variant: "rowList"
|
|
width: parent.width
|
|
height: visible ? 280 : 0
|
|
rowH: 50
|
|
rowGap: 0
|
|
rowArt: false
|
|
phase: root.skelPhase
|
|
}
|
|
|
|
// ---- Toolbar: quality badge + track search + select toggle ----
|
|
// AlbumPageView.slint:686-786 numbers: 52px band, the search box
|
|
// cut from 280 to 168 "so the toolbar gives room to the square
|
|
// select button beside it", 16px gap, a 30x30 radius-6 square
|
|
// (NOT a round CircleAction) that goes accent while select mode
|
|
// is on.
|
|
Item {
|
|
width: parent.width
|
|
height: 52
|
|
QualityBadgeFull {
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
tier: root.album ? (root.album.qualityTier || "") : ""
|
|
detail: root.album ? (root.album.qualityDetail || "") : ""
|
|
}
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 16
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 168
|
|
height: 34
|
|
radius: 6
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
color: theme.surfaceElevated
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
spacing: 7
|
|
QbzIcon {
|
|
name: "search"
|
|
width: 14
|
|
height: 14
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
tintName: "muted"
|
|
}
|
|
Item {
|
|
width: parent.width - 21
|
|
height: parent.height
|
|
clip: true
|
|
TextInput {
|
|
id: searchInput
|
|
anchors.fill: parent
|
|
color: theme.textPrimary
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
selectByMouse: true
|
|
onTextEdited: root.trackQuery = text
|
|
}
|
|
Text {
|
|
visible: searchInput.text === ""
|
|
anchors.fill: parent
|
|
text: QbzSession.tr("Search tracks...", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Multi-select toggle. Unlike the Qobuz AlbumView.qml's
|
|
// (dimmed and inert — that bridge has no bulk seam), this
|
|
// one is LIVE: QbzLocal.bulkAction already carries the
|
|
// "track" scope this page needs.
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 30
|
|
height: 30
|
|
radius: 6
|
|
border.width: 1
|
|
border.color: root.multiSelect ? theme.accent : theme.borderSubtle
|
|
color: (root.multiSelect || selectArea.containsMouse)
|
|
? theme.surfaceHover
|
|
: theme.surfaceElevated
|
|
QbzIcon {
|
|
name: "square-check-big"
|
|
width: 15
|
|
height: 15
|
|
anchors.centerIn: parent
|
|
tintName: root.multiSelect
|
|
? "accent"
|
|
: (selectArea.containsMouse ? "textPrimary" : "secondary")
|
|
}
|
|
MouseArea {
|
|
id: selectArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
root.multiSelect = !root.multiSelect
|
|
if (!root.multiSelect) root.selected = ({})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Bulk action bar ----
|
|
// Only actions with a LIVE backend, in a context where they mean
|
|
// something. Dropped from the Slint album bar's seven:
|
|
// * add-to-favorites / remove-favorites — local hearts are not
|
|
// wired at all (local_bulk.rs's arm is a log-only no-op, and
|
|
// the store is keyed by file path behind a private handle);
|
|
// * make-offline — these tracks ARE the local files.
|
|
// Added: add-to-mixtape, which the two Local Library bars already
|
|
// offer and which `local_bulk::apply` serves live.
|
|
QbzMultiSelectBar {
|
|
visible: root.multiSelect
|
|
width: parent.width
|
|
selectedCount: root.selectedCount
|
|
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 },
|
|
{ "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.bulkAction(id) }
|
|
}
|
|
// The Slint's 8px gutter under the bar (AlbumPageView.slint:808).
|
|
Item { visible: root.multiSelect; width: 1; height: 8 }
|
|
|
|
// ---- Column header ----
|
|
// rows/TrackListHeader.qml, on rows/TrackCols.qml geometry — the
|
|
// same object the LocalTrackRows below lay their cells out from.
|
|
// (LocalAlbumView.slint:501-540 hardcodes a second set here —
|
|
// 16px gaps, Duration 80, Quality 80 — that its own rows do not
|
|
// use; the port does not copy that hazard.)
|
|
//
|
|
// `trailingReserve: 72` is the slice LocalTrackRow spends OUTSIDE
|
|
// the shared row: it narrows the shared TrackRow by 26 (source
|
|
// glyph gutter) + 46 (its own re-drawn ⋯: 32 + one 14px gap), so
|
|
// the labelled columns end 72px short of the row's right edge.
|
|
// Same constant, one place: LocalTrackRow.qml:82.
|
|
TrackListHeader {
|
|
width: parent.width
|
|
bandHeight: 40
|
|
labelSpacing: 0.5
|
|
showFavorite: false
|
|
showMenu: false
|
|
trailingReserve: 72
|
|
}
|
|
|
|
// ---- Track list ----
|
|
// An album's track count is bounded (the windowed views are for
|
|
// the library-scale surfaces), so this mounts like the Slint.
|
|
Column {
|
|
width: parent.width
|
|
spacing: 0
|
|
Repeater {
|
|
model: root.visibleTracks
|
|
delegate: Column {
|
|
id: trackBlock
|
|
required property var modelData
|
|
required property int index
|
|
width: page.width
|
|
spacing: 0
|
|
|
|
// Disc divider + its per-disc ⋯ menu.
|
|
Item {
|
|
visible: root.discHeader(trackBlock.index) > 0
|
|
width: parent.width
|
|
height: visible ? 40 : 0
|
|
Text {
|
|
x: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("Disc", QbzSession.trRev) + " "
|
|
+ root.discHeader(trackBlock.index)
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
font.weight: theme.weightSemibold
|
|
font.letterSpacing: 0.5
|
|
}
|
|
Rectangle {
|
|
x: parent.width - 44
|
|
width: 32
|
|
height: 32
|
|
radius: theme.radiusSm
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: discArea.containsMouse ? theme.surfaceElevated : "transparent"
|
|
QbzIcon {
|
|
name: "ellipsis"
|
|
width: 16
|
|
height: 16
|
|
anchors.centerIn: parent
|
|
tintName: discArea.containsMouse ? "textPrimary" : "muted"
|
|
}
|
|
MouseArea {
|
|
id: discArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: function (mouse) {
|
|
discMenu.openAtCursor(discArea, mouse.x, mouse.y)
|
|
}
|
|
}
|
|
CardMenu {
|
|
id: discMenu
|
|
menuWidth: 200
|
|
entries: [
|
|
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
|
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-start", "action": "next" },
|
|
{ "label": QbzSession.tr("Play later", QbzSession.trRev), "icon": "list-plus", "action": "later" },
|
|
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
|
]
|
|
onPicked: function (a) {
|
|
QbzLocal.albumDiscAction(root.discHeader(trackBlock.index), a)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LocalTrackRow {
|
|
width: page.width
|
|
item: trackBlock.modelData
|
|
number: trackBlock.modelData.number > 0
|
|
? trackBlock.modelData.number : trackBlock.index + 1
|
|
showAlbum: false
|
|
showArtwork: false
|
|
// The checkbox LocalTrackRow already draws over
|
|
// the number cell for the Tracks tab — no fork,
|
|
// no new component (rule 5).
|
|
selectMode: root.multiSelect
|
|
checked: root.selected[trackBlock.modelData.id] === true
|
|
onPlayRequested: QbzLocal.playAlbumTrack(
|
|
root.album.id, trackBlock.modelData.id)
|
|
onEnqueueRequested: function (m) {
|
|
QbzLocal.enqueue("track", trackBlock.modelData.id, m)
|
|
}
|
|
onToggleSelect: root.toggleSelected(trackBlock.modelData.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QbzScrollBar {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
target: flick
|
|
visible: flick.contentHeight > flick.height
|
|
}
|
|
|
|
// Local/Plex artists have no catalog id, so "go to artist" is a NAME
|
|
// route into the Local Library Artists tab (the Slint's source-aware
|
|
// open-artist).
|
|
function openArtist(name) {
|
|
QbzLocal.openArtistByName(name)
|
|
QbzShell.navigateTo("local")
|
|
}
|
|
}
|