A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 1015 lines 40 kB view raw
1use crate::api::rockbox::v1alpha1::library_service_client::LibraryServiceClient; 2use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 3use crate::api::rockbox::v1alpha1::system_service_client::SystemServiceClient; 4use crate::api::rockbox::v1alpha1::{ 5 GetGlobalStatusRequest, GetGlobalStatusResponse, PlayAllTracksRequest, PlayLikedTracksRequest, 6 ScanLibraryRequest, SearchRequest, SearchResponse, 7}; 8use crate::app::RbApplication; 9use crate::config; 10use crate::state::AppState; 11use crate::types::track::Track; 12use crate::ui::media_controls::MediaControls; 13use crate::ui::pages::album_details::AlbumDetails; 14use crate::ui::pages::albums::Albums; 15use crate::ui::pages::artist_details::ArtistDetails; 16use crate::ui::pages::current_playlist::CurrentPlaylist; 17use crate::ui::pages::search::Search; 18use crate::ui::pages::songs::Songs; 19use crate::ui::pages::{artists::Artists, files::Files, likes::Likes}; 20use crate::ui::{about_dialog, preferences_dialog}; 21use adw::prelude::*; 22use adw::subclass::prelude::*; 23use adw::{ 24 NavigationPage, NavigationView, OverlaySplitView, StatusPage, TabBar, TabView, ToastOverlay, 25 ViewStack, ViewStackPage, 26}; 27use anyhow::Error; 28use glib::subclass; 29use gtk::{ 30 gio, glib, Box, Button, CompositeTemplate, ListBox, MenuButton, Overlay, ScrolledWindow, 31 SearchBar, SearchEntry, ToggleButton, 32}; 33use preferences_dialog::RbPreferencesDialog; 34use std::cell::{Cell, RefCell}; 35use std::env; 36use std::thread; 37use tokio::sync::mpsc; 38 39mod imp { 40 use super::*; 41 42 #[derive(Debug, Default, CompositeTemplate)] 43 #[template(resource = "/io/github/tsirysndr/Rockbox/gtk/window.ui")] 44 pub struct RbApplicationWindow { 45 #[template_child] 46 pub show_sidebar_button: TemplateChild<Button>, 47 #[template_child] 48 pub primary_menu_button: TemplateChild<MenuButton>, 49 #[template_child] 50 pub go_back_button: TemplateChild<Button>, 51 #[template_child] 52 pub play_all_button: TemplateChild<Button>, 53 #[template_child] 54 pub shuffle_all_button: TemplateChild<Button>, 55 56 #[template_child] 57 pub search_bar: TemplateChild<SearchBar>, 58 #[template_child] 59 pub search_entry: TemplateChild<SearchEntry>, 60 #[template_child] 61 pub search_button: TemplateChild<ToggleButton>, 62 63 #[template_child] 64 pub overlay_split_view: TemplateChild<OverlaySplitView>, 65 #[template_child] 66 pub navigation_view: TemplateChild<NavigationView>, 67 #[template_child] 68 pub sidebar_navigation_page: TemplateChild<NavigationPage>, 69 #[template_child] 70 pub sidebar: TemplateChild<ListBox>, 71 #[template_child] 72 pub albums_row_box: TemplateChild<Box>, 73 #[template_child] 74 pub artists_row_box: TemplateChild<Box>, 75 #[template_child] 76 pub songs_row_box: TemplateChild<Box>, 77 #[template_child] 78 pub likes_row_box: TemplateChild<Box>, 79 #[template_child] 80 pub files_row_box: TemplateChild<Box>, 81 82 #[template_child] 83 pub toast_overlay: TemplateChild<ToastOverlay>, 84 #[template_child] 85 pub library_page: TemplateChild<NavigationPage>, 86 #[template_child] 87 pub main_stack: TemplateChild<ViewStack>, 88 #[template_child] 89 pub albums_page: TemplateChild<ViewStackPage>, 90 #[template_child] 91 pub albums_scrolled_window: TemplateChild<ScrolledWindow>, 92 #[template_child] 93 pub albums: TemplateChild<Albums>, 94 #[template_child] 95 pub songs_page: TemplateChild<ViewStackPage>, 96 #[template_child] 97 pub songs_scrolled_window: TemplateChild<ScrolledWindow>, 98 #[template_child] 99 pub songs: TemplateChild<Songs>, 100 #[template_child] 101 pub likes_page: TemplateChild<ViewStackPage>, 102 #[template_child] 103 pub likes_scrolled_window: TemplateChild<ScrolledWindow>, 104 #[template_child] 105 pub likes: TemplateChild<Likes>, 106 #[template_child] 107 pub files_page: TemplateChild<ViewStackPage>, 108 #[template_child] 109 pub files: TemplateChild<Files>, 110 #[template_child] 111 pub artists_page: TemplateChild<ViewStackPage>, 112 #[template_child] 113 pub artists_scrolled_window: TemplateChild<ScrolledWindow>, 114 #[template_child] 115 pub artists: TemplateChild<Artists>, 116 #[template_child] 117 pub artist_details_page: TemplateChild<ViewStackPage>, 118 #[template_child] 119 pub artist_details: TemplateChild<ArtistDetails>, 120 #[template_child] 121 pub artist_tracks_page: TemplateChild<ViewStackPage>, 122 #[template_child] 123 pub artist_tracks: TemplateChild<Songs>, 124 #[template_child] 125 pub artist_tracks_scrolled_window: TemplateChild<ScrolledWindow>, 126 #[template_child] 127 pub album_details_page: TemplateChild<ViewStackPage>, 128 #[template_child] 129 pub album_details: TemplateChild<AlbumDetails>, 130 #[template_child] 131 pub search_page: TemplateChild<ViewStackPage>, 132 #[template_child] 133 pub search: TemplateChild<Search>, 134 #[template_child] 135 pub current_playlist_page: TemplateChild<ViewStackPage>, 136 #[template_child] 137 pub current_playlist: TemplateChild<CurrentPlaylist>, 138 #[template_child] 139 pub library_overlay: TemplateChild<Overlay>, 140 #[template_child] 141 pub media_control_bar: TemplateChild<MediaControls>, 142 #[template_child] 143 pub notice_no_results: TemplateChild<StatusPage>, 144 #[template_child] 145 pub placeholder_page: TemplateChild<ViewStackPage>, 146 147 pub show_sidebar: Cell<bool>, 148 pub state: glib::WeakRef<AppState>, 149 pub current_track: RefCell<Option<Track>>, 150 pub show_placeholder: Cell<bool>, 151 } 152 153 #[glib::object_subclass] 154 impl ObjectSubclass for RbApplicationWindow { 155 const NAME: &'static str = "RbApplicationWindow"; 156 type ParentType = adw::ApplicationWindow; 157 type Type = super::RbApplicationWindow; 158 159 fn new() -> Self { 160 Self { 161 show_sidebar: Cell::new(true), 162 state: glib::WeakRef::new(), 163 show_placeholder: Cell::new(false), 164 ..Default::default() 165 } 166 } 167 168 fn class_init(klass: &mut Self::Class) { 169 Self::bind_template(klass); 170 171 klass.install_action("win.show_sidebar", None, move |win, _action, _parameter| { 172 let self_ = imp::RbApplicationWindow::from_obj(win); 173 self_.toggle_sidebar(); 174 }); 175 176 klass.install_action("win.go_back", None, move |win, _action, _parameter| { 177 let self_ = imp::RbApplicationWindow::from_obj(win); 178 self_.go_back(); 179 }); 180 181 klass.install_action("app.play_all", None, move |win, _action, _parameter| { 182 let self_ = imp::RbApplicationWindow::from_obj(win); 183 self_.play_all(); 184 }); 185 186 klass.install_action( 187 "app.refresh_library", 188 None, 189 move |win, _action, _parameter| { 190 let self_ = imp::RbApplicationWindow::from_obj(win); 191 self_.refresh_library(); 192 }, 193 ); 194 195 klass.install_action("app.shuffle_all", None, move |win, _action, _parameter| { 196 let self_ = imp::RbApplicationWindow::from_obj(win); 197 self_.shuffle_all(); 198 }); 199 200 klass.install_action("app.preferences", None, move |win, _action, _parameter| { 201 let preferences_window = RbPreferencesDialog::default(); 202 preferences_window.present(Some(win)); 203 }); 204 205 klass.install_action("app.about", None, move |win, _action, _parameter| { 206 about_dialog::show(win); 207 }); 208 209 klass.install_action("app.quit", None, move |win, _action, _parameter| { 210 win.close(); 211 }); 212 213 klass.install_action( 214 "win.toggle_search", 215 None, 216 move |win, _action, _parameter| { 217 let self_ = imp::RbApplicationWindow::from_obj(win); 218 self_.toggle_searchbar(); 219 }, 220 ); 221 222 klass.install_action("app.copy_command", None, move |win, _action, _parameter| { 223 const CMD: &str = "rockbox start"; 224 win.clipboard().set_text(CMD); 225 win.add_message_toast("Copied to clipboard"); 226 }); 227 } 228 229 fn instance_init(obj: &subclass::InitializingObject<Self>) { 230 obj.init_template(); 231 } 232 } 233 234 impl ObjectImpl for RbApplicationWindow { 235 fn constructed(&self) { 236 self.parent_constructed(); 237 238 self.verify_rockboxd(); 239 240 let weak_self = self.downgrade(); 241 self.albums_scrolled_window 242 .connect_edge_reached(move |_, pos| { 243 if pos == gtk::PositionType::Bottom { 244 let self_ = match weak_self.upgrade() { 245 Some(self_) => self_, 246 None => return, 247 }; 248 let size = self_.albums.imp().size.get(); 249 let all_albums = self_.albums.imp().all_albums.borrow(); 250 let next_range_end = (size + 5).min(all_albums.len()); 251 252 if size >= all_albums.len() { 253 return; 254 } 255 256 let next_albums = all_albums[size..next_range_end].to_vec(); 257 258 if next_albums.is_empty() { 259 return; 260 } 261 262 self_.albums.imp().size.set(size + next_albums.len()); 263 self_ 264 .albums 265 .imp() 266 .create_albums_widgets(Some(next_albums), None); 267 } 268 }); 269 270 let weak_self = self.downgrade(); 271 self.artists_scrolled_window 272 .connect_edge_reached(move |_, pos| { 273 if pos == gtk::PositionType::Bottom { 274 let self_ = match weak_self.upgrade() { 275 Some(self_) => self_, 276 None => return, 277 }; 278 let size = self_.artists.imp().size.get(); 279 let all_artists = self_.artists.imp().all_artists.borrow(); 280 let next_range_end = (size + 5).min(all_artists.len()); 281 282 if size >= all_artists.len() { 283 return; 284 } 285 286 let next_artists = all_artists[size..next_range_end].to_vec(); 287 288 if next_artists.is_empty() { 289 return; 290 } 291 292 self_.artists.imp().size.set(size + next_artists.len()); 293 self_ 294 .artists 295 .imp() 296 .create_artists_widgets(Some(next_artists), None); 297 } 298 }); 299 300 let weak_self = self.downgrade(); 301 self.songs_scrolled_window 302 .connect_edge_reached(move |_, pos| { 303 if pos == gtk::PositionType::Bottom { 304 let self_ = match weak_self.upgrade() { 305 Some(self_) => self_, 306 None => return, 307 }; 308 let size = self_.songs.imp().size.get(); 309 let all_songs = self_.songs.imp().all_tracks.borrow(); 310 let next_range_end = (size + 3).min(all_songs.len()); 311 312 if size >= all_songs.len() { 313 return; 314 } 315 316 let next_songs = all_songs[size..next_range_end].to_vec(); 317 318 if next_songs.is_empty() { 319 return; 320 } 321 322 self_.songs.imp().size.set(size + next_songs.len()); 323 self_ 324 .songs 325 .imp() 326 .create_songs_widgets(Some(next_songs), None); 327 } 328 }); 329 330 let weak_self = self.downgrade(); 331 self.artist_tracks_scrolled_window 332 .connect_edge_reached(move |_, pos| { 333 if pos == gtk::PositionType::Bottom { 334 let self_ = match weak_self.upgrade() { 335 Some(self_) => self_, 336 None => return, 337 }; 338 let size = self_.artist_tracks.imp().size.get(); 339 let all_songs = self_.artist_tracks.imp().all_tracks.borrow(); 340 let next_range_end = (size + 3).min(all_songs.len()); 341 342 if size >= all_songs.len() { 343 return; 344 } 345 346 let next_songs = all_songs[size..next_range_end].to_vec(); 347 348 if next_songs.is_empty() { 349 return; 350 } 351 352 self_.artist_tracks.imp().size.set(size + next_songs.len()); 353 self_ 354 .artist_tracks 355 .imp() 356 .create_songs_widgets(Some(next_songs), None); 357 } 358 }); 359 360 let weak_self = self.downgrade(); 361 self.likes_scrolled_window 362 .connect_edge_reached(move |_, pos| { 363 if pos == gtk::PositionType::Bottom { 364 let self_ = match weak_self.upgrade() { 365 Some(self_) => self_, 366 None => return, 367 }; 368 let size = self_.likes.imp().size.get(); 369 let likes = self_.likes.imp().likes.borrow(); 370 let next_range_end = (size + 3).min(likes.len()); 371 372 if size >= likes.len() { 373 return; 374 } 375 376 let next_likes = likes[size..next_range_end].to_vec(); 377 378 if next_likes.is_empty() { 379 return; 380 } 381 382 self_.likes.imp().size.set(size + next_likes.len()); 383 self_ 384 .likes 385 .imp() 386 .create_songs_widgets(Some(next_likes), None); 387 } 388 }); 389 390 let sidebar = self.sidebar.get(); 391 sidebar.select_row(Some(&sidebar.row_at_index(0).unwrap())); 392 let weak_self = self.downgrade(); 393 sidebar.connect_row_selected(move |_, row| { 394 let self_ = match weak_self.upgrade() { 395 Some(self_) => self_, 396 None => return, 397 }; 398 let row = row.unwrap(); 399 let row = row.clone().downcast::<gtk::ListBoxRow>().unwrap(); 400 let label = row 401 .child() 402 .unwrap() 403 .downcast::<gtk::Box>() 404 .unwrap() 405 .last_child() 406 .unwrap() 407 .downcast::<gtk::Label>() 408 .unwrap() 409 .text() 410 .to_string(); 411 412 if let Some(state) = self_.state.upgrade() { 413 match label.as_str() { 414 "Albums" => { 415 let main_stack = self_.main_stack.get(); 416 if !self_.show_placeholder.get() { 417 main_stack.set_visible_child_name("albums-page"); 418 } 419 let library_page = self_.library_page.get(); 420 library_page.set_title("Albums"); 421 state.new_navigation_from("Albums", "albums-page"); 422 let play_all_button = self_.play_all_button.get(); 423 let shuffle_all_button = self_.shuffle_all_button.get(); 424 play_all_button.set_visible(false); 425 shuffle_all_button.set_visible(false); 426 } 427 "Artists" => { 428 let main_stack = self_.main_stack.get(); 429 if !self_.show_placeholder.get() { 430 main_stack.set_visible_child_name("artists-page"); 431 } 432 let library_page = self_.library_page.get(); 433 library_page.set_title("Artists"); 434 state.new_navigation_from("Artists", "artists-page"); 435 let play_all_button = self_.play_all_button.get(); 436 let shuffle_all_button = self_.shuffle_all_button.get(); 437 play_all_button.set_visible(false); 438 shuffle_all_button.set_visible(false); 439 } 440 "Songs" => { 441 let main_stack = self_.main_stack.get(); 442 if !self_.show_placeholder.get() { 443 main_stack.set_visible_child_name("songs-page"); 444 } 445 let library_page = self_.library_page.get(); 446 library_page.set_title("Songs"); 447 state.new_navigation_from("Songs", "songs-page"); 448 let play_all_button = self_.play_all_button.get(); 449 let shuffle_all_button = self_.shuffle_all_button.get(); 450 451 if !state.tracks().is_empty() { 452 play_all_button.set_visible(true); 453 shuffle_all_button.set_visible(true); 454 } 455 } 456 "Likes" => { 457 let main_stack = self_.main_stack.get(); 458 if !self_.show_placeholder.get() { 459 main_stack.set_visible_child_name("likes-page"); 460 } 461 let library_page = self_.library_page.get(); 462 library_page.set_title("Likes"); 463 state.new_navigation_from("Likes", "likes-page"); 464 let play_all_button = self_.play_all_button.get(); 465 let shuffle_all_button = self_.shuffle_all_button.get(); 466 play_all_button.set_visible(true); 467 shuffle_all_button.set_visible(true); 468 469 let likes = self_.likes.get(); 470 glib::idle_add_local(move || { 471 likes.imp().size.set(20); 472 likes.load_likes(); 473 474 if state.liked_tracks().is_empty() { 475 play_all_button.set_visible(false); 476 shuffle_all_button.set_visible(false); 477 } 478 479 glib::ControlFlow::Break 480 }); 481 } 482 "Files" => { 483 let main_stack = self_.main_stack.get(); 484 if !self_.show_placeholder.get() { 485 main_stack.set_visible_child_name("files-page"); 486 } 487 let library_page = self_.library_page.get(); 488 library_page.set_title("Files"); 489 state.new_navigation_from("Files", "files-page"); 490 let play_all_button = self_.play_all_button.get(); 491 let shuffle_all_button = self_.shuffle_all_button.get(); 492 play_all_button.set_visible(false); 493 shuffle_all_button.set_visible(false); 494 } 495 _ => {} 496 } 497 498 let media_control_bar = self_.media_control_bar.get(); 499 if media_control_bar.imp().playlist_displayed.get() { 500 media_control_bar.show_playlist(); 501 } 502 503 let search_bar = self_.search_bar.get(); 504 search_bar.set_search_mode(false); 505 let state = self_.state.upgrade().unwrap(); 506 state.set_search_mode(false); 507 } 508 509 let go_back_button = self_.go_back_button.get(); 510 go_back_button.set_visible(false); 511 }); 512 } 513 } 514 515 impl WidgetImpl for RbApplicationWindow {} 516 impl WindowImpl for RbApplicationWindow {} 517 impl ApplicationWindowImpl for RbApplicationWindow {} 518 impl AdwApplicationWindowImpl for RbApplicationWindow {} 519 520 impl RbApplicationWindow { 521 fn verify_rockboxd(&self) { 522 let rt = tokio::runtime::Runtime::new().unwrap(); 523 let global_status = rt.block_on(async { 524 let url = build_url(); 525 let mut client = SystemServiceClient::connect(url).await?; 526 let response = client.get_global_status(GetGlobalStatusRequest {}).await?; 527 Ok::<GetGlobalStatusResponse, Error>(response.into_inner()) 528 }); 529 530 match global_status { 531 Ok(_) => { 532 self.show_placeholder.set(false); 533 self.media_control_bar.set_visible(true); 534 } 535 Err(_) => { 536 let main_stack = self.main_stack.get(); 537 main_stack.set_visible_child_name("placeholder-page"); 538 self.show_placeholder.set(true); 539 self.media_control_bar.set_visible(false); 540 } 541 } 542 } 543 544 fn toggle_sidebar(&self) { 545 let current_state = self.show_sidebar.get(); 546 self.show_sidebar.set(!current_state); 547 self.overlay_split_view.set_show_sidebar(!current_state); 548 } 549 550 fn toggle_searchbar(&self) { 551 let state = self.state.upgrade().unwrap(); 552 let search_bar = self.search_bar.get(); 553 let search_mode = state.search_mode(); 554 search_bar.set_search_mode(!search_mode); 555 state.set_search_mode(!search_mode); 556 let search_entry = self.search_entry.get(); 557 558 if !search_mode { 559 search_entry.grab_focus(); 560 561 let main_stack = self.main_stack.get(); 562 let library_page = self.library_page.get(); 563 let go_back_button = self.go_back_button.get(); 564 let state = self.state.upgrade().unwrap(); 565 566 if !self.show_placeholder.get() { 567 main_stack.set_visible_child_name("search-page"); 568 } 569 570 library_page.set_title("Search Results"); 571 go_back_button.set_visible(true); 572 state.push_navigation("Search", "search-page"); 573 let self_weak = self.downgrade(); 574 575 search_entry.connect_changed(move |entry| { 576 let self_ = match self_weak.upgrade() { 577 Some(self_) => self_, 578 None => return, 579 }; 580 581 let text = entry.text(); 582 let text = text.to_string(); 583 self_.search_term(text.clone()); 584 }); 585 } else { 586 self.go_back(); 587 } 588 } 589 590 pub fn hide_top_buttons(&self, hide: bool) { 591 let play_all_button = self.play_all_button.get(); 592 let shuffle_all_button = self.shuffle_all_button.get(); 593 play_all_button.set_visible(!hide); 594 shuffle_all_button.set_visible(!hide); 595 } 596 597 fn go_back(&self) { 598 let main_stack = self.main_stack.get(); 599 let state = self.state.upgrade().unwrap(); 600 601 let poped_page = state.pop_navigation(); 602 let current_page = state.current_page(); 603 604 if current_page.1 == "files-page" && poped_page.1 == "files-page" { 605 let files = self.files.get(); 606 files.go_back(); 607 return; 608 } 609 610 if poped_page.1 == "search-page" { 611 let search_bar = self.search_bar.get(); 612 search_bar.set_search_mode(false); 613 state.set_search_mode(false); 614 615 self.search.imp().album_results.clear(false); 616 self.search.imp().artist_results.clear(false); 617 self.search.imp().track_results.clear(false); 618 self.search_entry.get().set_text(""); 619 620 if self.show_placeholder.get() { 621 main_stack.set_visible_child_name("placeholder-page"); 622 } 623 } 624 625 if current_page.1 == "search-page" { 626 let search_bar = self.search_bar.get(); 627 search_bar.set_search_mode(true); 628 state.set_search_mode(true); 629 } 630 631 if !self.show_placeholder.get() { 632 main_stack.set_visible_child_name(current_page.1.as_str()); 633 } 634 635 let library_page = self.library_page.get(); 636 library_page.set_title(current_page.0.as_str()); 637 638 let go_back_button = self.go_back_button.get(); 639 if state.navigation_stack_len() == 1 { 640 go_back_button.set_visible(false); 641 } 642 if current_page.1 == "files-page" 643 && (poped_page.1 == "album-details-page" || poped_page.1 == "artist-details-page") 644 { 645 let files = self.files.get(); 646 let default_string = String::from(""); 647 let state = self.state.upgrade().unwrap(); 648 let current_path = state.current_path().unwrap_or(String::from("")); 649 let music_directory = files.imp().music_directory.borrow(); 650 let music_directory_ref = music_directory.as_ref().unwrap_or(&default_string); 651 652 go_back_button.set_visible( 653 current_path != *music_directory_ref && current_path != *default_string, 654 ); 655 } 656 657 if current_page.1 == "songs-page" || current_page.1 == "likes-page" { 658 self.hide_top_buttons(false); 659 } else { 660 self.hide_top_buttons(true); 661 } 662 } 663 664 fn play_all(&self) { 665 let state = self.state.upgrade().unwrap(); 666 if state.current_page().1 == "songs-page" { 667 thread::spawn(move || { 668 let rt = tokio::runtime::Runtime::new().unwrap(); 669 let url = build_url(); 670 let _ = rt.block_on(async { 671 let mut client = PlaybackServiceClient::connect(url).await?; 672 client 673 .play_all_tracks(PlayAllTracksRequest { 674 shuffle: Some(false), 675 position: Some(0), 676 }) 677 .await?; 678 Ok::<(), Error>(()) 679 }); 680 }); 681 } 682 683 if state.current_page().1 == "likes-page" { 684 thread::spawn(move || { 685 let rt = tokio::runtime::Runtime::new().unwrap(); 686 let url = build_url(); 687 let _ = rt.block_on(async { 688 let mut client = PlaybackServiceClient::connect(url).await?; 689 client 690 .play_liked_tracks(PlayLikedTracksRequest { 691 shuffle: Some(false), 692 position: Some(0), 693 }) 694 .await?; 695 Ok::<(), Error>(()) 696 }); 697 }); 698 } 699 } 700 701 fn shuffle_all(&self) { 702 let state = self.state.upgrade().unwrap(); 703 if state.current_page().1 == "songs-page" { 704 thread::spawn(move || { 705 let rt = tokio::runtime::Runtime::new().unwrap(); 706 let url = build_url(); 707 let _ = rt.block_on(async { 708 let mut client = PlaybackServiceClient::connect(url).await?; 709 client 710 .play_all_tracks(PlayAllTracksRequest { 711 shuffle: Some(true), 712 position: Some(0), 713 }) 714 .await?; 715 Ok::<(), Error>(()) 716 }); 717 }); 718 } 719 720 if state.current_page().1 == "likes-page" { 721 thread::spawn(move || { 722 let rt = tokio::runtime::Runtime::new().unwrap(); 723 let url = build_url(); 724 let _ = rt.block_on(async { 725 let mut client = PlaybackServiceClient::connect(url).await?; 726 client 727 .play_liked_tracks(PlayLikedTracksRequest { 728 shuffle: Some(true), 729 position: Some(0), 730 }) 731 .await?; 732 Ok::<(), Error>(()) 733 }); 734 }); 735 } 736 } 737 738 pub fn refresh_library(&self) { 739 let self_weak = self.downgrade(); 740 let (tx, mut rx) = mpsc::channel(32); 741 thread::spawn(move || { 742 let rt = tokio::runtime::Runtime::new().unwrap(); 743 let url = build_url(); 744 let _ = rt.block_on(async { 745 let mut client = LibraryServiceClient::connect(url).await?; 746 client 747 .scan_library(ScanLibraryRequest { path: None }) 748 .await?; 749 tx.send(0).await?; 750 Ok::<(), Error>(()) 751 }); 752 }); 753 754 let self_ = match self_weak.upgrade() { 755 Some(self_) => self_, 756 None => return, 757 }; 758 glib::spawn_future_local(async move { 759 while let Some(_) = rx.recv().await { 760 let albums = self_.albums.get(); 761 albums.imp().size.set(15); 762 albums.load_pictures(); 763 } 764 }); 765 } 766 767 pub fn search_term(&self, term: String) { 768 if term.len() < 3 { 769 self.search.imp().album_results.clear(false); 770 self.search.imp().artist_results.clear(false); 771 self.search.imp().track_results.clear(false); 772 773 return; 774 } 775 776 let state = self.state.upgrade().unwrap(); 777 let albums = state 778 .albums() 779 .into_iter() 780 .filter(|album| { 781 album.title.to_lowercase().contains(&term.to_lowercase()) 782 || album.artist.to_lowercase().contains(&term.to_lowercase()) 783 }) 784 .collect::<Vec<_>>(); 785 786 let artists = state 787 .artists() 788 .into_iter() 789 .filter(|artist| artist.name.to_lowercase().contains(&term.to_lowercase())) 790 .collect::<Vec<_>>(); 791 792 let tracks = state 793 .tracks() 794 .into_iter() 795 .filter(|track| { 796 track.title.to_lowercase().contains(&term.to_lowercase()) 797 || track.artist.to_lowercase().contains(&term.to_lowercase()) 798 || track.album.to_lowercase().contains(&term.to_lowercase()) 799 }) 800 .take(10) 801 .collect::<Vec<_>>(); 802 803 state.set_search_results(SearchResponse { 804 albums, 805 artists, 806 tracks, 807 }); 808 self.search.load_results(); 809 } 810 } 811} 812 813glib::wrapper! { 814 pub struct RbApplicationWindow( 815 ObjectSubclass<imp::RbApplicationWindow>) 816 @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow, 817 @implements gio::ActionMap, gio::ActionGroup; 818} 819 820impl RbApplicationWindow { 821 pub fn new(state: AppState) -> Self { 822 let window: Self = glib::Object::new::<Self>(); 823 824 let likes = window.imp().likes.get(); 825 let main_stack = window.imp().main_stack.get(); 826 let library_page = window.imp().library_page.get(); 827 let albums = window.imp().albums.get(); 828 let album_details = window.imp().album_details.get(); 829 let artists = window.imp().artists.get(); 830 let artist_details = window.imp().artist_details.get(); 831 let files = window.imp().files.get(); 832 let current_playlist = window.imp().current_playlist.get(); 833 let media_control_bar = window.imp().media_control_bar.get(); 834 let go_back_button = window.imp().go_back_button.get(); 835 let play_all_button = window.imp().play_all_button.get(); 836 let shuffle_all_button = window.imp().shuffle_all_button.get(); 837 let songs = window.imp().songs.get(); 838 let artist_tracks = window.imp().artist_tracks.get(); 839 let search = window.imp().search.get(); 840 let album_results = search.imp().album_results.get(); 841 let artist_results = search.imp().artist_results.get(); 842 let track_results = search.imp().track_results.get(); 843 844 songs.imp().likes_page.replace(Some(likes.clone())); 845 track_results.imp().likes_page.replace(Some(likes.clone())); 846 artist_tracks.imp().likes_page.replace(Some(likes.clone())); 847 848 window.imp().state.set(Some(&state)); 849 artists.imp().state.set(Some(&state)); 850 artist_details.imp().state.set(Some(&state)); 851 media_control_bar.imp().state.set(Some(&state)); 852 files.imp().state.set(Some(&state)); 853 current_playlist.imp().state.set(Some(&state)); 854 likes.imp().state.set(Some(&state)); 855 songs.imp().state.set(Some(&state)); 856 artist_tracks.imp().state.set(Some(&state)); 857 album_details.imp().state.set(Some(&state)); 858 artist_results.imp().state.set(Some(&state)); 859 track_results.imp().state.set(Some(&state)); 860 album_results.imp().state.set(Some(&state)); 861 albums.imp().state.set(Some(&state)); 862 search.imp().state.set(Some(&state)); 863 864 artist_results.imp().search_mode.set(true); 865 album_results.imp().search_mode.set(true); 866 track_results.imp().search_mode.set(true); 867 868 media_control_bar 869 .imp() 870 .search_bar 871 .replace(Some(window.imp().search_bar.get().clone())); 872 873 artists 874 .imp() 875 .search_bar 876 .replace(Some(window.imp().search_bar.get().clone())); 877 878 albums 879 .imp() 880 .search_bar 881 .replace(Some(window.imp().search_bar.get().clone())); 882 883 artist_results 884 .imp() 885 .search_bar 886 .replace(Some(window.imp().search_bar.get().clone())); 887 888 album_results 889 .imp() 890 .search_bar 891 .replace(Some(window.imp().search_bar.get().clone())); 892 893 files.get_music_directory(); 894 895 artist_details 896 .imp() 897 .artist_tracks 898 .replace(Some(artist_tracks.clone())); 899 900 artist_details 901 .imp() 902 .play_all_button 903 .replace(Some(play_all_button.clone())); 904 artist_details 905 .imp() 906 .shuffle_all_button 907 .replace(Some(shuffle_all_button.clone())); 908 909 album_details 910 .imp() 911 .play_all_button 912 .replace(Some(play_all_button.clone())); 913 album_details 914 .imp() 915 .shuffle_all_button 916 .replace(Some(shuffle_all_button.clone())); 917 918 current_playlist 919 .imp() 920 .play_all_button 921 .replace(Some(play_all_button.clone())); 922 current_playlist 923 .imp() 924 .shuffle_all_button 925 .replace(Some(shuffle_all_button.clone())); 926 927 media_control_bar 928 .imp() 929 .library_page 930 .replace(Some(library_page.clone())); 931 media_control_bar 932 .imp() 933 .album_details 934 .replace(Some(album_details.clone())); 935 media_control_bar 936 .imp() 937 .artist_details 938 .replace(Some(artist_details.clone())); 939 media_control_bar 940 .imp() 941 .main_stack 942 .replace(Some(main_stack.clone())); 943 media_control_bar 944 .imp() 945 .go_back_button 946 .replace(Some(go_back_button.clone())); 947 media_control_bar 948 .imp() 949 .current_playlist 950 .replace(Some(current_playlist.clone())); 951 952 albums.imp().set_main_stack(main_stack.clone()); 953 albums.imp().set_library_page(library_page.clone()); 954 albums 955 .imp() 956 .set_go_back_button(window.imp().go_back_button.get().clone()); 957 albums.imp().set_album_details(album_details.clone()); 958 959 artists.imp().set_main_stack(main_stack.clone()); 960 artists.imp().set_library_page(library_page.clone()); 961 artists 962 .imp() 963 .set_go_back_button(window.imp().go_back_button.get().clone()); 964 artists.imp().set_artist_details(artist_details.clone()); 965 artists.imp().set_album_details(album_details.clone()); 966 967 files.imp().set_main_stack(main_stack.clone()); 968 files 969 .imp() 970 .set_go_back_button(window.imp().go_back_button.get().clone()); 971 972 album_results.imp().set_main_stack(main_stack.clone()); 973 album_results.imp().set_library_page(library_page.clone()); 974 album_results 975 .imp() 976 .set_go_back_button(window.imp().go_back_button.get().clone()); 977 978 artist_results.imp().set_main_stack(main_stack.clone()); 979 artist_results.imp().set_library_page(library_page.clone()); 980 artist_results 981 .imp() 982 .set_go_back_button(window.imp().go_back_button.get().clone()); 983 artist_results 984 .imp() 985 .set_artist_details(artist_details.clone()); 986 artist_results 987 .imp() 988 .set_album_details(album_details.clone()); 989 album_results.imp().set_album_details(album_details.clone()); 990 991 window 992 } 993 994 pub fn add_message_toast(&self, message: &str) { 995 let toast = adw::Toast::new(message); 996 self.imp().toast_overlay.add_toast(toast); 997 } 998} 999 1000impl Default for RbApplicationWindow { 1001 fn default() -> Self { 1002 RbApplication::default() 1003 .active_window() 1004 .unwrap() 1005 .downcast() 1006 .unwrap() 1007 } 1008} 1009 1010fn build_url() -> String { 1011 let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 1012 let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 1013 1014 format!("tcp://{}:{}", host, port) 1015}