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