A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd

gtk: add context menu

+779 -44
+2 -2
cli/src/api/rockbox.v1alpha1.rs
··· 2220 2220 pub shuffle: ::core::option::Option<bool>, 2221 2221 #[prost(bool, optional, tag = "3")] 2222 2222 pub recurse: ::core::option::Option<bool>, 2223 - #[prost(bool, optional, tag = "4")] 2224 - pub position: ::core::option::Option<bool>, 2223 + #[prost(int32, optional, tag = "4")] 2224 + pub position: ::core::option::Option<i32>, 2225 2225 } 2226 2226 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2227 2227 pub struct PlayDirectoryResponse {}
cli/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+1 -1
crates/rpc/proto/rockbox/v1alpha1/playback.proto
··· 138 138 string path = 1; 139 139 optional bool shuffle = 2; 140 140 optional bool recurse = 3; 141 - optional bool position = 4; 141 + optional int32 position = 4; 142 142 } 143 143 144 144 message PlayDirectoryResponse {}
+2 -2
crates/rpc/src/api/rockbox.v1alpha1.rs
··· 2804 2804 pub shuffle: ::core::option::Option<bool>, 2805 2805 #[prost(bool, optional, tag = "3")] 2806 2806 pub recurse: ::core::option::Option<bool>, 2807 - #[prost(bool, optional, tag = "4")] 2808 - pub position: ::core::option::Option<bool>, 2807 + #[prost(int32, optional, tag = "4")] 2808 + pub position: ::core::option::Option<i32>, 2809 2809 } 2810 2810 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2811 2811 pub struct PlayDirectoryResponse {}
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+8 -4
gtk/Cargo.toml
··· 4 4 version = "0.1.0" 5 5 6 6 [dependencies] 7 - adw = {version = "0.7", package = "libadwaita", features = ["v1_6"]} 7 + adw = { version = "0.7", package = "libadwaita", features = ["v1_6"] } 8 8 anyhow = "1.0.93" 9 9 chrono = "0.4.38" 10 10 futures = "0.3.31" 11 - gtk = {version = "0.9", package = "gtk4", features = ["gnome_46"]} 11 + gtk = { version = "0.9", package = "gtk4", features = ["gnome_46"] } 12 12 gtk-blueprint = "0.2.0" 13 13 md5 = "0.7.0" 14 14 prost = "0.13.2" 15 - reqwest = {version = "0.12.7", features = ["rustls-tls", "json", "blocking"], default-features = false} 16 - tokio = {version = "1.36.0", features = ["full"]} 15 + reqwest = { version = "0.12.7", features = [ 16 + "rustls-tls", 17 + "json", 18 + "blocking", 19 + ], default-features = false } 20 + tokio = { version = "1.36.0", features = ["full"] } 17 21 tonic = "0.12.2" 18 22 tonic-reflection = "0.12.2" 19 23 tonic-web = "0.12.2"
+14 -2
gtk/data/gtk/album_details.blp
··· 160 160 orientation: vertical; 161 161 margin-end: 10; 162 162 163 - Button album_more_button { 163 + MenuButton album_more_button { 164 164 tooltip-text: _("More"); 165 + menu-model: context_menu; 165 166 child: Image { 166 167 icon-name: "options-symbolic"; 167 168 pixel-size: 24; ··· 179 180 ] 180 181 } 181 182 182 - Button { 183 + MenuButton { 183 184 label: "More"; 185 + menu-model: context_menu; 184 186 halign: center; 185 187 valign: center; 186 188 ··· 201 203 orientation: vertical; 202 204 } 203 205 } 206 + 207 + menu context_menu { 208 + section { 209 + item (_("Play Next"), "app.album.play-next") 210 + item (_("Play Last"), "app.album.play-last") 211 + item (_("Add Shuffled"), "app.album.add-shuffled") 212 + item (_("Play Last Shuffled"), "app.album.play-last-shuffled") 213 + item (_("Play Shuffled"), "app.album.play-shuffled") 214 + } 215 + }
+16 -2
gtk/data/gtk/artist_details.blp
··· 141 141 Box { 142 142 orientation: vertical; 143 143 144 - Button more_button { 144 + MenuButton more_button { 145 145 tooltip-text: _("More"); 146 146 halign: center; 147 147 valign: center; 148 148 margin-start: 0; 149 149 margin-top: 0; 150 150 margin-bottom: 0; 151 + menu-model: context_menu; 152 + 151 153 child: Image { 152 154 icon-name: "options-symbolic"; 153 155 pixel-size: 24; ··· 158 160 ] 159 161 } 160 162 161 - Button { 163 + MenuButton { 162 164 label: "More"; 163 165 halign: center; 164 166 valign: center; 167 + menu-model: context_menu; 165 168 166 169 styles [ 167 170 "transparent-button" ··· 218 221 } 219 222 } 220 223 224 + 225 + menu context_menu { 226 + section { 227 + item (_("Play"), "app.artist.play") 228 + item (_("Play Next"), "app.artist.play-next") 229 + item (_("Play Last"), "app.artist.play-last") 230 + item (_("Add Shuffled"), "app.artist.add-shuffled") 231 + item (_("Play Last Shuffled"), "app.artist.play-last-shuffled") 232 + item (_("Play Shuffled"), "app.artist.shuffled") 233 + } 234 + }
+41 -1
gtk/data/gtk/file.blp
··· 29 29 } 30 30 } 31 31 32 - Button { 32 + MenuButton file_menu { 33 33 tooltip-text: _("More"); 34 + menu-model: file_context_menu; 35 + visible: false; 36 + child: Image { 37 + icon-name: "options-symbolic"; 38 + pixel-size: 24; 39 + }; 34 40 41 + halign: center; 42 + valign: center; 43 + width-request: 40; 44 + height-request: 40; 45 + 46 + styles [ 47 + "transparent-button" 48 + ] 49 + } 50 + 51 + MenuButton directory_menu { 52 + tooltip-text: _("More"); 53 + menu-model: directory_context_menu; 54 + visible: true; 35 55 child: Image { 36 56 icon-name: "options-symbolic"; 37 57 pixel-size: 24; ··· 49 69 50 70 } 51 71 72 + menu directory_context_menu { 73 + section { 74 + item (_("Play"), "app.dir.play") 75 + item (_("Play Next"), "app.dir.play-next") 76 + item (_("Play Last"), "app.dir.play-last") 77 + item (_("Add Shuffled"), "app.dir.add-shuffled") 78 + item (_("Play Last Shuffled"), "app.dir.play-last-shuffled") 79 + item (_("Play Shuffled"), "app.dir.play-shuffled") 80 + } 81 + } 82 + 83 + 84 + menu file_context_menu { 85 + section { 86 + item (_("Play"), "app.dir.play") 87 + item (_("Play Next"), "app.dir.play-next") 88 + item (_("Play Last"), "app.dir.play-last") 89 + item (_("Add Shuffled"), "app.dir.add-shuffled") 90 + } 91 + }
+9 -1
gtk/data/gtk/media_controls.blp
··· 152 152 ] 153 153 } 154 154 155 - Button more_button { 155 + MenuButton more_button { 156 156 tooltip-text: _("More"); 157 + menu-model: context_menu; 157 158 child: Image { 158 159 icon-name: "options-symbolic"; 159 160 pixel-size: 24; ··· 234 235 } 235 236 } 236 237 } 238 + 239 + menu context_menu { 240 + section { 241 + item (_("Go to Artist"), "app.go-to-artist") 242 + item (_("Go to Album"), "app.go-to-album") 243 + } 244 + }
+9 -2
gtk/data/gtk/song.blp
··· 98 98 ] 99 99 } 100 100 101 - Button more_button { 101 + MenuButton more_button { 102 102 tooltip-text: _("More"); 103 - 103 + menu-model: context_menu; 104 104 child: Image { 105 105 icon-name: "options-symbolic"; 106 106 pixel-size: 24; ··· 140 140 ] 141 141 } 142 142 143 + menu context_menu { 144 + section { 145 + item (_("Play Next"), "app.play-next") 146 + item (_("Play Last"), "app.play-last") 147 + item (_("Add Shuffled"), "app.add-shuffled") 148 + } 149 + }
+1 -1
gtk/proto/rockbox/v1alpha1/playback.proto
··· 138 138 string path = 1; 139 139 optional bool shuffle = 2; 140 140 optional bool recurse = 3; 141 - optional bool position = 4; 141 + optional int32 position = 4; 142 142 } 143 143 144 144 message PlayDirectoryResponse {}
+8
gtk/src/constants.rs
··· 1 + pub const PLAYLIST_PREPEND: i32 = -1; 2 + pub const PLAYLIST_INSERT: i32 = -2; 3 + pub const PLAYLIST_INSERT_LAST: i32 = -3; 4 + pub const PLAYLIST_INSERT_FIRST: i32 = -4; 5 + pub const PLAYLIST_INSERT_SHUFFLED: i32 = -5; 6 + pub const PLAYLIST_REPLACE: i32 = -6; 7 + pub const PLAYLIST_INSERT_LAST_SHUFFLED: i32 = -7; 8 + pub const PLAYLIST_INSERT_LAST_ROTATED: i32 = -8;
+1
gtk/src/main.rs
··· 4 4 #[rustfmt::skip] 5 5 mod config; 6 6 pub mod app; 7 + pub mod constants; 7 8 pub mod navigation; 8 9 pub mod state; 9 10 pub mod time;
+8 -1
gtk/src/styles.css
··· 63 63 } 64 64 65 65 .transparent-button { 66 - border-radius: 20px; 66 + background-color: transparent; 67 + } 68 + 69 + menubutton { 70 + background-color: transparent; 71 + } 72 + 73 + menubutton > button { 67 74 background-color: transparent; 68 75 } 69 76
+226 -3
gtk/src/ui/file.rs
··· 1 1 use crate::api::rockbox::v1alpha1::browse_service_client::BrowseServiceClient; 2 - use crate::api::rockbox::v1alpha1::{TreeGetEntriesRequest, TreeGetEntriesResponse}; 2 + use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 3 + use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient; 4 + use crate::api::rockbox::v1alpha1::{ 5 + InsertDirectoryRequest, InsertTracksRequest, PlayDirectoryRequest, PlayTrackRequest, 6 + TreeGetEntriesRequest, TreeGetEntriesResponse, 7 + }; 8 + use crate::constants::*; 3 9 use crate::state::AppState; 4 10 use adw::prelude::*; 5 11 use adw::subclass::prelude::*; 6 12 use anyhow::Error; 7 13 use glib::subclass; 8 14 use gtk::glib; 9 - use gtk::{CompositeTemplate, Image, Label, ListBox}; 15 + use gtk::{CompositeTemplate, Image, Label, ListBox, MenuButton}; 10 16 use std::cell::{Cell, RefCell}; 11 - use std::env; 17 + use std::{env, thread}; 12 18 13 19 mod imp { 14 20 ··· 23 29 pub file_name: TemplateChild<Label>, 24 30 #[template_child] 25 31 pub row: TemplateChild<gtk::Box>, 32 + #[template_child] 33 + pub file_menu: TemplateChild<MenuButton>, 34 + #[template_child] 35 + pub directory_menu: TemplateChild<MenuButton>, 26 36 27 37 pub files: RefCell<Option<ListBox>>, 28 38 pub go_back_button: RefCell<Option<gtk::Button>>, ··· 39 49 40 50 fn class_init(klass: &mut Self::Class) { 41 51 Self::bind_template(klass); 52 + 53 + klass.install_action("app.dir.play-next", None, move |file, _action, _target| { 54 + file.play_next(); 55 + }); 56 + 57 + klass.install_action("app.dir.play-last", None, move |file, _action, _target| { 58 + file.play_last(); 59 + }); 60 + 61 + klass.install_action( 62 + "app.dir.add-shuffled", 63 + None, 64 + move |file, _action, _target| { 65 + file.add_shuffled(); 66 + }, 67 + ); 68 + 69 + klass.install_action( 70 + "app.dir.play-last-shuffled", 71 + None, 72 + move |file, _action, _target| { 73 + file.play_last_shuffled(); 74 + }, 75 + ); 76 + 77 + klass.install_action( 78 + "app.dir.play-shuffled", 79 + None, 80 + move |file, _action, _target| { 81 + file.play(true); 82 + }, 83 + ); 84 + 85 + klass.install_action("app.dir.play", None, move |file, _action, _target| { 86 + file.play(false); 87 + }); 42 88 } 43 89 44 90 fn instance_init(obj: &subclass::InitializingObject<Self>) { ··· 147 193 file.imp().state.set(Some(&state)); 148 194 file.imp().set_path(entry.name.clone()); 149 195 file.imp().set_is_dir(entry.attr == 16); 196 + 197 + match entry.attr == 16 { 198 + true => { 199 + file.imp().file_menu.set_visible(false); 200 + file.imp().directory_menu.set_visible(true); 201 + } 202 + false => { 203 + file.imp().file_menu.set_visible(true); 204 + file.imp().directory_menu.set_visible(false); 205 + } 206 + } 207 + 150 208 files_ref.append(&file); 151 209 } 152 210 } 211 + } 212 + 213 + pub fn play_next(&self) { 214 + let path = self.imp().path.borrow(); 215 + let path = path.clone(); 216 + let is_dir = self.imp().is_dir.get(); 217 + thread::spawn(move || { 218 + let rt = tokio::runtime::Runtime::new().unwrap(); 219 + let url = build_url(); 220 + let _ = rt.block_on(async { 221 + let mut client = PlaylistServiceClient::connect(url).await?; 222 + match is_dir { 223 + true => { 224 + client 225 + .insert_directory(InsertDirectoryRequest { 226 + directory: path, 227 + position: PLAYLIST_INSERT_FIRST, 228 + ..Default::default() 229 + }) 230 + .await?; 231 + } 232 + false => { 233 + client 234 + .insert_tracks(InsertTracksRequest { 235 + tracks: vec![path], 236 + position: PLAYLIST_INSERT_FIRST, 237 + ..Default::default() 238 + }) 239 + .await?; 240 + } 241 + } 242 + Ok::<(), Error>(()) 243 + }); 244 + }); 245 + } 246 + 247 + pub fn play_last(&self) { 248 + let path = self.imp().path.borrow(); 249 + let path = path.clone(); 250 + let is_dir = self.imp().is_dir.get(); 251 + thread::spawn(move || { 252 + let rt = tokio::runtime::Runtime::new().unwrap(); 253 + let url = build_url(); 254 + let _ = rt.block_on(async { 255 + let mut client = PlaylistServiceClient::connect(url).await?; 256 + match is_dir { 257 + true => { 258 + client 259 + .insert_directory(InsertDirectoryRequest { 260 + directory: path, 261 + position: PLAYLIST_INSERT_LAST, 262 + ..Default::default() 263 + }) 264 + .await?; 265 + } 266 + false => { 267 + client 268 + .insert_tracks(InsertTracksRequest { 269 + tracks: vec![path], 270 + position: PLAYLIST_INSERT_LAST, 271 + ..Default::default() 272 + }) 273 + .await?; 274 + } 275 + } 276 + Ok::<(), Error>(()) 277 + }); 278 + }); 279 + } 280 + 281 + pub fn add_shuffled(&self) { 282 + let path = self.imp().path.borrow(); 283 + let path = path.clone(); 284 + let is_dir = self.imp().is_dir.get(); 285 + thread::spawn(move || { 286 + let rt = tokio::runtime::Runtime::new().unwrap(); 287 + let url = build_url(); 288 + let _ = rt.block_on(async { 289 + let mut client = PlaylistServiceClient::connect(url).await?; 290 + match is_dir { 291 + true => { 292 + client 293 + .insert_directory(InsertDirectoryRequest { 294 + directory: path, 295 + position: PLAYLIST_INSERT_SHUFFLED, 296 + ..Default::default() 297 + }) 298 + .await?; 299 + } 300 + false => { 301 + client 302 + .insert_tracks(InsertTracksRequest { 303 + tracks: vec![path], 304 + position: PLAYLIST_INSERT_SHUFFLED, 305 + ..Default::default() 306 + }) 307 + .await?; 308 + } 309 + } 310 + Ok::<(), Error>(()) 311 + }); 312 + }); 313 + } 314 + 315 + pub fn play_last_shuffled(&self) { 316 + let path = self.imp().path.borrow(); 317 + let path = path.clone(); 318 + let is_dir = self.imp().is_dir.get(); 319 + thread::spawn(move || { 320 + let rt = tokio::runtime::Runtime::new().unwrap(); 321 + let url = build_url(); 322 + let _ = rt.block_on(async { 323 + let mut client = PlaylistServiceClient::connect(url).await?; 324 + match is_dir { 325 + true => { 326 + client 327 + .insert_directory(InsertDirectoryRequest { 328 + directory: path, 329 + position: PLAYLIST_INSERT_LAST_SHUFFLED, 330 + ..Default::default() 331 + }) 332 + .await?; 333 + } 334 + false => { 335 + client 336 + .insert_tracks(InsertTracksRequest { 337 + tracks: vec![path], 338 + position: PLAYLIST_INSERT_LAST_SHUFFLED, 339 + ..Default::default() 340 + }) 341 + .await?; 342 + } 343 + } 344 + Ok::<(), Error>(()) 345 + }); 346 + }); 347 + } 348 + 349 + pub fn play(&self, shuffle: bool) { 350 + let path = self.imp().path.borrow(); 351 + let path = path.clone(); 352 + let is_dir = self.imp().is_dir.get(); 353 + thread::spawn(move || { 354 + let rt = tokio::runtime::Runtime::new().unwrap(); 355 + let url = build_url(); 356 + let _ = rt.block_on(async { 357 + let mut client = PlaybackServiceClient::connect(url).await?; 358 + match is_dir { 359 + true => { 360 + client 361 + .play_directory(PlayDirectoryRequest { 362 + path, 363 + shuffle: Some(shuffle), 364 + recurse: Some(false), 365 + ..Default::default() 366 + }) 367 + .await?; 368 + } 369 + false => { 370 + client.play_track(PlayTrackRequest { path }).await?; 371 + } 372 + } 373 + Ok::<(), Error>(()) 374 + }); 375 + }); 153 376 } 154 377 } 155 378
+26 -9
gtk/src/ui/media_controls.rs
··· 1 1 use std::{env, thread}; 2 2 3 - use adw::prelude::*; 4 - use adw::subclass::prelude::*; 5 - use anyhow::Error; 6 - use glib::subclass; 7 - use gtk::glib; 8 - use gtk::pango::EllipsizeMode; 9 - use gtk::{Button, CompositeTemplate, Image, Label, Scale}; 10 - 11 3 use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 12 4 use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 13 5 use crate::api::rockbox::v1alpha1::settings_service_client::SettingsServiceClient; ··· 21 13 use crate::types::track::Track; 22 14 use crate::ui::pages::album_details::AlbumDetails; 23 15 use crate::ui::pages::current_playlist::CurrentPlaylist; 16 + use adw::prelude::*; 17 + use adw::subclass::prelude::*; 18 + use anyhow::Error; 19 + use glib::subclass; 20 + use gtk::gdk_pixbuf::Pixbuf; 21 + use gtk::glib; 22 + use gtk::pango::EllipsizeMode; 23 + use gtk::{Button, CompositeTemplate, Image, Label, MenuButton, Scale}; 24 24 use std::cell::{Cell, RefCell}; 25 25 use tokio::sync::mpsc; 26 26 ··· 62 62 #[template_child] 63 63 pub heart_button: TemplateChild<Button>, 64 64 #[template_child] 65 - pub more_button: TemplateChild<Button>, 65 + pub more_button: TemplateChild<MenuButton>, 66 66 #[template_child] 67 67 pub heart_icon: TemplateChild<Image>, 68 68 #[template_child] ··· 138 138 media_controls.show_playlist(); 139 139 }, 140 140 ); 141 + 142 + klass.install_action( 143 + "app.go-to-artist", 144 + None, 145 + move |_media_controls, _action, _target| {}, 146 + ); 147 + 148 + klass.install_action( 149 + "app.go-to-album", 150 + None, 151 + move |_media_controls, _action, _target| {}, 152 + ); 141 153 } 142 154 143 155 fn instance_init(obj: &subclass::InitializingObject<Self>) { ··· 272 284 let home = std::env::var("HOME").unwrap(); 273 285 let path = format!("{}/.config/rockbox.org/covers/{}", home, filename); 274 286 album_art.set_from_file(Some(&path)); 287 + } else { 288 + album_art 289 + .set_resource(Some("/mg/tsirysndr/Rockbox/icons/jpg/albumart.jpg")); 275 290 } 276 291 277 292 match state.is_liked_track(&track.id) { ··· 468 483 let home = std::env::var("HOME").unwrap(); 469 484 let path = format!("{}/.config/rockbox.org/covers/{}", home, filename); 470 485 album_art.set_from_file(Some(&path)); 486 + } else { 487 + album_art.set_resource(Some("/mg/tsirysndr/Rockbox/icons/jpg/albumart.jpg")); 471 488 } 472 489 473 490 let state = self.imp().state.upgrade().unwrap();
+155 -3
gtk/src/ui/pages/album_details.rs
··· 1 1 use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 2 - use crate::api::rockbox::v1alpha1::{Album, GetAlbumRequest, Track}; 2 + use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 3 + use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient; 4 + use crate::api::rockbox::v1alpha1::{ 5 + Album, GetAlbumRequest, InsertAlbumRequest, PlayAlbumRequest, Track, 6 + }; 7 + use crate::constants::*; 3 8 use crate::state::AppState; 4 9 use crate::ui::album_tracks::AlbumTracks; 5 10 use adw::prelude::*; ··· 8 13 use glib::subclass; 9 14 use gtk::glib; 10 15 use gtk::{CompositeTemplate, Image, Label}; 16 + use std::cell::RefCell; 11 17 use std::{env, thread}; 12 18 13 19 mod imp { ··· 31 37 pub album_tracklist: TemplateChild<gtk::Box>, 32 38 33 39 pub state: glib::WeakRef<AppState>, 40 + pub album_id: RefCell<String>, 34 41 } 35 42 36 43 #[glib::object_subclass] ··· 45 52 klass.install_action( 46 53 "app.play-album", 47 54 None, 48 - move |_album_details, _action, _target| {}, 55 + move |album_details, _action, _target| { 56 + album_details.play_album(false); 57 + }, 49 58 ); 50 59 51 60 klass.install_action( 52 61 "app.shuffle-album", 53 62 None, 54 - move |_album_details, _action, _target| {}, 63 + move |album_details, _action, _target| { 64 + album_details.play_album(true); 65 + }, 66 + ); 67 + 68 + klass.install_action( 69 + "app.album.play-next", 70 + None, 71 + move |album_details, _action, _target| { 72 + album_details.play_next(); 73 + }, 74 + ); 75 + 76 + klass.install_action( 77 + "app.album.play-last", 78 + None, 79 + move |album_details, _action, _target| { 80 + album_details.play_last(); 81 + }, 82 + ); 83 + 84 + klass.install_action( 85 + "app.album.add-shuffled", 86 + None, 87 + move |album_details, _action, _target| { 88 + album_details.add_shuffled(); 89 + }, 90 + ); 91 + 92 + klass.install_action( 93 + "app.album.play-last-shuffled", 94 + None, 95 + move |album_details, _action, _target| { 96 + album_details.play_last_shuffled(); 97 + }, 98 + ); 99 + 100 + klass.install_action( 101 + "app.album.play-shuffled", 102 + None, 103 + move |album_details, _action, _target| { 104 + album_details.play_album(true); 105 + }, 55 106 ); 56 107 } 57 108 ··· 72 123 impl AlbumDetails { 73 124 pub fn load_album(&self, id: &str) { 74 125 let id = id.to_string(); 126 + self.album_id.replace(id.clone()); 75 127 let handle = thread::spawn(move || { 76 128 let rt = tokio::runtime::Runtime::new().unwrap(); 77 129 rt.block_on(async { ··· 162 214 impl AlbumDetails { 163 215 pub fn new() -> Self { 164 216 glib::Object::new() 217 + } 218 + 219 + pub fn play_album(&self, shuffle: bool) { 220 + let album_id = self.imp().album_id.borrow(); 221 + let album_id = album_id.clone(); 222 + thread::spawn(move || { 223 + let rt = tokio::runtime::Runtime::new().unwrap(); 224 + let url = build_url(); 225 + let _ = rt.block_on(async { 226 + let mut client = PlaybackServiceClient::connect(url).await?; 227 + client 228 + .play_album(PlayAlbumRequest { 229 + album_id, 230 + shuffle: Some(shuffle), 231 + ..Default::default() 232 + }) 233 + .await?; 234 + Ok::<(), Error>(()) 235 + }); 236 + }); 237 + } 238 + 239 + pub fn play_next(&self) { 240 + let album_id = self.imp().album_id.borrow(); 241 + let album_id = album_id.clone(); 242 + thread::spawn(move || { 243 + let rt = tokio::runtime::Runtime::new().unwrap(); 244 + let url = build_url(); 245 + let _ = rt.block_on(async { 246 + let mut client = PlaylistServiceClient::connect(url).await?; 247 + client 248 + .insert_album(InsertAlbumRequest { 249 + album_id, 250 + position: PLAYLIST_INSERT_FIRST, 251 + ..Default::default() 252 + }) 253 + .await?; 254 + Ok::<(), Error>(()) 255 + }); 256 + }); 257 + } 258 + 259 + pub fn add_shuffled(&self) { 260 + let album_id = self.imp().album_id.borrow(); 261 + let album_id = album_id.clone(); 262 + thread::spawn(move || { 263 + let rt = tokio::runtime::Runtime::new().unwrap(); 264 + let url = build_url(); 265 + let _ = rt.block_on(async { 266 + let mut client = PlaylistServiceClient::connect(url).await?; 267 + client 268 + .insert_album(InsertAlbumRequest { 269 + album_id, 270 + position: PLAYLIST_INSERT_SHUFFLED, 271 + ..Default::default() 272 + }) 273 + .await?; 274 + Ok::<(), Error>(()) 275 + }); 276 + }); 277 + } 278 + 279 + pub fn play_last_shuffled(&self) { 280 + let album_id = self.imp().album_id.borrow(); 281 + let album_id = album_id.clone(); 282 + thread::spawn(move || { 283 + let rt = tokio::runtime::Runtime::new().unwrap(); 284 + let url = build_url(); 285 + let _ = rt.block_on(async { 286 + let mut client = PlaylistServiceClient::connect(url).await?; 287 + client 288 + .insert_album(InsertAlbumRequest { 289 + album_id, 290 + position: PLAYLIST_INSERT_LAST_SHUFFLED, 291 + ..Default::default() 292 + }) 293 + .await?; 294 + Ok::<(), Error>(()) 295 + }); 296 + }); 297 + } 298 + 299 + pub fn play_last(&self) { 300 + let album_id = self.imp().album_id.borrow(); 301 + let album_id = album_id.clone(); 302 + thread::spawn(move || { 303 + let rt = tokio::runtime::Runtime::new().unwrap(); 304 + let url = build_url(); 305 + let _ = rt.block_on(async { 306 + let mut client = PlaylistServiceClient::connect(url).await?; 307 + client 308 + .insert_album(InsertAlbumRequest { 309 + album_id, 310 + position: PLAYLIST_INSERT_LAST, 311 + ..Default::default() 312 + }) 313 + .await?; 314 + Ok::<(), Error>(()) 315 + }); 316 + }); 165 317 } 166 318 }
+152 -6
gtk/src/ui/pages/artist_details.rs
··· 1 1 use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 2 - use crate::api::rockbox::v1alpha1::{GetArtistRequest, GetArtistResponse}; 2 + use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 3 + use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient; 4 + use crate::api::rockbox::v1alpha1::{ 5 + GetArtistRequest, GetArtistResponse, InsertTracksRequest, PlayArtistTracksRequest, 6 + }; 7 + use crate::constants::*; 3 8 use crate::state::AppState; 4 9 use crate::time::format_milliseconds; 5 10 use crate::ui::pages::album_details::AlbumDetails; ··· 12 17 use gtk::pango::EllipsizeMode; 13 18 use gtk::{CompositeTemplate, FlowBox, Image, Label, ListBox, Orientation}; 14 19 use std::cell::RefCell; 15 - use std::env; 20 + use std::{env, thread}; 16 21 17 22 mod imp { 18 23 ··· 36 41 pub album_details: RefCell<Option<AlbumDetails>>, 37 42 pub library_page: RefCell<Option<adw::NavigationPage>>, 38 43 pub state: glib::WeakRef<AppState>, 44 + pub artist_id: RefCell<String>, 39 45 } 40 46 41 47 #[glib::object_subclass] ··· 48 54 Self::bind_template(klass); 49 55 50 56 klass.install_action( 51 - "app.play-artist-tracks", 57 + "app.artist.play", 58 + None, 59 + move |artist_details, _action, _target| { 60 + artist_details.play(false); 61 + }, 62 + ); 63 + 64 + klass.install_action( 65 + "app.artist.shuffle", 66 + None, 67 + move |artist_details, _action, _target| { 68 + artist_details.play(true); 69 + }, 70 + ); 71 + 72 + klass.install_action( 73 + "app.artist.play-next", 74 + None, 75 + move |artist_details, _action, _target| { 76 + artist_details.play_next(); 77 + }, 78 + ); 79 + 80 + klass.install_action( 81 + "app.artist.play-last", 52 82 None, 53 - move |_artist_details, _action, _target| {}, 83 + move |artist_details, _action, _target| { 84 + artist_details.play_last(); 85 + }, 54 86 ); 55 87 56 88 klass.install_action( 57 - "app.shuffle-artist-tracks", 89 + "app.artist.add-shuffled", 58 90 None, 59 - move |_artist_details, _action, _target| {}, 91 + move |artist_details, _action, _target| { 92 + artist_details.add_shuffled(); 93 + }, 94 + ); 95 + 96 + klass.install_action( 97 + "app.artist.play-last-shuffled", 98 + None, 99 + move |artist_details, _action, _target| { 100 + artist_details.play_last_shuffled(); 101 + }, 60 102 ); 61 103 } 62 104 ··· 90 132 pub fn load_artist(&self, id: &str) { 91 133 let id = id.to_string(); 92 134 let rt = tokio::runtime::Runtime::new().unwrap(); 135 + self.artist_id.replace(id.clone()); 93 136 let response_ = rt.block_on(async { 94 137 let url = build_url(); 95 138 let mut client = LibraryServiceClient::connect(url).await?; ··· 271 314 let state = self.imp().state.upgrade().unwrap(); 272 315 state.push_navigation("Album", "album-details-page"); 273 316 } 317 + } 318 + 319 + pub fn play(&self, shuffle: bool) { 320 + let artist_id = self.imp().artist_id.borrow(); 321 + let artist_id = artist_id.clone(); 322 + thread::spawn(move || { 323 + let rt = tokio::runtime::Runtime::new().unwrap(); 324 + let url = build_url(); 325 + let _ = rt.block_on(async { 326 + let mut client = PlaybackServiceClient::connect(url).await?; 327 + client 328 + .play_artist_tracks(PlayArtistTracksRequest { 329 + artist_id, 330 + shuffle: Some(shuffle), 331 + ..Default::default() 332 + }) 333 + .await?; 334 + Ok::<(), Error>(()) 335 + }); 336 + }); 337 + } 338 + 339 + pub fn play_next(&self) { 340 + let artist_id = self.imp().artist_id.borrow(); 341 + let artist_id = artist_id.clone(); 342 + thread::spawn(move || { 343 + let rt = tokio::runtime::Runtime::new().unwrap(); 344 + let url = build_url(); 345 + let _ = rt.block_on(async { 346 + let mut client = PlaylistServiceClient::connect(url).await?; 347 + // TODO: call the correct rpc method 348 + client 349 + .insert_tracks(InsertTracksRequest { 350 + tracks: vec![], 351 + position: PLAYLIST_INSERT_FIRST, 352 + ..Default::default() 353 + }) 354 + .await?; 355 + Ok::<(), Error>(()) 356 + }); 357 + }); 358 + } 359 + 360 + pub fn play_last(&self) { 361 + let artist_id = self.imp().artist_id.borrow(); 362 + let artist_id = artist_id.clone(); 363 + thread::spawn(move || { 364 + let rt = tokio::runtime::Runtime::new().unwrap(); 365 + let url = build_url(); 366 + let _ = rt.block_on(async { 367 + let mut client = PlaylistServiceClient::connect(url).await?; 368 + // TODO: call the correct rpc method 369 + client 370 + .insert_tracks(InsertTracksRequest { 371 + tracks: vec![], 372 + position: PLAYLIST_INSERT_LAST, 373 + ..Default::default() 374 + }) 375 + .await?; 376 + Ok::<(), Error>(()) 377 + }); 378 + }); 379 + } 380 + 381 + pub fn add_shuffled(&self) { 382 + let artist_id = self.imp().artist_id.borrow(); 383 + let artist_id = artist_id.clone(); 384 + thread::spawn(move || { 385 + let rt = tokio::runtime::Runtime::new().unwrap(); 386 + let url = build_url(); 387 + let _ = rt.block_on(async { 388 + let mut client = PlaylistServiceClient::connect(url).await?; 389 + // TODO: call the correct rpc method 390 + client 391 + .insert_tracks(InsertTracksRequest { 392 + tracks: vec![], 393 + position: PLAYLIST_INSERT_SHUFFLED, 394 + ..Default::default() 395 + }) 396 + .await?; 397 + Ok::<(), Error>(()) 398 + }); 399 + }); 400 + } 401 + 402 + pub fn play_last_shuffled(&self) { 403 + let artist_id = self.imp().artist_id.borrow(); 404 + let artist_id = artist_id.clone(); 405 + thread::spawn(move || { 406 + let rt = tokio::runtime::Runtime::new().unwrap(); 407 + let url = build_url(); 408 + let _ = rt.block_on(async { 409 + let mut client = PlaylistServiceClient::connect(url).await?; 410 + client 411 + .insert_tracks(InsertTracksRequest { 412 + tracks: vec![], 413 + position: PLAYLIST_INSERT_LAST_SHUFFLED, 414 + ..Default::default() 415 + }) 416 + .await?; 417 + Ok::<(), Error>(()) 418 + }); 419 + }); 274 420 } 275 421 } 276 422
+12
gtk/src/ui/pages/files.rs
··· 134 134 file.imp().state.set(Some(&state)); 135 135 file.imp().set_path(entry.name.clone()); 136 136 file.imp().set_is_dir(entry.attr == 16); 137 + 138 + match entry.attr == 16 { 139 + true => { 140 + file.imp().file_menu.set_visible(false); 141 + file.imp().directory_menu.set_visible(true); 142 + } 143 + false => { 144 + file.imp().file_menu.set_visible(true); 145 + file.imp().directory_menu.set_visible(false); 146 + } 147 + } 148 + 137 149 self.imp().files.append(&file); 138 150 } 139 151 }
+88 -4
gtk/src/ui/song.rs
··· 1 1 use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 2 - use crate::api::rockbox::v1alpha1::{LikeTrackRequest, Track, UnlikeTrackRequest}; 2 + use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient; 3 + use crate::api::rockbox::v1alpha1::{ 4 + InsertTracksRequest, LikeTrackRequest, Track, UnlikeTrackRequest, 5 + }; 6 + use crate::constants::*; 3 7 use crate::state::AppState; 4 8 use adw::subclass::prelude::*; 5 9 use anyhow::Error; 6 10 use glib::subclass; 7 11 use gtk::glib; 8 - use gtk::{Button, CompositeTemplate, Image, Label}; 12 + use gtk::{Button, CompositeTemplate, Image, Label, MenuButton}; 9 13 use std::cell::RefCell; 10 14 use std::env; 11 15 use std::thread; ··· 34 38 #[template_child] 35 39 pub heart_icon: TemplateChild<Image>, 36 40 #[template_child] 37 - pub more_button: TemplateChild<Button>, 41 + pub more_button: TemplateChild<MenuButton>, 38 42 39 43 pub track: RefCell<Option<Track>>, 40 44 pub state: glib::WeakRef<AppState>, ··· 52 56 klass.install_action("app.like-song", None, move |song, _action, _target| { 53 57 song.like(); 54 58 }); 59 + 60 + klass.install_action("app.play-next", None, move |song, _action, _target| { 61 + song.play_next(); 62 + }); 63 + 64 + klass.install_action("app.play-last", None, move |song, _action, _target| { 65 + song.play_last(); 66 + }); 67 + 68 + klass.install_action("app.add-shuffled", None, move |song, _action, _target| { 69 + song.add_shuffled(); 70 + }); 55 71 } 56 72 57 73 fn instance_init(obj: &subclass::InitializingObject<Self>) { ··· 101 117 thread::spawn(move || { 102 118 let rt = tokio::runtime::Runtime::new().unwrap(); 103 119 let url = build_url(); 104 - let _ = rt.block_on(async { 120 + let result = rt.block_on(async { 105 121 let mut client = LibraryServiceClient::connect(url).await.unwrap(); 106 122 match is_liked { 107 123 true => { ··· 120 136 } 121 137 } 122 138 139 + Ok::<(), Error>(()) 140 + }); 141 + 142 + match result { 143 + Ok(_) => {} 144 + Err(e) => eprintln!("Error liking track: {:?}", e), 145 + } 146 + }); 147 + } 148 + 149 + pub fn play_next(&self) { 150 + let track = self.imp().track.borrow(); 151 + let track = track.as_ref().unwrap(); 152 + let track = track.clone(); 153 + thread::spawn(move || { 154 + let rt = tokio::runtime::Runtime::new().unwrap(); 155 + let url = build_url(); 156 + let _ = rt.block_on(async { 157 + let mut client = PlaylistServiceClient::connect(url).await?; 158 + client 159 + .insert_tracks(InsertTracksRequest { 160 + tracks: vec![track.path.clone()], 161 + position: PLAYLIST_INSERT_FIRST, 162 + ..Default::default() 163 + }) 164 + .await?; 165 + Ok::<(), Error>(()) 166 + }); 167 + }); 168 + } 169 + 170 + pub fn play_last(&self) { 171 + let track = self.imp().track.borrow(); 172 + let track = track.as_ref().unwrap(); 173 + let track = track.clone(); 174 + thread::spawn(move || { 175 + let rt = tokio::runtime::Runtime::new().unwrap(); 176 + let url = build_url(); 177 + let _ = rt.block_on(async { 178 + let mut client = PlaylistServiceClient::connect(url).await?; 179 + client 180 + .insert_tracks(InsertTracksRequest { 181 + tracks: vec![track.path.clone()], 182 + position: PLAYLIST_INSERT_LAST, 183 + ..Default::default() 184 + }) 185 + .await?; 186 + Ok::<(), Error>(()) 187 + }); 188 + }); 189 + } 190 + 191 + pub fn add_shuffled(&self) { 192 + let track = self.imp().track.borrow(); 193 + let track = track.as_ref().unwrap(); 194 + let track = track.clone(); 195 + thread::spawn(move || { 196 + let rt = tokio::runtime::Runtime::new().unwrap(); 197 + let url = build_url(); 198 + let _ = rt.block_on(async { 199 + let mut client = PlaylistServiceClient::connect(url).await?; 200 + client 201 + .insert_tracks(InsertTracksRequest { 202 + tracks: vec![track.path.clone()], 203 + position: PLAYLIST_INSERT_SHUFFLED, 204 + ..Default::default() 205 + }) 206 + .await?; 123 207 Ok::<(), Error>(()) 124 208 }); 125 209 });