From 9c255d0cfec0d970c4bb8bc5f41f677ca4fabd02 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 28 Nov 2020 23:07:31 +0100 Subject: [PATCH] Allow to upload instruments --- musicus/res/ui/instrument_editor.ui | 203 +++++++++++++---- musicus/res/ui/instrument_selector.ui | 157 +++++++++++--- musicus/src/backend/client/instruments.rs | 18 ++ musicus/src/backend/client/mod.rs | 9 +- musicus/src/dialogs/instrument_editor.rs | 110 +++++++--- musicus/src/dialogs/instrument_selector.rs | 204 +++++++++++------- .../dialogs/recording/performance_editor.rs | 8 +- musicus/src/dialogs/work/work_editor.rs | 8 +- musicus_server/src/main.rs | 4 + musicus_server/src/routes/instruments.rs | 71 ++++++ 10 files changed, 611 insertions(+), 181 deletions(-) create mode 100644 musicus/src/backend/client/instruments.rs diff --git a/musicus/res/ui/instrument_editor.ui b/musicus/res/ui/instrument_editor.ui index 7a5be09..25ad2b4 100644 --- a/musicus/res/ui/instrument_editor.ui +++ b/musicus/res/ui/instrument_editor.ui @@ -9,80 +9,203 @@ True dialog - + True False - vertical + crossfade - + True False - Instrument + vertical - - Cancel + True - True - True - - - - - Save - True - True - True - + False + Instrument + + + 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 instrument! + + + False + True + 0 + + + + + False + False + 0 + + + + + + + + False + True 1 - False - True - 0 + content - - + True False - 18 - 12 - 6 + vertical - + True False - end - Name + Instrument - 0 - 0 + False + True + 0 - + True - True - True + False + True + True - 1 - 0 + False + True + 1 - False - True + loading 1 diff --git a/musicus/res/ui/instrument_selector.ui b/musicus/res/ui/instrument_selector.ui index 75e1975..4cff41d 100644 --- a/musicus/res/ui/instrument_selector.ui +++ b/musicus/res/ui/instrument_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 instruments … + + + False + True + 0 + + + + + Show instruments from the Musicus server + True + True + False + True + True + + + False + True + 1 + + @@ -65,34 +92,116 @@ - + True - True - in + False + crossfade - + True False + True + + + loading + + + + + True + True - - True - False - - - True - False - No instruments 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/instruments.rs b/musicus/src/backend/client/instruments.rs new file mode 100644 index 0000000..bbb2d65 --- /dev/null +++ b/musicus/src/backend/client/instruments.rs @@ -0,0 +1,18 @@ +use super::Backend; +use crate::database::Instrument; +use anyhow::Result; + +impl Backend { + /// Get all available instruments from the server. + pub async fn get_instruments(&self) -> Result> { + let body = self.get("instruments").await?; + let instruments: Vec = serde_json::from_str(&body)?; + Ok(instruments) + } + + /// Post a new instrument to the server and return the ID. + pub async fn post_instrument(&self, data: &Instrument) -> Result<()> { + self.post("instruments", serde_json::to_string(data)?).await?; + Ok(()) + } +} diff --git a/musicus/src/backend/client/mod.rs b/musicus/src/backend/client/mod.rs index 5013670..11be303 100644 --- a/musicus/src/backend/client/mod.rs +++ b/musicus/src/backend/client/mod.rs @@ -7,12 +7,15 @@ use isahc::prelude::*; use serde::Serialize; use std::time::Duration; -pub mod persons; -pub use persons::*; - pub mod ensembles; pub use ensembles::*; +pub mod instruments; +pub use instruments::*; + +pub mod persons; +pub use persons::*; + /// Credentials used for login. #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] diff --git a/musicus/src/dialogs/instrument_editor.rs b/musicus/src/dialogs/instrument_editor.rs index ed3fe8e..996d988 100644 --- a/musicus/src/dialogs/instrument_editor.rs +++ b/musicus/src/dialogs/instrument_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 InstrumentEditor -where - F: Fn(Instrument) -> () + 'static, -{ +/// A dialog for creating or editing a instrument. +pub struct InstrumentEditor { 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 InstrumentEditor -where - F: Fn(Instrument) -> () + 'static, -{ +impl InstrumentEditor { + /// Create a new instrument editor and optionally initialize it. pub fn new>( backend: Rc, parent: &P, instrument: Option, - callback: F, ) -> Rc { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_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 instrument { Some(instrument) => { name_entry.set_text(&instrument.name); + instrument.id } None => generate_id(), }; - let result = Rc::new(InstrumentEditor { - 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 instrument = Instrument { - 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 c = glib::MainContext::default(); - let clone = result.clone(); - c.spawn_local(async move { - clone.backend.db().update_instrument(instrument.clone()).await.unwrap(); - clone.window.close(); - (clone.callback)(instrument.clone()); }); })); - result.window.set_transient_for(Some(parent)); + this.window.set_transient_for(Some(parent)); - result + this } + /// Set the closure to be called if the instrument was saved. + pub fn set_saved_cb () + 'static>(&self, cb: F) { + self.saved_cb.replace(Some(Box::new(cb))); + } + + /// Show the instrument editor. pub fn show(&self) { self.window.show(); } + + /// Save the instrument and possibly upload it to the server. + async fn save(self: Rc) -> Result<()> { + let name = self.name_entry.get_text().to_string(); + + let instrument = Instrument { + id: self.id.clone(), + name, + }; + + let upload = self.upload_switch.get_active(); + if upload { + self.backend.post_instrument(&instrument).await?; + } + + self.backend.db().update_instrument(instrument.clone()).await?; + self.backend.library_changed(); + + if let Some(cb) = &*self.saved_cb.borrow() { + cb(instrument.clone()); + } + + Ok(()) + } } diff --git a/musicus/src/dialogs/instrument_selector.rs b/musicus/src/dialogs/instrument_selector.rs index 172e0da..f2172ee 100644 --- a/musicus/src/dialogs/instrument_selector.rs +++ b/musicus/src/dialogs/instrument_selector.rs @@ -1,111 +1,161 @@ use super::InstrumentEditor; use crate::backend::Backend; -use crate::database::*; -use crate::widgets::*; +use crate::database::Instrument; +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 InstrumentSelector -where - F: Fn(Instrument) -> () + 'static, -{ +/// A dialog for selecting a instrument. +pub struct InstrumentSelector { 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 InstrumentSelector -where - F: Fn(Instrument) -> () + 'static, -{ - pub fn new>(backend: Rc, parent: &P, callback: F) -> Rc { +impl InstrumentSelector { + pub fn new

(backend: Rc, parent: &P) -> Rc + where + P: IsA, + { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_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(InstrumentSelector { - backend: backend, - window: window, - callback: callback, - search_entry: search_entry, - list: list, + window.set_transient_for(Some(parent)); + + let list = List::::new(&gettext("No instruments 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 instruments = clone.backend.db().get_instruments().await.unwrap(); + // Connect signals and callbacks - for (index, instrument) in instruments.iter().enumerate() { - let label = gtk::Label::new(Some(&instrument.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 instruments => move |_, row| { - clone.window.close(); - let row = row.get_child().unwrap().downcast::().unwrap(); - let index: usize = row.get_index().try_into().unwrap(); - (clone.callback)(instruments[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(); - - search.is_empty() || instruments[index] - .name - .to_lowercase() - .contains(&clone.search_entry.get_text().to_string().to_lowercase()) - })))); - }); - - 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 = InstrumentEditor::new( - result.backend.clone(), - &result.window, + this.backend.clone(), + &this.window, None, - clone!(@strong result => move |instrument| { - result.window.close(); - (result.callback)(instrument); - }), ); + editor.set_saved_cb(clone!(@strong this => move |instrument| { + if let Some(cb) = &*this.selected_cb.borrow() { + cb(instrument); + } + + 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_instruments().await { + Ok(instruments) => { + clone.list.show_items(instruments); + 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 instruments = clone.backend.db().get_instruments().await.unwrap(); + clone.list.show_items(instruments); + 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(|instrument: &Instrument| { + let label = gtk::Label::new(Some(&instrument.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 |instrument: &Instrument| { + let search = search_entry.get_text().to_string().to_lowercase(); + search.is_empty() || instrument.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 instrument. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Show the instrument 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 af95b7d..abfd0b2 100644 --- a/musicus/src/dialogs/recording/performance_editor.rs +++ b/musicus/src/dialogs/recording/performance_editor.rs @@ -107,10 +107,14 @@ impl PerformanceEditor { })); role_button.connect_clicked(clone!(@strong this => move |_| { - InstrumentSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |role| { + let dialog = InstrumentSelector::new(this.backend.clone(), &this.window); + + dialog.set_selected_cb(clone!(@strong this => move |role| { this.show_role(Some(&role)); this.role.replace(Some(role)); - })).show(); + })); + + dialog.show(); })); this.reset_role_button diff --git a/musicus/src/dialogs/work/work_editor.rs b/musicus/src/dialogs/work/work_editor.rs index 5693c7e..f83334b 100644 --- a/musicus/src/dialogs/work/work_editor.rs +++ b/musicus/src/dialogs/work/work_editor.rs @@ -178,7 +178,9 @@ impl WorkEditor { }); add_instrument_button.connect_clicked(clone!(@strong this => move |_| { - InstrumentSelector::new(this.backend.clone(), &this.parent, clone!(@strong this => move |instrument| { + let dialog = InstrumentSelector::new(this.backend.clone(), &this.parent); + + dialog.set_selected_cb(clone!(@strong this => move |instrument| { let mut instruments = this.instruments.borrow_mut(); let index = match this.instrument_list.get_selected_index() { @@ -189,7 +191,9 @@ impl WorkEditor { instruments.insert(index, instrument); this.instrument_list.show_items(instruments.clone()); this.instrument_list.select_index(index); - })).show(); + })); + + dialog.show(); })); remove_instrument_button.connect_clicked(clone!(@strong this => move |_| { diff --git a/musicus_server/src/main.rs b/musicus_server/src/main.rs index 30267b6..98b39d7 100644 --- a/musicus_server/src/main.rs +++ b/musicus_server/src/main.rs @@ -35,6 +35,10 @@ async fn main() -> std::io::Result<()> { .service(update_ensemble) .service(delete_ensemble) .service(get_ensembles) + .service(get_instrument) + .service(update_instrument) + .service(delete_instrument) + .service(get_instruments) }); server.bind("127.0.0.1:8087")?.run().await diff --git a/musicus_server/src/routes/instruments.rs b/musicus_server/src/routes/instruments.rs index e69de29..2320fb6 100644 --- a/musicus_server/src/routes/instruments.rs +++ b/musicus_server/src/routes/instruments.rs @@ -0,0 +1,71 @@ +use super::authenticate; +use crate::database; +use crate::database::{DbPool, Instrument}; +use crate::error::ServerError; +use actix_web::{delete, get, post, web, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; + +/// Get an existing instrument. +#[get("/instruments/{id}")] +pub async fn get_instrument( + db: web::Data, + id: web::Path, +) -> Result { + let data = web::block(move || { + let conn = db.into_inner().get()?; + database::get_instrument(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) + }) + .await?; + + Ok(HttpResponse::Ok().json(data)) +} + +/// Add a new instrument or update an existin one. The user must be authorized to do that. +#[post("/instruments")] +pub async fn update_instrument( + 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_instrument(&conn, &data.into_inner(), &user)?; + + Ok(()) + }) + .await?; + + Ok(HttpResponse::Ok().finish()) +} + +#[get("/instruments")] +pub async fn get_instruments(db: web::Data) -> Result { + let data = web::block(move || { + let conn = db.into_inner().get()?; + Ok(database::get_instruments(&conn)?) + }) + .await?; + + Ok(HttpResponse::Ok().json(data)) +} + +#[delete("/instruments/{id}")] +pub async fn delete_instrument( + 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_instrument(&conn, &id.into_inner(), &user)?; + + Ok(()) + }) + .await?; + + Ok(HttpResponse::Ok().finish()) +}