diff --git a/musicus/res/ui/person_editor.ui b/musicus/res/ui/person_editor.ui index bf5bd05..4e2490e 100644 --- a/musicus/res/ui/person_editor.ui +++ b/musicus/res/ui/person_editor.ui @@ -9,103 +9,226 @@ True dialog - + True False - vertical + crossfade - + True False - Person + vertical - - Cancel + True - True - True - - - - - Save - True - True - True - + False + Person + + + Cancel + True + True + True + + + + + Save + True + True + True + + + + end + 1 + + - end + False + True + 0 + + + + + + True + False + 18 + 12 + 6 + + + True + False + end + First name + + + 0 + 0 + + + + + True + True + True + + + 1 + 0 + + + + + True + False + end + Last name + + + 0 + 1 + + + + + True + True + True + + + 1 + 1 + + + + + True + False + end + Publish + + + 0 + 2 + + + + + True + True + start + True + + + 1 + 2 + + + + + False + True + 1 + + + + + True + False + False + + + False + 6 + end + + + + + + False + False + 0 + + + + + False + 16 + + + True + False + Failed to save person! + + + False + True + 0 + + + + + False + False + 0 + + + + + + + + False + True 1 - False - True - 0 + content - - + True False - 18 - 12 - 6 + vertical - + True False - end - First name + Person - 0 - 0 + False + True + 0 - - True - True - True - - - 1 - 0 - - - - + True False - end - Last name + True + True - 0 - 1 - - - - - True - True - True - - - 1 - 1 + False + True + 1 - False - True + loading 1 diff --git a/musicus/src/backend/client/mod.rs b/musicus/src/backend/client/mod.rs index 9b6101b..0327964 100644 --- a/musicus/src/backend/client/mod.rs +++ b/musicus/src/backend/client/mod.rs @@ -5,6 +5,7 @@ use gio::prelude::*; use isahc::http::StatusCode; use isahc::prelude::*; use serde::Serialize; +use std::time::Duration; pub mod persons; pub use persons::*; @@ -64,6 +65,7 @@ impl Backend { pub async fn set_login_data(&self, data: LoginData) -> Result<()> { secure::store_login_data(data.clone()).await?; self.login_data.replace(Some(data)); + self.token.replace(None); Ok(()) } @@ -82,7 +84,6 @@ impl Backend { StatusCode::OK => { let token = response.text_async().await?; self.set_token(&token); - println!("{}", &token); true } StatusCode::UNAUTHORIZED => false, @@ -91,4 +92,65 @@ impl Backend { Ok(success) } + + /// Make an unauthenticated get request to the server. + async fn get(&self, url: &str) -> Result { + let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?; + + let mut response = Request::get(format!("{}/{}", server_url, url)) + .timeout(Duration::from_secs(10)) + .body(())? + .send_async() + .await?; + + let body = response.text_async().await?; + + Ok(body) + } + + /// Make an authenticated post request to the server. + async fn post(&self, url: &str, body: String) -> Result { + let body = match self.get_token() { + Some(_) => { + let mut response = self.post_priv(url, body.clone()).await?; + + // Try one more time (maybe the token was expired) + if response.status() == StatusCode::UNAUTHORIZED { + if self.login().await? { + response = self.post_priv(url, body).await?; + } else { + bail!("Login failed!"); + } + } + + response.text_async().await? + } + None => { + let mut response = if self.login().await? { + self.post_priv(url, body).await? + } else { + bail!("Login failed!"); + }; + + response.text_async().await? + } + }; + + Ok(body) + } + + /// Post something to the server assuming there is a valid login token. + async fn post_priv(&self, url: &str, body: String) -> Result> { + let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?; + let token = self.get_token().ok_or(anyhow!("No login token!"))?; + + let response = Request::post(format!("{}/{}", server_url, url)) + .header("Authorization", format!("Bearer {}", token)) + .header("Content-Type", "application/json") + .body(body)? + .send_async() + .await?; + + Ok(response) + } } diff --git a/musicus/src/backend/client/persons.rs b/musicus/src/backend/client/persons.rs index af13f6f..3d56009 100644 --- a/musicus/src/backend/client/persons.rs +++ b/musicus/src/backend/client/persons.rs @@ -1,24 +1,18 @@ use super::Backend; use crate::database::Person; -use anyhow::{anyhow, Result}; -use isahc::prelude::*; -use std::time::Duration; +use anyhow::Result; 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 body = self.get("persons").await?; let persons: Vec = serde_json::from_str(&body)?; - Ok(persons) } + + /// Post a new person to the server and return the ID. + pub async fn post_person(&self, data: &Person) -> Result<()> { + self.post("persons", serde_json::to_string(data)?).await?; + Ok(()) + } } diff --git a/musicus/src/backend/library.rs b/musicus/src/backend/library.rs index 456eb03..ea0caf3 100644 --- a/musicus/src/backend/library.rs +++ b/musicus/src/backend/library.rs @@ -30,6 +30,10 @@ impl Backend { pub async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> { self.set_state(BackendState::Loading); + if let Some(db) = &*self.database.borrow() { + db.stop().await?; + } + self.music_library_path.replace(Some(path.clone())); let mut db_path = path.clone(); @@ -66,6 +70,12 @@ impl Backend { self.player.borrow().clone() } + /// Notify the frontend that the library was changed. + pub fn library_changed(&self) { + self.set_state(BackendState::Loading); + self.set_state(BackendState::Ready); + } + /// Get an interface to the player and panic if there is none. pub fn pl(&self) -> Rc { self.get_player().unwrap() diff --git a/musicus/src/database/recordings.rs b/musicus/src/database/recordings.rs index 886f067..29ed3c4 100644 --- a/musicus/src/database/recordings.rs +++ b/musicus/src/database/recordings.rs @@ -2,9 +2,7 @@ use super::schema::{ensembles, performances, persons, recordings}; use super::{Database, Ensemble, Instrument, Person, Work}; use anyhow::{anyhow, Error, Result}; use diesel::prelude::*; -use diesel::{Insertable, Queryable}; use serde::{Deserialize, Serialize}; -use std::convert::TryInto; /// Database table data for a recording. #[derive(Insertable, Queryable, Debug, Clone)] diff --git a/musicus/src/dialogs/person_editor.rs b/musicus/src/dialogs/person_editor.rs index 8f1545b..452b0a3 100644 --- a/musicus/src/dialogs/person_editor.rs +++ b/musicus/src/dialogs/person_editor.rs @@ -1,85 +1,129 @@ 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 PersonEditor -where - F: Fn(Person) -> () + 'static, -{ +/// A dialog for creating or editing a person. +pub struct PersonEditor { backend: Rc, id: String, window: libhandy::Window, - callback: F, - id: u32, + stack: gtk::Stack, + info_bar: gtk::InfoBar, first_name_entry: gtk::Entry, last_name_entry: gtk::Entry, + upload_switch: gtk::Switch, + saved_cb: RefCell ()>>>, } -impl PersonEditor -where - F: Fn(Person) -> () + 'static, -{ +impl PersonEditor { + /// Create a new person editor and optionally initialize it. pub fn new>( backend: Rc, parent: &P, person: Option, - callback: F, ) -> Rc { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_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, first_name_entry); get_widget!(builder, gtk::Entry, last_name_entry); + get_widget!(builder, gtk::Switch, upload_switch); let id = match person { Some(person) => { first_name_entry.set_text(&person.first_name); last_name_entry.set_text(&person.last_name); + person.id } - None => rand::random::().into(), + None => generate_id(), }; - let result = Rc::new(PersonEditor { - backend: backend, - window: window, - callback: callback, - id: id, - first_name_entry: first_name_entry, - last_name_entry: last_name_entry, + let this = Rc::new(Self { + backend, + id, + window, + stack, + info_bar, + first_name_entry, + last_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 person = Person { - id: result.id, - first_name: result.first_name_entry.get_text().to_string(), - last_name: result.last_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_person(person.clone()).await.unwrap(); - clone.window.close(); - (clone.callback)(person.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 person was saved. + pub fn set_saved_cb () + 'static>(&self, cb: F) { + self.saved_cb.replace(Some(Box::new(cb))); + } + + /// Show the person editor. pub fn show(&self) { self.window.show(); } + + /// Save the person and possibly upload it to the server. + async fn save(self: Rc) -> Result<()> { + let first_name = self.first_name_entry.get_text().to_string(); + let last_name = self.last_name_entry.get_text().to_string(); + + let person = Person { + id: self.id.clone(), + first_name, + last_name, + }; + + let upload = self.upload_switch.get_active(); + if upload { + self.backend.post_person(&person).await?; + } + + self.backend.db().update_person(person.clone()).await?; + self.backend.library_changed(); + + if let Some(cb) = &*self.saved_cb.borrow() { + cb(person.clone()); + } + + Ok(()) + } } diff --git a/musicus/src/dialogs/person_selector.rs b/musicus/src/dialogs/person_selector.rs index 6c8633d..379f0e9 100644 --- a/musicus/src/dialogs/person_selector.rs +++ b/musicus/src/dialogs/person_selector.rs @@ -58,15 +58,16 @@ impl PersonSelector { 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.set_saved_cb(clone!(@strong this => move |person| { + if let Some(cb) = &*this.selected_cb.borrow() { + cb(person); + } + + this.window.close(); + })); + editor.show(); }));