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

gtk: make tracks in play queue clickable

+228 -124
+100 -92
gtk/data/gtk/song.blp
··· 2 3 template $Song : Box { 4 orientation: horizontal; 5 - spacing: 10; 6 halign: fill; 7 - valign: center; 8 hexpand: true; 9 - margin-bottom: 10; 10 - margin-top: 10; 11 - margin-start: 10; 12 - margin-end: 10; 13 14 - Label track_number { 15 - label: ""; 16 - halign: start; 17 valign: center; 18 margin-end: 10; 19 20 - styles [ 21 - "track-number" 22 - ] 23 - } 24 25 - Box album_art_container { 26 - margin-end: 10; 27 - visible: false; 28 - 29 - Image album_art { 30 - width-request: 50; 31 - height-request: 50; 32 - resource: "/mg/tsirysndr/Rockbox/icons/jpg/albumart.jpg"; 33 } 34 35 - styles [ 36 - "media-album-art" 37 - ] 38 - } 39 40 - Box { 41 - hexpand: true; 42 - valign: center; 43 - halign: fill; 44 - orientation: vertical; 45 46 - Label track_title { 47 - label: ""; 48 - halign: start; 49 - valign: center; 50 hexpand: true; 51 - margin-bottom: 3; 52 53 - styles [ 54 - "track-title" 55 - ] 56 } 57 58 - Label artist { 59 label: ""; 60 - halign: start; 61 valign: center; 62 - hexpand: true; 63 64 styles [ 65 - "track-artist" 66 ] 67 } 68 - } 69 - 70 - Label track_duration { 71 - label: ""; 72 - halign: end; 73 - valign: center; 74 - 75 - styles [ 76 - "track-duration" 77 - ] 78 - } 79 80 - Button heart_button { 81 - tooltip-text: _("Like"); 82 - child: Image heart_icon { 83 - icon-name: "heart-outline-symbolic"; 84 - pixel-size: 24; 85 - }; 86 - action-name: "app.like-song"; 87 - halign: center; 88 - valign: center; 89 - margin-end: 0; 90 - margin-start: 0; 91 - margin-top: 0; 92 - margin-bottom: 0; 93 - width-request: 40; 94 - height-request: 40; 95 96 - styles [ 97 - "transparent-button" 98 - ] 99 - } 100 101 - MenuButton more_button { 102 - tooltip-text: _("More"); 103 - menu-model: context_menu; 104 - child: Image { 105 - icon-name: "options-symbolic"; 106 - pixel-size: 24; 107 - }; 108 109 - halign: center; 110 - valign: center; 111 - margin-end: 0; 112 - margin-start: 0; 113 - margin-top: 0; 114 - margin-bottom: 0; 115 - width-request: 40; 116 - height-request: 40; 117 118 - styles [ 119 - "transparent-button" 120 - ] 121 } 122 - } 123 124 125 - Button remove_button { 126 tooltip-text: _("Remove"); 127 visible: false; 128 ··· 138 styles [ 139 "transparent-button" 140 ] 141 } 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")
··· 2 3 template $Song : Box { 4 orientation: horizontal; 5 halign: fill; 6 hexpand: true; 7 8 + Box container { 9 + orientation: horizontal; 10 + spacing: 10; 11 + halign: fill; 12 valign: center; 13 + hexpand: true; 14 + margin-bottom: 10; 15 + margin-top: 10; 16 + margin-start: 10; 17 margin-end: 10; 18 19 + Label track_number { 20 + label: ""; 21 + halign: start; 22 + valign: center; 23 + margin-end: 10; 24 25 + styles [ 26 + "track-number" 27 + ] 28 } 29 30 + Box album_art_container { 31 + margin-end: 10; 32 + visible: false; 33 + 34 + Image album_art { 35 + width-request: 50; 36 + height-request: 50; 37 + resource: "/mg/tsirysndr/Rockbox/icons/jpg/albumart.jpg"; 38 + } 39 40 + styles [ 41 + "media-album-art" 42 + ] 43 + } 44 45 + Box { 46 hexpand: true; 47 + valign: center; 48 + halign: fill; 49 + orientation: vertical; 50 51 + Label track_title { 52 + label: ""; 53 + halign: start; 54 + valign: center; 55 + hexpand: true; 56 + margin-bottom: 3; 57 + 58 + styles [ 59 + "track-title" 60 + ] 61 + } 62 + 63 + Label artist { 64 + label: ""; 65 + halign: start; 66 + valign: center; 67 + hexpand: true; 68 + 69 + styles [ 70 + "track-artist" 71 + ] 72 + } 73 } 74 75 + Label track_duration { 76 label: ""; 77 + halign: end; 78 valign: center; 79 80 styles [ 81 + "track-duration" 82 ] 83 } 84 85 + Button heart_button { 86 + tooltip-text: _("Like"); 87 + child: Image heart_icon { 88 + icon-name: "heart-outline-symbolic"; 89 + pixel-size: 24; 90 + }; 91 + action-name: "app.like-song"; 92 + halign: center; 93 + valign: center; 94 + margin-end: 0; 95 + margin-start: 0; 96 + margin-top: 0; 97 + margin-bottom: 0; 98 + width-request: 40; 99 + height-request: 40; 100 101 + styles [ 102 + "transparent-button" 103 + ] 104 + } 105 106 + MenuButton more_button { 107 + tooltip-text: _("More"); 108 + menu-model: context_menu; 109 + child: Image { 110 + icon-name: "options-symbolic"; 111 + pixel-size: 24; 112 + }; 113 114 + halign: center; 115 + valign: center; 116 + margin-end: 0; 117 + margin-start: 0; 118 + margin-top: 0; 119 + margin-bottom: 0; 120 + width-request: 40; 121 + height-request: 40; 122 123 + styles [ 124 + "transparent-button" 125 + ] 126 + } 127 } 128 129 130 + Button remove_button { 131 tooltip-text: _("Remove"); 132 visible: false; 133 ··· 143 styles [ 144 "transparent-button" 145 ] 146 + } 147 } 148 + 149 150 menu context_menu { 151 section { 152 + item (_("Play"), "app.play-song") 153 item (_("Play Next"), "app.play-next") 154 item (_("Play Last"), "app.play-last") 155 item (_("Add Shuffled"), "app.add-shuffled")
+12
gtk/src/ui/file.rs
··· 120 }); 121 122 self.row.add_controller(click); 123 } 124 } 125
··· 120 }); 121 122 self.row.add_controller(click); 123 + 124 + let self_weak = self.downgrade(); 125 + let gesture = gtk::GestureClick::new(); 126 + let is_dir = self.is_dir.get(); 127 + gesture.connect_pressed(move |gestrure, n_press, _, _| { 128 + if n_press == 2 && !is_dir { 129 + if let Some(self_) = self_weak.upgrade() { 130 + let obj = self_.obj(); 131 + obj.play(false); 132 + } 133 + } 134 + }); 135 } 136 } 137
+3 -3
gtk/src/ui/pages/artist_details.rs
··· 351 if let Some(main_stack) = self.imp().main_stack.borrow().as_ref() { 352 main_stack.set_visible_child_name("artist-tracks-page"); 353 354 - let title = artist_name.clone(); 355 356 if let Some(library_page) = self.imp().library_page.borrow().as_ref() { 357 - library_page.set_title(&title); 358 } 359 let state = self.imp().state.upgrade().unwrap(); 360 - state.push_navigation(&title, "artist-tracks-page"); 361 let artist_tracks = self.imp().artist_tracks.borrow(); 362 let artist_tracks_ref = artist_tracks.as_ref(); 363 let artist_tracks_ref = artist_tracks_ref.unwrap();
··· 351 if let Some(main_stack) = self.imp().main_stack.borrow().as_ref() { 352 main_stack.set_visible_child_name("artist-tracks-page"); 353 354 + let title = artist_name; 355 356 if let Some(library_page) = self.imp().library_page.borrow().as_ref() { 357 + library_page.set_title(title); 358 } 359 let state = self.imp().state.upgrade().unwrap(); 360 + state.push_navigation(title, "artist-tracks-page"); 361 let artist_tracks = self.imp().artist_tracks.borrow(); 362 let artist_tracks_ref = artist_tracks.as_ref(); 363 let artist_tracks_ref = artist_tracks_ref.unwrap();
+46 -27
gtk/src/ui/pages/current_playlist.rs
··· 10 use anyhow::Error; 11 use glib::subclass; 12 use gtk::glib; 13 - use gtk::pango::EllipsizeMode; 14 use gtk::{CompositeTemplate, Image, Label, ListBox, ScrolledWindow}; 15 use std::cell::{Cell, RefCell}; 16 use std::env; ··· 92 } 93 94 self_.size.set(size + next_tracks.len()); 95 - 96 for track in next_tracks { 97 - let song = create_song_widget(Track { 98 - title: track.title.clone(), 99 - artist: track.artist.clone(), 100 - album_art: track.album_art.clone(), 101 - ..Default::default() 102 - }); 103 104 self_.next_tracks.append(&song); 105 } 106 } 107 }); ··· 159 obj.imp().now_playing.append(&label); 160 161 if let Some(track) = state.current_track() { 162 - let song = create_song_widget(track); 163 obj.imp().now_playing.append(&song); 164 } 165 ··· 171 true => 10, 172 false => next_tracks.len(), 173 }; 174 for track in next_tracks.into_iter().take(limit) { 175 - let song = create_song_widget(Track { 176 - title: track.title.clone(), 177 - artist: track.artist.clone(), 178 - album_art: track.album_art.clone(), 179 - ..Default::default() 180 - }); 181 182 song.imp().album_art_container.set_visible(true); 183 obj.imp().next_tracks.append(&song); 184 } 185 } 186 }); ··· 218 match state.current_track() { 219 Some(track) => { 220 self.imp().track_title.set_text(&track.title); 221 - self.imp().track_title.set_ellipsize(EllipsizeMode::End); 222 - self.imp().track_title.set_max_width_chars(80); 223 self.imp().track_artist.set_text(&track.artist); 224 - self.imp().track_artist.set_ellipsize(EllipsizeMode::End); 225 - self.imp().track_artist.set_max_width_chars(80); 226 self.imp().track_index.set_text(&format!( 227 "{} of {}", 228 self.imp().current_index.get() + 1, ··· 270 let state = self.imp().state.upgrade().unwrap(); 271 272 if let Some(track) = state.current_track() { 273 - let song = create_song_widget(track); 274 self.imp().now_playing.append(&song); 275 } 276 ··· 283 false => next_tracks.len(), 284 }; 285 286 for track in next_tracks.into_iter().take(limit) { 287 - let song = create_song_widget(Track { 288 - title: track.title.clone(), 289 - artist: track.artist.clone(), 290 - album_art: track.album_art.clone(), 291 - ..Default::default() 292 - }); 293 self.imp().next_tracks.append(&song); 294 } 295 } 296 ··· 315 } 316 } 317 318 - fn create_song_widget(track: Track) -> Song { 319 let song = Song::new(); 320 song.imp().track_number.set_visible(false); 321 song.imp().track_title.set_text(&track.title); ··· 327 song.imp().track_duration.set_visible(false); 328 song.imp().heart_button.set_visible(false); 329 song.imp().more_button.set_visible(false); 330 331 match track.album_art { 332 Some(filename) => {
··· 10 use anyhow::Error; 11 use glib::subclass; 12 use gtk::glib; 13 + use gtk::pango::{EllipsizeMode, WrapMode}; 14 use gtk::{CompositeTemplate, Image, Label, ListBox, ScrolledWindow}; 15 use std::cell::{Cell, RefCell}; 16 use std::env; ··· 92 } 93 94 self_.size.set(size + next_tracks.len()); 95 + let mut i = index + size; 96 for track in next_tracks { 97 + let song = create_song_widget( 98 + Track { 99 + title: track.title.clone(), 100 + artist: track.artist.clone(), 101 + album_art: track.album_art.clone(), 102 + ..Default::default() 103 + }, 104 + i as i32, 105 + ); 106 107 self_.next_tracks.append(&song); 108 + i += 1; 109 } 110 } 111 }); ··· 163 obj.imp().now_playing.append(&label); 164 165 if let Some(track) = state.current_track() { 166 + let song = create_song_widget(track, playlist.index as i32); 167 obj.imp().now_playing.append(&song); 168 } 169 ··· 175 true => 10, 176 false => next_tracks.len(), 177 }; 178 + let mut i = index; 179 for track in next_tracks.into_iter().take(limit) { 180 + let song = create_song_widget( 181 + Track { 182 + title: track.title.clone(), 183 + artist: track.artist.clone(), 184 + album_art: track.album_art.clone(), 185 + ..Default::default() 186 + }, 187 + i as i32, 188 + ); 189 190 song.imp().album_art_container.set_visible(true); 191 obj.imp().next_tracks.append(&song); 192 + i += 1; 193 } 194 } 195 }); ··· 227 match state.current_track() { 228 Some(track) => { 229 self.imp().track_title.set_text(&track.title); 230 + self.imp().track_title.set_wrap_mode(WrapMode::WordChar); 231 + self.imp().track_title.set_max_width_chars(20); 232 + self.imp().track_title.set_wrap(true); 233 self.imp().track_artist.set_text(&track.artist); 234 + self.imp().track_artist.set_wrap_mode(WrapMode::WordChar); 235 + self.imp().track_artist.set_max_width_chars(20); 236 + self.imp().track_artist.set_wrap(true); 237 self.imp().track_index.set_text(&format!( 238 "{} of {}", 239 self.imp().current_index.get() + 1, ··· 281 let state = self.imp().state.upgrade().unwrap(); 282 283 if let Some(track) = state.current_track() { 284 + let song = create_song_widget(track, index as i32 - 1); 285 self.imp().now_playing.append(&song); 286 } 287 ··· 294 false => next_tracks.len(), 295 }; 296 297 + let mut i = index; 298 for track in next_tracks.into_iter().take(limit) { 299 + let song = create_song_widget( 300 + Track { 301 + title: track.title.clone(), 302 + artist: track.artist.clone(), 303 + album_art: track.album_art.clone(), 304 + ..Default::default() 305 + }, 306 + i as i32, 307 + ); 308 self.imp().next_tracks.append(&song); 309 + i += 1; 310 } 311 } 312 ··· 331 } 332 } 333 334 + fn create_song_widget(track: Track, index: i32) -> Song { 335 let song = Song::new(); 336 song.imp().track_number.set_visible(false); 337 song.imp().track_title.set_text(&track.title); ··· 343 song.imp().track_duration.set_visible(false); 344 song.imp().heart_button.set_visible(false); 345 song.imp().more_button.set_visible(false); 346 + song.imp().index.set(index); 347 + song.imp().is_playlist.set(true); 348 + song.imp().track.replace(Some(track.clone().into())); 349 350 match track.album_art { 351 Some(filename) => {
+67 -2
gtk/src/ui/song.rs
··· 1 use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 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::*; 7 use crate::state::AppState; ··· 9 use anyhow::Error; 10 use glib::subclass; 11 use gtk::glib; 12 use gtk::{Button, CompositeTemplate, Image, Label, MenuButton}; 13 - use std::cell::RefCell; 14 use std::env; 15 use std::thread; 16 ··· 21 #[derive(Debug, Default, CompositeTemplate)] 22 #[template(file = "./gtk/song.ui")] 23 pub struct Song { 24 #[template_child] 25 pub album_art_container: TemplateChild<gtk::Box>, 26 #[template_child] ··· 42 43 pub track: RefCell<Option<Track>>, 44 pub state: glib::WeakRef<AppState>, 45 } 46 47 #[glib::object_subclass] ··· 52 53 fn class_init(klass: &mut Self::Class) { 54 Self::bind_template(klass); 55 56 klass.install_action("app.like-song", None, move |song, _action, _target| { 57 song.like(); ··· 78 impl ObjectImpl for Song { 79 fn constructed(&self) { 80 self.parent_constructed(); 81 } 82 } 83 ··· 204 ..Default::default() 205 }) 206 .await?; 207 Ok::<(), Error>(()) 208 }); 209 });
··· 1 use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 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 + InsertTracksRequest, LikeTrackRequest, PlayTrackRequest, StartRequest, Track, 6 + UnlikeTrackRequest, 7 }; 8 use crate::constants::*; 9 use crate::state::AppState; ··· 11 use anyhow::Error; 12 use glib::subclass; 13 use gtk::glib; 14 + use gtk::prelude::WidgetExt; 15 use gtk::{Button, CompositeTemplate, Image, Label, MenuButton}; 16 + use std::cell::{Cell, RefCell}; 17 use std::env; 18 use std::thread; 19 ··· 24 #[derive(Debug, Default, CompositeTemplate)] 25 #[template(file = "./gtk/song.ui")] 26 pub struct Song { 27 + #[template_child] 28 + pub container: TemplateChild<gtk::Box>, 29 #[template_child] 30 pub album_art_container: TemplateChild<gtk::Box>, 31 #[template_child] ··· 47 48 pub track: RefCell<Option<Track>>, 49 pub state: glib::WeakRef<AppState>, 50 + pub index: Cell<i32>, 51 + pub is_playlist: Cell<bool>, 52 } 53 54 #[glib::object_subclass] ··· 59 60 fn class_init(klass: &mut Self::Class) { 61 Self::bind_template(klass); 62 + 63 + klass.install_action("app.play-song", None, move |song, _action, _target| { 64 + song.play_song(); 65 + }); 66 67 klass.install_action("app.like-song", None, move |song, _action, _target| { 68 song.like(); ··· 89 impl ObjectImpl for Song { 90 fn constructed(&self) { 91 self.parent_constructed(); 92 + self.index.set(0); 93 + self.is_playlist.set(false); 94 + 95 + let container = self.container.get(); 96 + let self_weak = self.downgrade(); 97 + 98 + let gesture = gtk::GestureClick::new(); 99 + gesture.connect_pressed(move |_, n_press, _, _| { 100 + if n_press == 2 { 101 + let self_ = match self_weak.upgrade() { 102 + Some(self_) => self_, 103 + None => return, 104 + }; 105 + let obj = self_.obj(); 106 + obj.play_song(); 107 + } 108 + }); 109 + container.add_controller(gesture); 110 } 111 } 112 ··· 233 ..Default::default() 234 }) 235 .await?; 236 + Ok::<(), Error>(()) 237 + }); 238 + }); 239 + } 240 + 241 + pub fn play_song(&self) { 242 + let track = self.imp().track.borrow(); 243 + let track = track.as_ref().unwrap(); 244 + let track = track.clone(); 245 + let index = self.imp().index.get(); 246 + let is_playlist = self.imp().is_playlist.get(); 247 + thread::spawn(move || { 248 + let rt = tokio::runtime::Runtime::new().unwrap(); 249 + let _ = rt.block_on(async { 250 + let url = build_url(); 251 + let mut client = PlaybackServiceClient::connect(url).await?; 252 + 253 + match is_playlist { 254 + true => { 255 + let url = build_url(); 256 + let mut client = PlaylistServiceClient::connect(url).await?; 257 + client 258 + .start(StartRequest { 259 + start_index: Some(index), 260 + ..Default::default() 261 + }) 262 + .await?; 263 + } 264 + false => { 265 + client 266 + .play_track(PlayTrackRequest { 267 + path: track.path.clone(), 268 + }) 269 + .await?; 270 + } 271 + } 272 Ok::<(), Error>(()) 273 }); 274 });