diff --git a/musicus/res/ui/person_selector.ui b/musicus/res/ui/person_selector.ui index 9f737a5..72db289 100644 --- a/musicus/res/ui/person_selector.ui +++ b/musicus/res/ui/person_selector.ui @@ -6,12 +6,10 @@ False True - 350 - 300 True dialog - + True False vertical @@ -43,7 +41,168 @@ - + + True + False + True + + + True + False + vertical + 6 + + + True + True + True + edit-find-symbolic + False + False + Search persons … + + + False + True + 0 + + + + + Show persons from the Musicus server + True + True + False + True + True + + + False + True + 1 + + + + + + + False + True + 1 + + + + + True + False + crossfade + + + True + False + True + + + loading + + + + + True + True + + + + + + content + 1 + + + + + True + False + center + center + 18 + vertical + 18 + + + True + False + 0.5019607843137255 + 80 + network-error-symbolic + + + False + True + 0 + + + + + True + False + 0.5019607843137255 + An error occured! + + + + + + False + True + 1 + + + + + True + False + 0.5019607843137255 + The server was not reachable or responded with an error. Please check your internet connection. + center + True + 40 + + + False + True + 2 + + + + + Try again + True + True + True + center + + + + False + True + 3 + + + + + error + 2 + + + + + True + True + 3 + diff --git a/musicus/src/backend/client.rs b/musicus/src/backend/client/mod.rs similarity index 98% rename from musicus/src/backend/client.rs rename to musicus/src/backend/client/mod.rs index 8899fe7..9b6101b 100644 --- a/musicus/src/backend/client.rs +++ b/musicus/src/backend/client/mod.rs @@ -6,6 +6,9 @@ use isahc::http::StatusCode; use isahc::prelude::*; use serde::Serialize; +pub mod persons; +pub use persons::*; + /// Credentials used for login. #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] diff --git a/musicus/src/backend/client/persons.rs b/musicus/src/backend/client/persons.rs new file mode 100644 index 0000000..af13f6f --- /dev/null +++ b/musicus/src/backend/client/persons.rs @@ -0,0 +1,24 @@ +use super::Backend; +use crate::database::Person; +use anyhow::{anyhow, Result}; +use isahc::prelude::*; +use std::time::Duration; + +impl Backend { + /// Get all available persons from the server. + pub async fn get_persons(&self) -> Result> { + let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?; + + let mut response = Request::get(format!("{}/persons", server_url)) + .timeout(Duration::from_secs(10)) + .body(())? + .send_async() + .await?; + + let body = response.text_async().await?; + + let persons: Vec = serde_json::from_str(&body)?; + + Ok(persons) + } +} diff --git a/musicus/src/dialogs/person_selector.rs b/musicus/src/dialogs/person_selector.rs index bcdf165..6c8633d 100644 --- a/musicus/src/dialogs/person_selector.rs +++ b/musicus/src/dialogs/person_selector.rs @@ -1,60 +1,161 @@ use super::PersonEditor; use crate::backend::Backend; -use crate::database::*; -use crate::widgets::*; +use crate::database::Person; +use crate::widgets::List; +use gettextrs::gettext; use gio::prelude::*; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; +use std::cell::RefCell; use std::rc::Rc; +/// A dialog for selecting a person. pub struct PersonSelector { + backend: Rc, window: libhandy::Window, + server_check_button: gtk::CheckButton, + stack: gtk::Stack, + list: Rc>, + selected_cb: RefCell ()>>>, } impl PersonSelector { - pub fn new(backend: Rc, parent: &P, callback: F) -> Self + pub fn new

(backend: Rc, parent: &P) -> Rc where P: IsA, - F: Fn(Person) -> () + 'static, { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_selector.ui"); get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Box, vbox); get_widget!(builder, gtk::Button, add_button); + get_widget!(builder, gtk::CheckButton, server_check_button); + get_widget!(builder, gtk::SearchEntry, search_entry); + get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::ScrolledWindow, scroll); + get_widget!(builder, gtk::Button, try_again_button); - let callback = Rc::new(callback); - - let list = PersonList::new(backend.clone()); - - list.set_selected(clone!(@strong window, @strong callback => move |person| { - window.close(); - callback(person.clone()); - })); - - vbox.pack_start(&list.widget, true, true, 0); window.set_transient_for(Some(parent)); - add_button.connect_clicked( - clone!(@strong backend, @strong window, @strong callback => move |_| { - let editor = PersonEditor::new( - backend.clone(), - &window, - None, - clone!(@strong window, @strong callback => move |person| { - window.close(); - callback(person); - }), - ); + let list = List::::new(&gettext("No persons found.")); + scroll.add(&list.widget); - editor.show(); + let this = Rc::new(Self { + backend, + window, + server_check_button, + stack, + list, + selected_cb: RefCell::new(None), + }); + + // Connect signals and callbacks + + add_button.connect_clicked(clone!(@strong this => move |_| { + let editor = PersonEditor::new( + this.backend.clone(), + &this.window, + None, + clone!(@strong this => move |person| { + if let Some(cb) = &*this.selected_cb.borrow() { + cb(person); + } + + this.window.close(); + }), + ); + + editor.show(); + })); + + search_entry.connect_search_changed(clone!(@strong this => move |_| { + this.list.invalidate_filter(); + })); + + let load_online = Rc::new(clone!(@strong this => move || { + this.stack.set_visible_child_name("loading"); + + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + match clone.backend.get_persons().await { + Ok(persons) => { + clone.list.show_items(persons); + clone.stack.set_visible_child_name("content"); + } + Err(_) => { + clone.list.show_items(Vec::new()); + clone.stack.set_visible_child_name("error"); + } + } + }); + })); + + let load_local = Rc::new(clone!(@strong this => move || { + this.stack.set_visible_child_name("loading"); + + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + let persons = clone.backend.db().get_persons().await.unwrap(); + clone.list.show_items(persons); + clone.stack.set_visible_child_name("content"); + }); + })); + + this.server_check_button.connect_toggled( + clone!(@strong this, @strong load_local, @strong load_online => move |_| { + if this.server_check_button.get_active() { + load_online(); + } else { + load_local(); + } }), ); - Self { window } + this.list.set_make_widget(|person: &Person| { + let label = gtk::Label::new(Some(&person.name_lf())); + label.set_halign(gtk::Align::Start); + label.set_margin_start(6); + label.set_margin_end(6); + label.set_margin_top(6); + label.set_margin_bottom(6); + label.upcast() + }); + + this.list + .set_filter(clone!(@strong search_entry => move |person: &Person| { + let search = search_entry.get_text().to_string().to_lowercase(); + let name = person.name_fl().to_lowercase(); + search.is_empty() || name.contains(&search) + })); + + this.list.set_selected(clone!(@strong this => move |work| { + if let Some(cb) = &*this.selected_cb.borrow() { + cb(work.clone()); + } + + this.window.close(); + })); + + try_again_button.connect_clicked(clone!(@strong load_online => move |_| { + load_online(); + })); + + // Initialize + load_online(); + + this } + /// Set the closure to be called when the user has selected a person. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Show the person selector. pub fn show(&self) { self.window.show(); } diff --git a/musicus/src/dialogs/recording/performance_editor.rs b/musicus/src/dialogs/recording/performance_editor.rs index 84c58ea..d2fdea6 100644 --- a/musicus/src/dialogs/recording/performance_editor.rs +++ b/musicus/src/dialogs/recording/performance_editor.rs @@ -81,12 +81,16 @@ impl PerformanceEditor { })); person_button.connect_clicked(clone!(@strong this => move |_| { - PersonSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |person| { + let dialog = PersonSelector::new(this.backend.clone(), &this.window); + + dialog.set_selected_cb(clone!(@strong this => move |person| { this.show_person(Some(&person)); this.person.replace(Some(person)); this.show_ensemble(None); this.ensemble.replace(None); - })).show(); + })); + + dialog.show(); })); ensemble_button.connect_clicked(clone!(@strong this => move |_| { diff --git a/musicus/src/dialogs/work/part_editor.rs b/musicus/src/dialogs/work/part_editor.rs index 4e9784a..cda7944 100644 --- a/musicus/src/dialogs/work/part_editor.rs +++ b/musicus/src/dialogs/work/part_editor.rs @@ -76,10 +76,14 @@ impl PartEditor { })); composer_button.connect_clicked(clone!(@strong this => move |_| { - PersonSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |person| { + let dialog = PersonSelector::new(this.backend.clone(), &this.window); + + dialog.set_selected_cb(clone!(@strong this => move |person| { this.show_composer(Some(&person)); this.composer.replace(Some(person)); - })).show(); + })); + + dialog.show(); })); this.reset_composer_button diff --git a/musicus/src/dialogs/work/work_editor.rs b/musicus/src/dialogs/work/work_editor.rs index 33da150..f2e8d1f 100644 --- a/musicus/src/dialogs/work/work_editor.rs +++ b/musicus/src/dialogs/work/work_editor.rs @@ -156,10 +156,14 @@ impl WorkEditor { })); composer_button.connect_clicked(clone!(@strong this => move |_| { - PersonSelector::new(this.backend.clone(), &this.parent, clone!(@strong this => move |person| { + let dialog = PersonSelector::new(this.backend.clone(), &this.parent); + + dialog.set_selected_cb(clone!(@strong this => move |person| { this.show_composer(&person); this.composer.replace(Some(person)); - })).show(); + })); + + dialog.show(); })); this.instrument_list.set_make_widget(|instrument| { diff --git a/musicus/src/meson.build b/musicus/src/meson.build index 36cb69d..59d302a 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -33,7 +33,8 @@ run_command( ) sources = files( - 'backend/client.rs', + 'backend/client/mod.rs', + 'backend/client/persons.rs', 'backend/library.rs', 'backend/mod.rs', 'backend/secure.rs',