feat(qt): custom covers for playlists + the app-wide custom-art sweep

Two halves of one feature:

- Playlist custom covers (Qt-first; no Slint counterpart yet): the
  playlist header cover gets the cover menu (Add/Change/Remove) and the
  lightbox on left-click. The store is a NEW file,
  custom_playlist_covers.json — a `playlists` key inside the shared
  custom_artwork.json would be silently dropped the next time the Slint
  app wrote an override (its store round-trips only known keys). The
  override beats the playlist's own Qobuz graphic AND the member-cover
  mosaic on the header, the sidebar rows, the cards and the library
  grid.
- Album custom covers now apply everywhere, not just the album header:
  doc builders route an album's art URL through
  cover_artwork_qt::prefer_album_cover (the override's path flows
  through the ordinary artwork pipeline — artwork_qt::classify's
  LocalFile arm resolves it synchronously, no QML change). Swept: album
  page carousels (more-from-artist / suggestions / Last.fm similar),
  artist page releases, search result cards, and the playlist mosaics
  (a member album with a custom cover contributes THAT art to the
  collage tiles).

km
This commit is contained in:
vicrodh committed 2026-08-11 01:38:18 -06:00
1 parent 83b8bd8890
commit a53e8a7af7
10 files changed
+277 -18

No files matched your search

+79
View File
@@ -375,8 +375,87 @@ Rectangle {
phase: root.skelPhase
settleMs: 4000
}
// Left-click: the lightbox (the shared CoverLightbox, same as
// the album page). Right-click: the custom-cover menu (Qt
// port addition — the Slint playlist header has no menu).
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
plCoverMenu.openAtCursor(plCoverAnchor, mouse.x, mouse.y)
} else {
var src = (doc.coverUrl || "") !== "" ? doc.coverUrl
: ((doc.covers || []).length > 0 ? doc.covers[0] : "")
if (src !== "") plCoverLightbox.openWith(src)
}
}
}
Item { id: plCoverAnchor; anchors.fill: parent }
}
// The playlist cover menu's rows, rebuilt per open (Add vs
// Change+Remove flips on hasCustomCover). The store is Qt-first
// (custom_playlist_covers.json — a `playlists` key inside the
// shared custom_artwork.json would be dropped on the Slint
// app's next write).
function buildPlCoverMenuModel() {
var rows = []
if (doc.hasCustomCover === true) {
rows.push({ "label": QbzSession.tr("Change cover", QbzSession.trRev), "icon": "image-plus", "action": "add" })
rows.push({ "label": QbzSession.tr("Remove cover", QbzSession.trRev), "icon": "trash-2", "action": "remove" })
} else {
rows.push({ "label": QbzSession.tr("Add cover", QbzSession.trRev), "icon": "image-plus", "action": "add" })
}
return rows
}
QbzContextMenu {
id: plCoverMenu
menuWidth: 196
onAboutToShow: plCoverRepeater.model = root.buildPlCoverMenuModel()
Repeater {
id: plCoverRepeater
model: []
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 0
height: 33
radius: 5
color: pcmiArea.containsMouse ? theme.surfaceHover : "transparent"
Row {
anchors.fill: parent
anchors.leftMargin: 8
spacing: 8
QbzIcon { name: modelData.icon; width: 15; height: 15; anchors.verticalCenter: parent.verticalCenter; tintName: "secondary" }
Text {
height: parent.height
width: parent.width - 23
text: modelData.label
color: theme.textSecondary
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
MouseArea {
id: pcmiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
plCoverMenu.close()
if (modelData.action === "add") QbzBridge.playlistCoverAdd(doc.id || "")
else if (modelData.action === "remove") QbzBridge.playlistCoverRemove(doc.id || "")
}
}
}
}
}
CoverLightbox { id: plCoverLightbox }
// Metadata.
Column {
width: parent.width - 150 - 24