diff --git a/musicus/res/ui/ensemble_editor.ui b/musicus/res/ui/ensemble_editor.ui index 22f5534..4e3ec68 100644 --- a/musicus/res/ui/ensemble_editor.ui +++ b/musicus/res/ui/ensemble_editor.ui @@ -9,81 +9,204 @@ True dialog - + True False - vertical + crossfade - + True False - Ensemble + vertical - - Cancel + True - True - True - - - - - Save - True - True - True - + False + Ensemble + + + Cancel + True + True + True + + + + + Save + True + True + True + + + + end + 1 + + - end + False + True + 0 + + + + + + True + False + 18 + 12 + 6 + + + True + False + end + Name + + + 0 + 0 + + + + + True + True + True + + + 1 + 0 + + + + + True + False + end + Publish + + + 0 + 1 + + + + + True + True + start + True + + + 1 + 1 + + + + + False + True + 1 + + + + + True + False + False + + + False + 6 + end + + + + + + False + False + 0 + + + + + False + 16 + + + True + False + Failed to save ensemble! + + + False + True + 0 + + + + + False + False + 0 + + + + + + + + False + True 1 - False - True - 1 + content - - + True False - 18 - 12 - 6 + vertical - + True False - end - Name + Ensemble - 0 - 0 + False + True + 0 - + True - True - True + False + True + True - 1 - 0 + False + True + 1 - False - True - 2 + loading + 1 diff --git a/musicus/res/ui/ensemble_selector.ui b/musicus/res/ui/ensemble_selector.ui index e36f185..1d60b15 100644 --- a/musicus/res/ui/ensemble_selector.ui +++ b/musicus/res/ui/ensemble_selector.ui @@ -6,8 +6,6 @@ False True - 350 - 300 True dialog @@ -48,13 +46,42 @@ False True - + True - True - True - edit-find-symbolic - False - False + False + vertical + 6 + + + True + True + True + edit-find-symbolic + False + False + Search ensembles … + + + False + True + 0 + + + + + Show ensembles from the Musicus server + True + True + False + True + True + + + False + True + 1 + + @@ -65,34 +92,116 @@ - + True - True + False + crossfade - + True False - none + True + + + loading + + + + + True + True - - True - False - - - True - False - No ensembles found. - - - + + + 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 - 2 + 3 diff --git a/musicus/src/backend/client/ensembles.rs b/musicus/src/backend/client/ensembles.rs new file mode 100644 index 0000000..ab9b127 --- /dev/null +++ b/musicus/src/backend/client/ensembles.rs @@ -0,0 +1,18 @@ +use super::Backend; +use crate::database::Ensemble; +use anyhow::Result; + +impl Backend { + /// Get all available ensembles from the server. + pub async fn get_ensembles(&self) -> Result> { + let body = self.get("ensembles").await?; + let ensembles: Vec = serde_json::from_str(&body)?; + Ok(ensembles) + } + + /// Post a new ensemble to the server and return the ID. + pub async fn post_ensemble(&self, data: &Ensemble) -> Result<()> { + self.post("ensembles", serde_json::to_string(data)?).await?; + Ok(()) + } +} diff --git a/musicus/src/backend/client/mod.rs b/musicus/src/backend/client/mod.rs index 0327964..5013670 100644 --- a/musicus/src/backend/client/mod.rs +++ b/musicus/src/backend/client/mod.rs @@ -10,6 +10,9 @@ use std::time::Duration; pub mod persons; pub use persons::*; +pub mod ensembles; +pub use ensembles::*; + /// Credentials used for login. #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] diff --git a/musicus/src/dialogs/ensemble_editor.rs b/musicus/src/dialogs/ensemble_editor.rs index 64d2840..c5b6110 100644 --- a/musicus/src/dialogs/ensemble_editor.rs +++ b/musicus/src/dialogs/ensemble_editor.rs @@ -1,79 +1,123 @@ -use crate::backend::*; +use crate::backend::Backend; use crate::database::*; +use anyhow::Result; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; +use std::cell::RefCell; use std::rc::Rc; -pub struct EnsembleEditor -where - F: Fn(Ensemble) -> () + 'static, -{ +/// A dialog for creating or editing a ensemble. +pub struct EnsembleEditor { backend: Rc, - window: libhandy::Window, - callback: F, id: String, + window: libhandy::Window, + stack: gtk::Stack, + info_bar: gtk::InfoBar, name_entry: gtk::Entry, + upload_switch: gtk::Switch, + saved_cb: RefCell ()>>>, } -impl EnsembleEditor -where - F: Fn(Ensemble) -> () + 'static, -{ +impl EnsembleEditor { + /// Create a new ensemble editor and optionally initialize it. pub fn new>( backend: Rc, parent: &P, ensemble: Option, - callback: F, ) -> Rc { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui"); get_widget!(builder, libhandy::Window, window); get_widget!(builder, gtk::Button, cancel_button); get_widget!(builder, gtk::Button, save_button); + get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::InfoBar, info_bar); get_widget!(builder, gtk::Entry, name_entry); + get_widget!(builder, gtk::Switch, upload_switch); let id = match ensemble { Some(ensemble) => { name_entry.set_text(&ensemble.name); + ensemble.id } None => generate_id(), }; - let result = Rc::new(EnsembleEditor { - backend: backend, - window: window, - callback: callback, - id: id, - name_entry: name_entry, + let this = Rc::new(Self { + backend, + id, + window, + stack, + info_bar, + name_entry, + upload_switch, + saved_cb: RefCell::new(None), }); - cancel_button.connect_clicked(clone!(@strong result => move |_| { - result.window.close(); + // Connect signals and callbacks + + cancel_button.connect_clicked(clone!(@strong this => move |_| { + this.window.close(); })); - save_button.connect_clicked(clone!(@strong result => move |_| { - let ensemble = Ensemble { - id: result.id.clone(), - name: result.name_entry.get_text().to_string(), - }; + save_button.connect_clicked(clone!(@strong this => move |_| { + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + clone.stack.set_visible_child_name("loading"); + match clone.clone().save().await { + Ok(_) => { + clone.window.close(); + } + Err(_) => { + clone.info_bar.set_revealed(true); + clone.stack.set_visible_child_name("content"); + } + } - let clone = result.clone(); - let c = glib::MainContext::default(); - c.spawn_local(async move { - clone.backend.db().update_ensemble(ensemble.clone()).await.unwrap(); - (clone.callback)(ensemble.clone()); - clone.window.close(); }); })); - result.window.set_transient_for(Some(parent)); + this.window.set_transient_for(Some(parent)); - result + this } + /// Set the closure to be called if the ensemble was saved. + pub fn set_saved_cb () + 'static>(&self, cb: F) { + self.saved_cb.replace(Some(Box::new(cb))); + } + + /// Show the ensemble editor. pub fn show(&self) { self.window.show(); } + + /// Save the ensemble and possibly upload it to the server. + async fn save(self: Rc) -> Result<()> { + let name = self.name_entry.get_text().to_string(); + + let ensemble = Ensemble { + id: self.id.clone(), + name, + }; + + let upload = self.upload_switch.get_active(); + if upload { + self.backend.post_ensemble(&ensemble).await?; + } + + self.backend.db().update_ensemble(ensemble.clone()).await?; + self.backend.library_changed(); + + if let Some(cb) = &*self.saved_cb.borrow() { + cb(ensemble.clone()); + } + + Ok(()) + } } diff --git a/musicus/src/dialogs/ensemble_selector.rs b/musicus/src/dialogs/ensemble_selector.rs index 516fae5..fcfe81e 100644 --- a/musicus/src/dialogs/ensemble_selector.rs +++ b/musicus/src/dialogs/ensemble_selector.rs @@ -1,110 +1,161 @@ use super::EnsembleEditor; use crate::backend::Backend; -use crate::database::*; -use crate::widgets::*; +use crate::database::Ensemble; +use crate::widgets::List; +use gettextrs::gettext; use gio::prelude::*; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; -use std::convert::TryInto; +use std::cell::RefCell; use std::rc::Rc; -pub struct EnsembleSelector -where - F: Fn(Ensemble) -> () + 'static, -{ +/// A dialog for selecting a ensemble. +pub struct EnsembleSelector { backend: Rc, window: libhandy::Window, - callback: F, - list: gtk::ListBox, - search_entry: gtk::SearchEntry, + server_check_button: gtk::CheckButton, + stack: gtk::Stack, + list: Rc>, + selected_cb: RefCell ()>>>, } -impl EnsembleSelector -where - F: Fn(Ensemble) -> () + 'static, -{ - pub fn new>(backend: Rc, parent: &P, callback: F) -> Rc { +impl EnsembleSelector { + pub fn new

(backend: Rc, parent: &P) -> Rc + where + P: IsA, + { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui"); get_widget!(builder, libhandy::Window, window); 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::ListBox, list); + get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::ScrolledWindow, scroll); + get_widget!(builder, gtk::Button, try_again_button); - let result = Rc::new(EnsembleSelector { - backend: backend, - window: window, - callback: callback, - search_entry: search_entry, - list: list, + window.set_transient_for(Some(parent)); + + let list = List::::new(&gettext("No ensembles found.")); + scroll.add(&list.widget); + + let this = Rc::new(Self { + backend, + window, + server_check_button, + stack, + list, + selected_cb: RefCell::new(None), }); - let c = glib::MainContext::default(); - let clone = result.clone(); - c.spawn_local(async move { - let ensembles = clone.backend.db().get_ensembles().await.unwrap(); + // Connect signals and callbacks - for (index, ensemble) in ensembles.iter().enumerate() { - let label = gtk::Label::new(Some(&ensemble.name)); - 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); - - let row = SelectorRow::new(index.try_into().unwrap(), &label); - row.show_all(); - clone.list.insert(&row, -1); - } - - clone.list.connect_row_activated( - clone!(@strong clone, @strong ensembles => move |_, row| { - clone.window.close(); - let row = row.get_child().unwrap().downcast::().unwrap(); - let index: usize = row.get_index().try_into().unwrap(); - (clone.callback)(ensembles[index].clone()); - }), - ); - - clone - .list - .set_filter_func(Some(Box::new(clone!(@strong clone => move |row| { - let row = row.get_child().unwrap().downcast::().unwrap(); - let index: usize = row.get_index().try_into().unwrap(); - let search = clone.search_entry.get_text().to_string().to_lowercase(); - search.is_empty() || ensembles[index] - .name - .to_lowercase() - .contains(&search) - })))); - }); - - result - .search_entry - .connect_search_changed(clone!(@strong result => move |_| { - result.list.invalidate_filter(); - })); - - add_button.connect_clicked(clone!(@strong result => move |_| { + add_button.connect_clicked(clone!(@strong this => move |_| { let editor = EnsembleEditor::new( - result.backend.clone(), - &result.window, + this.backend.clone(), + &this.window, None, - clone!(@strong result => move |ensemble| { - result.window.close(); - (result.callback)(ensemble); - }), ); + editor.set_saved_cb(clone!(@strong this => move |ensemble| { + if let Some(cb) = &*this.selected_cb.borrow() { + cb(ensemble); + } + + this.window.close(); + })); + editor.show(); })); - result.window.set_transient_for(Some(parent)); + search_entry.connect_search_changed(clone!(@strong this => move |_| { + this.list.invalidate_filter(); + })); - result + 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_ensembles().await { + Ok(ensembles) => { + clone.list.show_items(ensembles); + 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 ensembles = clone.backend.db().get_ensembles().await.unwrap(); + clone.list.show_items(ensembles); + 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(); + } + }), + ); + + this.list.set_make_widget(|ensemble: &Ensemble| { + let label = gtk::Label::new(Some(&ensemble.name)); + 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 |ensemble: &Ensemble| { + let search = search_entry.get_text().to_string().to_lowercase(); + search.is_empty() || ensemble.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 ensemble. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Show the ensemble 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 d2fdea6..af95b7d 100644 --- a/musicus/src/dialogs/recording/performance_editor.rs +++ b/musicus/src/dialogs/recording/performance_editor.rs @@ -94,12 +94,16 @@ impl PerformanceEditor { })); ensemble_button.connect_clicked(clone!(@strong this => move |_| { - EnsembleSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |ensemble| { + let dialog = EnsembleSelector::new(this.backend.clone(), &this.window); + + dialog.set_selected_cb(clone!(@strong this => move |ensemble| { this.show_person(None); this.person.replace(None); this.show_ensemble(Some(&ensemble)); this.ensemble.replace(Some(ensemble)); - })).show(); + })); + + dialog.show(); })); role_button.connect_clicked(clone!(@strong this => move |_| { diff --git a/musicus/src/screens/ensemble_screen.rs b/musicus/src/screens/ensemble_screen.rs index 02cec8d..c4d5ce7 100644 --- a/musicus/src/screens/ensemble_screen.rs +++ b/musicus/src/screens/ensemble_screen.rs @@ -109,7 +109,7 @@ impl EnsembleScreen { })); edit_action.connect_activate(clone!(@strong result => move |_, _| { - EnsembleEditor::new(result.backend.clone(), &result.window, Some(result.ensemble.clone()), |_| {}).show(); + EnsembleEditor::new(result.backend.clone(), &result.window, Some(result.ensemble.clone())).show(); })); delete_action.connect_activate(clone!(@strong result => move |_, _| { @@ -117,6 +117,7 @@ impl EnsembleScreen { let clone = result.clone(); context.spawn_local(async move { clone.backend.db().delete_ensemble(&clone.ensemble.id).await.unwrap(); + clone.backend.library_changed(); }); })); diff --git a/musicus/src/screens/person_screen.rs b/musicus/src/screens/person_screen.rs index 802c682..06b90a6 100644 --- a/musicus/src/screens/person_screen.rs +++ b/musicus/src/screens/person_screen.rs @@ -153,6 +153,7 @@ impl PersonScreen { let clone = result.clone(); context.spawn_local(async move { clone.backend.db().delete_person(&clone.person.id).await.unwrap(); + clone.backend.library_changed(); }); })); diff --git a/musicus_server/src/main.rs b/musicus_server/src/main.rs index 3155f73..30267b6 100644 --- a/musicus_server/src/main.rs +++ b/musicus_server/src/main.rs @@ -31,6 +31,10 @@ async fn main() -> std::io::Result<()> { .service(update_person) .service(get_persons) .service(delete_person) + .service(get_ensemble) + .service(update_ensemble) + .service(delete_ensemble) + .service(get_ensembles) }); server.bind("127.0.0.1:8087")?.run().await diff --git a/musicus_server/src/routes/ensembles.rs b/musicus_server/src/routes/ensembles.rs index e69de29..11d671a 100644 --- a/musicus_server/src/routes/ensembles.rs +++ b/musicus_server/src/routes/ensembles.rs @@ -0,0 +1,71 @@ +use super::authenticate; +use crate::database; +use crate::database::{DbPool, Ensemble}; +use crate::error::ServerError; +use actix_web::{delete, get, post, web, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; + +/// Get an existing ensemble. +#[get("/ensembles/{id}")] +pub async fn get_ensemble( + db: web::Data, + id: web::Path, +) -> Result { + let data = web::block(move || { + let conn = db.into_inner().get()?; + database::get_ensemble(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) + }) + .await?; + + Ok(HttpResponse::Ok().json(data)) +} + +/// Add a new ensemble or update an existin one. The user must be authorized to do that. +#[post("/ensembles")] +pub async fn update_ensemble( + auth: BearerAuth, + db: web::Data, + data: web::Json, +) -> Result { + web::block(move || { + let conn = db.into_inner().get()?; + let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; + + database::update_ensemble(&conn, &data.into_inner(), &user)?; + + Ok(()) + }) + .await?; + + Ok(HttpResponse::Ok().finish()) +} + +#[get("/ensembles")] +pub async fn get_ensembles(db: web::Data) -> Result { + let data = web::block(move || { + let conn = db.into_inner().get()?; + Ok(database::get_ensembles(&conn)?) + }) + .await?; + + Ok(HttpResponse::Ok().json(data)) +} + +#[delete("/ensembles/{id}")] +pub async fn delete_ensemble( + auth: BearerAuth, + db: web::Data, + id: web::Path, +) -> Result { + web::block(move || { + let conn = db.into_inner().get()?; + let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; + + database::delete_ensemble(&conn, &id.into_inner(), &user)?; + + Ok(()) + }) + .await?; + + Ok(HttpResponse::Ok().finish()) +}