{
+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())
+}