A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
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}