feat(qt): Add to playlist for local albums everywhere
Playlists can be fully local or mixed, so hiding the row on local albums was wrong — the resolution logic already existed end to end. The quick view drops its !localAlbum gates (album and track menus) and a new 'playlist' arm hands the overlay's row snapshot to the picker in LOCAL MODE, fired before close so the generation guard cannot refuse it. The local album cards and list rows reuse the existing bulk 'album' scope (group key -> rows -> picker). My QBZ's heterogeneous cells stay out via an explicit opt-in, since their local ids are container items the bulk seam cannot resolve. cc
This commit is contained in:
5 files changed
+52
-6
No files matched your search
@@ -140,6 +140,11 @@ Rectangle {
|
||||
// album page agree. Do NOT let a host paint its own heart on top of the
|
||||
// card — see the `selectMode` note below for the same mistake's other half.
|
||||
property bool catalogAffordances: !root.localMode
|
||||
/// Local-mode mounts whose `albumId` is a Local Library group key opt in
|
||||
/// to the album-level "Add to playlist" row (LocalAlbumCollection sets
|
||||
/// it). My QBZ's heterogeneous cells stay out: their local ids are
|
||||
/// container items the local bulk seam cannot resolve.
|
||||
property bool localPlaylistAffordance: false
|
||||
// Quick View is broader than the catalog-only heart/block family.
|
||||
// Catalog cards keep it by default; local and heterogeneous hosts opt in
|
||||
// only for rows whose id is genuinely an album key.
|
||||
@@ -318,6 +323,10 @@ Rectangle {
|
||||
// #442 "Play later" — end of the manual block.
|
||||
m.push({ "label": t("Play later", r), "icon": "list-plus", "action": "later" })
|
||||
m.push({ "label": t("Add to queue", r), "icon": "list-end", "action": "queue" })
|
||||
// Local albums get the row too (owner, 2026-08-31: playlists can
|
||||
// be 100% local or mixed) — via the existing local bulk seam.
|
||||
if (root.localMode && root.localPlaylistAffordance)
|
||||
m.push({ "label": t("Add to playlist", r), "icon": "list-music", "action": "add-playlist" })
|
||||
}
|
||||
if (root.favoriteAffordance) {
|
||||
// Keep removal reachable even after Qobuz withdraws the old id.
|
||||
@@ -374,7 +383,16 @@ Rectangle {
|
||||
// denormalized snapshot and a file:// cache path is dead on any other
|
||||
// machine, the same reason the pin payload uses it.
|
||||
if (a === "favorite") { root.toggleFavorite(); return }
|
||||
if (a === "add-playlist") { QbzPlaylistPicker.openForAlbum(root.albumId); return }
|
||||
if (a === "add-playlist") {
|
||||
if (root.localMode)
|
||||
// The bulk "album" scope resolves a local group key to its
|
||||
// rows and opens the picker in LOCAL MODE (local_bulk.rs).
|
||||
QbzLocal.bulkAction("album",
|
||||
JSON.stringify([String(root.albumId)]), "add-to-playlist")
|
||||
else
|
||||
QbzPlaylistPicker.openForAlbum(root.albumId)
|
||||
return
|
||||
}
|
||||
if (a === "mixtape") { QbzAlbum.addToMixtape(root.albumId); return }
|
||||
if (a === "cache-album") { QbzAlbum.albumCacheOffline(root.albumId); return }
|
||||
if (a === "block") {
|
||||
|
||||
@@ -164,9 +164,12 @@ Item {
|
||||
// queue's, so the row is unconditional.
|
||||
entries.push({ "label": t("Play later"), "icon": "list-plus", "action": "later" })
|
||||
entries.push({ "label": t("Add to queue"), "icon": "list-end", "action": "queue" })
|
||||
if (!root.localAlbum) {
|
||||
// "Add to playlist" is for BOTH paths (owner, 2026-08-31: playlists
|
||||
// can be 100% local or mixed — the local arm rides the quick view's
|
||||
// own row snapshot). Mixtape/share stay catalog-only.
|
||||
entries.push({ "sep": true })
|
||||
entries.push({ "label": t("Add to playlist"), "icon": "list-music", "action": "playlist" })
|
||||
if (!root.localAlbum) {
|
||||
entries.push({ "label": t("Add to mixtape"), "icon": "cassette-tape", "action": "mixtape" })
|
||||
entries.push({ "label": t("Share Qobuz link"), "icon": "link", "action": "share-qobuz" })
|
||||
entries.push({ "label": t("Share Album.link"), "icon": "link", "action": "share-albumlink" })
|
||||
@@ -182,11 +185,9 @@ Item {
|
||||
entries.push({ "label": t("Play next"), "icon": "list-start", "action": "next" })
|
||||
entries.push({ "label": t("Play later"), "icon": "list-plus", "action": "later" })
|
||||
entries.push({ "label": t("Add to queue"), "icon": "list-end", "action": "queue" })
|
||||
if (!root.localAlbum) {
|
||||
entries.push({ "sep": true })
|
||||
entries.push({ "label": t("Add to playlist"), "icon": "list-music", "action": "playlist" })
|
||||
}
|
||||
}
|
||||
// Quick View deliberately has no favourite or offline-cache actions,
|
||||
// inline or in this menu. Context stays about playback/navigation.
|
||||
if (track.available !== false && !root.localAlbum) {
|
||||
@@ -213,6 +214,14 @@ Item {
|
||||
// Play-later row that would perform the same write.
|
||||
root.albumPlaybackAction(root.localAlbum ? "queue" : "later")
|
||||
} else if (action === "playlist") {
|
||||
if (root.localAlbum) {
|
||||
// Rust still holds this quick view's row snapshot — fire the
|
||||
// action BEFORE closing (close bumps the generation and the
|
||||
// context would be refused as stale).
|
||||
QbzAlbum.quickViewLocalAction("playlist", "")
|
||||
root.closeQuickView()
|
||||
return
|
||||
}
|
||||
var ids = []
|
||||
for (var i = 0; i < root.tracks.length; i++) {
|
||||
if (/^\d+$/.test(root.tracks[i].id || "")
|
||||
@@ -241,6 +250,12 @@ Item {
|
||||
else if (action === "next" || action === "later" || action === "queue")
|
||||
root.enqueueTrack(track.id, action)
|
||||
else if (action === "playlist") {
|
||||
if (root.localAlbum) {
|
||||
// Same before-close ordering as the album arm above.
|
||||
QbzAlbum.quickViewLocalAction("playlist", String(track.id))
|
||||
root.closeQuickView()
|
||||
return
|
||||
}
|
||||
root.closeQuickView()
|
||||
QbzPlaylistPicker.openForTrack(track.id)
|
||||
} else if (action === "share") {
|
||||
|
||||
@@ -468,6 +468,7 @@ Item {
|
||||
AlbumCard {
|
||||
id: albumCard
|
||||
localMode: true
|
||||
localPlaylistAffordance: true
|
||||
quickViewAffordance: true
|
||||
pinAffordance: true
|
||||
albumId: cardCell.slot.id
|
||||
|
||||
@@ -97,6 +97,7 @@ Rectangle {
|
||||
{ "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" },
|
||||
{ "label": QbzSession.tr("Add to playlist", QbzSession.trRev), "icon": "list-music", "action": "add-to-playlist" },
|
||||
{ "label": QbzSession.tr("Album info", QbzSession.trRev), "icon": "info", "action": "album-info" }
|
||||
]
|
||||
if (root.item.favoriteable === true) {
|
||||
@@ -114,7 +115,9 @@ Rectangle {
|
||||
if (a === "open") root.opened()
|
||||
else if (a === "play") root.playRequested()
|
||||
else if (a === "favorite") root.favoriteRequested()
|
||||
else if (a === "album-info")
|
||||
else if (a === "album-info" || a === "add-to-playlist")
|
||||
// Both resolve the group key through the same bulk
|
||||
// "album" scope (local_bulk.rs resolve_blocking).
|
||||
QbzLocal.bulkAction("album", JSON.stringify([String(root.item.id)]), a)
|
||||
else root.enqueueRequested(a)
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ pub(crate) fn local_action(action: String, track_id: String) {
|
||||
return;
|
||||
}
|
||||
let start = selected_position.unwrap_or(0);
|
||||
let rows = if matches!(action.as_str(), "next" | "later" | "queue") {
|
||||
let rows = if matches!(action.as_str(), "next" | "later" | "queue" | "playlist") {
|
||||
match selected_id {
|
||||
Some(id) => context
|
||||
.tracks
|
||||
@@ -396,6 +396,15 @@ pub(crate) fn local_action(action: String, track_id: String) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "playlist" never touches the queue: source-aware refs, then the
|
||||
// app-wide picker in LOCAL MODE (the local_bulk "add-to-playlist" tail).
|
||||
// Sync on purpose — the QML fires this BEFORE closeQuickView, and the
|
||||
// rows are already snapshotted above.
|
||||
if action == "playlist" {
|
||||
crate::local_album_actions::open_picker_for_rows(&rows);
|
||||
return;
|
||||
}
|
||||
|
||||
let runtime = crate::app();
|
||||
crate::spawn(async move {
|
||||
match action.as_str() {
|
||||
|
||||
Reference in new issue
Block a user