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 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) ----------------
|
||||
// Same three-line rule as AlbumView (the .slint says "same rule as
|
||||
// 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.
|
||||
readonly property var jumpTabs: {
|
||||
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 rawSections = artist.releaseSections || []
|
||||
var rawAppears = artist.appearsOn || []
|
||||
@@ -740,7 +795,7 @@ Rectangle {
|
||||
var aid = (artist && artist.id) ? artist.id : ""
|
||||
if (aid !== "" && key === "artist:" + aid)
|
||||
root.setToggleState("artist", value)
|
||||
else if (key.indexOf("track:") === 0)
|
||||
else if (key.indexOf("track:") === 0 || key.indexOf("album:") === 0)
|
||||
root.setToggleState(key, value)
|
||||
}
|
||||
// Header pin, same seam: the overflow-menu row flips `artistPin`
|
||||
@@ -807,11 +862,28 @@ Rectangle {
|
||||
syncArtistState()
|
||||
invalidatePortraitOverride()
|
||||
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
|
||||
// re-fire when the parsed value actually changes (same stale race).
|
||||
// (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
|
||||
// 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
|
||||
/// looks on screen — `selectedIdsInOrder` already walks them that way.
|
||||
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) {
|
||||
root.selected = sel.next(root.selected, id,
|
||||
root.topTracks.concat(root.appearsOn),
|
||||
var lists = selectableLists()
|
||||
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)
|
||||
}
|
||||
function selectedIdsInOrder() {
|
||||
var out = []
|
||||
var lists = [root.topTracks, root.appearsOn]
|
||||
var lists = selectableLists()
|
||||
for (var l = 0; l < lists.length; l++) {
|
||||
var rows = lists[l]
|
||||
for (var i = 0; i < rows.length; i++)
|
||||
@@ -853,7 +934,7 @@ Rectangle {
|
||||
function bulkAction(action) {
|
||||
if (action === "select-all") {
|
||||
var m = {}
|
||||
var lists = [root.topTracks, root.appearsOn]
|
||||
var lists = selectableLists()
|
||||
for (var l = 0; l < lists.length; l++)
|
||||
for (var i = 0; i < lists[l].length; i++) m[lists[l][i].id] = true
|
||||
root.selected = m
|
||||
@@ -881,6 +962,12 @@ Rectangle {
|
||||
return
|
||||
loadedArtistId = id
|
||||
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
|
||||
// MusicBrainz is off (an empty Network tab is worse than none).
|
||||
netTab = (artist.network && artist.network.mbAvailable) ? "network" : "magazine"
|
||||
@@ -909,7 +996,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function dispatchLibCovers() {
|
||||
var items = libraryTab.libItems || []
|
||||
var items = root.libItems || []
|
||||
var urls = []
|
||||
for (var i = 0; i < items.length; i++) if (items[i].imageUrl) urls.push(items[i].imageUrl)
|
||||
dispatchArtwork(urls)
|
||||
@@ -1021,10 +1108,15 @@ Rectangle {
|
||||
|
||||
function scrollToSection(id) {
|
||||
root.activeJumpTab = id
|
||||
for (var i = 0; i < sectionAnchors.children.length; i++) {
|
||||
var c = sectionAnchors.children[i]
|
||||
// Anchors resolve against the column of the ACTIVE mode: the catalog
|
||||
// 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) {
|
||||
flick.contentY = sectionAnchors.y + c.y - 48
|
||||
flick.contentY = host.y + c.y - 48
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1047,6 +1139,17 @@ Rectangle {
|
||||
property var row: ({})
|
||||
property int rowIndex: 0
|
||||
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
|
||||
// body is the toggle and the leading cell swaps to a round checkbox
|
||||
// (TrackRow.slint:173-177, 392-417).
|
||||
@@ -1248,7 +1351,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
enabled: !popRow.selectMode && !popRow.pulledDead
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzPlayer.playArtistTrack(row.id)
|
||||
onClicked: popRow.play()
|
||||
}
|
||||
}
|
||||
// Title + artist.
|
||||
@@ -1539,7 +1642,7 @@ Rectangle {
|
||||
}
|
||||
onPicked: function (a) {
|
||||
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 === "later") QbzPlayer.enqueueTrack(id, "later")
|
||||
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
|
||||
// menu still opens.
|
||||
onDoubleClicked: if (!popRow.selectMode && !popRow.pulledDead)
|
||||
QbzPlayer.playArtistTrack(row.id)
|
||||
popRow.play()
|
||||
onClicked: {
|
||||
if (popRow.selectMode) popRow.toggleSelect(mouse.modifiers)
|
||||
else mouse.accepted = false
|
||||
@@ -2563,7 +2666,10 @@ Rectangle {
|
||||
id: topMenuBtn
|
||||
name: "ellipsis"
|
||||
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 }
|
||||
@@ -2810,66 +2916,146 @@ Rectangle {
|
||||
}
|
||||
|
||||
// ================= 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 {
|
||||
id: libraryTab
|
||||
visible: root.artistTab === "library" && !QbzArtist.artistLoading
|
||||
width: page.bodyWidth
|
||||
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 {
|
||||
visible: libraryTab.libTracks.length > 0
|
||||
text: QbzSession.tr("Tracks", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontHeading
|
||||
font.weight: theme.weightSemibold
|
||||
// Same bulk bar as the catalog column: `bulkAction` routes on
|
||||
// the active mode's lists (`selectableLists`).
|
||||
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": "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 {
|
||||
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 {
|
||||
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: ({
|
||||
"id": modelData.id, "number": index + 1, "title": modelData.title,
|
||||
"artist": modelData.artist, "artistId": modelData.artistId,
|
||||
"album": modelData.album, "albumId": modelData.albumId,
|
||||
"duration": modelData.duration, "qualityTier": modelData.qualityTier,
|
||||
"explicit": modelData.explicit, "artUrl": modelData.imageUrl,
|
||||
"isFavorite": modelData.isFavorite,
|
||||
"id": item.id, "number": index + 1, "title": item.title,
|
||||
"artist": item.artist, "artistId": item.artistId,
|
||||
"album": item.album, "albumId": item.albumId,
|
||||
"duration": item.duration, "qualityTier": item.qualityTier,
|
||||
"explicit": item.explicit, "artUrl": item.imageUrl,
|
||||
"isFavorite": item.isFavorite === true,
|
||||
"qobuzUnavailable": item.qobuzUnavailable === true,
|
||||
"cacheStatus": item.cacheStatus || 0,
|
||||
})
|
||||
rowIndex: index
|
||||
showAlbum: true
|
||||
selectMode: root.multiSelect
|
||||
checked: root.selected[row.id] === true
|
||||
playHandler: function (id) {
|
||||
QbzLibrary.libraryPlayVisible(JSON.stringify(root.libTrackIds()), id)
|
||||
}
|
||||
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 {
|
||||
visible: libraryTab.libAlbums.length > 0
|
||||
property string anchorId: "albums"
|
||||
visible: root.libAlbums.length > 0
|
||||
text: QbzSession.tr("Albums", QbzSession.trRev)
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontHeading
|
||||
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 {
|
||||
visible: libraryTab.libAlbums.length > 0
|
||||
visible: root.libAlbums.length > 0
|
||||
width: parent.width
|
||||
columns: Math.max(1, Math.floor((width + 24) / 224))
|
||||
columnSpacing: 24
|
||||
rowSpacing: 24
|
||||
Repeater {
|
||||
model: libraryTab.libAlbums
|
||||
model: root.libAlbums
|
||||
delegate: AlbumCard {
|
||||
albumId: modelData.id
|
||||
title: modelData.title
|
||||
@@ -2886,7 +3072,19 @@ Rectangle {
|
||||
// row (library_qt `map_album`).
|
||||
isPinned: modelData.isPinned === true
|
||||
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
|
||||
}
|
||||
|
||||
// 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) ---------
|
||||
// BOUNDED, not full-height. ArtistPageView.slint:1178-1195:
|
||||
// natural-top = the live viewport y of the body row + the JUMP TO bar
|
||||
@@ -2974,6 +3167,31 @@ Rectangle {
|
||||
tabs: root.jumpTabs
|
||||
activeTabId: root.activeJumpTab
|
||||
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) }
|
||||
onSearchEdited: function (text) { root.searchQuery = text }
|
||||
}
|
||||
@@ -3606,6 +3824,10 @@ Rectangle {
|
||||
QbzContextMenu {
|
||||
id: topMenu
|
||||
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 {
|
||||
model: [
|
||||
{ "label": QbzSession.tr("Play all next", QbzSession.trRev), "icon": "list-start", "action": "next-all" },
|
||||
@@ -3643,6 +3865,21 @@ Rectangle {
|
||||
onClicked: {
|
||||
topMenu.close()
|
||||
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)
|
||||
// "Play all next" INSERTS at the cursor; it does
|
||||
// not replay the artist. It used to call
|
||||
@@ -3666,7 +3903,8 @@ Rectangle {
|
||||
// a Qobuz catalog track (`/artist/page`
|
||||
// top_tracks), so the catalog arm is correct.
|
||||
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))
|
||||
if (ids.length > 0)
|
||||
QbzPlaylistPicker.openForTracks(JSON.stringify(ids))
|
||||
|
||||
Reference in new issue
Block a user