mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 11:47:25 +01:00 
			
		
		
		
	Use navigator for main window
This commit is contained in:
		
							parent
							
								
									43023fdb5b
								
							
						
					
					
						commit
						88e1c97143
					
				
					 15 changed files with 619 additions and 768 deletions
				
			
		|  | @ -7,7 +7,6 @@ edition = "2018" | |||
| anyhow = "1.0.33" | ||||
| async-trait = "0.1.42" | ||||
| discid = "0.4.4" | ||||
| futures = "0.3.6" | ||||
| futures-channel = "0.3.5" | ||||
| gettext-rs = "0.5.0" | ||||
| gstreamer = "0.16.4" | ||||
|  |  | |||
							
								
								
									
										205
									
								
								crates/musicus/src/screens/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								crates/musicus/src/screens/main.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,205 @@ | |||
| use super::{EnsembleScreen, PersonScreen, PlayerScreen}; | ||||
| use crate::config; | ||||
| use crate::import::SourceSelector; | ||||
| use crate::navigator::{Navigator, NavigatorWindow, NavigationHandle, Screen}; | ||||
| use crate::preferences::Preferences; | ||||
| use crate::widgets::{List, PlayerBar, Widget}; | ||||
| use gettextrs::gettext; | ||||
| use glib::clone; | ||||
| use gtk::prelude::*; | ||||
| use gtk_macros::get_widget; | ||||
| use libadwaita::prelude::*; | ||||
| use musicus_backend::db::{Ensemble, Person}; | ||||
| use std::cell::RefCell; | ||||
| use std::rc::Rc; | ||||
| 
 | ||||
| /// Either a person or an ensemble to be shown in the list.
 | ||||
| #[derive(Clone, Debug)] | ||||
| pub enum PersonOrEnsemble { | ||||
|     Person(Person), | ||||
|     Ensemble(Ensemble), | ||||
| } | ||||
| 
 | ||||
| impl PersonOrEnsemble { | ||||
|     /// Get a short textual representation of the item.
 | ||||
|     pub fn get_title(&self) -> String { | ||||
|         match self { | ||||
|             PersonOrEnsemble::Person(person) => person.name_lf(), | ||||
|             PersonOrEnsemble::Ensemble(ensemble) => ensemble.name.clone(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The main screen of the app, once it's set up and finished loading. The screen assumes that the
 | ||||
| /// music library and the player are available and initialized.
 | ||||
| pub struct MainScreen { | ||||
|     handle: NavigationHandle<()>, | ||||
|     widget: gtk::Box, | ||||
|     leaflet: libadwaita::Leaflet, | ||||
|     search_entry: gtk::SearchEntry, | ||||
|     stack: gtk::Stack, | ||||
|     poe_list: Rc<List>, | ||||
|     navigator: Rc<Navigator>, | ||||
|     poes: RefCell<Vec<PersonOrEnsemble>>, | ||||
| } | ||||
| 
 | ||||
| impl Screen<(), ()> for MainScreen { | ||||
|     /// Create a new main screen.
 | ||||
|     fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> { | ||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/main_screen.ui"); | ||||
| 
 | ||||
|         get_widget!(builder, gtk::Box, widget); | ||||
|         get_widget!(builder, libadwaita::Leaflet, leaflet); | ||||
|         get_widget!(builder, gtk::Button, add_button); | ||||
|         get_widget!(builder, gtk::SearchEntry, search_entry); | ||||
|         get_widget!(builder, gtk::Stack, stack); | ||||
|         get_widget!(builder, gtk::ScrolledWindow, scroll); | ||||
|         get_widget!(builder, gtk::Box, empty_screen); | ||||
| 
 | ||||
|         let actions = gio::SimpleActionGroup::new(); | ||||
|         let preferences_action = gio::SimpleAction::new("preferences", None); | ||||
|         let about_action = gio::SimpleAction::new("about", None); | ||||
|         actions.add_action(&preferences_action); | ||||
|         actions.add_action(&about_action); | ||||
|         widget.insert_action_group("widget", Some(&actions)); | ||||
| 
 | ||||
|         let poe_list = List::new(); | ||||
|         poe_list.widget.add_css_class("navigation-sidebar"); | ||||
|         poe_list.enable_selection(); | ||||
| 
 | ||||
|         let navigator = Navigator::new(Rc::clone(&handle.backend), &handle.window, &empty_screen); | ||||
| 
 | ||||
|         scroll.set_child(Some(&poe_list.widget)); | ||||
|         leaflet.append(&navigator.widget); | ||||
| 
 | ||||
|         let player_bar = PlayerBar::new(); | ||||
|         widget.append(&player_bar.widget); | ||||
|         player_bar.set_player(Some(Rc::clone(&handle.backend.pl()))); | ||||
| 
 | ||||
|         let this = Rc::new(Self { | ||||
|             handle, | ||||
|             widget, | ||||
|             leaflet, | ||||
|             search_entry, | ||||
|             stack, | ||||
|             poe_list, | ||||
|             navigator, | ||||
|             poes: RefCell::new(Vec::new()), | ||||
|         }); | ||||
| 
 | ||||
|         preferences_action.connect_activate(clone!(@weak this => move |_, _| { | ||||
|             Preferences::new(Rc::clone(&this.handle.backend), &this.handle.window).show(); | ||||
|         })); | ||||
| 
 | ||||
|         about_action.connect_activate(clone!(@weak this => move |_, _| { | ||||
|             this.show_about_dialog(); | ||||
|         })); | ||||
| 
 | ||||
|         add_button.connect_clicked(clone!(@weak this => move |_| { | ||||
|             spawn!(@clone this, async move { | ||||
|                 let window = NavigatorWindow::new(Rc::clone(&this.handle.backend)); | ||||
|                 replace!(window.navigator, SourceSelector).await; | ||||
|             }); | ||||
|         })); | ||||
| 
 | ||||
|         this.search_entry.connect_search_changed(clone!(@weak this => move |_| { | ||||
|             this.poe_list.invalidate_filter(); | ||||
|         })); | ||||
| 
 | ||||
|         this.poe_list.set_make_widget_cb(clone!(@weak this => move |index| { | ||||
|             let poe = &this.poes.borrow()[index]; | ||||
| 
 | ||||
|             let row = libadwaita::ActionRow::new(); | ||||
|             row.set_activatable(true); | ||||
|             row.set_title(Some(&poe.get_title())); | ||||
| 
 | ||||
|             let poe = poe.to_owned(); | ||||
|             row.connect_activated(clone!(@weak this => move |_| { | ||||
|                 let poe = poe.clone(); | ||||
|                 spawn!(@clone this, async move { | ||||
|                     this.leaflet.set_visible_child(&this.navigator.widget); | ||||
| 
 | ||||
|                     match poe { | ||||
|                         PersonOrEnsemble::Person(person) => { | ||||
|                             replace!(this.navigator, PersonScreen, person).await; | ||||
|                         } | ||||
|                         PersonOrEnsemble::Ensemble(ensemble) => { | ||||
|                             replace!(this.navigator, EnsembleScreen, ensemble).await; | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             })); | ||||
| 
 | ||||
|             row.upcast() | ||||
|         })); | ||||
| 
 | ||||
|         this.poe_list.set_filter_cb(clone!(@weak this => move |index| { | ||||
|             let poe = &this.poes.borrow()[index]; | ||||
|             let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); | ||||
|             let title = poe.get_title().to_lowercase(); | ||||
|             search.is_empty() || title.contains(&search) | ||||
|         })); | ||||
| 
 | ||||
|         this.navigator.set_back_cb(clone!(@weak this => move || { | ||||
|             this.leaflet.set_visible_child_name("sidebar"); | ||||
|         })); | ||||
| 
 | ||||
|         player_bar.set_playlist_cb(clone!(@weak this => move || { | ||||
|             spawn!(@clone this, async move { | ||||
|                 push!(this.handle, PlayerScreen).await; | ||||
|             }); | ||||
|         })); | ||||
| 
 | ||||
|         // Load the content asynchronously.
 | ||||
| 
 | ||||
|         spawn!(@clone this, async move { | ||||
|             let mut poes = Vec::new(); | ||||
| 
 | ||||
|             let persons = this.handle.backend.db().get_persons().await.unwrap(); | ||||
|             let ensembles = this.handle.backend.db().get_ensembles().await.unwrap(); | ||||
| 
 | ||||
|             for person in persons { | ||||
|                 poes.push(PersonOrEnsemble::Person(person)); | ||||
|             } | ||||
| 
 | ||||
|             for ensemble in ensembles { | ||||
|                 poes.push(PersonOrEnsemble::Ensemble(ensemble)); | ||||
|             } | ||||
| 
 | ||||
|             let length = poes.len(); | ||||
|             this.poes.replace(poes); | ||||
|             this.poe_list.update(length); | ||||
| 
 | ||||
|             this.stack.set_visible_child_name("content"); | ||||
|         }); | ||||
| 
 | ||||
|         this | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Widget for MainScreen { | ||||
|     fn get_widget(&self) -> gtk::Widget { | ||||
|         self.widget.clone().upcast() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl MainScreen { | ||||
|     /// Show a dialog with information on this application.
 | ||||
|     fn show_about_dialog(&self) { | ||||
|         let dialog = gtk::AboutDialogBuilder::new() | ||||
|             .transient_for(&self.handle.window) | ||||
|             .modal(true) | ||||
|             .logo_icon_name("de.johrpan.musicus") | ||||
|             .program_name(&gettext("Musicus")) | ||||
|             .version(config::VERSION) | ||||
|             .comments(&gettext("The classical music player and organizer.")) | ||||
|             .website("https://github.com/johrpan/musicus") | ||||
|             .website_label(&gettext("Further information and source code")) | ||||
|             .copyright("© 2020 Elias Projahn") | ||||
|             .license_type(gtk::License::Agpl30) | ||||
|             .authors(vec![String::from("Elias Projahn <johrpan@gmail.com>")]) | ||||
|             .build(); | ||||
| 
 | ||||
|         dialog.show(); | ||||
|     } | ||||
| } | ||||
|  | @ -1,14 +1,20 @@ | |||
| pub mod ensemble; | ||||
| pub use ensemble::*; | ||||
| 
 | ||||
| pub mod main; | ||||
| pub use main::*; | ||||
| 
 | ||||
| pub mod person; | ||||
| pub use person::*; | ||||
| 
 | ||||
| pub mod player_screen; | ||||
| pub use player_screen::*; | ||||
| pub mod player; | ||||
| pub use player::*; | ||||
| 
 | ||||
| pub mod work; | ||||
| pub use work::*; | ||||
| 
 | ||||
| pub mod welcome; | ||||
| pub use welcome::*; | ||||
| 
 | ||||
| pub mod recording; | ||||
| pub use recording::*; | ||||
|  |  | |||
|  | @ -1,10 +1,11 @@ | |||
| use crate::widgets::*; | ||||
| use crate::navigator::{NavigationHandle, Screen}; | ||||
| use crate::widgets::{List, Widget}; | ||||
| use gettextrs::gettext; | ||||
| use glib::clone; | ||||
| use gtk::prelude::*; | ||||
| use gtk_macros::get_widget; | ||||
| use libadwaita::prelude::*; | ||||
| use musicus_backend::{Player, PlaylistItem}; | ||||
| use musicus_backend::PlaylistItem; | ||||
| use std::cell::{Cell, RefCell}; | ||||
| use std::rc::Rc; | ||||
| 
 | ||||
|  | @ -23,7 +24,8 @@ enum ListItem { | |||
| } | ||||
| 
 | ||||
| pub struct PlayerScreen { | ||||
|     pub widget: gtk::Box, | ||||
|     handle: NavigationHandle<()>, | ||||
|     widget: gtk::Box, | ||||
|     title_label: gtk::Label, | ||||
|     subtitle_label: gtk::Label, | ||||
|     previous_button: gtk::Button, | ||||
|  | @ -37,15 +39,13 @@ pub struct PlayerScreen { | |||
|     list: Rc<List>, | ||||
|     playlist: RefCell<Vec<PlaylistItem>>, | ||||
|     items: RefCell<Vec<ListItem>>, | ||||
|     player: RefCell<Option<Rc<Player>>>, | ||||
|     seeking: Cell<bool>, | ||||
|     current_item: Cell<usize>, | ||||
|     current_track: Cell<usize>, | ||||
|     back_cb: RefCell<Option<Box<dyn Fn()>>>, | ||||
| } | ||||
| 
 | ||||
| impl PlayerScreen { | ||||
|     pub fn new() -> Rc<Self> { | ||||
| impl Screen<(), ()> for PlayerScreen { | ||||
|     fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> { | ||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui"); | ||||
| 
 | ||||
|         get_widget!(builder, gtk::Box, widget); | ||||
|  | @ -68,6 +68,7 @@ impl PlayerScreen { | |||
|         frame.set_child(Some(&list.widget)); | ||||
| 
 | ||||
|         let this = Rc::new(Self { | ||||
|             handle, | ||||
|             widget, | ||||
|             title_label, | ||||
|             subtitle_label, | ||||
|  | @ -82,45 +83,87 @@ impl PlayerScreen { | |||
|             list, | ||||
|             items: RefCell::new(Vec::new()), | ||||
|             playlist: RefCell::new(Vec::new()), | ||||
|             player: RefCell::new(None), | ||||
|             seeking: Cell::new(false), | ||||
|             current_item: Cell::new(0), | ||||
|             current_track: Cell::new(0), | ||||
|             back_cb: RefCell::new(None), | ||||
|         }); | ||||
| 
 | ||||
|         back_button.connect_clicked(clone!(@strong this => move |_| { | ||||
|             if let Some(cb) = &*this.back_cb.borrow() { | ||||
|                 cb(); | ||||
|         let player = &this.handle.backend.pl(); | ||||
| 
 | ||||
|         player.add_playlist_cb(clone!(@weak this => move |playlist| { | ||||
|             this.playlist.replace(playlist); | ||||
|             this.show_playlist(); | ||||
|         })); | ||||
| 
 | ||||
|         player.add_track_cb(clone!(@weak this, @weak player => move |current_item, current_track| { | ||||
|             this.previous_button.set_sensitive(this.handle.backend.pl().has_previous()); | ||||
|             this.next_button.set_sensitive(this.handle.backend.pl().has_next()); | ||||
| 
 | ||||
|             let item = &this.playlist.borrow()[current_item]; | ||||
|             let track = &item.track_set.tracks[current_track]; | ||||
| 
 | ||||
|             let mut parts = Vec::<String>::new(); | ||||
|             for part in &track.work_parts { | ||||
|                 parts.push(item.track_set.recording.work.parts[*part].title.clone()); | ||||
|             } | ||||
| 
 | ||||
|             let mut title = item.track_set.recording.work.get_title(); | ||||
|             if !parts.is_empty() { | ||||
|                 title = format!("{}: {}", title, parts.join(", ")); | ||||
|             } | ||||
| 
 | ||||
|             this.title_label.set_text(&title); | ||||
|             this.subtitle_label.set_text(&item.track_set.recording.get_performers()); | ||||
|             this.position_label.set_text("0:00"); | ||||
| 
 | ||||
|             this.current_item.set(current_item); | ||||
|             this.current_track.set(current_track); | ||||
| 
 | ||||
|             this.show_playlist(); | ||||
|         })); | ||||
| 
 | ||||
|         player.add_duration_cb(clone!(@weak this => move |ms| { | ||||
|             let min = ms / 60000; | ||||
|             let sec = (ms % 60000) / 1000; | ||||
|             this.duration_label.set_text(&format!("{}:{:02}", min, sec)); | ||||
|             this.position.set_upper(ms as f64); | ||||
|         })); | ||||
| 
 | ||||
|         player.add_playing_cb(clone!(@weak this => move |playing| { | ||||
|             this.play_button.set_child(Some(if playing { | ||||
|                 &this.pause_image | ||||
|             } else { | ||||
|                 &this.play_image | ||||
|             })); | ||||
|         })); | ||||
| 
 | ||||
|         player.add_position_cb(clone!(@weak this => move |ms| { | ||||
|             if !this.seeking.get() { | ||||
|                 let min = ms / 60000; | ||||
|                 let sec = (ms % 60000) / 1000; | ||||
|                 this.position_label.set_text(&format!("{}:{:02}", min, sec)); | ||||
|                 this.position.set_value(ms as f64); | ||||
|             } | ||||
|         })); | ||||
| 
 | ||||
|         this.previous_button.connect_clicked(clone!(@strong this => move |_| { | ||||
|             if let Some(player) = &*this.player.borrow() { | ||||
|                 player.previous().unwrap(); | ||||
|             } | ||||
|         back_button.connect_clicked(clone!(@weak this => move |_| { | ||||
|             this.handle.pop(None); | ||||
|         })); | ||||
| 
 | ||||
|         this.play_button.connect_clicked(clone!(@strong this => move |_| { | ||||
|             if let Some(player) = &*this.player.borrow() { | ||||
|                 player.play_pause(); | ||||
|             } | ||||
|         this.previous_button.connect_clicked(clone!(@weak this => move |_| { | ||||
|             this.handle.backend.pl().previous().unwrap(); | ||||
|         })); | ||||
| 
 | ||||
|         this.next_button.connect_clicked(clone!(@strong this => move |_| { | ||||
|             if let Some(player) = &*this.player.borrow() { | ||||
|                 player.next().unwrap(); | ||||
|             } | ||||
|         this.play_button.connect_clicked(clone!(@weak this => move |_| { | ||||
|             this.handle.backend.pl().play_pause(); | ||||
|         })); | ||||
| 
 | ||||
|         stop_button.connect_clicked(clone!(@strong this => move |_| { | ||||
|             if let Some(player) = &*this.player.borrow() { | ||||
|                 if let Some(cb) = &*this.back_cb.borrow() { | ||||
|                     cb(); | ||||
|                 } | ||||
|         this.next_button.connect_clicked(clone!(@weak this => move |_| { | ||||
|             this.handle.backend.pl().next().unwrap(); | ||||
|         })); | ||||
| 
 | ||||
|                 player.clear(); | ||||
|             } | ||||
|         stop_button.connect_clicked(clone!(@weak this => move |_| { | ||||
|             this.handle.backend.pl().clear(); | ||||
|         })); | ||||
| 
 | ||||
|         // position_scale.connect_button_press_event(clone!(@strong seeking => move |_, _| {
 | ||||
|  | @ -149,7 +192,7 @@ impl PlayerScreen { | |||
|             } | ||||
|         })); | ||||
| 
 | ||||
|         this.list.set_make_widget_cb(clone!(@strong this => move |index| { | ||||
|         this.list.set_make_widget_cb(clone!(@weak this => move |index| { | ||||
|             match this.items.borrow()[index] { | ||||
|                 ListItem::Header(item_index) => { | ||||
|                     let playlist_item = &this.playlist.borrow()[item_index]; | ||||
|  | @ -184,10 +227,8 @@ impl PlayerScreen { | |||
|                     row.set_activatable(true); | ||||
|                     row.set_title(Some(&title)); | ||||
| 
 | ||||
|                     row.connect_activated(clone!(@strong this => move |_| { | ||||
|                         if let Some(player) = &*this.player.borrow() { | ||||
|                             player.set_track(item_index, track_index).unwrap(); | ||||
|                         } | ||||
|                     row.connect_activated(clone!(@weak this => move |_| { | ||||
|                         this.handle.backend.pl().set_track(item_index, track_index).unwrap(); | ||||
|                     })); | ||||
| 
 | ||||
|                     let icon = if playing { | ||||
|  | @ -208,139 +249,13 @@ impl PlayerScreen { | |||
|             } | ||||
|         })); | ||||
| 
 | ||||
|         // list.set_make_widget(clone!(
 | ||||
|         //     @strong current_item,
 | ||||
|         //     @strong current_track
 | ||||
|         //     => move |element: &PlaylistElement| {
 | ||||
|         //         let title_label = gtk::Label::new(Some(&element.title));
 | ||||
|         //         title_label.set_ellipsize(pango::EllipsizeMode::End);
 | ||||
|         //         title_label.set_halign(gtk::Align::Start);
 | ||||
| 
 | ||||
|         //         let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
 | ||||
|         //         vbox.append(&title_label);
 | ||||
| 
 | ||||
|         //         if let Some(subtitle) = &element.subtitle {
 | ||||
|         //             let subtitle_label = gtk::Label::new(Some(&subtitle));
 | ||||
|         //             subtitle_label.set_ellipsize(pango::EllipsizeMode::End);
 | ||||
|         //             subtitle_label.set_halign(gtk::Align::Start);
 | ||||
|         //             subtitle_label.set_opacity(0.5);
 | ||||
|         //             vbox.append(&subtitle_label);
 | ||||
|         //         }
 | ||||
| 
 | ||||
|         //         let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
 | ||||
|         //         hbox.set_margin_top(6);
 | ||||
|         //         hbox.set_margin_bottom(6);
 | ||||
|         //         hbox.set_margin_start(6);
 | ||||
|         //         hbox.set_margin_end(6);
 | ||||
| 
 | ||||
|         //         if element.playable {
 | ||||
|         //             let image = gtk::Image::new();
 | ||||
| 
 | ||||
|         //             if element.item == current_item.get() && element.track == current_track.get() {
 | ||||
|         //                 image.set_from_icon_name(
 | ||||
|         //                     Some("media-playback-start-symbolic"),
 | ||||
|         //                     gtk::IconSize::Button,
 | ||||
|         //                 );
 | ||||
|         //             }
 | ||||
| 
 | ||||
|         //             hbox.append(&image);
 | ||||
|         //         } else if element.item > 0 {
 | ||||
|         //             hbox.set_margin_top(18);
 | ||||
|         //         }
 | ||||
|         //         hbox.append(&vbox);
 | ||||
|         //         hbox.upcast()
 | ||||
|         //     }
 | ||||
|         // ));
 | ||||
| 
 | ||||
|         // list.set_selected(clone!(@strong player => move |element| {
 | ||||
|         //     if let Some(player) = &*player.borrow() {
 | ||||
|         //         player.set_track(element.item, element.track).unwrap();
 | ||||
|         //     }
 | ||||
|         // }));
 | ||||
|         player.send_data(); | ||||
| 
 | ||||
|         this | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     pub fn set_player(self: Rc<Self>, player: Option<Rc<Player>>) { | ||||
|         self.player.replace(player.clone()); | ||||
| 
 | ||||
|         if let Some(player) = player { | ||||
|             player.add_playlist_cb(clone!(@strong self as this => move |playlist| { | ||||
|                 this.playlist.replace(playlist); | ||||
|                 this.show_playlist(); | ||||
|             })); | ||||
| 
 | ||||
|             player.add_track_cb(clone!(@strong self as this, @strong player => move |current_item, current_track| { | ||||
|                 this.previous_button.set_sensitive(player.has_previous()); | ||||
|                 this.next_button.set_sensitive(player.has_next()); | ||||
| 
 | ||||
|                 let item = &this.playlist.borrow()[current_item]; | ||||
|                 let track = &item.track_set.tracks[current_track]; | ||||
| 
 | ||||
|                 let mut parts = Vec::<String>::new(); | ||||
|                 for part in &track.work_parts { | ||||
|                     parts.push(item.track_set.recording.work.parts[*part].title.clone()); | ||||
|                 } | ||||
| 
 | ||||
|                 let mut title = item.track_set.recording.work.get_title(); | ||||
|                 if !parts.is_empty() { | ||||
|                     title = format!("{}: {}", title, parts.join(", ")); | ||||
|                 } | ||||
| 
 | ||||
|                 this.title_label.set_text(&title); | ||||
|                 this.subtitle_label.set_text(&item.track_set.recording.get_performers()); | ||||
|                 this.position_label.set_text("0:00"); | ||||
| 
 | ||||
|                 this.current_item.set(current_item); | ||||
|                 this.current_track.set(current_track); | ||||
| 
 | ||||
|                 this.show_playlist(); | ||||
|             })); | ||||
| 
 | ||||
|             player.add_duration_cb(clone!( | ||||
|                 @strong self.duration_label as duration_label, | ||||
|                 @strong self.position as position | ||||
|                 => move |ms| { | ||||
|                     let min = ms / 60000; | ||||
|                     let sec = (ms % 60000) / 1000; | ||||
|                     duration_label.set_text(&format!("{}:{:02}", min, sec)); | ||||
|                     position.set_upper(ms as f64); | ||||
|                 } | ||||
|             )); | ||||
| 
 | ||||
|             player.add_playing_cb(clone!( | ||||
|                 @strong self.play_button as play_button, | ||||
|                 @strong self.play_image as play_image, | ||||
|                 @strong self.pause_image as pause_image | ||||
|                 => move |playing| { | ||||
|                     play_button.set_child(Some(if playing { | ||||
|                         &pause_image | ||||
|                     } else { | ||||
|                         &play_image | ||||
|                     })); | ||||
|                 } | ||||
|             )); | ||||
| 
 | ||||
|             player.add_position_cb(clone!( | ||||
|                 @strong self.position_label as position_label, | ||||
|                 @strong self.position as position, | ||||
|                 @strong self.seeking as seeking | ||||
|                 => move |ms| { | ||||
|                     if !seeking.get() { | ||||
|                         let min = ms / 60000; | ||||
|                         let sec = (ms % 60000) / 1000; | ||||
|                         position_label.set_text(&format!("{}:{:02}", min, sec)); | ||||
|                         position.set_value(ms as f64); | ||||
|                     } | ||||
|                 } | ||||
|             )); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_back_cb<F: Fn() -> () + 'static>(&self, cb: F) { | ||||
|         self.back_cb.replace(Some(Box::new(cb))); | ||||
|     } | ||||
| 
 | ||||
| impl PlayerScreen { | ||||
|     /// Update the user interface according to the playlist.
 | ||||
|     fn show_playlist(&self) { | ||||
|         let playlist = self.playlist.borrow(); | ||||
|  | @ -370,3 +285,9 @@ impl PlayerScreen { | |||
|         self.list.update(length); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Widget for PlayerScreen { | ||||
|     fn get_widget(&self) -> gtk::Widget { | ||||
|         self.widget.clone().upcast() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										84
									
								
								crates/musicus/src/screens/welcome.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								crates/musicus/src/screens/welcome.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| use crate::navigator::{NavigationHandle, Screen}; | ||||
| use crate::widgets::Widget; | ||||
| use gettextrs::gettext; | ||||
| use glib::clone; | ||||
| use gtk::prelude::*; | ||||
| use std::rc::Rc; | ||||
| 
 | ||||
| /// A screen displaying a welcome message and the necessary means to set up the application. This
 | ||||
| /// screen doesn't access the backend except for setting the initial values and is safe to be used
 | ||||
| /// while the backend is loading.
 | ||||
| pub struct WelcomeScreen { | ||||
|     handle: NavigationHandle<()>, | ||||
|     widget: gtk::Box, | ||||
| } | ||||
| 
 | ||||
| impl Screen<(), ()> for WelcomeScreen { | ||||
|     fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> { | ||||
|         let widget = gtk::BoxBuilder::new() | ||||
|             .orientation(gtk::Orientation::Vertical) | ||||
|             .build(); | ||||
| 
 | ||||
|         let header = libadwaita::HeaderBarBuilder::new() | ||||
|             .title_widget(&libadwaita::WindowTitle::new(Some("Musicus"), None)) | ||||
|             .build(); | ||||
| 
 | ||||
|         let button = gtk::ButtonBuilder::new() | ||||
|             .halign(gtk::Align::Center) | ||||
|             .label(&gettext("Select folder")) | ||||
|             .build(); | ||||
| 
 | ||||
|         let welcome = libadwaita::StatusPageBuilder::new() | ||||
|             .icon_name("folder-music-symbolic") | ||||
|             .title(&gettext("Welcome to Musicus!")) | ||||
|             .description(&gettext("Get startet by selecting the folder containing your music \ | ||||
|                 files! Musicus will create a new database there or open one that already exists."))
 | ||||
|             .child(&button) | ||||
|             .build(); | ||||
| 
 | ||||
|         button.add_css_class("suggested-action"); | ||||
| 
 | ||||
|         widget.append(&header); | ||||
|         widget.append(&welcome); | ||||
| 
 | ||||
|         let this = Rc::new(Self { | ||||
|             handle, | ||||
|             widget, | ||||
|         }); | ||||
| 
 | ||||
|         button.connect_clicked(clone!(@weak this => move |_| { | ||||
|             let dialog = gtk::FileChooserDialog::new( | ||||
|                 Some(&gettext("Select music library folder")), | ||||
|                 Some(&this.handle.window), | ||||
|                 gtk::FileChooserAction::SelectFolder, | ||||
|                 &[ | ||||
|                     (&gettext("Cancel"), gtk::ResponseType::Cancel), | ||||
|                     (&gettext("Select"), gtk::ResponseType::Accept), | ||||
|                 ]); | ||||
| 
 | ||||
|             dialog.connect_response(clone!(@weak this => move |dialog, response| { | ||||
|                 if let gtk::ResponseType::Accept = response { | ||||
|                     if let Some(file) = dialog.get_file() { | ||||
|                         if let Some(path) = file.get_path() { | ||||
|                             spawn!(@clone this, async move { | ||||
|                                 this.handle.backend.set_music_library_path(path).await.unwrap(); | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 dialog.hide(); | ||||
|             })); | ||||
| 
 | ||||
|             dialog.show(); | ||||
|         })); | ||||
| 
 | ||||
|         this | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Widget for WelcomeScreen { | ||||
|     fn get_widget(&self) -> gtk::Widget { | ||||
|         self.widget.clone().upcast() | ||||
|     } | ||||
| } | ||||
|  | @ -15,9 +15,6 @@ pub use list::*; | |||
| pub mod player_bar; | ||||
| pub use player_bar::*; | ||||
| 
 | ||||
| pub mod poe_list; | ||||
| pub use poe_list::*; | ||||
| 
 | ||||
| pub mod screen; | ||||
| pub use screen::*; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,121 +0,0 @@ | |||
| use super::*; | ||||
| use glib::clone; | ||||
| use gtk_macros::get_widget; | ||||
| use libadwaita::prelude::*; | ||||
| use musicus_backend::Backend; | ||||
| use musicus_backend::db::{Person, Ensemble}; | ||||
| use std::cell::RefCell; | ||||
| use std::rc::Rc; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub enum PersonOrEnsemble { | ||||
|     Person(Person), | ||||
|     Ensemble(Ensemble), | ||||
| } | ||||
| 
 | ||||
| impl PersonOrEnsemble { | ||||
|     pub fn get_title(&self) -> String { | ||||
|         match self { | ||||
|             PersonOrEnsemble::Person(person) => person.name_lf(), | ||||
|             PersonOrEnsemble::Ensemble(ensemble) => ensemble.name.clone(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct PoeList { | ||||
|     pub widget: gtk::Box, | ||||
|     backend: Rc<Backend>, | ||||
|     stack: gtk::Stack, | ||||
|     search_entry: gtk::SearchEntry, | ||||
|     list: Rc<List>, | ||||
|     data: RefCell<Vec<PersonOrEnsemble>>, | ||||
|     selected_cb: RefCell<Option<Box<dyn Fn(&PersonOrEnsemble)>>>, | ||||
| } | ||||
| 
 | ||||
| impl PoeList { | ||||
|     pub fn new(backend: Rc<Backend>) -> Rc<Self> { | ||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/poe_list.ui"); | ||||
| 
 | ||||
|         get_widget!(builder, gtk::Box, widget); | ||||
|         get_widget!(builder, gtk::SearchEntry, search_entry); | ||||
|         get_widget!(builder, gtk::Stack, stack); | ||||
|         get_widget!(builder, gtk::ScrolledWindow, scrolled_window); | ||||
| 
 | ||||
|         let list = List::new(); | ||||
|         list.widget.add_css_class("navigation-sidebar"); | ||||
|         list.enable_selection(); | ||||
| 
 | ||||
|         scrolled_window.set_child(Some(&list.widget)); | ||||
| 
 | ||||
|         let this = Rc::new(Self { | ||||
|             widget, | ||||
|             backend, | ||||
|             stack, | ||||
|             search_entry, | ||||
|             list, | ||||
|             data: RefCell::new(Vec::new()), | ||||
|             selected_cb: RefCell::new(None), | ||||
|         }); | ||||
| 
 | ||||
|         this.search_entry.connect_search_changed(clone!(@strong this => move |_| { | ||||
|             this.list.invalidate_filter(); | ||||
|         })); | ||||
| 
 | ||||
|         this.list.set_make_widget_cb(clone!(@strong this => move |index| { | ||||
|             let poe = &this.data.borrow()[index]; | ||||
| 
 | ||||
|             let row = libadwaita::ActionRow::new(); | ||||
|             row.set_activatable(true); | ||||
|             row.set_title(Some(&poe.get_title())); | ||||
| 
 | ||||
|             let poe = poe.to_owned(); | ||||
|             row.connect_activated(clone!(@strong this => move |_| { | ||||
|                 if let Some(cb) = &*this.selected_cb.borrow() { | ||||
|                     cb(&poe); | ||||
|                 } | ||||
|             })); | ||||
| 
 | ||||
|             row.upcast() | ||||
|         })); | ||||
| 
 | ||||
|         this.list.set_filter_cb(clone!(@strong this => move |index| { | ||||
|             let poe = &this.data.borrow()[index]; | ||||
|             let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); | ||||
|             let title = poe.get_title().to_lowercase(); | ||||
|             search.is_empty() || title.contains(&search) | ||||
|         })); | ||||
| 
 | ||||
|         this | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_selected_cb<F: Fn(&PersonOrEnsemble) + 'static>(&self, cb: F) { | ||||
|         self.selected_cb.replace(Some(Box::new(cb))); | ||||
|     } | ||||
| 
 | ||||
|     pub fn reload(self: Rc<Self>) { | ||||
|         self.stack.set_visible_child_name("loading"); | ||||
| 
 | ||||
|         let context = glib::MainContext::default(); | ||||
|         let backend = self.backend.clone(); | ||||
| 
 | ||||
|         context.spawn_local(async move { | ||||
|             let persons = backend.db().get_persons().await.unwrap(); | ||||
|             let ensembles = backend.db().get_ensembles().await.unwrap(); | ||||
|             let mut poes: Vec<PersonOrEnsemble> = Vec::new(); | ||||
| 
 | ||||
|             for person in persons { | ||||
|                 poes.push(PersonOrEnsemble::Person(person)); | ||||
|             } | ||||
| 
 | ||||
|             for ensemble in ensembles { | ||||
|                 poes.push(PersonOrEnsemble::Ensemble(ensemble)); | ||||
|             } | ||||
| 
 | ||||
|             let length = poes.len(); | ||||
|             self.data.replace(poes); | ||||
|             self.list.update(length); | ||||
| 
 | ||||
|             self.stack.set_visible_child_name("content"); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | @ -1,217 +1,93 @@ | |||
| use crate::config; | ||||
| use crate::import::SourceSelector; | ||||
| use crate::preferences::Preferences; | ||||
| use crate::screens::{EnsembleScreen, PersonScreen, PlayerScreen}; | ||||
| use crate::widgets::*; | ||||
| use crate::navigator::{Navigator, NavigatorWindow}; | ||||
| use futures::prelude::*; | ||||
| use gettextrs::gettext; | ||||
| use gio::prelude::*; | ||||
| use glib::clone; | ||||
| use crate::screens::{MainScreen, WelcomeScreen}; | ||||
| use crate::navigator::Navigator; | ||||
| use gtk::prelude::*; | ||||
| use gtk_macros::{action, get_widget}; | ||||
| use musicus_backend::{Backend, BackendState}; | ||||
| use std::rc::Rc; | ||||
| 
 | ||||
| /// The main window of this application. This will also handle initializing and managing the
 | ||||
| /// backend.
 | ||||
| pub struct Window { | ||||
|     backend: Rc<Backend>, | ||||
|     window: libadwaita::ApplicationWindow, | ||||
|     stack: gtk::Stack, | ||||
|     leaflet: libadwaita::Leaflet, | ||||
|     sidebar_box: gtk::Box, | ||||
|     poe_list: Rc<PoeList>, | ||||
|     backend: Rc<Backend>, | ||||
|     navigator: Rc<Navigator>, | ||||
|     player_bar: PlayerBar, | ||||
|     player_screen: Rc<PlayerScreen>, | ||||
| } | ||||
| 
 | ||||
| impl Window { | ||||
|     pub fn new(app: >k::Application) -> Rc<Self> { | ||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/window.ui"); | ||||
| 
 | ||||
|         get_widget!(builder, libadwaita::ApplicationWindow, window); | ||||
|         get_widget!(builder, gtk::Stack, stack); | ||||
|         get_widget!(builder, gtk::Button, select_music_library_path_button); | ||||
|         get_widget!(builder, gtk::Box, content_box); | ||||
|         get_widget!(builder, libadwaita::Leaflet, leaflet); | ||||
|         get_widget!(builder, gtk::Button, add_button); | ||||
|         get_widget!(builder, gtk::Box, sidebar_box); | ||||
|         get_widget!(builder, gtk::Box, empty_screen); | ||||
| 
 | ||||
|         let backend = Rc::new(Backend::new()); | ||||
| 
 | ||||
|         let player_screen = PlayerScreen::new(); | ||||
|         stack.add_named(&player_screen.widget, Some("player_screen")); | ||||
|         let window = libadwaita::ApplicationWindow::new(app); | ||||
|         window.set_title(Some("Musicus")); | ||||
|         window.set_default_size(1000, 707); | ||||
| 
 | ||||
|         let poe_list = PoeList::new(backend.clone()); | ||||
|         let navigator = Navigator::new(backend.clone(), &window, &empty_screen); | ||||
|         navigator.set_back_cb(clone!(@strong leaflet, @strong sidebar_box => move || { | ||||
|             leaflet.set_visible_child(&sidebar_box); | ||||
|         })); | ||||
|         let loading_screen = gtk::BoxBuilder::new() | ||||
|             .orientation(gtk::Orientation::Vertical) | ||||
|             .build(); | ||||
| 
 | ||||
|         let player_bar = PlayerBar::new(); | ||||
|         content_box.append(&player_bar.widget); | ||||
|         let header = libadwaita::HeaderBarBuilder::new() | ||||
|             .title_widget(&libadwaita::WindowTitle::new(Some("Musicus"), None)) | ||||
|             .build(); | ||||
| 
 | ||||
|         let result = Rc::new(Self { | ||||
|         let spinner = gtk::SpinnerBuilder::new() | ||||
|             .hexpand(true) | ||||
|             .vexpand(true) | ||||
|             .halign(gtk::Align::Center) | ||||
|             .valign(gtk::Align::Center) | ||||
|             .width_request(32) | ||||
|             .height_request(32) | ||||
|             .spinning(true) | ||||
|             .build(); | ||||
| 
 | ||||
|         loading_screen.append(&header); | ||||
|         loading_screen.append(&spinner); | ||||
| 
 | ||||
| 
 | ||||
|         let navigator = Navigator::new(Rc::clone(&backend), &window, &loading_screen); | ||||
|         libadwaita::ApplicationWindowExt::set_child(&window, Some(&navigator.widget)); | ||||
| 
 | ||||
|         let this = Rc::new(Self { | ||||
|             backend, | ||||
|             window, | ||||
|             stack, | ||||
|             leaflet, | ||||
|             sidebar_box, | ||||
|             poe_list, | ||||
|             navigator, | ||||
|             player_bar, | ||||
|             player_screen, | ||||
|         }); | ||||
| 
 | ||||
|         result.window.set_application(Some(app)); | ||||
| 
 | ||||
|         select_music_library_path_button.connect_clicked(clone!(@strong result => move |_| { | ||||
|             let dialog = gtk::FileChooserDialog::new( | ||||
|                 Some(&gettext("Select music library folder")), | ||||
|                 Some(&result.window), | ||||
|                 gtk::FileChooserAction::SelectFolder, | ||||
|                 &[ | ||||
|                     (&gettext("Cancel"), gtk::ResponseType::Cancel), | ||||
|                     (&gettext("Select"), gtk::ResponseType::Accept), | ||||
|                 ]); | ||||
| 
 | ||||
|             dialog.connect_response(clone!(@strong result => move |dialog, response| { | ||||
|                 if let gtk::ResponseType::Accept = response { | ||||
|                     if let Some(file) = dialog.get_file() { | ||||
|                         if let Some(path) = file.get_path() { | ||||
|                             let context = glib::MainContext::default(); | ||||
|                             let backend = result.backend.clone(); | ||||
|                             context.spawn_local(async move { | ||||
|                                 backend.set_music_library_path(path).await.unwrap(); | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 dialog.hide(); | ||||
|             })); | ||||
| 
 | ||||
|             dialog.show(); | ||||
|         })); | ||||
| 
 | ||||
|         add_button.connect_clicked(clone!(@strong result => move |_| { | ||||
|             spawn!(@clone result, async move { | ||||
|                 let window = NavigatorWindow::new(result.backend.clone()); | ||||
|                 replace!(window.navigator, SourceSelector).await; | ||||
|             }); | ||||
|         })); | ||||
| 
 | ||||
|         result | ||||
|             .player_bar | ||||
|             .set_playlist_cb(clone!(@strong result => move || { | ||||
|                 result.stack.set_visible_child_name("player_screen"); | ||||
|             })); | ||||
| 
 | ||||
|         result | ||||
|             .player_screen | ||||
|             .set_back_cb(clone!(@strong result => move || { | ||||
|                 result.stack.set_visible_child_name("content"); | ||||
|             })); | ||||
| 
 | ||||
|         action!( | ||||
|             result.window, | ||||
|             "preferences", | ||||
|             clone!(@strong result => move |_, _| { | ||||
|                 Preferences::new(result.backend.clone(), &result.window).show(); | ||||
|             }) | ||||
|         ); | ||||
| 
 | ||||
|         action!( | ||||
|             result.window, | ||||
|             "about", | ||||
|             clone!(@strong result => move |_, _| { | ||||
|                 result.show_about_dialog(); | ||||
|             }) | ||||
|         ); | ||||
| 
 | ||||
|         let context = glib::MainContext::default(); | ||||
|         let clone = result.clone(); | ||||
|         context.spawn_local(async move { | ||||
|             let mut state_stream = clone.backend.state_stream.borrow_mut(); | ||||
|             while let Some(state) = state_stream.next().await { | ||||
|         spawn!(@clone this, async move { | ||||
|             while let Some(state) = this.backend.next_state().await { | ||||
|                 match state { | ||||
|                     BackendState::NoMusicLibrary => { | ||||
|                         clone.stack.set_visible_child_name("empty"); | ||||
|                     } | ||||
|                     BackendState::Loading => { | ||||
|                         clone.stack.set_visible_child_name("loading"); | ||||
|                     } | ||||
|                     BackendState::Ready => { | ||||
|                         clone.stack.set_visible_child_name("content"); | ||||
|                         clone.poe_list.clone().reload(); | ||||
|                         clone.navigator.reset(); | ||||
| 
 | ||||
|                         let player = clone.backend.get_player().unwrap(); | ||||
| 
 | ||||
|                         player.set_raise_cb(clone!(@weak clone => move || { | ||||
|                             clone.present(); | ||||
|                         })); | ||||
| 
 | ||||
|                         clone.player_bar.set_player(Some(player.clone())); | ||||
|                         clone.player_screen.clone().set_player(Some(player)); | ||||
|                     } | ||||
|                     BackendState::Loading => this.navigator.reset(), | ||||
|                     BackendState::NoMusicLibrary => this.show_welcome_screen(), | ||||
|                     BackendState::Ready => this.show_main_screen(), | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         let clone = result.clone(); | ||||
|         context.spawn_local(async move { | ||||
|         spawn!(@clone this, async move { | ||||
|             // This is not done in the async block above, because backend state changes may happen
 | ||||
|             // while this method is running.
 | ||||
|             clone.backend.clone().init().await.unwrap(); | ||||
|             this.backend.init().await.unwrap(); | ||||
|         }); | ||||
| 
 | ||||
|         result.leaflet.append(&result.navigator.widget); | ||||
| 
 | ||||
|         result | ||||
|             .poe_list | ||||
|             .set_selected_cb(clone!(@strong result => move |poe| { | ||||
|                 result.leaflet.set_visible_child(&result.navigator.widget); | ||||
|                 let poe = poe.to_owned(); | ||||
|                 spawn!(@clone result, async move { | ||||
|                     match poe { | ||||
|                         PersonOrEnsemble::Person(person) => { | ||||
|                             replace!(result.navigator, PersonScreen, person.clone()).await; | ||||
|                         } | ||||
|                         PersonOrEnsemble::Ensemble(ensemble) => { | ||||
|                             replace!(result.navigator, EnsembleScreen, ensemble.clone()).await; | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             })); | ||||
| 
 | ||||
|         result | ||||
|             .sidebar_box | ||||
|             .append(&result.poe_list.widget); | ||||
| 
 | ||||
|         result | ||||
|         this | ||||
|     } | ||||
| 
 | ||||
|     /// Present this window to the user.
 | ||||
|     pub fn present(&self) { | ||||
|         self.window.present(); | ||||
|     } | ||||
| 
 | ||||
|     fn show_about_dialog(&self) { | ||||
|         let dialog = gtk::AboutDialogBuilder::new() | ||||
|             .transient_for(&self.window) | ||||
|             .modal(true) | ||||
|             .logo_icon_name("de.johrpan.musicus") | ||||
|             .program_name(&gettext("Musicus")) | ||||
|             .version(config::VERSION) | ||||
|             .comments(&gettext("The classical music player and organizer.")) | ||||
|             .website("https://github.com/johrpan/musicus") | ||||
|             .website_label(&gettext("Further information and source code")) | ||||
|             .copyright("© 2020 Elias Projahn") | ||||
|             .license_type(gtk::License::Agpl30) | ||||
|             .authors(vec![String::from("Elias Projahn <johrpan@gmail.com>")]) | ||||
|             .build(); | ||||
|     /// Replace the current screen with the welcome screen.
 | ||||
|     fn show_welcome_screen(self: &Rc<Self>) { | ||||
|         let this = self; | ||||
|         spawn!(@clone this, async move { | ||||
|             replace!(this.navigator, WelcomeScreen).await; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|         dialog.show(); | ||||
|     /// Replace the current screen with the main screen.
 | ||||
|     fn show_main_screen(self: &Rc<Self>) { | ||||
|         let this = self; | ||||
|         spawn!(@clone this, async move { | ||||
|             replace!(this.navigator, MainScreen).await; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ edition = "2018" | |||
| 
 | ||||
| [dependencies] | ||||
| fragile = "1.0.0" | ||||
| futures = "0.3.6" | ||||
| futures-channel = "0.3.5" | ||||
| gio = "0.9.1" | ||||
| glib = "0.10.3" | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| use futures::prelude::*; | ||||
| use futures_channel::mpsc; | ||||
| use gio::prelude::*; | ||||
| use log::warn; | ||||
|  | @ -40,7 +41,7 @@ pub enum BackendState { | |||
| pub struct Backend { | ||||
|     /// A future resolving to the next state of the backend. Initially, this should be assumed to
 | ||||
|     /// be BackendState::Loading. Changes should be awaited before calling init().
 | ||||
|     pub state_stream: RefCell<mpsc::Receiver<BackendState>>, | ||||
|     state_stream: RefCell<mpsc::Receiver<BackendState>>, | ||||
| 
 | ||||
|     /// The internal sender to publish the state via state_stream.
 | ||||
|     state_sender: RefCell<mpsc::Sender<BackendState>>, | ||||
|  | @ -80,8 +81,14 @@ impl Backend { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Wait for the next state change. Initially, the state should be assumed to be
 | ||||
|     /// BackendState::Loading. Changes should be awaited before calling init().
 | ||||
|     pub async fn next_state(&self) -> Option<BackendState> { | ||||
|         self.state_stream.borrow_mut().next().await | ||||
|     } | ||||
| 
 | ||||
|     /// Initialize the backend updating the state accordingly.
 | ||||
|     pub async fn init(self: Rc<Backend>) -> Result<()> { | ||||
|     pub async fn init(&self) -> Result<()> { | ||||
|         self.init_library().await?; | ||||
| 
 | ||||
|         if let Some(url) = self.settings.get_string("server-url") { | ||||
|  | @ -97,6 +104,12 @@ impl Backend { | |||
|             _ => (), | ||||
|         } | ||||
| 
 | ||||
|         if self.get_music_library_path().is_none() { | ||||
|             self.set_state(BackendState::NoMusicLibrary); | ||||
|         } else { | ||||
|             self.set_state(BackendState::Ready); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -353,6 +353,24 @@ impl Player { | |||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn send_data(&self) { | ||||
|         for cb in &*self.playlist_cbs.borrow() { | ||||
|             cb(self.playlist.borrow().clone()); | ||||
|         } | ||||
| 
 | ||||
|         for cb in &*self.track_cbs.borrow() { | ||||
|             cb(self.current_item.get().unwrap(), self.current_track.get().unwrap()); | ||||
|         } | ||||
| 
 | ||||
|         for cb in &*self.duration_cbs.borrow() { | ||||
|             cb(self.player.get_duration().mseconds().unwrap()); | ||||
|         } | ||||
| 
 | ||||
|         for cb in &*self.playing_cbs.borrow() { | ||||
|             cb(self.is_playing()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn clear(&self) { | ||||
|         self.player.stop(); | ||||
|         self.playing.set(false); | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
|     <gresource prefix="/de/johrpan/musicus"> | ||||
|         <file preprocess="xml-stripblanks">ui/editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/login_dialog.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/main_screen.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/medium_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/performance_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/player_bar.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/player_screen.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/poe_list.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/preferences.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/recording_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/register_dialog.ui</file> | ||||
|  | @ -19,7 +19,6 @@ | |||
|         <file preprocess="xml-stripblanks">ui/track_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/track_selector.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/track_set_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/window.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/work_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/work_part_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/work_section_editor.ui</file> | ||||
|  |  | |||
							
								
								
									
										147
									
								
								res/ui/main_screen.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								res/ui/main_screen.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,147 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <interface> | ||||
|   <requires lib="gtk" version="4.0" /> | ||||
|   <requires lib="libadwaita" version="1.0" /> | ||||
|   <object class="GtkBox" id="empty_screen"> | ||||
|     <property name="orientation">vertical</property> | ||||
|     <child> | ||||
|       <object class="AdwHeaderBar"> | ||||
|         <property name="title-widget"> | ||||
|           <object class="GtkLabel"/> | ||||
|         </property> | ||||
|       </object> | ||||
|     </child> | ||||
|     <child> | ||||
|       <object class="AdwStatusPage"> | ||||
|         <property name="icon-name">folder-music-symbolic</property> | ||||
|         <property name="title" translatable="yes">Welcome to Musicus!</property> | ||||
|         <property name="description" translatable="yes">Get startet by selecting something from the sidebar or adding new things to your library using the button in the top left corner.</property> | ||||
|         <property name="vexpand">true</property> | ||||
|       </object> | ||||
|     </child> | ||||
|   </object> | ||||
|   <object class="GtkBox" id="widget"> | ||||
|     <property name="orientation">vertical</property> | ||||
|     <child> | ||||
|       <object class="AdwLeaflet" id="leaflet"> | ||||
|         <property name="vexpand">true</property> | ||||
|         <child> | ||||
|           <object class="AdwLeafletPage"> | ||||
|             <property name="name">sidebar</property> | ||||
|             <property name="child"> | ||||
|               <object class="GtkBox"> | ||||
|                 <property name="hexpand">False</property> | ||||
|                 <property name="orientation">vertical</property> | ||||
|                 <child> | ||||
|                   <object class="AdwHeaderBar"> | ||||
|                     <property name="show-start-title-buttons">false</property> | ||||
|                     <property name="show-end-title-buttons">false</property> | ||||
|                     <property name="title-widget"> | ||||
|                       <object class="GtkLabel"> | ||||
|                         <property name="label">Musicus</property> | ||||
|                         <style> | ||||
|                           <class name="title"/> | ||||
|                         </style> | ||||
|                       </object> | ||||
|                     </property> | ||||
|                     <child> | ||||
|                       <object class="GtkButton" id="add_button"> | ||||
|                         <property name="receives-default">True</property> | ||||
|                         <child> | ||||
|                           <object class="GtkImage"> | ||||
|                             <property name="icon-name">list-add-symbolic</property> | ||||
|                           </object> | ||||
|                         </child> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                     <child type="end"> | ||||
|                       <object class="GtkMenuButton"> | ||||
|                         <property name="receives-default">True</property> | ||||
|                         <property name="icon-name">open-menu-symbolic</property> | ||||
|                         <property name="menu-model">menu</property> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                   </object> | ||||
|                 </child> | ||||
|                 <child> | ||||
|                   <object class="GtkSearchBar"> | ||||
|                     <property name="search-mode-enabled">True</property> | ||||
|                     <child> | ||||
|                       <object class="AdwClamp"> | ||||
|                         <property name="maximum-size">400</property> | ||||
|                         <property name="tightening-threshold">300</property> | ||||
|                         <property name="hexpand">true</property> | ||||
|                         <child> | ||||
|                           <object class="GtkSearchEntry" id="search_entry"> | ||||
|                             <property name="placeholder-text" translatable="yes">Search persons and ensembles …</property> | ||||
|                           </object> | ||||
|                         </child> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                   </object> | ||||
|                 </child> | ||||
|                 <child> | ||||
|                   <object class="GtkStack" id="stack"> | ||||
|                     <property name="hexpand">True</property> | ||||
|                     <property name="transition-type">crossfade</property> | ||||
|                     <child> | ||||
|                       <object class="GtkStackPage"> | ||||
|                         <property name="name">loading</property> | ||||
|                         <property name="child"> | ||||
|                           <object class="GtkSpinner"> | ||||
|                             <property name="spinning">True</property> | ||||
|                             <property name="hexpand">True</property> | ||||
|                             <property name="vexpand">True</property> | ||||
|                             <property name="halign">center</property> | ||||
|                             <property name="valign">center</property> | ||||
|                           </object> | ||||
|                         </property> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                     <child> | ||||
|                       <object class="GtkStackPage"> | ||||
|                         <property name="name">content</property> | ||||
|                         <property name="child"> | ||||
|                           <object class="GtkScrolledWindow" id="scroll"> | ||||
|                             <child> | ||||
|                               <placeholder/> | ||||
|                             </child> | ||||
|                           </object> | ||||
|                         </property> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                   </object> | ||||
|                 </child> | ||||
|               </object> | ||||
|             </property> | ||||
|           </object> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="AdwLeafletPage"> | ||||
|             <property name="navigatable">False</property> | ||||
|             <property name="child"> | ||||
|               <object class="GtkSeparator"> | ||||
|                 <property name="orientation">vertical</property> | ||||
|                 <style> | ||||
|                   <class name="sidebar" /> | ||||
|                 </style> | ||||
|               </object> | ||||
|             </property> | ||||
|           </object> | ||||
|         </child> | ||||
|       </object> | ||||
|     </child> | ||||
|   </object> | ||||
|   <menu id="menu"> | ||||
|     <section> | ||||
|       <item> | ||||
|         <attribute name="label" translatable="yes">Preferences</attribute> | ||||
|         <attribute name="action">widget.preferences</attribute> | ||||
|       </item> | ||||
|       <item> | ||||
|         <attribute name="label" translatable="yes">About Musicus</attribute> | ||||
|         <attribute name="action">widget.about</attribute> | ||||
|       </item> | ||||
|     </section> | ||||
|   </menu> | ||||
| </interface> | ||||
|  | @ -1,57 +0,0 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <interface> | ||||
|   <requires lib="gtk" version="4.0"/> | ||||
|   <requires lib="libadwaita" version="1.0"/> | ||||
|   <object class="GtkBox" id="widget"> | ||||
|     <property name="orientation">vertical</property> | ||||
|     <child> | ||||
|       <object class="GtkSearchBar"> | ||||
|         <property name="search-mode-enabled">True</property> | ||||
|         <child> | ||||
|           <object class="AdwClamp"> | ||||
|             <property name="maximum-size">400</property> | ||||
|             <property name="tightening-threshold">300</property> | ||||
|             <property name="hexpand">true</property> | ||||
|             <child> | ||||
|               <object class="GtkSearchEntry" id="search_entry"> | ||||
|                 <property name="placeholder-text" translatable="yes">Search persons and ensembles …</property> | ||||
|               </object> | ||||
|             </child> | ||||
|           </object> | ||||
|         </child> | ||||
|       </object> | ||||
|     </child> | ||||
|     <child> | ||||
|       <object class="GtkStack" id="stack"> | ||||
|         <property name="hexpand">True</property> | ||||
|         <property name="transition-type">crossfade</property> | ||||
|         <child> | ||||
|           <object class="GtkStackPage"> | ||||
|             <property name="name">loading</property> | ||||
|             <property name="child"> | ||||
|               <object class="GtkSpinner"> | ||||
|                 <property name="spinning">True</property> | ||||
|                 <property name="hexpand">True</property> | ||||
|                 <property name="vexpand">True</property> | ||||
|                 <property name="halign">center</property> | ||||
|                 <property name="valign">center</property> | ||||
|               </object> | ||||
|             </property> | ||||
|           </object> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkStackPage"> | ||||
|             <property name="name">content</property> | ||||
|             <property name="child"> | ||||
|               <object class="GtkScrolledWindow" id="scrolled_window"> | ||||
|                 <child> | ||||
|                   <placeholder/> | ||||
|                 </child> | ||||
|               </object> | ||||
|             </property> | ||||
|           </object> | ||||
|         </child> | ||||
|       </object> | ||||
|     </child> | ||||
|   </object> | ||||
| </interface> | ||||
							
								
								
									
										237
									
								
								res/ui/window.ui
									
										
									
									
									
								
							
							
						
						
									
										237
									
								
								res/ui/window.ui
									
										
									
									
									
								
							|  | @ -1,237 +0,0 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <interface> | ||||
|   <requires lib="gtk" version="4.0" /> | ||||
|   <requires lib="libadwaita" version="1.0" /> | ||||
|   <object class="GtkBox" id="empty_screen"> | ||||
|     <property name="orientation">vertical</property> | ||||
|     <child> | ||||
|       <object class="AdwHeaderBar"> | ||||
|         <property name="title-widget"> | ||||
|           <object class="GtkLabel"/> | ||||
|         </property> | ||||
|       </object> | ||||
|     </child> | ||||
|     <child> | ||||
|       <object class="GtkBox"> | ||||
|         <property name="vexpand">True</property> | ||||
|         <property name="halign">center</property> | ||||
|         <property name="valign">center</property> | ||||
|         <property name="margin-top">18</property> | ||||
|         <property name="margin-bottom">18</property> | ||||
|         <property name="margin-start">18</property> | ||||
|         <property name="margin-end">18</property> | ||||
|         <property name="orientation">vertical</property> | ||||
|         <property name="spacing">18</property> | ||||
|         <child> | ||||
|           <object class="GtkImage"> | ||||
|             <property name="opacity">0.5</property> | ||||
|             <property name="pixel-size">80</property> | ||||
|             <property name="icon-name">folder-music-symbolic</property> | ||||
|           </object> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkLabel"> | ||||
|             <property name="opacity">0.5</property> | ||||
|             <property name="label" translatable="yes">Welcome to Musicus!</property> | ||||
|             <attributes> | ||||
|               <attribute name="size" value="16384" /> | ||||
|             </attributes> | ||||
|           </object> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkLabel"> | ||||
|             <property name="opacity">0.5</property> | ||||
|             <property name="label" translatable="yes">Get startet by selecting something from the sidebar or adding new things to your library using the button in the top left corner.</property> | ||||
|             <property name="justify">center</property> | ||||
|             <property name="wrap">True</property> | ||||
|             <property name="max-width-chars">40</property> | ||||
|           </object> | ||||
|         </child> | ||||
|       </object> | ||||
|     </child> | ||||
|   </object> | ||||
|   <object class="AdwApplicationWindow" id="window"> | ||||
|     <property name="default-width">800</property> | ||||
|     <property name="default-height">566</property> | ||||
|     <child> | ||||
|       <object class="GtkStack" id="stack"> | ||||
|         <property name="transition-type">crossfade</property> | ||||
|         <child> | ||||
|           <object class="GtkStackPage"> | ||||
|             <property name="name">empty</property> | ||||
|             <property name="child"> | ||||
|               <object class="GtkBox"> | ||||
|                 <property name="orientation">vertical</property> | ||||
|                 <child> | ||||
|                   <object class="AdwHeaderBar"> | ||||
|                     <property name="title-widget"> | ||||
|                       <object class="GtkLabel"> | ||||
|                         <property name="label">Musicus</property> | ||||
|                         <style> | ||||
|                           <class name="title"/> | ||||
|                         </style> | ||||
|                       </object> | ||||
|                     </property> | ||||
|                   </object> | ||||
|                 </child> | ||||
|                 <child> | ||||
|                   <object class="GtkBox"> | ||||
|                     <property name="vexpand">True</property> | ||||
|                     <property name="halign">center</property> | ||||
|                     <property name="valign">center</property> | ||||
|                     <property name="margin-top">18</property> | ||||
|                     <property name="margin-bottom">18</property> | ||||
|                     <property name="margin-start">18</property> | ||||
|                     <property name="margin-end">18</property> | ||||
|                     <property name="orientation">vertical</property> | ||||
|                     <property name="spacing">18</property> | ||||
|                     <child> | ||||
|                       <object class="GtkImage"> | ||||
|                         <property name="opacity">0.5019607843137255</property> | ||||
|                         <property name="pixel-size">80</property> | ||||
|                         <property name="icon-name">folder-music-symbolic</property> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                     <child> | ||||
|                       <object class="GtkLabel"> | ||||
|                         <property name="opacity">0.5019607843137255</property> | ||||
|                         <property name="label" translatable="yes">Welcome to Musicus!</property> | ||||
|                         <attributes> | ||||
|                           <attribute name="size" value="16384" /> | ||||
|                         </attributes> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                     <child> | ||||
|                       <object class="GtkLabel"> | ||||
|                         <property name="opacity">0.5019607843137255</property> | ||||
|                         <property name="label" translatable="yes">Get startet by selecting the folder containing your music files! Musicus will create a new database there or open one that already exists.</property> | ||||
|                         <property name="justify">center</property> | ||||
|                         <property name="wrap">True</property> | ||||
|                         <property name="max-width-chars">40</property> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                     <child> | ||||
|                       <object class="GtkButton" id="select_music_library_path_button"> | ||||
|                         <property name="label" translatable="yes">Select folder</property> | ||||
|                         <property name="receives-default">True</property> | ||||
|                         <property name="halign">center</property> | ||||
|                         <style> | ||||
|                           <class name="suggested-action" /> | ||||
|                         </style> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                   </object> | ||||
|                 </child> | ||||
|               </object> | ||||
|             </property> | ||||
|           </object> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkStackPage"> | ||||
|             <property name="name">loading</property> | ||||
|             <property name="child"> | ||||
|               <object class="GtkBox"> | ||||
|                 <property name="orientation">vertical</property> | ||||
|                 <child> | ||||
|                   <object class="AdwHeaderBar"> | ||||
|                   </object> | ||||
|                 </child> | ||||
|                 <child> | ||||
|                   <object class="GtkSpinner"> | ||||
|                     <property name="spinning">True</property> | ||||
|                     <property name="vexpand">True</property> | ||||
|                     <property name="hexpand">True</property> | ||||
|                     <property name="valign">center</property> | ||||
|                     <property name="halign">center</property> | ||||
|                   </object> | ||||
|                 </child> | ||||
|               </object> | ||||
|             </property> | ||||
|           </object> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkStackPage"> | ||||
|             <property name="name">content</property> | ||||
|             <property name="child"> | ||||
|               <object class="GtkBox" id="content_box"> | ||||
|                 <property name="orientation">vertical</property> | ||||
|                 <child> | ||||
|                   <object class="AdwLeaflet" id="leaflet"> | ||||
|                     <property name="vexpand">true</property> | ||||
|                     <child> | ||||
|                       <object class="AdwLeafletPage"> | ||||
|                         <property name="child"> | ||||
|                           <object class="GtkBox" id="sidebar_box"> | ||||
|                             <property name="width-request">250</property> | ||||
|                             <property name="hexpand">False</property> | ||||
|                             <property name="orientation">vertical</property> | ||||
|                             <child> | ||||
|                               <object class="AdwHeaderBar"> | ||||
|                                 <property name="show-start-title-buttons">false</property> | ||||
|                                 <property name="show-end-title-buttons">false</property> | ||||
|                                 <property name="title-widget"> | ||||
|                                   <object class="GtkLabel"> | ||||
|                                     <property name="label">Musicus</property> | ||||
|                                     <style> | ||||
|                                       <class name="title"/> | ||||
|                                     </style> | ||||
|                                   </object> | ||||
|                                 </property> | ||||
|                                 <child> | ||||
|                                   <object class="GtkButton" id="add_button"> | ||||
|                                     <property name="receives-default">True</property> | ||||
|                                     <child> | ||||
|                                       <object class="GtkImage"> | ||||
|                                         <property name="icon-name">list-add-symbolic</property> | ||||
|                                       </object> | ||||
|                                     </child> | ||||
|                                   </object> | ||||
|                                 </child> | ||||
|                                 <child type="end"> | ||||
|                                   <object class="GtkMenuButton"> | ||||
|                                     <property name="receives-default">True</property> | ||||
|                                     <property name="icon-name">open-menu-symbolic</property> | ||||
|                                     <property name="menu-model">menu</property> | ||||
|                                   </object> | ||||
|                                 </child> | ||||
|                               </object> | ||||
|                             </child> | ||||
|                           </object> | ||||
|                         </property> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                     <child> | ||||
|                       <object class="AdwLeafletPage"> | ||||
|                         <property name="navigatable">False</property> | ||||
|                         <property name="child"> | ||||
|                           <object class="GtkSeparator"> | ||||
|                             <property name="orientation">vertical</property> | ||||
|                             <style> | ||||
|                               <class name="sidebar" /> | ||||
|                             </style> | ||||
|                           </object> | ||||
|                         </property> | ||||
|                       </object> | ||||
|                     </child> | ||||
|                   </object> | ||||
|                 </child> | ||||
|               </object> | ||||
|             </property> | ||||
|           </object> | ||||
|         </child> | ||||
|       </object> | ||||
|     </child> | ||||
|   </object> | ||||
|   <menu id="menu"> | ||||
|     <section> | ||||
|       <item> | ||||
|         <attribute name="label" translatable="yes">Preferences</attribute> | ||||
|         <attribute name="action">win.preferences</attribute> | ||||
|       </item> | ||||
|       <item> | ||||
|         <attribute name="label" translatable="yes">About Musicus</attribute> | ||||
|         <attribute name="action">win.about</attribute> | ||||
|       </item> | ||||
|     </section> | ||||
|   </menu> | ||||
| </interface> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn