feat(queue): Play later everywhere — every context menu and bulk bar (#442)

Extends Play later (end of the manual block) from track rows to every
surface that already offered Play next / Add to queue:

- playback.rs: enqueue_album_later, enqueue_playlist_later (mixed
  sidecar interleave), enqueue_artist_top_later, enqueue_tracks_later,
  enqueue_local_tracks_later — all NATURAL order (each add_track_later
  appends, no reverse), QConnect controller routing via
  play_later_batch_on_peer_if_active, and post-add queue sync.
- local_playlist::enqueue_later_by_id (source-aware rows, offline-only
  stamp preserved), myqbz_play RowMode::PlayLater + BulkEnqueueMode.
- 19 dispatch arms in main.rs (album/playlist/artist/label/mix/
  favorites/local-library/album-page/disc/myqbz dialects).
- 12 context menus + 10 bulk bars in .slint: list-plus icon between
  Play next and Add to queue, popup heights bumped.

The immersive tap-action dropdown is deliberately untouched (it is a
configurable single-tap behavior setting, not a context menu).

km
This commit is contained in:
vicrodh committed 2026-07-26 21:08:12 -06:00
1 parent 09380d4634
commit ad6c98ff7d
22 files changed
+785 -92

No files matched your search

@@ -794,6 +794,7 @@ export component AlbumPageView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "add-to-favorites", label: @tr("Add to Library"), icon: @image-url("../assets/icons/heart.svg"), danger: false, needs-selection: true },
@@ -817,6 +817,7 @@ export component ArtistPageView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "add-to-mixtape", label: @tr("Add to Mixtape/Collection"), icon: @image-url("../assets/icons/cassette-tape.svg"), danger: false, needs-selection: true },
+9 -2
View File
@@ -382,8 +382,8 @@ export component AlbumCard inherits Rectangle {
x: more-btn.absolute-position.x + more-btn.width / 2 - artwork-box.absolute-position.x;
y: more-btn.absolute-position.y + more-btn.height / 2 - artwork-box.absolute-position.y;
width: 196px;
// 6 items max (Open/Play/Play-next/Queue + favorite + block).
height: 6 * 33px + 10px;
// 7 items max (Open/Play/Play-next/Play-later/Queue + favorite + block).
height: 7 * 33px + 10px;
ContextMenu {
ContextMenuItem {
icon: @image-url("../assets/icons/library-big.svg");
@@ -406,6 +406,13 @@ export component AlbumCard inherits Rectangle {
root.media-action("album", root.album.id, "play-next");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("album", root.album.id, "play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
+10 -3
View File
@@ -49,7 +49,7 @@ component OverlayButton inherits Rectangle {
export component PlaylistCard inherits Rectangle {
in property <SearchPlaylistItem> playlist;
// kind ("playlist"), playlist id, action ("play" | "play-next" |
// "queue" | "favorite" | "open").
// "play-later" | "queue" | "favorite" | "open").
callback media-action(string, string, string);
// When true, a click on the card BODY opens the playlist ("open")
// instead of playing it — the Favorites grid wants body=open, the play
@@ -188,8 +188,8 @@ export component PlaylistCard inherits Rectangle {
// + 33px/row) so the panel never clips or over-reserves.
playlist-menu := PopupWindow {
property <int> item-count: root.playlist.is-owned
? 4
: (root.playlist.is-copied ? 4 : 5);
? 5
: (root.playlist.is-copied ? 5 : 6);
x: 26px;
y: 56px;
width: 196px;
@@ -209,6 +209,13 @@ export component PlaylistCard inherits Rectangle {
root.media-action("playlist", root.playlist.id, "play-next");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("playlist", root.playlist.id, "play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
+10 -3
View File
@@ -79,7 +79,7 @@ export component TrackCard inherits Rectangle {
animate opacity { duration: 250ms; }
callback clicked(string);
// kind ("track"), track id, action ("play" | "queue" | "play-next" |
// "favorite" | "open").
// "play-later" | "favorite" | "open").
callback media-action(string, string, string);
in property <length> card-height: 246px;
@@ -202,8 +202,8 @@ export component TrackCard inherits Rectangle {
x: 26px;
y: 56px;
width: 196px;
// Play / Play-next / Queue / Go-to-artist / Go-to-album / favorite.
height: 6 * 33px + 10px;
// Play / Play-next / Play-later / Queue / Go-to-artist / Go-to-album / favorite.
height: 7 * 33px + 10px;
ContextMenu {
ContextMenuItem {
icon: @image-url("../assets/icons/play.svg");
@@ -219,6 +219,13 @@ export component TrackCard inherits Rectangle {
root.media-action("track", root.track.id, "play-next");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("track", root.track.id, "play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
+14 -3
View File
@@ -1467,9 +1467,9 @@ export component FavoritesView inherits Rectangle {
x: row-rect.menu-x;
y: row-rect.menu-y;
width: 196px;
height: (item.kind == "track" ? 6
: item.kind == "album" ? 6
: item.kind == "playlist" ? 4
height: (item.kind == "track" ? 7
: item.kind == "album" ? 7
: item.kind == "playlist" ? 5
: 1) * 33px + 10px;
ContextMenu {
if item.kind == "track" || item.kind == "album" || item.kind == "playlist": ContextMenuItem {
@@ -1490,6 +1490,15 @@ export component FavoritesView inherits Rectangle {
else { FavoritesActions.playlist-action(item.id, "play-next"); }
}
}
if item.kind == "track" || item.kind == "album" || item.kind == "playlist": ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
if (item.kind == "track") { FavoritesActions.track-action(item.id, "play-later"); }
else if (item.kind == "album") { FavoritesActions.album-action(item.id, "play-later"); }
else { FavoritesActions.playlist-action(item.id, "play-later"); }
}
}
if item.kind == "track" || item.kind == "album" || item.kind == "playlist": ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
@@ -1563,6 +1572,7 @@ export component FavoritesView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "add-to-mixtape", label: @tr("Add to Mixtape/Collection"), icon: @image-url("../assets/icons/cassette-tape.svg"), danger: false, needs-selection: true },
@@ -1727,6 +1737,7 @@ export component FavoritesView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "remove-selected", label: @tr("Remove from Library"), icon: @image-url("../assets/icons/heart-crack.svg"), danger: true, needs-selection: true },
{ id: "clear", label: @tr("Clear"), icon: @image-url("../assets/icons/x.svg"), danger: false, needs-selection: true },
+20 -5
View File
@@ -201,9 +201,9 @@ export component LabelPageView inherits Rectangle {
root.media-action("label", LabelState.id, "shuffle");
}
}
// ⋯ Overflow — Play next / Add to queue / Share (the
// Qobuz web label menu). Share is Qobuz-link only:
// Song.link/Album.link have no label equivalent.
// ⋯ Overflow — Play next / Play later / Add to queue /
// Share (the Qobuz web label menu). Share is Qobuz-link
// only: Song.link/Album.link have no label equivalent.
Rectangle {
width: 32px;
height: 32px;
@@ -222,7 +222,7 @@ export component LabelPageView inherits Rectangle {
x: 0px;
y: 38px;
width: 224px;
height: 3 * 33px + 10px;
height: 4 * 33px + 10px;
close-policy: close-on-click;
ContextMenu {
ContextMenuItem {
@@ -232,6 +232,13 @@ export component LabelPageView inherits Rectangle {
root.media-action("label", "", "top-play-next");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("label", "", "top-play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
@@ -363,7 +370,7 @@ export component LabelPageView inherits Rectangle {
x: 32px - self.width;
y: 38px;
width: 224px;
height: 2 * 33px + 10px;
height: 3 * 33px + 10px;
close-policy: close-on-click;
ContextMenu {
ContextMenuItem {
@@ -373,6 +380,13 @@ export component LabelPageView inherits Rectangle {
root.media-action("label", "", "top-play-next");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("label", "", "top-play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
@@ -396,6 +410,7 @@ export component LabelPageView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "add-to-mixtape", label: @tr("Add to Mixtape/Collection"), icon: @image-url("../assets/icons/cassette-tape.svg"), danger: false, needs-selection: true },
@@ -1241,6 +1241,7 @@ export component LocalLibraryView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "add-to-mixtape", label: @tr("Add to Mixtape/Collection"), icon: @image-url("../assets/icons/cassette-tape.svg"), danger: false, needs-selection: true },
@@ -1407,6 +1408,7 @@ export component LocalLibraryView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "remove-favorites", label: @tr("Remove from favorites"), icon: @image-url("../assets/icons/heart-crack.svg"), danger: true, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
@@ -1808,6 +1810,14 @@ export component LocalLibraryView inherits Rectangle {
clicked => { LocalLibraryActions.folders-bulk-action("play-next"); }
}
}
VerticalLayout {
alignment: center;
RailBulkBtn {
icon: @image-url("../assets/icons/list-plus.svg");
tooltip: @tr("Play later");
clicked => { LocalLibraryActions.folders-bulk-action("play-later"); }
}
}
VerticalLayout {
alignment: center;
RailBulkBtn {
+1
View File
@@ -220,6 +220,7 @@ export component MixView inherits Rectangle {
actions: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "add-to-mixtape", label: @tr("Add to Mixtape/Collection"), icon: @image-url("../assets/icons/cassette-tape.svg"), danger: false, needs-selection: true },
+14 -7
View File
@@ -440,9 +440,9 @@ component DetailRow inherits Rectangle {
width: 184px;
// Explicit height: without it the PopupWindow's box does not
// resolve and the in-flow rows below paint over it (the only
// PopupWindow in the codebase that omitted this). 4 items
// (32px) + 1 separator (1px) + padding (8px) + spacing (4px).
height: 145px;
// PopupWindow in the codebase that omitted this). 5 items
// (32px) + 1 separator (1px) + padding (8px) + spacing (5px).
height: 178px;
// Act-and-dismiss menu — match the app idiom (close on any
// click; items also act before the implicit close).
close-policy: PopupClosePolicy.close-on-click;
@@ -466,6 +466,11 @@ component DetailRow inherits Rectangle {
label: @tr("Play next");
chosen => { MyQbzDetailActions.item-action(root.item.source-item-id, "play-next"); }
}
RowMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
chosen => { MyQbzDetailActions.item-action(root.item.source-item-id, "play-later"); }
}
RowMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
@@ -1233,10 +1238,11 @@ export component MixtapeDetailView inherits Rectangle {
// Spec 12 §13: a bottom-docked bar that appears while items are selected
// (placement='bottom' default). Mirrors the local-library MultiSelectBar
// chrome (a flat [BulkAction] row — the §13.1 split-queue dropdown isn't
// expressible in MultiSelectBar, so its two actions are flattened to the
// adjacent "Add to queue" + "Play next" buttons, same idiom the Local
// Library tracks bar uses). The §13.1 action set, in order: Add to queue
// (play-later), Play next, Add to playlist, Add to Mixtape/Collection,
// expressible in MultiSelectBar, so its queue actions are flattened to
// adjacent buttons, same idiom the Local Library tracks bar uses). The
// action set, in order: Add to queue, Play later (#442 — the manual-block
// tail; §13.1's own "play-later" label mapped to plain append before
// #442 existed), Play next, Add to playlist, Add to Mixtape/Collection,
// Remove from this collection (danger), Clear (exit-select). The bar emits
// the id; the Rust side routes it.
if MyQbzDetailState.select-mode && MyQbzDetailState.selected-count > 0: Rectangle {
@@ -1251,6 +1257,7 @@ export component MixtapeDetailView inherits Rectangle {
selected-count: MyQbzDetailState.selected-count;
actions: [
{ id: "add-to-queue", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "play-later", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "play-next", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "add-to-mixtape", label: @tr("Add to Mixtape/Collection"), icon: @image-url("../assets/icons/cassette-tape.svg"), danger: false, needs-selection: true },
+6 -3
View File
@@ -310,12 +310,14 @@ export component PlaylistView inherits Rectangle {
property <bool> show-upload-confirm: false;
// Bulk-bar action sets (typed properties — a ternary over bare array
// literals can't coerce to [BulkAction]). Tauri parity (spec §1.5):
// Play next / Add to queue / Add to playlist over the selection,
// Remove owner-only — followed playlists keep the queueing actions.
// literals can't coerce to [BulkAction]). Tauri parity (spec §1.5) + #442:
// Play next / Play later / Add to queue / Add to playlist over the
// selection, Remove owner-only — followed playlists keep the queueing
// actions.
property <[BulkAction]> bulk-actions-owner: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next-selected", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later-selected", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue-selected", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "add-selected-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
{ id: "remove-selected", label: @tr("Remove"), icon: @image-url("../assets/icons/trash-2.svg"), danger: true, needs-selection: true },
@@ -323,6 +325,7 @@ export component PlaylistView inherits Rectangle {
property <[BulkAction]> bulk-actions-follower: [
{ id: "select-all", label: @tr("Select all"), icon: @image-url("../assets/icons/square-check-big.svg"), danger: false, needs-selection: false },
{ id: "play-next-selected", label: @tr("Play next"), icon: @image-url("../assets/icons/list-start.svg"), danger: false, needs-selection: true },
{ id: "play-later-selected", label: @tr("Play later"), icon: @image-url("../assets/icons/list-plus.svg"), danger: false, needs-selection: true },
{ id: "queue-selected", label: @tr("Add to queue"), icon: @image-url("../assets/icons/list-end.svg"), danger: false, needs-selection: true },
{ id: "add-selected-to-playlist", label: @tr("Add to playlist"), icon: @image-url("../assets/icons/list-music.svg"), danger: false, needs-selection: true },
];
@@ -6,6 +6,7 @@
//
// Item -> media-action("album", <album-id>, <action>) mapping:
// Play next -> "play-next"
// Play later -> "play-later"
// Add to queue -> "queue"
// --
// Add to playlist -> "add-to-playlist"
@@ -39,11 +40,11 @@ export component AlbumContextMenu inherits PopupWindow {
callback media-action(string, string, string);
width: 224px;
// 9 rows x 33px + 4 separators x 1px + ContextMenu's 5/5 padding. The
// 10 rows x 33px + 4 separators x 1px + ContextMenu's 5/5 padding. The
// offline row swaps label/action on `fully-cached` (count constant); the
// block row swaps label/action on `is-blocked` (count constant); the pin
// row swaps label/action on `is-pinned` (count constant).
height: 9 * 33px + 4 * 1px + 10px;
height: 10 * 33px + 4 * 1px + 10px;
// Default close-policy is close-on-click, which races the row's TouchArea:
// the popup can dismiss on the press before the item's `clicked` fires, so
// some rows (e.g. Share Qobuz link) needed a second open+click to register.
@@ -60,6 +61,16 @@ export component AlbumContextMenu inherits PopupWindow {
root.close();
}
}
// #442 "Play later" — the END of the manual block (after every
// play-next / play-later already queued, before the source resumes).
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("album", root.album-id, "play-later");
root.close();
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
+10 -3
View File
@@ -150,7 +150,7 @@ export component AlbumListRow inherits Rectangle {
callback clicked(string);
// kind ("album" | "artist"), id, action ("play" | "queue" |
// "play-next" | "favorite" | "open").
// "play-next" | "play-later" | "favorite" | "open").
callback media-action(string, string, string);
// Hover spans the row body + the art / artist / overflow affordances so
@@ -382,8 +382,8 @@ export component AlbumListRow inherits Rectangle {
x: -170px;
y: 34px;
width: 196px;
// 6 items max (Open/Play/Play-next/Queue/favorite + block).
height: 6 * 33px + 10px;
// 7 items max (Open/Play/Play-next/Play-later/Queue/favorite + block).
height: 7 * 33px + 10px;
ContextMenu {
ContextMenuItem {
icon: @image-url("../assets/icons/library-big.svg");
@@ -406,6 +406,13 @@ export component AlbumListRow inherits Rectangle {
root.media-action("album", root.album.id, "play-next");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("album", root.album.id, "play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
@@ -11,6 +11,7 @@
// Shuffle -> disc-action(disc, "shuffle")
// --
// Add to queue -> disc-action(disc, "queue")
// Play later -> disc-action(disc, "play-later")
// Play next -> disc-action(disc, "play-next")
import { Theme } from "../foundation/semantic-colors.slint";
@@ -22,8 +23,8 @@ export component DiscContextMenu inherits PopupWindow {
callback disc-action(int, string);
width: 200px;
// 4 rows x 33px + 1 separator x 1px + ContextMenu's 5/5 padding.
height: 4 * 33px + 1 * 1px + 10px;
// 5 rows x 33px + 1 separator x 1px + ContextMenu's 5/5 padding.
height: 5 * 33px + 1 * 1px + 10px;
ContextMenu {
ContextMenuItem {
@@ -48,6 +49,13 @@ export component DiscContextMenu inherits PopupWindow {
root.disc-action(root.disc, "queue");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.disc-action(root.disc, "play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-start.svg");
label: @tr("Play next");
@@ -13,7 +13,7 @@ import { DiscContextMenu } from "DiscContextMenu.slint";
export component DiscHeaderMenu inherits Rectangle {
in property <int> disc;
// (disc, action) — action: "play" | "shuffle" | "queue" | "play-next".
// (disc, action) — action: "play" | "shuffle" | "queue" | "play-later" | "play-next".
callback disc-action(int, string);
width: 32px;
@@ -115,7 +115,8 @@ export component PlaylistListRow inherits Rectangle {
x: -166px;
y: 32px;
width: 196px;
height: 150px;
// 5 rows x 33px + ContextMenu's 5/5 padding.
height: 5 * 33px + 10px;
ContextMenu {
ContextMenuItem {
icon: @image-url("../assets/icons/play.svg");
@@ -131,6 +132,13 @@ export component PlaylistListRow inherits Rectangle {
root.media-action("playlist", root.playlist.id, "play-next");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("playlist", root.playlist.id, "play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-end.svg");
label: @tr("Add to queue");
@@ -129,9 +129,9 @@ export component TransportControls inherits HorizontalLayout {
}
add-menu := PopupWindow {
x: 0px;
y: -222px;
y: -255px;
width: 232px;
height: 214px;
height: 247px;
close-policy: close-on-click;
ContextMenu {
ContextMenuItem {
@@ -155,6 +155,13 @@ export component TransportControls inherits HorizontalLayout {
root.media-action("track", NowPlayingState.track-id, "queue");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-plus.svg");
label: @tr("Play later");
clicked => {
root.media-action("track", NowPlayingState.track-id, "play-later");
}
}
ContextMenuItem {
icon: @image-url("../assets/icons/list-start.svg");
label: @tr("Play next");
+19 -18
View File
@@ -1029,12 +1029,12 @@ export global AlbumActions {
// Toggle multi-select (track selection) mode on the album track list.
callback toggle-multi-select();
// Bulk-bar action over the selected rows (id = "select-all" | "play-next"
// | "queue" | "add-to-playlist" | "add-to-favorites" | "make-offline" |
// "clear").
// | "play-later" | "queue" | "add-to-playlist" | "add-to-favorites" |
// "make-offline" | "clear").
callback bulk-action(string);
// Per-disc context-menu action on a multi-disc album's "Disc N" header —
// scoped to that disc's tracks only (action = "play" | "shuffle" | "queue"
// | "play-next"). The whole-album header buttons are unaffected.
// | "play-later" | "play-next"). The whole-album header buttons are unaffected.
callback disc-action(int /* disc number */, string /* action */);
// Open an external link (Last.fm / Discogs / MusicBrainz) in the system
// browser. The url is prebuilt by the controller (AlbumState.*-url).
@@ -1365,13 +1365,13 @@ export global LocalLibraryActions {
callback open-album(string /* album id */);
// Open an artist (lands with the Artists slice).
callback open-artist(string /* artist id */);
// Per-card action (play / queue / play-next / toggle-select). The Albums
// grid honors the shared AlbumCollectionView contract.
// Per-card action (play / queue / play-next / play-later / toggle-select).
// The Albums grid honors the shared AlbumCollectionView contract.
callback album-action(string /* album id */, string /* action */);
// Toggle the Albums-grid multi-select mode.
callback albums-toggle-multi-select();
// Albums bulk bar action: select-all | clear | queue | play-next |
// add-to-playlist | add-to-mixtape.
// play-later | add-to-playlist | add-to-mixtape.
callback albums-bulk-action(string /* action */);
// Windowed grid: the visible row band changed (item indices into
// albums-visible, prefetch margin included) — dispatch artwork for it.
@@ -1381,7 +1381,7 @@ export global LocalLibraryActions {
callback tracks-search(string /* query */);
callback tracks-load-more();
callback tracks-retry();
// Per-row play/queue/toggle-select (action: play|play-next|queue|toggle-select).
// Per-row play/queue/toggle-select (action: play|play-next|play-later|queue|toggle-select).
callback track-action(string /* track id */, string /* action */);
// Group-by mode (off|album|artist|name), persisted.
callback tracks-set-group(string /* mode */);
@@ -1389,7 +1389,7 @@ export global LocalLibraryActions {
callback tracks-set-sort(string /* key */);
// Toggle the multi-select checkbox column.
callback tracks-toggle-multi-select();
// Bulk bar action: select-all | clear | queue | play-next | add-to-playlist.
// Bulk bar action: select-all | clear | queue | play-next | play-later | add-to-playlist.
callback tracks-bulk-action(string /* action */);
// --- Folders tab ---
@@ -1416,7 +1416,7 @@ export global LocalLibraryActions {
callback folders-toggle-select-mode();
callback folders-toggle-folder-select(string /* folder path */);
callback folders-toggle-track-select(string /* track path */);
callback folders-bulk-action(string /* select-all|clear|queue|play-next|add-to-playlist */);
callback folders-bulk-action(string /* select-all|clear|queue|play-next|play-later|add-to-playlist */);
// --- Ephemeral folder (ad-hoc play) ---
// Open the native folder picker → scan → show the ephemeral pane.
@@ -1501,8 +1501,8 @@ export global LocalAlbumActions {
callback add-to-playlist();
callback add-to-mixtape();
callback play-track(string /* track id */);
// Per-row context-menu actions beyond "play" (play-next / queue /
// add-to-playlist) — resolved against the open version's track cache.
// Per-row context-menu actions beyond "play" (play-next / play-later /
// queue / add-to-playlist) — resolved against the open version's track cache.
callback track-menu-action(string /* track id */, string /* action */);
callback select-version(int);
// Client-side track filter (mirrors AlbumActions.search), applied over the
@@ -1510,7 +1510,8 @@ export global LocalAlbumActions {
callback search(string /* query */);
// Per-disc context-menu action on a multi-disc local album's "Disc N"
// header — scoped to that disc's tracks only (action = "play" | "shuffle" |
// "queue" | "play-next"). The whole-album header buttons are unaffected.
// "queue" | "play-later" | "play-next"). The whole-album header buttons are
// unaffected.
callback disc-action(int /* disc number */, string /* action */);
}
@@ -3659,8 +3660,8 @@ export global ArtistActions {
// Toggle Popular Tracks multi-select mode.
callback toggle-top-tracks-select();
// Bulk-bar action over the selected Popular Tracks (id = "select-all" |
// "play-next" | "queue" | "add-to-playlist" | "add-to-favorites" |
// "clear").
// "play-next" | "play-later" | "queue" | "add-to-playlist" |
// "add-to-favorites" | "clear").
callback top-tracks-bulk-action(string);
// Popular Tracks section "more" menu (id = "next-all" | "queue-all" |
// "shuffle-all" | "playlist-all").
@@ -5147,8 +5148,8 @@ export global FavoritesState {
in property <[DiscoverSection]> albums-grouped: []; // group-by sections
in-out property <string> albums-view-mode: "grid"; // "grid" | "list"
// Albums LIST-mode multi-select (owner 2026-07-24): checkbox column on
// the rows + the bulk bar (un-favorite / play-next / queue). List only —
// switching to grid forces it off.
// the rows + the bulk bar (un-favorite / play-next / play-later / queue).
// List only — switching to grid forces it off.
in-out property <bool> albums-multi-select: false;
in property <int> albums-selected-count: 0;
in-out property <string> albums-search: "";
@@ -5212,7 +5213,7 @@ export global FavoritesActions {
callback toggle-multi-select();
callback bulk-action(string /* select-all | remove-selected | clear */);
// Albums list-mode multi-select: toggle the edit mode + the bulk bar
// actions (select-all | play-next | queue | remove-selected | clear).
// actions (select-all | play-next | play-later | queue | remove-selected | clear).
callback albums-toggle-multi-select();
callback albums-bulk-action(string);
callback album-action(string /* album id */, string /* action */);
@@ -5227,7 +5228,7 @@ export global FavoritesActions {
callback labels-random();
callback search-playlists(string /* query */);
callback playlists-set-view(string /* grid | list */);
callback playlist-action(string /* playlist id */, string /* play | play-next | queue | share | favorite | follow | unfollow | copy */);
callback playlist-action(string /* playlist id */, string /* play | play-next | play-later | queue | share | favorite | follow | unfollow | copy */);
callback search-artists(string /* query */);
callback artists-set-view(string /* grid | sidepanel */);
callback artists-set-group(string /* off | alpha */);