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 2 3 3 template $Song : Box { 4 4 orientation: horizontal; 5 - spacing: 10; 6 5 halign: fill; 7 - valign: center; 8 6 hexpand: true; 9 - margin-bottom: 10; 10 - margin-top: 10; 11 - margin-start: 10; 12 - margin-end: 10; 13 7 14 - Label track_number { 15 - label: ""; 16 - halign: start; 8 + Box container { 9 + orientation: horizontal; 10 + spacing: 10; 11 + halign: fill; 17 12 valign: center; 13 + hexpand: true; 14 + margin-bottom: 10; 15 + margin-top: 10; 16 + margin-start: 10; 18 17 margin-end: 10; 19 18 20 - styles [ 21 - "track-number" 22 - ] 23 - } 19 + Label track_number { 20 + label: ""; 21 + halign: start; 22 + valign: center; 23 + margin-end: 10; 24 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"; 25 + styles [ 26 + "track-number" 27 + ] 33 28 } 34 29 35 - styles [ 36 - "media-album-art" 37 - ] 38 - } 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 39 40 - Box { 41 - hexpand: true; 42 - valign: center; 43 - halign: fill; 44 - orientation: vertical; 40 + styles [ 41 + "media-album-art" 42 + ] 43 + } 45 44 46 - Label track_title { 47 - label: ""; 48 - halign: start; 49 - valign: center; 45 + Box { 50 46 hexpand: true; 51 - margin-bottom: 3; 47 + valign: center; 48 + halign: fill; 49 + orientation: vertical; 52 50 53 - styles [ 54 - "track-title" 55 - ] 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 + } 56 73 } 57 74 58 - Label artist { 75 + Label track_duration { 59 76 label: ""; 60 - halign: start; 77 + halign: end; 61 78 valign: center; 62 - hexpand: true; 63 79 64 80 styles [ 65 - "track-artist" 81 + "track-duration" 66 82 ] 67 83 } 68 - } 69 - 70 - Label track_duration { 71 - label: ""; 72 - halign: end; 73 - valign: center; 74 - 75 - styles [ 76 - "track-duration" 77 - ] 78 - } 79 84 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; 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; 95 100 96 - styles [ 97 - "transparent-button" 98 - ] 99 - } 101 + styles [ 102 + "transparent-button" 103 + ] 104 + } 100 105 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 - }; 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 + }; 108 113 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; 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; 117 122 118 - styles [ 119 - "transparent-button" 120 - ] 123 + styles [ 124 + "transparent-button" 125 + ] 126 + } 121 127 } 122 - } 123 128 124 129 125 - Button remove_button { 130 + Button remove_button { 126 131 tooltip-text: _("Remove"); 127 132 visible: false; 128 133 ··· 138 143 styles [ 139 144 "transparent-button" 140 145 ] 146 + } 141 147 } 148 + 142 149 143 150 menu context_menu { 144 151 section { 152 + item (_("Play"), "app.play-song") 145 153 item (_("Play Next"), "app.play-next") 146 154 item (_("Play Last"), "app.play-last") 147 155 item (_("Add Shuffled"), "app.add-shuffled")
+12
gtk/src/ui/file.rs
··· 120 120 }); 121 121 122 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 + }); 123 135 } 124 136 } 125 137
+3 -3
gtk/src/ui/pages/artist_details.rs
··· 351 351 if let Some(main_stack) = self.imp().main_stack.borrow().as_ref() { 352 352 main_stack.set_visible_child_name("artist-tracks-page"); 353 353 354 - let title = artist_name.clone(); 354 + let title = artist_name; 355 355 356 356 if let Some(library_page) = self.imp().library_page.borrow().as_ref() { 357 - library_page.set_title(&title); 357 + library_page.set_title(title); 358 358 } 359 359 let state = self.imp().state.upgrade().unwrap(); 360 - state.push_navigation(&title, "artist-tracks-page"); 360 + state.push_navigation(title, "artist-tracks-page"); 361 361 let artist_tracks = self.imp().artist_tracks.borrow(); 362 362 let artist_tracks_ref = artist_tracks.as_ref(); 363 363 let artist_tracks_ref = artist_tracks_ref.unwrap();
+46 -27
gtk/src/ui/pages/current_playlist.rs
··· 10 10 use anyhow::Error; 11 11 use glib::subclass; 12 12 use gtk::glib; 13 - use gtk::pango::EllipsizeMode; 13 + use gtk::pango::{EllipsizeMode, WrapMode}; 14 14 use gtk::{CompositeTemplate, Image, Label, ListBox, ScrolledWindow}; 15 15 use std::cell::{Cell, RefCell}; 16 16 use std::env; ··· 92 92 } 93 93 94 94 self_.size.set(size + next_tracks.len()); 95 - 95 + let mut i = index + size; 96 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 - }); 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 + ); 103 106 104 107 self_.next_tracks.append(&song); 108 + i += 1; 105 109 } 106 110 } 107 111 }); ··· 159 163 obj.imp().now_playing.append(&label); 160 164 161 165 if let Some(track) = state.current_track() { 162 - let song = create_song_widget(track); 166 + let song = create_song_widget(track, playlist.index as i32); 163 167 obj.imp().now_playing.append(&song); 164 168 } 165 169 ··· 171 175 true => 10, 172 176 false => next_tracks.len(), 173 177 }; 178 + let mut i = index; 174 179 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 - }); 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 + ); 181 189 182 190 song.imp().album_art_container.set_visible(true); 183 191 obj.imp().next_tracks.append(&song); 192 + i += 1; 184 193 } 185 194 } 186 195 }); ··· 218 227 match state.current_track() { 219 228 Some(track) => { 220 229 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); 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); 223 233 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); 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); 226 237 self.imp().track_index.set_text(&format!( 227 238 "{} of {}", 228 239 self.imp().current_index.get() + 1, ··· 270 281 let state = self.imp().state.upgrade().unwrap(); 271 282 272 283 if let Some(track) = state.current_track() { 273 - let song = create_song_widget(track); 284 + let song = create_song_widget(track, index as i32 - 1); 274 285 self.imp().now_playing.append(&song); 275 286 } 276 287 ··· 283 294 false => next_tracks.len(), 284 295 }; 285 296 297 + let mut i = index; 286 298 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 - }); 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 + ); 293 308 self.imp().next_tracks.append(&song); 309 + i += 1; 294 310 } 295 311 } 296 312 ··· 315 331 } 316 332 } 317 333 318 - fn create_song_widget(track: Track) -> Song { 334 + fn create_song_widget(track: Track, index: i32) -> Song { 319 335 let song = Song::new(); 320 336 song.imp().track_number.set_visible(false); 321 337 song.imp().track_title.set_text(&track.title); ··· 327 343 song.imp().track_duration.set_visible(false); 328 344 song.imp().heart_button.set_visible(false); 329 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())); 330 349 331 350 match track.album_art { 332 351 Some(filename) => {
+67 -2
gtk/src/ui/song.rs
··· 1 1 use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 2 + use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 2 3 use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient; 3 4 use crate::api::rockbox::v1alpha1::{ 4 - InsertTracksRequest, LikeTrackRequest, Track, UnlikeTrackRequest, 5 + InsertTracksRequest, LikeTrackRequest, PlayTrackRequest, StartRequest, Track, 6 + UnlikeTrackRequest, 5 7 }; 6 8 use crate::constants::*; 7 9 use crate::state::AppState; ··· 9 11 use anyhow::Error; 10 12 use glib::subclass; 11 13 use gtk::glib; 14 + use gtk::prelude::WidgetExt; 12 15 use gtk::{Button, CompositeTemplate, Image, Label, MenuButton}; 13 - use std::cell::RefCell; 16 + use std::cell::{Cell, RefCell}; 14 17 use std::env; 15 18 use std::thread; 16 19 ··· 21 24 #[derive(Debug, Default, CompositeTemplate)] 22 25 #[template(file = "./gtk/song.ui")] 23 26 pub struct Song { 27 + #[template_child] 28 + pub container: TemplateChild<gtk::Box>, 24 29 #[template_child] 25 30 pub album_art_container: TemplateChild<gtk::Box>, 26 31 #[template_child] ··· 42 47 43 48 pub track: RefCell<Option<Track>>, 44 49 pub state: glib::WeakRef<AppState>, 50 + pub index: Cell<i32>, 51 + pub is_playlist: Cell<bool>, 45 52 } 46 53 47 54 #[glib::object_subclass] ··· 52 59 53 60 fn class_init(klass: &mut Self::Class) { 54 61 Self::bind_template(klass); 62 + 63 + klass.install_action("app.play-song", None, move |song, _action, _target| { 64 + song.play_song(); 65 + }); 55 66 56 67 klass.install_action("app.like-song", None, move |song, _action, _target| { 57 68 song.like(); ··· 78 89 impl ObjectImpl for Song { 79 90 fn constructed(&self) { 80 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); 81 110 } 82 111 } 83 112 ··· 204 233 ..Default::default() 205 234 }) 206 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 + } 207 272 Ok::<(), Error>(()) 208 273 }); 209 274 });