From e1aa0a71dc95d9385d657107b42a3e1be0e7db78 Mon Sep 17 00:00:00 2001
From: vicrodh <103399283+vicrodh@users.noreply.github.com>
Date: Tue, 19 May 2026 20:04:53 -0600
Subject: [PATCH] feat(slint): full track context menu (13 items, ported from
Tauri)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New shared TrackContextMenu primitive with every option the Tauri
TrackMenu shows (flat — no submenus): Play now / Play next / Add to
queue / Create radio / Add to library / Add to mixtape / Add to
playlist / Share Qobuz link / Share Song.link / Make available offline
/ Go to album / Go to artist / Track info. Items already wired in the
MVP (play / play-next / queue / favorite) fire as before; the rest emit
their media-action and stay no-op until the backend lands. Used by
SearchTrackRow, AlbumPageView TrackRow, and ArtistPageView TrackRow.
cc
---
crates/qbz-slint/ui/album/AlbumPageView.slint | 41 ++-----
.../qbz-slint/ui/artist/ArtistPageView.slint | 39 +------
crates/qbz-slint/ui/assets/icons/link.svg | 1 +
.../ui/primitives/TrackContextMenu.slint | 109 ++++++++++++++++++
.../qbz-slint/ui/search/SearchTrackRow.slint | 43 ++-----
5 files changed, 130 insertions(+), 103 deletions(-)
create mode 100644 crates/qbz-slint/ui/assets/icons/link.svg
create mode 100644 crates/qbz-slint/ui/primitives/TrackContextMenu.slint
diff --git a/crates/qbz-slint/ui/album/AlbumPageView.slint b/crates/qbz-slint/ui/album/AlbumPageView.slint
index 0228b91e9..5074f1e02 100644
--- a/crates/qbz-slint/ui/album/AlbumPageView.slint
+++ b/crates/qbz-slint/ui/album/AlbumPageView.slint
@@ -15,6 +15,7 @@ import { ContextMenu } from "../primitives/ContextMenu.slint";
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
+import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
import { NavButtons } from "../shell/NavButtons.slint";
component CircleAction inherits Rectangle {
@@ -239,40 +240,12 @@ component TrackRow inherits Rectangle {
}
}
- track-menu := PopupWindow {
- x: parent.width - 208px;
- y: 46px;
- width: 196px;
- height: 109px;
- ContextMenu {
- ContextMenuItem {
- icon: @image-url("../assets/icons/play.svg");
- label: @tr("Play");
- clicked => {
- root.media-action("track", root.track.id, "play");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/list-plus.svg");
- label: @tr("Play next");
- clicked => {
- root.media-action("track", root.track.id, "play-next");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/list-end.svg");
- label: @tr("Add to queue");
- clicked => {
- root.media-action("track", root.track.id, "queue");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/heart.svg");
- label: @tr("Add to favorites");
- clicked => {
- root.media-action("track", root.track.id, "favorite");
- }
- }
+ track-menu := TrackContextMenu {
+ x: parent.width - self.width - 12px;
+ y: 48px;
+ track-id: root.track.id;
+ media-action(k, i, a) => {
+ root.media-action(k, i, a);
}
}
}
diff --git a/crates/qbz-slint/ui/artist/ArtistPageView.slint b/crates/qbz-slint/ui/artist/ArtistPageView.slint
index 6db78411b..8bd061cbb 100644
--- a/crates/qbz-slint/ui/artist/ArtistPageView.slint
+++ b/crates/qbz-slint/ui/artist/ArtistPageView.slint
@@ -13,6 +13,7 @@ import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ContextMenu } from "../primitives/ContextMenu.slint";
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
+import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
import { Carousel } from "../discover/Carousel.slint";
import { NavButtons } from "../shell/NavButtons.slint";
@@ -147,40 +148,12 @@ component TrackRow inherits Rectangle {
}
}
- track-menu := PopupWindow {
- x: parent.width - 208px;
+ track-menu := TrackContextMenu {
+ x: parent.width - self.width - 12px;
y: 44px;
- width: 196px;
- height: 109px;
- ContextMenu {
- ContextMenuItem {
- icon: @image-url("../assets/icons/play.svg");
- label: @tr("Play");
- clicked => {
- root.media-action("track", root.track.id, "play");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/list-plus.svg");
- label: @tr("Play next");
- clicked => {
- root.media-action("track", root.track.id, "play-next");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/list-end.svg");
- label: @tr("Add to queue");
- clicked => {
- root.media-action("track", root.track.id, "queue");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/heart.svg");
- label: @tr("Add to favorites");
- clicked => {
- root.media-action("track", root.track.id, "favorite");
- }
- }
+ track-id: root.track.id;
+ media-action(k, i, a) => {
+ root.media-action(k, i, a);
}
}
}
diff --git a/crates/qbz-slint/ui/assets/icons/link.svg b/crates/qbz-slint/ui/assets/icons/link.svg
new file mode 100644
index 000000000..f2d15541f
--- /dev/null
+++ b/crates/qbz-slint/ui/assets/icons/link.svg
@@ -0,0 +1 @@
+
diff --git a/crates/qbz-slint/ui/primitives/TrackContextMenu.slint b/crates/qbz-slint/ui/primitives/TrackContextMenu.slint
new file mode 100644
index 000000000..32bacbec0
--- /dev/null
+++ b/crates/qbz-slint/ui/primitives/TrackContextMenu.slint
@@ -0,0 +1,109 @@
+// Track-row "more" context menu — flat, ported 1:1 from the Tauri
+// TrackMenu component. Items that have no Slint wiring yet emit a
+// media-action with an action string the runtime currently ignores
+// (no-op until backend wiring lands).
+
+import { ContextMenu } from "ContextMenu.slint";
+import { ContextMenuItem } from "ContextMenuItem.slint";
+
+export component TrackContextMenu inherits PopupWindow {
+ in property track-id;
+ callback media-action(string, string, string);
+
+ width: 224px;
+ height: 392px;
+
+ ContextMenu {
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/play.svg");
+ label: @tr("Play now");
+ clicked => {
+ root.media-action("track", root.track-id, "play");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/list-plus.svg");
+ label: @tr("Play next");
+ clicked => {
+ root.media-action("track", root.track-id, "play-next");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/list-end.svg");
+ label: @tr("Add to queue");
+ clicked => {
+ root.media-action("track", root.track-id, "queue");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/radio.svg");
+ label: @tr("Create radio");
+ clicked => {
+ root.media-action("track", root.track-id, "create-radio");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/heart.svg");
+ label: @tr("Add to library");
+ clicked => {
+ root.media-action("track", root.track-id, "favorite");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/cassette-tape.svg");
+ label: @tr("Add to mixtape");
+ clicked => {
+ root.media-action("track", root.track-id, "add-to-mixtape");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/list-music.svg");
+ label: @tr("Add to playlist");
+ clicked => {
+ root.media-action("track", root.track-id, "add-to-playlist");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/link.svg");
+ label: @tr("Share Qobuz link");
+ clicked => {
+ root.media-action("track", root.track-id, "share-qobuz");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/link.svg");
+ label: @tr("Share Song.link");
+ clicked => {
+ root.media-action("track", root.track-id, "share-songlink");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/cloud-download.svg");
+ label: @tr("Make available offline");
+ clicked => {
+ root.media-action("track", root.track-id, "download");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/disc-3.svg");
+ label: @tr("Go to album");
+ clicked => {
+ root.media-action("track", root.track-id, "go-to-album");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/user.svg");
+ label: @tr("Go to artist");
+ clicked => {
+ root.media-action("track", root.track-id, "go-to-artist");
+ }
+ }
+ ContextMenuItem {
+ icon: @image-url("../assets/icons/info.svg");
+ label: @tr("Track info");
+ clicked => {
+ root.media-action("track", root.track-id, "track-info");
+ }
+ }
+ }
+}
diff --git a/crates/qbz-slint/ui/search/SearchTrackRow.slint b/crates/qbz-slint/ui/search/SearchTrackRow.slint
index 6a24993f0..f216e6590 100644
--- a/crates/qbz-slint/ui/search/SearchTrackRow.slint
+++ b/crates/qbz-slint/ui/search/SearchTrackRow.slint
@@ -10,9 +10,8 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { SearchTrackItem } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
-import { ContextMenu } from "../primitives/ContextMenu.slint";
-import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
+import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
export component SearchTrackRow inherits Rectangle {
in property track;
@@ -153,40 +152,12 @@ export component SearchTrackRow inherits Rectangle {
}
}
- track-menu := PopupWindow {
- x: parent.width - 208px;
- y: 46px;
- width: 196px;
- height: 109px;
- ContextMenu {
- ContextMenuItem {
- icon: @image-url("../assets/icons/play.svg");
- label: @tr("Play");
- clicked => {
- root.media-action("track", root.track.id, "play");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/list-plus.svg");
- label: @tr("Play next");
- clicked => {
- root.media-action("track", root.track.id, "play-next");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/list-end.svg");
- label: @tr("Add to queue");
- clicked => {
- root.media-action("track", root.track.id, "queue");
- }
- }
- ContextMenuItem {
- icon: @image-url("../assets/icons/heart.svg");
- label: @tr("Add to favorites");
- clicked => {
- root.media-action("track", root.track.id, "favorite");
- }
- }
+ track-menu := TrackContextMenu {
+ x: parent.width - self.width - 12px;
+ y: 48px;
+ track-id: root.track.id;
+ media-action(k, i, a) => {
+ root.media-action(k, i, a);
}
}
}