Use navigator for main window

This commit is contained in:
Elias Projahn 2021-02-05 15:50:31 +01:00
parent 43023fdb5b
commit 88e1c97143
15 changed files with 619 additions and 768 deletions

View file

@ -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"

View 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();
}
}

View file

@ -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::*;

View file

@ -1,10 +1,11 @@
use crate::widgets::*;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{List, Widget};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use musicus_backend::{Player, PlaylistItem};
use musicus_backend::PlaylistItem;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
@ -23,7 +24,8 @@ enum ListItem {
}
pub struct PlayerScreen {
pub widget: gtk::Box,
handle: NavigationHandle<()>,
widget: gtk::Box,
title_label: gtk::Label,
subtitle_label: gtk::Label,
previous_button: gtk::Button,
@ -37,15 +39,13 @@ pub struct PlayerScreen {
list: Rc<List>,
playlist: RefCell<Vec<PlaylistItem>>,
items: RefCell<Vec<ListItem>>,
player: RefCell<Option<Rc<Player>>>,
seeking: Cell<bool>,
current_item: Cell<usize>,
current_track: Cell<usize>,
back_cb: RefCell<Option<Box<dyn Fn()>>>,
}
impl PlayerScreen {
pub fn new() -> Rc<Self> {
impl Screen<(), ()> for PlayerScreen {
fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui");
get_widget!(builder, gtk::Box, widget);
@ -68,6 +68,7 @@ impl PlayerScreen {
frame.set_child(Some(&list.widget));
let this = Rc::new(Self {
handle,
widget,
title_label,
subtitle_label,
@ -82,45 +83,87 @@ impl PlayerScreen {
list,
items: RefCell::new(Vec::new()),
playlist: RefCell::new(Vec::new()),
player: RefCell::new(None),
seeking: Cell::new(false),
current_item: Cell::new(0),
current_track: Cell::new(0),
back_cb: RefCell::new(None),
});
back_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.back_cb.borrow() {
cb();
let player = &this.handle.backend.pl();
player.add_playlist_cb(clone!(@weak this => move |playlist| {
this.playlist.replace(playlist);
this.show_playlist();
}));
player.add_track_cb(clone!(@weak this, @weak player => move |current_item, current_track| {
this.previous_button.set_sensitive(this.handle.backend.pl().has_previous());
this.next_button.set_sensitive(this.handle.backend.pl().has_next());
let item = &this.playlist.borrow()[current_item];
let track = &item.track_set.tracks[current_track];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.track_set.recording.work.parts[*part].title.clone());
}
let mut title = item.track_set.recording.work.get_title();
if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", "));
}
this.title_label.set_text(&title);
this.subtitle_label.set_text(&item.track_set.recording.get_performers());
this.position_label.set_text("0:00");
this.current_item.set(current_item);
this.current_track.set(current_track);
this.show_playlist();
}));
player.add_duration_cb(clone!(@weak this => move |ms| {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
this.duration_label.set_text(&format!("{}:{:02}", min, sec));
this.position.set_upper(ms as f64);
}));
player.add_playing_cb(clone!(@weak this => move |playing| {
this.play_button.set_child(Some(if playing {
&this.pause_image
} else {
&this.play_image
}));
}));
player.add_position_cb(clone!(@weak this => move |ms| {
if !this.seeking.get() {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
this.position_label.set_text(&format!("{}:{:02}", min, sec));
this.position.set_value(ms as f64);
}
}));
this.previous_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.previous().unwrap();
}
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
this.play_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.play_pause();
}
this.previous_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.backend.pl().previous().unwrap();
}));
this.next_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.next().unwrap();
}
this.play_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.backend.pl().play_pause();
}));
stop_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
if let Some(cb) = &*this.back_cb.borrow() {
cb();
}
this.next_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.backend.pl().next().unwrap();
}));
player.clear();
}
stop_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.backend.pl().clear();
}));
// position_scale.connect_button_press_event(clone!(@strong seeking => move |_, _| {
@ -149,7 +192,7 @@ impl PlayerScreen {
}
}));
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
match this.items.borrow()[index] {
ListItem::Header(item_index) => {
let playlist_item = &this.playlist.borrow()[item_index];
@ -184,10 +227,8 @@ impl PlayerScreen {
row.set_activatable(true);
row.set_title(Some(&title));
row.connect_activated(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.set_track(item_index, track_index).unwrap();
}
row.connect_activated(clone!(@weak this => move |_| {
this.handle.backend.pl().set_track(item_index, track_index).unwrap();
}));
let icon = if playing {
@ -208,139 +249,13 @@ impl PlayerScreen {
}
}));
// list.set_make_widget(clone!(
// @strong current_item,
// @strong current_track
// => move |element: &PlaylistElement| {
// let title_label = gtk::Label::new(Some(&element.title));
// title_label.set_ellipsize(pango::EllipsizeMode::End);
// title_label.set_halign(gtk::Align::Start);
// let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
// vbox.append(&title_label);
// if let Some(subtitle) = &element.subtitle {
// let subtitle_label = gtk::Label::new(Some(&subtitle));
// subtitle_label.set_ellipsize(pango::EllipsizeMode::End);
// subtitle_label.set_halign(gtk::Align::Start);
// subtitle_label.set_opacity(0.5);
// vbox.append(&subtitle_label);
// }
// let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
// hbox.set_margin_top(6);
// hbox.set_margin_bottom(6);
// hbox.set_margin_start(6);
// hbox.set_margin_end(6);
// if element.playable {
// let image = gtk::Image::new();
// if element.item == current_item.get() && element.track == current_track.get() {
// image.set_from_icon_name(
// Some("media-playback-start-symbolic"),
// gtk::IconSize::Button,
// );
// }
// hbox.append(&image);
// } else if element.item > 0 {
// hbox.set_margin_top(18);
// }
// hbox.append(&vbox);
// hbox.upcast()
// }
// ));
// list.set_selected(clone!(@strong player => move |element| {
// if let Some(player) = &*player.borrow() {
// player.set_track(element.item, element.track).unwrap();
// }
// }));
player.send_data();
this
}
pub fn set_player(self: Rc<Self>, player: Option<Rc<Player>>) {
self.player.replace(player.clone());
if let Some(player) = player {
player.add_playlist_cb(clone!(@strong self as this => move |playlist| {
this.playlist.replace(playlist);
this.show_playlist();
}));
player.add_track_cb(clone!(@strong self as this, @strong player => move |current_item, current_track| {
this.previous_button.set_sensitive(player.has_previous());
this.next_button.set_sensitive(player.has_next());
let item = &this.playlist.borrow()[current_item];
let track = &item.track_set.tracks[current_track];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.track_set.recording.work.parts[*part].title.clone());
}
let mut title = item.track_set.recording.work.get_title();
if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", "));
}
this.title_label.set_text(&title);
this.subtitle_label.set_text(&item.track_set.recording.get_performers());
this.position_label.set_text("0:00");
this.current_item.set(current_item);
this.current_track.set(current_track);
this.show_playlist();
}));
player.add_duration_cb(clone!(
@strong self.duration_label as duration_label,
@strong self.position as position
=> move |ms| {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
duration_label.set_text(&format!("{}:{:02}", min, sec));
position.set_upper(ms as f64);
}
));
player.add_playing_cb(clone!(
@strong self.play_button as play_button,
@strong self.play_image as play_image,
@strong self.pause_image as pause_image
=> move |playing| {
play_button.set_child(Some(if playing {
&pause_image
} else {
&play_image
}));
}
));
player.add_position_cb(clone!(
@strong self.position_label as position_label,
@strong self.position as position,
@strong self.seeking as seeking
=> move |ms| {
if !seeking.get() {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
position_label.set_text(&format!("{}:{:02}", min, sec));
position.set_value(ms as f64);
}
}
));
}
}
pub fn set_back_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.back_cb.replace(Some(Box::new(cb)));
}
impl PlayerScreen {
/// Update the user interface according to the playlist.
fn show_playlist(&self) {
let playlist = self.playlist.borrow();
@ -370,3 +285,9 @@ impl PlayerScreen {
self.list.update(length);
}
}
impl Widget for PlayerScreen {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View 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()
}
}

View file

@ -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::*;

View file

@ -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");
});
}
}

View file

@ -1,217 +1,93 @@
use crate::config;
use crate::import::SourceSelector;
use crate::preferences::Preferences;
use crate::screens::{EnsembleScreen, PersonScreen, PlayerScreen};
use crate::widgets::*;
use crate::navigator::{Navigator, NavigatorWindow};
use futures::prelude::*;
use gettextrs::gettext;
use gio::prelude::*;
use glib::clone;
use crate::screens::{MainScreen, WelcomeScreen};
use crate::navigator::Navigator;
use gtk::prelude::*;
use gtk_macros::{action, get_widget};
use musicus_backend::{Backend, BackendState};
use std::rc::Rc;
/// The main window of this application. This will also handle initializing and managing the
/// backend.
pub struct Window {
backend: Rc<Backend>,
window: libadwaita::ApplicationWindow,
stack: gtk::Stack,
leaflet: libadwaita::Leaflet,
sidebar_box: gtk::Box,
poe_list: Rc<PoeList>,
backend: Rc<Backend>,
navigator: Rc<Navigator>,
player_bar: PlayerBar,
player_screen: Rc<PlayerScreen>,
}
impl Window {
pub fn new(app: &gtk::Application) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/window.ui");
get_widget!(builder, libadwaita::ApplicationWindow, window);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Button, select_music_library_path_button);
get_widget!(builder, gtk::Box, content_box);
get_widget!(builder, libadwaita::Leaflet, leaflet);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Box, sidebar_box);
get_widget!(builder, gtk::Box, empty_screen);
let backend = Rc::new(Backend::new());
let player_screen = PlayerScreen::new();
stack.add_named(&player_screen.widget, Some("player_screen"));
let window = libadwaita::ApplicationWindow::new(app);
window.set_title(Some("Musicus"));
window.set_default_size(1000, 707);
let poe_list = PoeList::new(backend.clone());
let navigator = Navigator::new(backend.clone(), &window, &empty_screen);
navigator.set_back_cb(clone!(@strong leaflet, @strong sidebar_box => move || {
leaflet.set_visible_child(&sidebar_box);
}));
let loading_screen = gtk::BoxBuilder::new()
.orientation(gtk::Orientation::Vertical)
.build();
let player_bar = PlayerBar::new();
content_box.append(&player_bar.widget);
let header = libadwaita::HeaderBarBuilder::new()
.title_widget(&libadwaita::WindowTitle::new(Some("Musicus"), None))
.build();
let result = Rc::new(Self {
let spinner = gtk::SpinnerBuilder::new()
.hexpand(true)
.vexpand(true)
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.width_request(32)
.height_request(32)
.spinning(true)
.build();
loading_screen.append(&header);
loading_screen.append(&spinner);
let navigator = Navigator::new(Rc::clone(&backend), &window, &loading_screen);
libadwaita::ApplicationWindowExt::set_child(&window, Some(&navigator.widget));
let this = Rc::new(Self {
backend,
window,
stack,
leaflet,
sidebar_box,
poe_list,
navigator,
player_bar,
player_screen,
});
result.window.set_application(Some(app));
select_music_library_path_button.connect_clicked(clone!(@strong result => move |_| {
let dialog = gtk::FileChooserDialog::new(
Some(&gettext("Select music library folder")),
Some(&result.window),
gtk::FileChooserAction::SelectFolder,
&[
(&gettext("Cancel"), gtk::ResponseType::Cancel),
(&gettext("Select"), gtk::ResponseType::Accept),
]);
dialog.connect_response(clone!(@strong result => move |dialog, response| {
if let gtk::ResponseType::Accept = response {
if let Some(file) = dialog.get_file() {
if let Some(path) = file.get_path() {
let context = glib::MainContext::default();
let backend = result.backend.clone();
context.spawn_local(async move {
backend.set_music_library_path(path).await.unwrap();
});
}
}
}
dialog.hide();
}));
dialog.show();
}));
add_button.connect_clicked(clone!(@strong result => move |_| {
spawn!(@clone result, async move {
let window = NavigatorWindow::new(result.backend.clone());
replace!(window.navigator, SourceSelector).await;
});
}));
result
.player_bar
.set_playlist_cb(clone!(@strong result => move || {
result.stack.set_visible_child_name("player_screen");
}));
result
.player_screen
.set_back_cb(clone!(@strong result => move || {
result.stack.set_visible_child_name("content");
}));
action!(
result.window,
"preferences",
clone!(@strong result => move |_, _| {
Preferences::new(result.backend.clone(), &result.window).show();
})
);
action!(
result.window,
"about",
clone!(@strong result => move |_, _| {
result.show_about_dialog();
})
);
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let mut state_stream = clone.backend.state_stream.borrow_mut();
while let Some(state) = state_stream.next().await {
spawn!(@clone this, async move {
while let Some(state) = this.backend.next_state().await {
match state {
BackendState::NoMusicLibrary => {
clone.stack.set_visible_child_name("empty");
}
BackendState::Loading => {
clone.stack.set_visible_child_name("loading");
}
BackendState::Ready => {
clone.stack.set_visible_child_name("content");
clone.poe_list.clone().reload();
clone.navigator.reset();
let player = clone.backend.get_player().unwrap();
player.set_raise_cb(clone!(@weak clone => move || {
clone.present();
}));
clone.player_bar.set_player(Some(player.clone()));
clone.player_screen.clone().set_player(Some(player));
}
BackendState::Loading => this.navigator.reset(),
BackendState::NoMusicLibrary => this.show_welcome_screen(),
BackendState::Ready => this.show_main_screen(),
}
}
});
let clone = result.clone();
context.spawn_local(async move {
spawn!(@clone this, async move {
// This is not done in the async block above, because backend state changes may happen
// while this method is running.
clone.backend.clone().init().await.unwrap();
this.backend.init().await.unwrap();
});
result.leaflet.append(&result.navigator.widget);
result
.poe_list
.set_selected_cb(clone!(@strong result => move |poe| {
result.leaflet.set_visible_child(&result.navigator.widget);
let poe = poe.to_owned();
spawn!(@clone result, async move {
match poe {
PersonOrEnsemble::Person(person) => {
replace!(result.navigator, PersonScreen, person.clone()).await;
}
PersonOrEnsemble::Ensemble(ensemble) => {
replace!(result.navigator, EnsembleScreen, ensemble.clone()).await;
}
}
});
}));
result
.sidebar_box
.append(&result.poe_list.widget);
result
this
}
/// Present this window to the user.
pub fn present(&self) {
self.window.present();
}
fn show_about_dialog(&self) {
let dialog = gtk::AboutDialogBuilder::new()
.transient_for(&self.window)
.modal(true)
.logo_icon_name("de.johrpan.musicus")
.program_name(&gettext("Musicus"))
.version(config::VERSION)
.comments(&gettext("The classical music player and organizer."))
.website("https://github.com/johrpan/musicus")
.website_label(&gettext("Further information and source code"))
.copyright("© 2020 Elias Projahn")
.license_type(gtk::License::Agpl30)
.authors(vec![String::from("Elias Projahn <johrpan@gmail.com>")])
.build();
/// Replace the current screen with the welcome screen.
fn show_welcome_screen(self: &Rc<Self>) {
let this = self;
spawn!(@clone this, async move {
replace!(this.navigator, WelcomeScreen).await;
});
}
dialog.show();
/// Replace the current screen with the main screen.
fn show_main_screen(self: &Rc<Self>) {
let this = self;
spawn!(@clone this, async move {
replace!(this.navigator, MainScreen).await;
});
}
}

View file

@ -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"

View file

@ -1,3 +1,4 @@
use futures::prelude::*;
use futures_channel::mpsc;
use gio::prelude::*;
use log::warn;
@ -40,7 +41,7 @@ pub enum BackendState {
pub struct Backend {
/// A future resolving to the next state of the backend. Initially, this should be assumed to
/// be BackendState::Loading. Changes should be awaited before calling init().
pub state_stream: RefCell<mpsc::Receiver<BackendState>>,
state_stream: RefCell<mpsc::Receiver<BackendState>>,
/// The internal sender to publish the state via state_stream.
state_sender: RefCell<mpsc::Sender<BackendState>>,
@ -80,8 +81,14 @@ impl Backend {
}
}
/// Wait for the next state change. Initially, the state should be assumed to be
/// BackendState::Loading. Changes should be awaited before calling init().
pub async fn next_state(&self) -> Option<BackendState> {
self.state_stream.borrow_mut().next().await
}
/// Initialize the backend updating the state accordingly.
pub async fn init(self: Rc<Backend>) -> Result<()> {
pub async fn init(&self) -> Result<()> {
self.init_library().await?;
if let Some(url) = self.settings.get_string("server-url") {
@ -97,6 +104,12 @@ impl Backend {
_ => (),
}
if self.get_music_library_path().is_none() {
self.set_state(BackendState::NoMusicLibrary);
} else {
self.set_state(BackendState::Ready);
}
Ok(())
}

View file

@ -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);

View file

@ -3,11 +3,11 @@
<gresource prefix="/de/johrpan/musicus">
<file preprocess="xml-stripblanks">ui/editor.ui</file>
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/main_screen.ui</file>
<file preprocess="xml-stripblanks">ui/medium_editor.ui</file>
<file preprocess="xml-stripblanks">ui/performance_editor.ui</file>
<file preprocess="xml-stripblanks">ui/player_bar.ui</file>
<file preprocess="xml-stripblanks">ui/player_screen.ui</file>
<file preprocess="xml-stripblanks">ui/poe_list.ui</file>
<file preprocess="xml-stripblanks">ui/preferences.ui</file>
<file preprocess="xml-stripblanks">ui/recording_editor.ui</file>
<file preprocess="xml-stripblanks">ui/register_dialog.ui</file>
@ -19,7 +19,6 @@
<file preprocess="xml-stripblanks">ui/track_editor.ui</file>
<file preprocess="xml-stripblanks">ui/track_selector.ui</file>
<file preprocess="xml-stripblanks">ui/track_set_editor.ui</file>
<file preprocess="xml-stripblanks">ui/window.ui</file>
<file preprocess="xml-stripblanks">ui/work_editor.ui</file>
<file preprocess="xml-stripblanks">ui/work_part_editor.ui</file>
<file preprocess="xml-stripblanks">ui/work_section_editor.ui</file>

147
res/ui/main_screen.ui Normal file
View 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>

View file

@ -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>

View file

@ -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>