fix(artist): in-library tab queues its own list, gets a Tracks section and a mode-aware jump-nav (#737)
A track clicked under "In library" played the artist's POPULAR queue: every row of the page went through `QbzPlayer.playArtistTrack`, whose queue is TOP_QUEUE and which starts that list at row 0 for any id it does not hold — the exact symptom in #737, including "it works only when the song is also popular". The library rows now play through `QbzLibrary.libraryPlayVisible` with the tab's full filtered list, the source-aware seam LibraryView uses; `PopularTrackRow` takes a `playHandler` instead of knowing which tab it is in. The tab itself becomes a first-class view over `artist.libraryItems`: - Tracks section modelled on Popular Tracks — header action cluster (Play all, select mode, the shared ⋯ menu re-pointed at the library list), 10-row preview built from a numeric model, Load more / View less opening the whole list. - JUMP TO follows the mode: in library only the Tracks / Albums sections that have rows, resolving anchors against the library column (the catalog set stayed up before and "Popular Tracks" scrolled to a section that was not drawn). - Purchases / favourites switches float in the strip while the tab is active, with the Library > Albums/Tracks semantics (neither = union, one = that source, both = intersection) and page-local state. - A cleared heart fades the row (purchases stay); the settle republishes the document and the row leaves. When the last row goes the page returns to the catalog instead of an empty tab. - Selection and bulk actions run over the active mode's lists; a mode switch clears the selection and re-arms the warm from cache. The view no longer parses `QbzLibrary.libraryJson` per binding. cc
This commit is contained in:
1 file changed
+289
-51
@@ -283,6 +283,51 @@ Rectangle {
|
|||||||
property string activeJumpTab: "popular-tracks"
|
property string activeJumpTab: "popular-tracks"
|
||||||
property string artistTab: "catalog"
|
property string artistTab: "catalog"
|
||||||
|
|
||||||
|
// ---- "In library" tab (#737 round, contract §3/§4) ---------------------
|
||||||
|
// The rows ride INSIDE the artist document (`artist.libraryItems`,
|
||||||
|
// artist_qt.rs) — favourites ∪ purchases of this artist, one row per
|
||||||
|
// entity, the purchase row representative and `isFavorite` meaning "also
|
||||||
|
// hearted". The two switches below are the `Library > Albums / Tracks`
|
||||||
|
// source switches with THEIR semantics (LibraryView.qml:566-590: neither
|
||||||
|
// = union, one = that source, both = intersection) but LOCAL state: the
|
||||||
|
// Library view's own switches must not reach into this page.
|
||||||
|
property bool libShowPurchases: false
|
||||||
|
property bool libShowFavorites: false
|
||||||
|
/// Preview 10 (Popular Tracks previews 5), "Load more" opens the whole
|
||||||
|
/// list at once — no paging, the rows are already in hand.
|
||||||
|
readonly property int libPreview: 10
|
||||||
|
property bool libTracksExpanded: false
|
||||||
|
readonly property var libItems: {
|
||||||
|
var src = artist.libraryItems || []
|
||||||
|
var out = []
|
||||||
|
for (var i = 0; i < src.length; i++) {
|
||||||
|
var it = src[i]
|
||||||
|
var purchased = it.group === "purchases"
|
||||||
|
var hearted = it.isFavorite === true
|
||||||
|
var included
|
||||||
|
if (root.libShowPurchases && root.libShowFavorites) included = purchased && hearted
|
||||||
|
else if (root.libShowPurchases) included = purchased
|
||||||
|
else if (root.libShowFavorites) included = hearted
|
||||||
|
else included = true
|
||||||
|
if (included) out.push(it)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
readonly property var libTracks: libItems.filter(function (x) { return x.kind === "track" })
|
||||||
|
readonly property var libAlbums: libItems.filter(function (x) { return x.kind === "album" })
|
||||||
|
/// The queue a row click / "Play all" builds from: the WHOLE filtered
|
||||||
|
/// list in render order, not only the revealed preview — a click on row
|
||||||
|
/// 3 must be followed by rows 4..n exactly like Popular Tracks queues its
|
||||||
|
/// full list. Handed to `QbzLibrary.libraryPlayVisible` / `libraryPlayAll`
|
||||||
|
/// (library_qt::play_from_visible), the source-aware seam LibraryView
|
||||||
|
/// uses — never `QbzPlayer.playArtistTrack`, whose queue is the POPULAR
|
||||||
|
/// list and which starts it at row 0 for any id it does not hold (#737).
|
||||||
|
function libTrackIds() {
|
||||||
|
var ids = []
|
||||||
|
for (var i = 0; i < root.libTracks.length; i++) ids.push(String(root.libTracks[i].id))
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Header atmosphere (ArtistPageView.slint:120-147) ----------------
|
// ---- Header atmosphere (ArtistPageView.slint:120-147) ----------------
|
||||||
// Same three-line rule as AlbumView (the .slint says "same rule as
|
// Same three-line rule as AlbumView (the .slint says "same rule as
|
||||||
// AlbumPageView" at :120). The pref is read LIVE off the settings
|
// AlbumPageView" at :120). The pref is read LIVE off the settings
|
||||||
@@ -684,6 +729,16 @@ Rectangle {
|
|||||||
// does not touch them, so the strip must not reshuffle per keystroke.
|
// does not touch them, so the strip must not reshuffle per keystroke.
|
||||||
readonly property var jumpTabs: {
|
readonly property var jumpTabs: {
|
||||||
var tabs = []
|
var tabs = []
|
||||||
|
// The strip is a SCROLL navigator over one vertical page, so it can
|
||||||
|
// only list what that page currently draws. In library mode that is
|
||||||
|
// the Tracks and Albums sections that have rows — the catalog set
|
||||||
|
// stayed up before and "Popular Tracks" scrolled to a section that
|
||||||
|
// was not there (owner capture, 2026-09-04).
|
||||||
|
if (root.artistTab === "library") {
|
||||||
|
if (root.libTracks.length > 0) tabs.push({ "id": "tracks", "label": QbzSession.tr("Tracks", QbzSession.trRev) })
|
||||||
|
if (root.libAlbums.length > 0) tabs.push({ "id": "albums", "label": QbzSession.tr("Albums", QbzSession.trRev) })
|
||||||
|
return tabs
|
||||||
|
}
|
||||||
var rawTop = artist.topTracks || []
|
var rawTop = artist.topTracks || []
|
||||||
var rawSections = artist.releaseSections || []
|
var rawSections = artist.releaseSections || []
|
||||||
var rawAppears = artist.appearsOn || []
|
var rawAppears = artist.appearsOn || []
|
||||||
@@ -740,7 +795,7 @@ Rectangle {
|
|||||||
var aid = (artist && artist.id) ? artist.id : ""
|
var aid = (artist && artist.id) ? artist.id : ""
|
||||||
if (aid !== "" && key === "artist:" + aid)
|
if (aid !== "" && key === "artist:" + aid)
|
||||||
root.setToggleState("artist", value)
|
root.setToggleState("artist", value)
|
||||||
else if (key.indexOf("track:") === 0)
|
else if (key.indexOf("track:") === 0 || key.indexOf("album:") === 0)
|
||||||
root.setToggleState(key, value)
|
root.setToggleState(key, value)
|
||||||
}
|
}
|
||||||
// Header pin, same seam: the overflow-menu row flips `artistPin`
|
// Header pin, same seam: the overflow-menu row flips `artistPin`
|
||||||
@@ -807,11 +862,28 @@ Rectangle {
|
|||||||
syncArtistState()
|
syncArtistState()
|
||||||
invalidatePortraitOverride()
|
invalidatePortraitOverride()
|
||||||
dispatchCovers()
|
dispatchCovers()
|
||||||
|
// The last owned row just left (a republish after an un-heart): the
|
||||||
|
// toggle unmounts, so the page must not stay on an empty tab with no
|
||||||
|
// way back.
|
||||||
|
if (artistTab === "library" && (artist.libraryCount || 0) === 0)
|
||||||
|
artistTab = "catalog"
|
||||||
}
|
}
|
||||||
// Cover dispatch keys off the raw document (artist.artUrl etc.), so
|
// Cover dispatch keys off the raw document (artist.artUrl etc.), so
|
||||||
// re-fire when the parsed value actually changes (same stale race).
|
// re-fire when the parsed value actually changes (same stale race).
|
||||||
// (the raw document drives the dispatch; onArtistChanged above covers it)
|
// (the raw document drives the dispatch; onArtistChanged above covers it)
|
||||||
onArtistTabChanged: if (artistTab === "library") dispatchLibCovers()
|
onArtistTabChanged: {
|
||||||
|
// A selection is a list of THIS mode's rows; it cannot survive the
|
||||||
|
// switch. The strip re-anchors on the first section the new mode
|
||||||
|
// draws (catalog keeps its Popular Tracks default).
|
||||||
|
setMultiSelect(false)
|
||||||
|
var first = root.jumpTabs.length > 0 ? root.jumpTabs[0].id : ""
|
||||||
|
root.activeJumpTab = artistTab === "catalog" ? "popular-tracks" : first
|
||||||
|
if (artistTab === "library") {
|
||||||
|
// Re-arm the warm, served from cache (main.rs warm_library).
|
||||||
|
QbzArtist.warmLibrary()
|
||||||
|
dispatchLibCovers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The document is republished several times per page (stories, then each
|
// The document is republished several times per page (stories, then each
|
||||||
// MusicBrainz section). Reset per-artist view state ONLY when the id
|
// MusicBrainz section). Reset per-artist view state ONLY when the id
|
||||||
@@ -835,14 +907,23 @@ Rectangle {
|
|||||||
/// range can run from the last top track into "Appears on" exactly as it
|
/// range can run from the last top track into "Appears on" exactly as it
|
||||||
/// looks on screen — `selectedIdsInOrder` already walks them that way.
|
/// looks on screen — `selectedIdsInOrder` already walks them that way.
|
||||||
SelectionModel { id: sel }
|
SelectionModel { id: sel }
|
||||||
|
/// The track lists a selection can span, in draw order, for the ACTIVE
|
||||||
|
/// mode: the catalog's Popular Tracks + Appears On, or the library tab's
|
||||||
|
/// Tracks. `onArtistTabChanged` clears the selection on a switch, so a
|
||||||
|
/// range never mixes the two.
|
||||||
|
function selectableLists() {
|
||||||
|
return root.artistTab === "library" ? [root.libTracks]
|
||||||
|
: [root.topTracks, root.appearsOn]
|
||||||
|
}
|
||||||
function toggleSelected(id, mods) {
|
function toggleSelected(id, mods) {
|
||||||
root.selected = sel.next(root.selected, id,
|
var lists = selectableLists()
|
||||||
root.topTracks.concat(root.appearsOn),
|
var all = lists.length > 1 ? lists[0].concat(lists[1]) : lists[0]
|
||||||
|
root.selected = sel.next(root.selected, id, all,
|
||||||
mods === undefined ? Qt.NoModifier : mods)
|
mods === undefined ? Qt.NoModifier : mods)
|
||||||
}
|
}
|
||||||
function selectedIdsInOrder() {
|
function selectedIdsInOrder() {
|
||||||
var out = []
|
var out = []
|
||||||
var lists = [root.topTracks, root.appearsOn]
|
var lists = selectableLists()
|
||||||
for (var l = 0; l < lists.length; l++) {
|
for (var l = 0; l < lists.length; l++) {
|
||||||
var rows = lists[l]
|
var rows = lists[l]
|
||||||
for (var i = 0; i < rows.length; i++)
|
for (var i = 0; i < rows.length; i++)
|
||||||
@@ -853,7 +934,7 @@ Rectangle {
|
|||||||
function bulkAction(action) {
|
function bulkAction(action) {
|
||||||
if (action === "select-all") {
|
if (action === "select-all") {
|
||||||
var m = {}
|
var m = {}
|
||||||
var lists = [root.topTracks, root.appearsOn]
|
var lists = selectableLists()
|
||||||
for (var l = 0; l < lists.length; l++)
|
for (var l = 0; l < lists.length; l++)
|
||||||
for (var i = 0; i < lists[l].length; i++) m[lists[l][i].id] = true
|
for (var i = 0; i < lists[l].length; i++) m[lists[l][i].id] = true
|
||||||
root.selected = m
|
root.selected = m
|
||||||
@@ -881,6 +962,12 @@ Rectangle {
|
|||||||
return
|
return
|
||||||
loadedArtistId = id
|
loadedArtistId = id
|
||||||
setMultiSelect(false)
|
setMultiSelect(false)
|
||||||
|
// A fresh artist opens on its catalog with the library tab's
|
||||||
|
// preview and switches at rest.
|
||||||
|
artistTab = "catalog"
|
||||||
|
libTracksExpanded = false
|
||||||
|
libShowPurchases = false
|
||||||
|
libShowFavorites = false
|
||||||
// Slint opens a fresh artist on Network, or on Magazine when
|
// Slint opens a fresh artist on Network, or on Magazine when
|
||||||
// MusicBrainz is off (an empty Network tab is worse than none).
|
// MusicBrainz is off (an empty Network tab is worse than none).
|
||||||
netTab = (artist.network && artist.network.mbAvailable) ? "network" : "magazine"
|
netTab = (artist.network && artist.network.mbAvailable) ? "network" : "magazine"
|
||||||
@@ -909,7 +996,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dispatchLibCovers() {
|
function dispatchLibCovers() {
|
||||||
var items = libraryTab.libItems || []
|
var items = root.libItems || []
|
||||||
var urls = []
|
var urls = []
|
||||||
for (var i = 0; i < items.length; i++) if (items[i].imageUrl) urls.push(items[i].imageUrl)
|
for (var i = 0; i < items.length; i++) if (items[i].imageUrl) urls.push(items[i].imageUrl)
|
||||||
dispatchArtwork(urls)
|
dispatchArtwork(urls)
|
||||||
@@ -1021,10 +1108,15 @@ Rectangle {
|
|||||||
|
|
||||||
function scrollToSection(id) {
|
function scrollToSection(id) {
|
||||||
root.activeJumpTab = id
|
root.activeJumpTab = id
|
||||||
for (var i = 0; i < sectionAnchors.children.length; i++) {
|
// Anchors resolve against the column of the ACTIVE mode: the catalog
|
||||||
var c = sectionAnchors.children[i]
|
// and library columns are siblings in the page, and a library tab
|
||||||
|
// resolved against the hidden catalog column would scroll to that
|
||||||
|
// column's coordinates.
|
||||||
|
var host = root.artistTab === "library" ? libraryTab : sectionAnchors
|
||||||
|
for (var i = 0; i < host.children.length; i++) {
|
||||||
|
var c = host.children[i]
|
||||||
if (c.anchorId === id) {
|
if (c.anchorId === id) {
|
||||||
flick.contentY = sectionAnchors.y + c.y - 48
|
flick.contentY = host.y + c.y - 48
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1047,6 +1139,17 @@ Rectangle {
|
|||||||
property var row: ({})
|
property var row: ({})
|
||||||
property int rowIndex: 0
|
property int rowIndex: 0
|
||||||
property bool showAlbum: true
|
property bool showAlbum: true
|
||||||
|
/// Where a play on this row goes — `function (id)`. Unset, the row
|
||||||
|
/// plays through `QbzPlayer.playArtistTrack`, whose queue is the
|
||||||
|
/// POPULAR list (artist_qt TOP_QUEUE). The "In library" tab passes
|
||||||
|
/// its own handler because a library row is NOT in that queue and
|
||||||
|
/// `top_queue` starts the popular list at row 0 for any id it does
|
||||||
|
/// not hold — issue #737. The row never asks which tab it is in.
|
||||||
|
property var playHandler: null
|
||||||
|
function play() {
|
||||||
|
if (popRow.playHandler) popRow.playHandler(String(popRow.row.id))
|
||||||
|
else QbzPlayer.playArtistTrack(popRow.row.id)
|
||||||
|
}
|
||||||
// Multi-select (the view's bulk bar): in select mode the whole row
|
// Multi-select (the view's bulk bar): in select mode the whole row
|
||||||
// body is the toggle and the leading cell swaps to a round checkbox
|
// body is the toggle and the leading cell swaps to a round checkbox
|
||||||
// (TrackRow.slint:173-177, 392-417).
|
// (TrackRow.slint:173-177, 392-417).
|
||||||
@@ -1248,7 +1351,7 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: !popRow.selectMode && !popRow.pulledDead
|
enabled: !popRow.selectMode && !popRow.pulledDead
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: QbzPlayer.playArtistTrack(row.id)
|
onClicked: popRow.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Title + artist.
|
// Title + artist.
|
||||||
@@ -1539,7 +1642,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
onPicked: function (a) {
|
onPicked: function (a) {
|
||||||
var id = popRow.row.id
|
var id = popRow.row.id
|
||||||
if (a === "play") QbzPlayer.playArtistTrack(id)
|
if (a === "play") popRow.play()
|
||||||
else if (a === "next") QbzPlayer.enqueueTrack(id, "next")
|
else if (a === "next") QbzPlayer.enqueueTrack(id, "next")
|
||||||
else if (a === "later") QbzPlayer.enqueueTrack(id, "later")
|
else if (a === "later") QbzPlayer.enqueueTrack(id, "later")
|
||||||
else if (a === "queue") QbzPlayer.enqueueTrack(id, "queue")
|
else if (a === "queue") QbzPlayer.enqueueTrack(id, "queue")
|
||||||
@@ -1579,7 +1682,7 @@ Rectangle {
|
|||||||
// toast, no error. The right-press area below is untouched, so the
|
// toast, no error. The right-press area below is untouched, so the
|
||||||
// menu still opens.
|
// menu still opens.
|
||||||
onDoubleClicked: if (!popRow.selectMode && !popRow.pulledDead)
|
onDoubleClicked: if (!popRow.selectMode && !popRow.pulledDead)
|
||||||
QbzPlayer.playArtistTrack(row.id)
|
popRow.play()
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (popRow.selectMode) popRow.toggleSelect(mouse.modifiers)
|
if (popRow.selectMode) popRow.toggleSelect(mouse.modifiers)
|
||||||
else mouse.accepted = false
|
else mouse.accepted = false
|
||||||
@@ -2563,7 +2666,10 @@ Rectangle {
|
|||||||
id: topMenuBtn
|
id: topMenuBtn
|
||||||
name: "ellipsis"
|
name: "ellipsis"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: function (mouse) { topMenu.openAtCursor(topMenuBtn, mouse.x, mouse.y) }
|
onClicked: function (mouse) {
|
||||||
|
topMenu.forLibrary = false
|
||||||
|
topMenu.openAtCursor(topMenuBtn, mouse.x, mouse.y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item { visible: topTracks.length > 0; width: 1; height: 10 }
|
Item { visible: topTracks.length > 0; width: 1; height: 10 }
|
||||||
@@ -2810,66 +2916,146 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================= In library tab =============================
|
// ================= In library tab =============================
|
||||||
|
// The user's OWN rows for this artist (`root.libItems`, off the
|
||||||
|
// artist document). Two first-class sections — Tracks modelled on
|
||||||
|
// Popular Tracks (header action cluster, 10-row preview, Load
|
||||||
|
// more) and the Albums grid — each carrying an `anchorId` so the
|
||||||
|
// JUMP TO strip resolves against THIS column in library mode.
|
||||||
Column {
|
Column {
|
||||||
id: libraryTab
|
id: libraryTab
|
||||||
visible: root.artistTab === "library" && !QbzArtist.artistLoading
|
visible: root.artistTab === "library" && !QbzArtist.artistLoading
|
||||||
width: page.bodyWidth
|
width: page.bodyWidth
|
||||||
spacing: 0
|
spacing: 0
|
||||||
readonly property var libItems: {
|
|
||||||
var out = []
|
|
||||||
var feed = libraryFeed()
|
|
||||||
for (var i = 0; i < feed.length; i++) {
|
|
||||||
if (feed[i].artistId === artist.id && (feed[i].kind === "track" || feed[i].kind === "album"))
|
|
||||||
out.push(feed[i])
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
readonly property var libAlbums: libItems.filter(function (x) { return x.kind === "album" })
|
|
||||||
readonly property var libTracks: libItems.filter(function (x) { return x.kind === "track" })
|
|
||||||
|
|
||||||
Text {
|
// Same bulk bar as the catalog column: `bulkAction` routes on
|
||||||
visible: libraryTab.libTracks.length > 0
|
// the active mode's lists (`selectableLists`).
|
||||||
text: QbzSession.tr("Tracks", QbzSession.trRev)
|
QbzMultiSelectBar {
|
||||||
color: theme.textPrimary
|
visible: root.multiSelect
|
||||||
font.pixelSize: theme.fontHeading
|
width: parent.width
|
||||||
font.weight: theme.weightSemibold
|
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": "make-offline", "label": QbzSession.tr("Make available offline", QbzSession.trRev), "icon": "cloud-download", "danger": false, "needsSelection": true },
|
||||||
|
{ "id": "clear", "label": QbzSession.tr("Clear", QbzSession.trRev), "icon": "x", "danger": false, "needsSelection": true }
|
||||||
|
]
|
||||||
|
onAction: function (id) { root.bulkAction(id) }
|
||||||
}
|
}
|
||||||
Item { visible: libraryTab.libTracks.length > 0; width: 1; height: 10 }
|
|
||||||
|
// --- Tracks ---------------------------------------------
|
||||||
|
// Header + action cluster: the Popular Tracks row above,
|
||||||
|
// with the actions re-pointed at the library list — Play all
|
||||||
|
// / Shuffle all through `libraryPlayAll`, the ⋯ menu shared
|
||||||
|
// with Popular Tracks (`topMenu.forLibrary`).
|
||||||
|
Row {
|
||||||
|
property string anchorId: "tracks"
|
||||||
|
visible: root.libTracks.length > 0
|
||||||
|
width: parent.width
|
||||||
|
spacing: 12
|
||||||
|
Text {
|
||||||
|
width: parent.width - 44 - 32 - 32 - 3 * 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: QbzSession.tr("Tracks", QbzSession.trRev)
|
||||||
|
color: theme.textPrimary
|
||||||
|
font.pixelSize: theme.fontHeading
|
||||||
|
font.weight: theme.weightSemibold
|
||||||
|
}
|
||||||
|
QbzCircleAction {
|
||||||
|
primary: true
|
||||||
|
name: "play-fill"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: QbzLibrary.libraryPlayAll(JSON.stringify(root.libTrackIds()), false)
|
||||||
|
}
|
||||||
|
QbzCircleAction {
|
||||||
|
name: "square-check-big"
|
||||||
|
active: root.multiSelect
|
||||||
|
btnEnabled: root.libTracks.length > 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: root.setMultiSelect(!root.multiSelect)
|
||||||
|
}
|
||||||
|
QbzCircleAction {
|
||||||
|
id: libMenuBtn
|
||||||
|
name: "ellipsis"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: function (mouse) {
|
||||||
|
topMenu.forLibrary = true
|
||||||
|
topMenu.openAtCursor(libMenuBtn, mouse.x, mouse.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item { visible: root.libTracks.length > 0; width: 1; height: 10 }
|
||||||
Repeater {
|
Repeater {
|
||||||
model: libraryTab.libTracks
|
// Numeric model: only the revealed rows are BUILT
|
||||||
|
// (declaring is building — the Popular Tracks expander's
|
||||||
|
// rule, feedback_qml_hidden_is_not_unbuilt).
|
||||||
|
model: root.libTracksExpanded ? root.libTracks.length
|
||||||
|
: Math.min(root.libTracks.length, root.libPreview)
|
||||||
delegate: PopularTrackRow {
|
delegate: PopularTrackRow {
|
||||||
|
readonly property var item: root.libTracks[index] || ({})
|
||||||
|
property bool revealed: false
|
||||||
|
// A cleared heart takes the row out — visually now,
|
||||||
|
// for real when the document republishes (the heart
|
||||||
|
// settles, `artist_qt::apply_favorite_change`
|
||||||
|
// recomputes the rows). A PURCHASE that loses its
|
||||||
|
// heart stays: it is still library (contract §3).
|
||||||
|
readonly property bool leaving:
|
||||||
|
!root.toggleState("track:" + item.id, item.isFavorite === true)
|
||||||
|
&& item.group !== "purchases"
|
||||||
|
height: 50
|
||||||
|
opacity: (revealed && !leaving) ? 1.0 : 0.0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
|
||||||
|
Component.onCompleted: revealed = true
|
||||||
row: ({
|
row: ({
|
||||||
"id": modelData.id, "number": index + 1, "title": modelData.title,
|
"id": item.id, "number": index + 1, "title": item.title,
|
||||||
"artist": modelData.artist, "artistId": modelData.artistId,
|
"artist": item.artist, "artistId": item.artistId,
|
||||||
"album": modelData.album, "albumId": modelData.albumId,
|
"album": item.album, "albumId": item.albumId,
|
||||||
"duration": modelData.duration, "qualityTier": modelData.qualityTier,
|
"duration": item.duration, "qualityTier": item.qualityTier,
|
||||||
"explicit": modelData.explicit, "artUrl": modelData.imageUrl,
|
"explicit": item.explicit, "artUrl": item.imageUrl,
|
||||||
"isFavorite": modelData.isFavorite,
|
"isFavorite": item.isFavorite === true,
|
||||||
|
"qobuzUnavailable": item.qobuzUnavailable === true,
|
||||||
|
"cacheStatus": item.cacheStatus || 0,
|
||||||
})
|
})
|
||||||
rowIndex: index
|
rowIndex: index
|
||||||
showAlbum: true
|
showAlbum: true
|
||||||
selectMode: root.multiSelect
|
selectMode: root.multiSelect
|
||||||
checked: root.selected[row.id] === true
|
checked: root.selected[row.id] === true
|
||||||
|
playHandler: function (id) {
|
||||||
|
QbzLibrary.libraryPlayVisible(JSON.stringify(root.libTrackIds()), id)
|
||||||
|
}
|
||||||
onToggleSelect: function (mods) { root.toggleSelected(row.id, mods) }
|
onToggleSelect: function (mods) { root.toggleSelected(row.id, mods) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item { visible: libraryTab.libAlbums.length > 0; width: 1; height: 24 }
|
Item { visible: root.libTracks.length > root.libPreview; width: 1; height: 4 }
|
||||||
|
QbzLoadMore {
|
||||||
|
visible: root.libTracks.length > root.libPreview
|
||||||
|
width: parent.width
|
||||||
|
label: root.libTracksExpanded ? QbzSession.tr("View less", QbzSession.trRev) : QbzSession.tr("Load more", QbzSession.trRev)
|
||||||
|
onClicked: root.libTracksExpanded = !root.libTracksExpanded
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Albums ---------------------------------------------
|
||||||
|
Item { visible: root.libAlbums.length > 0 && root.libTracks.length > 0; width: 1; height: 24 }
|
||||||
Text {
|
Text {
|
||||||
visible: libraryTab.libAlbums.length > 0
|
property string anchorId: "albums"
|
||||||
|
visible: root.libAlbums.length > 0
|
||||||
text: QbzSession.tr("Albums", QbzSession.trRev)
|
text: QbzSession.tr("Albums", QbzSession.trRev)
|
||||||
color: theme.textPrimary
|
color: theme.textPrimary
|
||||||
font.pixelSize: theme.fontHeading
|
font.pixelSize: theme.fontHeading
|
||||||
font.weight: theme.weightSemibold
|
font.weight: theme.weightSemibold
|
||||||
}
|
}
|
||||||
Item { visible: libraryTab.libAlbums.length > 0; width: 1; height: 10 }
|
Item { visible: root.libAlbums.length > 0; width: 1; height: 10 }
|
||||||
Grid {
|
Grid {
|
||||||
visible: libraryTab.libAlbums.length > 0
|
visible: root.libAlbums.length > 0
|
||||||
width: parent.width
|
width: parent.width
|
||||||
columns: Math.max(1, Math.floor((width + 24) / 224))
|
columns: Math.max(1, Math.floor((width + 24) / 224))
|
||||||
columnSpacing: 24
|
columnSpacing: 24
|
||||||
rowSpacing: 24
|
rowSpacing: 24
|
||||||
Repeater {
|
Repeater {
|
||||||
model: libraryTab.libAlbums
|
model: root.libAlbums
|
||||||
delegate: AlbumCard {
|
delegate: AlbumCard {
|
||||||
albumId: modelData.id
|
albumId: modelData.id
|
||||||
title: modelData.title
|
title: modelData.title
|
||||||
@@ -2886,7 +3072,19 @@ Rectangle {
|
|||||||
// row (library_qt `map_album`).
|
// row (library_qt `map_album`).
|
||||||
isPinned: modelData.isPinned === true
|
isPinned: modelData.isPinned === true
|
||||||
artworkUrl: modelData.imageUrl || ""
|
artworkUrl: modelData.imageUrl || ""
|
||||||
isFavorite: modelData.isFavorite
|
// The heart goes through the page's optimistic
|
||||||
|
// store so the card can fade out on the click,
|
||||||
|
// ahead of the settle that removes the row.
|
||||||
|
hostFavorite: true
|
||||||
|
isFavorite: root.toggleState("album:" + modelData.id, modelData.isFavorite === true)
|
||||||
|
onFavoriteRequested: {
|
||||||
|
var next = !root.toggleState("album:" + modelData.id, modelData.isFavorite === true)
|
||||||
|
root.setToggleState("album:" + modelData.id, next)
|
||||||
|
QbzLibrary.libraryToggleFavorite("album", modelData.id)
|
||||||
|
}
|
||||||
|
readonly property bool leaving: !isFavorite && modelData.group !== "purchases"
|
||||||
|
opacity: leaving ? 0.0 : 1.0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2913,11 +3111,6 @@ Rectangle {
|
|||||||
target: flick
|
target: flick
|
||||||
}
|
}
|
||||||
|
|
||||||
// Library feed access (the phase-5 document, parsed in LibraryView).
|
|
||||||
function libraryFeed() {
|
|
||||||
return JSON.parse(QbzLibrary.libraryJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Network sidebar (300px, surface-card + 1px left border) ---------
|
// --- Network sidebar (300px, surface-card + 1px left border) ---------
|
||||||
// BOUNDED, not full-height. ArtistPageView.slint:1178-1195:
|
// BOUNDED, not full-height. ArtistPageView.slint:1178-1195:
|
||||||
// natural-top = the live viewport y of the body row + the JUMP TO bar
|
// natural-top = the live viewport y of the body row + the JUMP TO bar
|
||||||
@@ -2974,6 +3167,31 @@ Rectangle {
|
|||||||
tabs: root.jumpTabs
|
tabs: root.jumpTabs
|
||||||
activeTabId: root.activeJumpTab
|
activeTabId: root.activeJumpTab
|
||||||
visible: root.jumpTabs.length > 0
|
visible: root.jumpTabs.length > 0
|
||||||
|
// "In library" source switches — `Library > Albums / Tracks`'s
|
||||||
|
// purchases / favourites toggles (LibraryToolbar.qml:376-377), on the
|
||||||
|
// shared QbzToggleButton, floating in the strip ONLY while the library
|
||||||
|
// tab is active. State is the page's own (`libShowPurchases` /
|
||||||
|
// `libShowFavorites`), never `QbzLibrary.sessionShow*`.
|
||||||
|
trailing: Row {
|
||||||
|
visible: root.artistTab === "library"
|
||||||
|
width: visible ? implicitWidth : 0
|
||||||
|
height: 44
|
||||||
|
spacing: 8
|
||||||
|
QbzToggleButton {
|
||||||
|
sm: true
|
||||||
|
name: "shopping-bag"
|
||||||
|
active: root.libShowPurchases
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: root.libShowPurchases = !root.libShowPurchases
|
||||||
|
}
|
||||||
|
QbzToggleButton {
|
||||||
|
sm: true
|
||||||
|
name: "heart"
|
||||||
|
active: root.libShowFavorites
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: root.libShowFavorites = !root.libShowFavorites
|
||||||
|
}
|
||||||
|
}
|
||||||
onTabClicked: function (id) { root.scrollToSection(id) }
|
onTabClicked: function (id) { root.scrollToSection(id) }
|
||||||
onSearchEdited: function (text) { root.searchQuery = text }
|
onSearchEdited: function (text) { root.searchQuery = text }
|
||||||
}
|
}
|
||||||
@@ -3606,6 +3824,10 @@ Rectangle {
|
|||||||
QbzContextMenu {
|
QbzContextMenu {
|
||||||
id: topMenu
|
id: topMenu
|
||||||
menuWidth: 224
|
menuWidth: 224
|
||||||
|
/// Opened from the library tab's Tracks header: the five rows act on
|
||||||
|
/// `root.libTrackIds()` through the library seams instead of the
|
||||||
|
/// TOP_QUEUE ones. Set by whichever ⋯ button opens the menu.
|
||||||
|
property bool forLibrary: false
|
||||||
Repeater {
|
Repeater {
|
||||||
model: [
|
model: [
|
||||||
{ "label": QbzSession.tr("Play all next", QbzSession.trRev), "icon": "list-start", "action": "next-all" },
|
{ "label": QbzSession.tr("Play all next", QbzSession.trRev), "icon": "list-start", "action": "next-all" },
|
||||||
@@ -3643,6 +3865,21 @@ Rectangle {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
topMenu.close()
|
topMenu.close()
|
||||||
var a = modelData.action
|
var a = modelData.action
|
||||||
|
// Library arm: the same five rows over the
|
||||||
|
// library tab's list — shuffle through the
|
||||||
|
// library play-all seam, the three enqueues
|
||||||
|
// through the bulk funnel with the whole list,
|
||||||
|
// and the picker over that list (the
|
||||||
|
// `playlist-all` branch below reads the mode).
|
||||||
|
if (topMenu.forLibrary && a !== "playlist-all") {
|
||||||
|
var lib = root.libTrackIds()
|
||||||
|
if (lib.length === 0) return
|
||||||
|
if (a === "shuffle-all") QbzLibrary.libraryPlayAll(JSON.stringify(lib), true)
|
||||||
|
else if (a === "next-all") QbzPlayer.bulkTracksAction(JSON.stringify(lib), "play-next", "artist", String(artist.id || ""))
|
||||||
|
else if (a === "later-all") QbzPlayer.bulkTracksAction(JSON.stringify(lib), "play-later", "artist", String(artist.id || ""))
|
||||||
|
else if (a === "queue-all") QbzPlayer.bulkTracksAction(JSON.stringify(lib), "queue", "artist", String(artist.id || ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (a === "shuffle-all") QbzPlayer.playArtistTop(true)
|
if (a === "shuffle-all") QbzPlayer.playArtistTop(true)
|
||||||
// "Play all next" INSERTS at the cursor; it does
|
// "Play all next" INSERTS at the cursor; it does
|
||||||
// not replay the artist. It used to call
|
// not replay the artist. It used to call
|
||||||
@@ -3666,7 +3903,8 @@ Rectangle {
|
|||||||
// a Qobuz catalog track (`/artist/page`
|
// a Qobuz catalog track (`/artist/page`
|
||||||
// top_tracks), so the catalog arm is correct.
|
// top_tracks), so the catalog arm is correct.
|
||||||
var ids = []
|
var ids = []
|
||||||
for (var i = 0; i < root.topTracks.length; i++)
|
if (topMenu.forLibrary) ids = root.libTrackIds()
|
||||||
|
else for (var i = 0; i < root.topTracks.length; i++)
|
||||||
ids.push(String(root.topTracks[i].id))
|
ids.push(String(root.topTracks[i].id))
|
||||||
if (ids.length > 0)
|
if (ids.length > 0)
|
||||||
QbzPlaylistPicker.openForTracks(JSON.stringify(ids))
|
QbzPlaylistPicker.openForTracks(JSON.stringify(ids))
|
||||||
|
|||||||
Reference in new issue
Block a user