diff --git a/crates/musicus/Cargo.toml b/crates/musicus/Cargo.toml index 7451c8d..36df77e 100644 --- a/crates/musicus/Cargo.toml +++ b/crates/musicus/Cargo.toml @@ -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" diff --git a/crates/musicus/src/screens/main.rs b/crates/musicus/src/screens/main.rs new file mode 100644 index 0000000..2f0c29a --- /dev/null +++ b/crates/musicus/src/screens/main.rs @@ -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, + navigator: Rc, + poes: RefCell>, +} + +impl Screen<(), ()> for MainScreen { + /// Create a new main screen. + fn new(_: (), handle: NavigationHandle<()>) -> Rc { + 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 ")]) + .build(); + + dialog.show(); + } +} diff --git a/crates/musicus/src/screens/mod.rs b/crates/musicus/src/screens/mod.rs index 395629d..e1e0d57 100644 --- a/crates/musicus/src/screens/mod.rs +++ b/crates/musicus/src/screens/mod.rs @@ -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::*; diff --git a/crates/musicus/src/screens/player_screen.rs b/crates/musicus/src/screens/player.rs similarity index 50% rename from crates/musicus/src/screens/player_screen.rs rename to crates/musicus/src/screens/player.rs index dc40fb7..34de98c 100644 --- a/crates/musicus/src/screens/player_screen.rs +++ b/crates/musicus/src/screens/player.rs @@ -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, playlist: RefCell>, items: RefCell>, - player: RefCell>>, seeking: Cell, current_item: Cell, current_track: Cell, - back_cb: RefCell>>, } -impl PlayerScreen { - pub fn new() -> Rc { +impl Screen<(), ()> for PlayerScreen { + fn new(_: (), handle: NavigationHandle<()>) -> Rc { 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::::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, player: Option>) { - 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::::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 () + '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() + } +} diff --git a/crates/musicus/src/screens/welcome.rs b/crates/musicus/src/screens/welcome.rs new file mode 100644 index 0000000..55e7f23 --- /dev/null +++ b/crates/musicus/src/screens/welcome.rs @@ -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 { + 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() + } +} diff --git a/crates/musicus/src/widgets/mod.rs b/crates/musicus/src/widgets/mod.rs index e1f8135..07cd38a 100644 --- a/crates/musicus/src/widgets/mod.rs +++ b/crates/musicus/src/widgets/mod.rs @@ -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::*; diff --git a/crates/musicus/src/widgets/poe_list.rs b/crates/musicus/src/widgets/poe_list.rs deleted file mode 100644 index 87f760f..0000000 --- a/crates/musicus/src/widgets/poe_list.rs +++ /dev/null @@ -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, - stack: gtk::Stack, - search_entry: gtk::SearchEntry, - list: Rc, - data: RefCell>, - selected_cb: RefCell>>, -} - -impl PoeList { - pub fn new(backend: Rc) -> Rc { - 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(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - pub fn reload(self: Rc) { - 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 = 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"); - }); - } -} diff --git a/crates/musicus/src/window.rs b/crates/musicus/src/window.rs index a9c8914..c55d78f 100644 --- a/crates/musicus/src/window.rs +++ b/crates/musicus/src/window.rs @@ -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, window: libadwaita::ApplicationWindow, - stack: gtk::Stack, - leaflet: libadwaita::Leaflet, - sidebar_box: gtk::Box, - poe_list: Rc, + backend: Rc, navigator: Rc, - player_bar: PlayerBar, - player_screen: Rc, } impl Window { pub fn new(app: >k::Application) -> Rc { - 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 ")]) - .build(); + /// Replace the current screen with the welcome screen. + fn show_welcome_screen(self: &Rc) { + 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) { + let this = self; + spawn!(@clone this, async move { + replace!(this.navigator, MainScreen).await; + }); } } diff --git a/crates/musicus_backend/Cargo.toml b/crates/musicus_backend/Cargo.toml index bc70aed..4f24d8c 100644 --- a/crates/musicus_backend/Cargo.toml +++ b/crates/musicus_backend/Cargo.toml @@ -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" diff --git a/crates/musicus_backend/src/lib.rs b/crates/musicus_backend/src/lib.rs index 80d2cc8..c21fc19 100644 --- a/crates/musicus_backend/src/lib.rs +++ b/crates/musicus_backend/src/lib.rs @@ -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>, + state_stream: RefCell>, /// The internal sender to publish the state via state_stream. state_sender: RefCell>, @@ -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 { + self.state_stream.borrow_mut().next().await + } + /// Initialize the backend updating the state accordingly. - pub async fn init(self: Rc) -> 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(()) } diff --git a/crates/musicus_backend/src/player.rs b/crates/musicus_backend/src/player.rs index 1e76370..0a397ba 100644 --- a/crates/musicus_backend/src/player.rs +++ b/crates/musicus_backend/src/player.rs @@ -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); diff --git a/res/musicus.gresource.xml b/res/musicus.gresource.xml index 8295cd6..1918ed5 100644 --- a/res/musicus.gresource.xml +++ b/res/musicus.gresource.xml @@ -3,11 +3,11 @@ ui/editor.ui ui/login_dialog.ui + ui/main_screen.ui ui/medium_editor.ui ui/performance_editor.ui ui/player_bar.ui ui/player_screen.ui - ui/poe_list.ui ui/preferences.ui ui/recording_editor.ui ui/register_dialog.ui @@ -19,7 +19,6 @@ ui/track_editor.ui ui/track_selector.ui ui/track_set_editor.ui - ui/window.ui ui/work_editor.ui ui/work_part_editor.ui ui/work_section_editor.ui diff --git a/res/ui/main_screen.ui b/res/ui/main_screen.ui new file mode 100644 index 0000000..7cd6ad2 --- /dev/null +++ b/res/ui/main_screen.ui @@ -0,0 +1,147 @@ + + + + + + vertical + + + + + + + + + + folder-music-symbolic + Welcome to Musicus! + Get startet by selecting something from the sidebar or adding new things to your library using the button in the top left corner. + true + + + + + vertical + + + true + + + sidebar + + + False + vertical + + + false + false + + + Musicus + + + + + + True + + + list-add-symbolic + + + + + + + True + open-menu-symbolic + menu + + + + + + + True + + + 400 + 300 + true + + + Search persons and ensembles … + + + + + + + + + True + crossfade + + + loading + + + True + True + True + center + center + + + + + + + content + + + + + + + + + + + + + + + + + + False + + + vertical + + + + + + + + + +
+ + Preferences + widget.preferences + + + About Musicus + widget.about + +
+
+ diff --git a/res/ui/poe_list.ui b/res/ui/poe_list.ui deleted file mode 100644 index 8016452..0000000 --- a/res/ui/poe_list.ui +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - vertical - - - True - - - 400 - 300 - true - - - Search persons and ensembles … - - - - - - - - - True - crossfade - - - loading - - - True - True - True - center - center - - - - - - - content - - - - - - - - - - - - - diff --git a/res/ui/window.ui b/res/ui/window.ui deleted file mode 100644 index 22e1465..0000000 --- a/res/ui/window.ui +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - vertical - - - - - - - - - - True - center - center - 18 - 18 - 18 - 18 - vertical - 18 - - - 0.5 - 80 - folder-music-symbolic - - - - - 0.5 - Welcome to Musicus! - - - - - - - - 0.5 - Get startet by selecting something from the sidebar or adding new things to your library using the button in the top left corner. - center - True - 40 - - - - - - - 800 - 566 - - - crossfade - - - empty - - - vertical - - - - - Musicus - - - - - - - - True - center - center - 18 - 18 - 18 - 18 - vertical - 18 - - - 0.5019607843137255 - 80 - folder-music-symbolic - - - - - 0.5019607843137255 - Welcome to Musicus! - - - - - - - - 0.5019607843137255 - Get startet by selecting the folder containing your music files! Musicus will create a new database there or open one that already exists. - center - True - 40 - - - - - Select folder - True - center - - - - - - - - - - - - loading - - - vertical - - - - - - - True - True - True - center - center - - - - - - - - - content - - - vertical - - - true - - - - - 250 - False - vertical - - - false - false - - - Musicus - - - - - - True - - - list-add-symbolic - - - - - - - True - open-menu-symbolic - menu - - - - - - - - - - - False - - - vertical - - - - - - - - - - - - - - - -
- - Preferences - win.preferences - - - About Musicus - win.about - -
-
-