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