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" | anyhow = "1.0.33" | ||||||
| async-trait = "0.1.42" | async-trait = "0.1.42" | ||||||
| discid = "0.4.4" | discid = "0.4.4" | ||||||
| futures = "0.3.6" |  | ||||||
| futures-channel = "0.3.5" | futures-channel = "0.3.5" | ||||||
| gettext-rs = "0.5.0" | gettext-rs = "0.5.0" | ||||||
| gstreamer = "0.16.4" | 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 mod ensemble; | ||||||
| pub use ensemble::*; | pub use ensemble::*; | ||||||
| 
 | 
 | ||||||
|  | pub mod main; | ||||||
|  | pub use main::*; | ||||||
|  | 
 | ||||||
| pub mod person; | pub mod person; | ||||||
| pub use person::*; | pub use person::*; | ||||||
| 
 | 
 | ||||||
| pub mod player_screen; | pub mod player; | ||||||
| pub use player_screen::*; | pub use player::*; | ||||||
| 
 | 
 | ||||||
| pub mod work; | pub mod work; | ||||||
| pub use work::*; | pub use work::*; | ||||||
| 
 | 
 | ||||||
|  | pub mod welcome; | ||||||
|  | pub use welcome::*; | ||||||
|  | 
 | ||||||
| pub mod recording; | pub mod recording; | ||||||
| pub use 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 gettextrs::gettext; | ||||||
| use glib::clone; | use glib::clone; | ||||||
| use gtk::prelude::*; | use gtk::prelude::*; | ||||||
| use gtk_macros::get_widget; | use gtk_macros::get_widget; | ||||||
| use libadwaita::prelude::*; | use libadwaita::prelude::*; | ||||||
| use musicus_backend::{Player, PlaylistItem}; | use musicus_backend::PlaylistItem; | ||||||
| use std::cell::{Cell, RefCell}; | use std::cell::{Cell, RefCell}; | ||||||
| use std::rc::Rc; | use std::rc::Rc; | ||||||
| 
 | 
 | ||||||
|  | @ -23,7 +24,8 @@ enum ListItem { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct PlayerScreen { | pub struct PlayerScreen { | ||||||
|     pub widget: gtk::Box, |     handle: NavigationHandle<()>, | ||||||
|  |     widget: gtk::Box, | ||||||
|     title_label: gtk::Label, |     title_label: gtk::Label, | ||||||
|     subtitle_label: gtk::Label, |     subtitle_label: gtk::Label, | ||||||
|     previous_button: gtk::Button, |     previous_button: gtk::Button, | ||||||
|  | @ -37,15 +39,13 @@ pub struct PlayerScreen { | ||||||
|     list: Rc<List>, |     list: Rc<List>, | ||||||
|     playlist: RefCell<Vec<PlaylistItem>>, |     playlist: RefCell<Vec<PlaylistItem>>, | ||||||
|     items: RefCell<Vec<ListItem>>, |     items: RefCell<Vec<ListItem>>, | ||||||
|     player: RefCell<Option<Rc<Player>>>, |  | ||||||
|     seeking: Cell<bool>, |     seeking: Cell<bool>, | ||||||
|     current_item: Cell<usize>, |     current_item: Cell<usize>, | ||||||
|     current_track: Cell<usize>, |     current_track: Cell<usize>, | ||||||
|     back_cb: RefCell<Option<Box<dyn Fn()>>>, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl PlayerScreen { | impl Screen<(), ()> for PlayerScreen { | ||||||
|     pub fn new() -> Rc<Self> { |     fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> { | ||||||
|         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui"); |         let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui"); | ||||||
| 
 | 
 | ||||||
|         get_widget!(builder, gtk::Box, widget); |         get_widget!(builder, gtk::Box, widget); | ||||||
|  | @ -68,6 +68,7 @@ impl PlayerScreen { | ||||||
|         frame.set_child(Some(&list.widget)); |         frame.set_child(Some(&list.widget)); | ||||||
| 
 | 
 | ||||||
|         let this = Rc::new(Self { |         let this = Rc::new(Self { | ||||||
|  |             handle, | ||||||
|             widget, |             widget, | ||||||
|             title_label, |             title_label, | ||||||
|             subtitle_label, |             subtitle_label, | ||||||
|  | @ -82,45 +83,87 @@ impl PlayerScreen { | ||||||
|             list, |             list, | ||||||
|             items: RefCell::new(Vec::new()), |             items: RefCell::new(Vec::new()), | ||||||
|             playlist: RefCell::new(Vec::new()), |             playlist: RefCell::new(Vec::new()), | ||||||
|             player: RefCell::new(None), |  | ||||||
|             seeking: Cell::new(false), |             seeking: Cell::new(false), | ||||||
|             current_item: Cell::new(0), |             current_item: Cell::new(0), | ||||||
|             current_track: Cell::new(0), |             current_track: Cell::new(0), | ||||||
|             back_cb: RefCell::new(None), |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         back_button.connect_clicked(clone!(@strong this => move |_| { |         let player = &this.handle.backend.pl(); | ||||||
|             if let Some(cb) = &*this.back_cb.borrow() { | 
 | ||||||
|                 cb(); |         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 |_| { |         back_button.connect_clicked(clone!(@weak this => move |_| { | ||||||
|             if let Some(player) = &*this.player.borrow() { |             this.handle.pop(None); | ||||||
|                 player.previous().unwrap(); |  | ||||||
|             } |  | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         this.play_button.connect_clicked(clone!(@strong this => move |_| { |         this.previous_button.connect_clicked(clone!(@weak this => move |_| { | ||||||
|             if let Some(player) = &*this.player.borrow() { |             this.handle.backend.pl().previous().unwrap(); | ||||||
|                 player.play_pause(); |  | ||||||
|             } |  | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         this.next_button.connect_clicked(clone!(@strong this => move |_| { |         this.play_button.connect_clicked(clone!(@weak this => move |_| { | ||||||
|             if let Some(player) = &*this.player.borrow() { |             this.handle.backend.pl().play_pause(); | ||||||
|                 player.next().unwrap(); |  | ||||||
|             } |  | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         stop_button.connect_clicked(clone!(@strong this => move |_| { |         this.next_button.connect_clicked(clone!(@weak this => move |_| { | ||||||
|             if let Some(player) = &*this.player.borrow() { |             this.handle.backend.pl().next().unwrap(); | ||||||
|                 if let Some(cb) = &*this.back_cb.borrow() { |         })); | ||||||
|                     cb(); |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 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 |_, _| {
 |         // 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] { |             match this.items.borrow()[index] { | ||||||
|                 ListItem::Header(item_index) => { |                 ListItem::Header(item_index) => { | ||||||
|                     let playlist_item = &this.playlist.borrow()[item_index]; |                     let playlist_item = &this.playlist.borrow()[item_index]; | ||||||
|  | @ -184,10 +227,8 @@ impl PlayerScreen { | ||||||
|                     row.set_activatable(true); |                     row.set_activatable(true); | ||||||
|                     row.set_title(Some(&title)); |                     row.set_title(Some(&title)); | ||||||
| 
 | 
 | ||||||
|                     row.connect_activated(clone!(@strong this => move |_| { |                     row.connect_activated(clone!(@weak this => move |_| { | ||||||
|                         if let Some(player) = &*this.player.borrow() { |                         this.handle.backend.pl().set_track(item_index, track_index).unwrap(); | ||||||
|                             player.set_track(item_index, track_index).unwrap(); |  | ||||||
|                         } |  | ||||||
|                     })); |                     })); | ||||||
| 
 | 
 | ||||||
|                     let icon = if playing { |                     let icon = if playing { | ||||||
|  | @ -208,139 +249,13 @@ impl PlayerScreen { | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         // list.set_make_widget(clone!(
 |         player.send_data(); | ||||||
|         //     @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();
 |  | ||||||
|         //     }
 |  | ||||||
|         // }));
 |  | ||||||
| 
 | 
 | ||||||
|         this |         this | ||||||
|     } |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     pub fn set_player(self: Rc<Self>, player: Option<Rc<Player>>) { | impl PlayerScreen { | ||||||
|         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))); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Update the user interface according to the playlist.
 |     /// Update the user interface according to the playlist.
 | ||||||
|     fn show_playlist(&self) { |     fn show_playlist(&self) { | ||||||
|         let playlist = self.playlist.borrow(); |         let playlist = self.playlist.borrow(); | ||||||
|  | @ -370,3 +285,9 @@ impl PlayerScreen { | ||||||
|         self.list.update(length); |         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 mod player_bar; | ||||||
| pub use player_bar::*; | pub use player_bar::*; | ||||||
| 
 | 
 | ||||||
| pub mod poe_list; |  | ||||||
| pub use poe_list::*; |  | ||||||
| 
 |  | ||||||
| pub mod screen; | pub mod screen; | ||||||
| pub use 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::screens::{MainScreen, WelcomeScreen}; | ||||||
| use crate::import::SourceSelector; | use crate::navigator::Navigator; | ||||||
| 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 gtk::prelude::*; | use gtk::prelude::*; | ||||||
| use gtk_macros::{action, get_widget}; |  | ||||||
| use musicus_backend::{Backend, BackendState}; | use musicus_backend::{Backend, BackendState}; | ||||||
| use std::rc::Rc; | use std::rc::Rc; | ||||||
| 
 | 
 | ||||||
|  | /// The main window of this application. This will also handle initializing and managing the
 | ||||||
|  | /// backend.
 | ||||||
| pub struct Window { | pub struct Window { | ||||||
|     backend: Rc<Backend>, |  | ||||||
|     window: libadwaita::ApplicationWindow, |     window: libadwaita::ApplicationWindow, | ||||||
|     stack: gtk::Stack, |     backend: Rc<Backend>, | ||||||
|     leaflet: libadwaita::Leaflet, |  | ||||||
|     sidebar_box: gtk::Box, |  | ||||||
|     poe_list: Rc<PoeList>, |  | ||||||
|     navigator: Rc<Navigator>, |     navigator: Rc<Navigator>, | ||||||
|     player_bar: PlayerBar, |  | ||||||
|     player_screen: Rc<PlayerScreen>, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Window { | impl Window { | ||||||
|     pub fn new(app: >k::Application) -> Rc<Self> { |     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 backend = Rc::new(Backend::new()); | ||||||
| 
 | 
 | ||||||
|         let player_screen = PlayerScreen::new(); |         let window = libadwaita::ApplicationWindow::new(app); | ||||||
|         stack.add_named(&player_screen.widget, Some("player_screen")); |         window.set_title(Some("Musicus")); | ||||||
|  |         window.set_default_size(1000, 707); | ||||||
| 
 | 
 | ||||||
|         let poe_list = PoeList::new(backend.clone()); |         let loading_screen = gtk::BoxBuilder::new() | ||||||
|         let navigator = Navigator::new(backend.clone(), &window, &empty_screen); |             .orientation(gtk::Orientation::Vertical) | ||||||
|         navigator.set_back_cb(clone!(@strong leaflet, @strong sidebar_box => move || { |             .build(); | ||||||
|             leaflet.set_visible_child(&sidebar_box); |  | ||||||
|         })); |  | ||||||
| 
 | 
 | ||||||
|         let player_bar = PlayerBar::new(); |         let header = libadwaita::HeaderBarBuilder::new() | ||||||
|         content_box.append(&player_bar.widget); |             .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, |             backend, | ||||||
|             window, |             window, | ||||||
|             stack, |  | ||||||
|             leaflet, |  | ||||||
|             sidebar_box, |  | ||||||
|             poe_list, |  | ||||||
|             navigator, |             navigator, | ||||||
|             player_bar, |  | ||||||
|             player_screen, |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         result.window.set_application(Some(app)); |         spawn!(@clone this, async move { | ||||||
| 
 |             while let Some(state) = this.backend.next_state().await { | ||||||
|         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 { |  | ||||||
|                 match state { |                 match state { | ||||||
|                     BackendState::NoMusicLibrary => { |                     BackendState::Loading => this.navigator.reset(), | ||||||
|                         clone.stack.set_visible_child_name("empty"); |                     BackendState::NoMusicLibrary => this.show_welcome_screen(), | ||||||
|                     } |                     BackendState::Ready => this.show_main_screen(), | ||||||
|                     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)); |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         let clone = result.clone(); |         spawn!(@clone this, async move { | ||||||
|         context.spawn_local(async move { |  | ||||||
|             // This is not done in the async block above, because backend state changes may happen
 |             // This is not done in the async block above, because backend state changes may happen
 | ||||||
|             // while this method is running.
 |             // while this method is running.
 | ||||||
|             clone.backend.clone().init().await.unwrap(); |             this.backend.init().await.unwrap(); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         result.leaflet.append(&result.navigator.widget); |         this | ||||||
| 
 |  | ||||||
|         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 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Present this window to the user.
 | ||||||
|     pub fn present(&self) { |     pub fn present(&self) { | ||||||
|         self.window.present(); |         self.window.present(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn show_about_dialog(&self) { |     /// Replace the current screen with the welcome screen.
 | ||||||
|         let dialog = gtk::AboutDialogBuilder::new() |     fn show_welcome_screen(self: &Rc<Self>) { | ||||||
|             .transient_for(&self.window) |         let this = self; | ||||||
|             .modal(true) |         spawn!(@clone this, async move { | ||||||
|             .logo_icon_name("de.johrpan.musicus") |             replace!(this.navigator, WelcomeScreen).await; | ||||||
|             .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(); |     /// 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] | [dependencies] | ||||||
| fragile = "1.0.0" | fragile = "1.0.0" | ||||||
|  | futures = "0.3.6" | ||||||
| futures-channel = "0.3.5" | futures-channel = "0.3.5" | ||||||
| gio = "0.9.1" | gio = "0.9.1" | ||||||
| glib = "0.10.3" | glib = "0.10.3" | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | use futures::prelude::*; | ||||||
| use futures_channel::mpsc; | use futures_channel::mpsc; | ||||||
| use gio::prelude::*; | use gio::prelude::*; | ||||||
| use log::warn; | use log::warn; | ||||||
|  | @ -40,7 +41,7 @@ pub enum BackendState { | ||||||
| pub struct Backend { | pub struct Backend { | ||||||
|     /// A future resolving to the next state of the backend. Initially, this should be assumed to
 |     /// 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().
 |     /// 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.
 |     /// The internal sender to publish the state via state_stream.
 | ||||||
|     state_sender: RefCell<mpsc::Sender<BackendState>>, |     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.
 |     /// 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?; |         self.init_library().await?; | ||||||
| 
 | 
 | ||||||
|         if let Some(url) = self.settings.get_string("server-url") { |         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(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -353,6 +353,24 @@ impl Player { | ||||||
|         Ok(()) |         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) { |     pub fn clear(&self) { | ||||||
|         self.player.stop(); |         self.player.stop(); | ||||||
|         self.playing.set(false); |         self.playing.set(false); | ||||||
|  |  | ||||||
|  | @ -3,11 +3,11 @@ | ||||||
|     <gresource prefix="/de/johrpan/musicus"> |     <gresource prefix="/de/johrpan/musicus"> | ||||||
|         <file preprocess="xml-stripblanks">ui/editor.ui</file> |         <file preprocess="xml-stripblanks">ui/editor.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/login_dialog.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/medium_editor.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/performance_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_bar.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/player_screen.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/preferences.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/recording_editor.ui</file> |         <file preprocess="xml-stripblanks">ui/recording_editor.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/register_dialog.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_editor.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/track_selector.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/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_editor.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/work_part_editor.ui</file> |         <file preprocess="xml-stripblanks">ui/work_part_editor.ui</file> | ||||||
|         <file preprocess="xml-stripblanks">ui/work_section_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