From bec0dfbf565e9d8d645b9254def4f0e35ff99ac6 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 29 Nov 2020 01:00:19 +0100 Subject: [PATCH] Allow to upload recordings --- musicus/res/ui/recording_editor.ui | 395 +++++++++++------- musicus/res/ui/recording_selector.ui | 165 ++++++++ musicus/res/ui/recording_selector_screen.ui | 94 +++++ musicus/res/ui/work_editor.ui | 22 +- musicus/src/backend/client/ensembles.rs | 2 +- musicus/src/backend/client/instruments.rs | 2 +- musicus/src/backend/client/mod.rs | 3 + musicus/src/backend/client/persons.rs | 2 +- musicus/src/backend/client/recordings.rs | 18 + musicus/src/backend/client/works.rs | 2 +- .../src/dialogs/recording/recording_editor.rs | 77 +++- .../dialogs/recording/recording_selector.rs | 109 ++++- .../recording_selector_person_screen.rs | 59 ++- .../recording_selector_work_screen.rs | 63 ++- musicus/src/dialogs/work/work_selector.rs | 6 +- musicus/src/meson.build | 1 + musicus_server/src/main.rs | 4 + musicus_server/src/routes/recordings.rs | 74 ++++ 18 files changed, 880 insertions(+), 218 deletions(-) create mode 100644 musicus/src/backend/client/recordings.rs diff --git a/musicus/res/ui/recording_editor.ui b/musicus/res/ui/recording_editor.ui index 50151eb..83f93a8 100644 --- a/musicus/res/ui/recording_editor.ui +++ b/musicus/res/ui/recording_editor.ui @@ -3,183 +3,253 @@ - + True False - vertical - + True False - Recording + vertical - - Cancel - True - True - True - - - - - Save - True - False - True - True - - - - end - 1 - - - - - False - True - 0 - - - - - True - True - - - + True False - 18 - 12 - 6 + Recording - - True - False - end - Comment - - - 0 - 1 - - - - + + Cancel True True True - True - - - True - False - start - Select … - end - - - - 1 - 0 - - + + Save True + False True + True + - 1 - 1 + end + 1 - - - True - False - end - Work - - - 0 - 0 - - - - - - - True - False - Overview - False + False + True + 0 - + True False - 18 - 6 + False - + + + + + False + True + 1 + + + + + True + True + False + + + True - True - in + False + 18 + 12 + 6 - + + True + False + end + Comment + + + 0 + 1 + + + + + True + True + True + True + + + True + False + start + Select … + end + + + + + 1 + 0 + + + + + True + True + + + 1 + 1 + + + + + True + False + end + Work + + + 0 + 0 + + + + + True + False + end + Publish + + + 0 + 2 + + + + + True + True + start + True + + + 1 + 2 + + + + + True + False + Overview + - True - True - 0 + False True False - 0 - vertical + 18 6 - + True True - True + in - - True - False - list-add-symbolic - + - False + True True 0 - + True - True - True + False + 0 + vertical + 6 - + True - False - edit-symbolic + True + True + + + True + False + list-add-symbolic + + + + False + True + 0 + + + + + True + True + True + + + True + False + edit-symbolic + + + + + False + True + 1 + + + + + True + True + True + + + True + False + list-remove-symbolic + + + + + False + True + 2 + @@ -188,52 +258,67 @@ 1 - - - True - True - True - - - True - False - list-remove-symbolic - - - - - False - True - 2 - - - False - True 1 + + + True + False + Performers + + + 1 + False + + - 1 - - - - - True - False - Performers - - - 1 - False + True + True + 2 - True - True + content + + + + + True + False + vertical + + + True + False + Recording + + + False + True + 0 + + + + + True + False + True + True + + + False + True + 1 + + + + + loading 1 diff --git a/musicus/res/ui/recording_selector.ui b/musicus/res/ui/recording_selector.ui index 89de248..c1100af 100644 --- a/musicus/res/ui/recording_selector.ui +++ b/musicus/res/ui/recording_selector.ui @@ -71,6 +71,171 @@ 0 + + + True + False + True + + + True + False + vertical + 6 + + + True + True + edit-find-symbolic + False + False + Search composers … + + + False + True + 0 + + + + + Show works from the server + True + True + False + True + True + + + False + True + 1 + + + + + + + False + True + 1 + + + + + True + False + False + crossfade + True + + + 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 + 2 + + sidebar diff --git a/musicus/res/ui/recording_selector_screen.ui b/musicus/res/ui/recording_selector_screen.ui index 55f31fe..ee1e7ae 100644 --- a/musicus/res/ui/recording_selector_screen.ui +++ b/musicus/res/ui/recording_selector_screen.ui @@ -38,6 +38,9 @@ True False + False + crossfade + True True @@ -48,6 +51,97 @@ 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 diff --git a/musicus/res/ui/work_editor.ui b/musicus/res/ui/work_editor.ui index c587094..82557d6 100644 --- a/musicus/res/ui/work_editor.ui +++ b/musicus/res/ui/work_editor.ui @@ -52,6 +52,26 @@ True False False + + + False + + + False + False + 0 + + + + + False + + + False + False + 0 + + @@ -442,7 +462,7 @@ True False - Ensemble + Work False diff --git a/musicus/src/backend/client/ensembles.rs b/musicus/src/backend/client/ensembles.rs index ab9b127..f87919e 100644 --- a/musicus/src/backend/client/ensembles.rs +++ b/musicus/src/backend/client/ensembles.rs @@ -10,7 +10,7 @@ impl Backend { Ok(ensembles) } - /// Post a new ensemble to the server and return the ID. + /// Post a new ensemble to the server. 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/instruments.rs b/musicus/src/backend/client/instruments.rs index bbb2d65..6b4a162 100644 --- a/musicus/src/backend/client/instruments.rs +++ b/musicus/src/backend/client/instruments.rs @@ -10,7 +10,7 @@ impl Backend { Ok(instruments) } - /// Post a new instrument to the server and return the ID. + /// Post a new instrument to the server. 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 490035a..7c7c3d5 100644 --- a/musicus/src/backend/client/mod.rs +++ b/musicus/src/backend/client/mod.rs @@ -16,6 +16,9 @@ pub use instruments::*; pub mod persons; pub use persons::*; +pub mod recordings; +pub use recordings::*; + pub mod works; pub use works::*; diff --git a/musicus/src/backend/client/persons.rs b/musicus/src/backend/client/persons.rs index 3d56009..b864189 100644 --- a/musicus/src/backend/client/persons.rs +++ b/musicus/src/backend/client/persons.rs @@ -10,7 +10,7 @@ impl Backend { Ok(persons) } - /// Post a new person to the server and return the ID. + /// Post a new person to the server. 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/client/recordings.rs b/musicus/src/backend/client/recordings.rs new file mode 100644 index 0000000..a836f25 --- /dev/null +++ b/musicus/src/backend/client/recordings.rs @@ -0,0 +1,18 @@ +use super::Backend; +use crate::database::Recording; +use anyhow::Result; + +impl Backend { + /// Get all available recordings from the server. + pub async fn get_recordings_for_work(&self, work_id: &str) -> Result> { + let body = self.get(&format!("works/{}/recordings", work_id)).await?; + let recordings: Vec = serde_json::from_str(&body)?; + Ok(recordings) + } + + /// Post a new recording to the server. + pub async fn post_recording(&self, data: &Recording) -> Result<()> { + self.post("recordings", serde_json::to_string(data)?).await?; + Ok(()) + } +} diff --git a/musicus/src/backend/client/works.rs b/musicus/src/backend/client/works.rs index 786299f..a28cf34 100644 --- a/musicus/src/backend/client/works.rs +++ b/musicus/src/backend/client/works.rs @@ -10,7 +10,7 @@ impl Backend { Ok(works) } - /// Post a new work to the server and return the ID. + /// Post a new work to the server. pub async fn post_work(&self, data: &Work) -> Result<()> { self.post("works", serde_json::to_string(data)?).await?; Ok(()) diff --git a/musicus/src/dialogs/recording/recording_editor.rs b/musicus/src/dialogs/recording/recording_editor.rs index 44afbce..50a77c8 100644 --- a/musicus/src/dialogs/recording/recording_editor.rs +++ b/musicus/src/dialogs/recording/recording_editor.rs @@ -3,6 +3,7 @@ use crate::backend::*; use crate::database::*; use crate::dialogs::*; use crate::widgets::*; +use anyhow::Result; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -13,12 +14,14 @@ use std::rc::Rc; /// A widget for creating or editing a recording. // TODO: Disable buttons if no performance is selected. pub struct RecordingEditor { - pub widget: gtk::Box, + pub widget: gtk::Stack, backend: Rc, parent: gtk::Window, save_button: gtk::Button, + info_bar: gtk::InfoBar, work_label: gtk::Label, comment_entry: gtk::Entry, + upload_switch: gtk::Switch, performance_list: Rc>, id: String, work: RefCell>, @@ -37,15 +40,16 @@ impl RecordingEditor { ) -> Rc { // Create UI - let builder = - gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui"); + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui"); - get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Stack, widget); get_widget!(builder, gtk::Button, cancel_button); get_widget!(builder, gtk::Button, save_button); + get_widget!(builder, gtk::InfoBar, info_bar); get_widget!(builder, gtk::Button, work_button); get_widget!(builder, gtk::Label, work_label); get_widget!(builder, gtk::Entry, comment_entry); + get_widget!(builder, gtk::Switch, upload_switch); get_widget!(builder, gtk::ScrolledWindow, scroll); get_widget!(builder, gtk::Button, add_performer_button); get_widget!(builder, gtk::Button, edit_performer_button); @@ -67,8 +71,10 @@ impl RecordingEditor { backend, parent: parent.clone().upcast(), save_button, + info_bar, work_label, comment_entry, + upload_switch, performance_list, id, work: RefCell::new(work), @@ -87,20 +93,20 @@ impl RecordingEditor { this.save_button .connect_clicked(clone!(@strong this => move |_| { - let recording = Recording { - id: this.id.clone(), - work: this.work.borrow().clone().expect("Tried to create recording without work!"), - comment: this.comment_entry.get_text().to_string(), - performances: this.performances.borrow().clone(), - }; - - let c = glib::MainContext::default(); + let context = glib::MainContext::default(); let clone = this.clone(); - c.spawn_local(async move { - clone.backend.db().update_recording(recording.clone().into()).await.unwrap(); - if let Some(cb) = &*clone.selected_cb.borrow() { - cb(recording.clone()); + context.spawn_local(async move { + clone.widget.set_visible_child_name("loading"); + match clone.clone().save().await { + Ok(_) => { + // We already called the callback. + } + Err(_) => { + clone.info_bar.set_revealed(true); + clone.widget.set_visible_child_name("content"); + } } + }); })); @@ -181,7 +187,8 @@ impl RecordingEditor { this.work_selected(work); } - this.performance_list.show_items(this.performances.borrow().clone()); + this.performance_list + .show_items(this.performances.borrow().clone()); this } @@ -198,7 +205,41 @@ impl RecordingEditor { /// Update the UI according to work. fn work_selected(&self, work: &Work) { - self.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title)); + self.work_label + .set_text(&format!("{}: {}", work.composer.name_fl(), work.title)); self.save_button.set_sensitive(true); } + + /// Save the recording and possibly upload it to the server. + async fn save(self: Rc) -> Result<()> { + let recording = Recording { + id: self.id.clone(), + work: self + .work + .borrow() + .clone() + .expect("Tried to create recording without work!"), + comment: self.comment_entry.get_text().to_string(), + performances: self.performances.borrow().clone(), + }; + + let upload = self.upload_switch.get_active(); + if upload { + self.backend.post_recording(&recording).await?; + } + + self.backend + .db() + .update_recording(recording.clone().into()) + .await + .unwrap(); + + self.backend.library_changed(); + + if let Some(cb) = &*self.selected_cb.borrow() { + cb(recording.clone()); + } + + Ok(()) + } } diff --git a/musicus/src/dialogs/recording/recording_selector.rs b/musicus/src/dialogs/recording/recording_selector.rs index a1f0e2f..a8ce9d4 100644 --- a/musicus/src/dialogs/recording/recording_selector.rs +++ b/musicus/src/dialogs/recording/recording_selector.rs @@ -2,6 +2,7 @@ use super::recording_selector_person_screen::*; use crate::backend::Backend; use crate::database::*; use crate::widgets::*; +use gettextrs::gettext; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; @@ -14,6 +15,9 @@ pub struct RecordingSelector { pub widget: libhandy::Leaflet, backend: Rc, sidebar_box: gtk::Box, + server_check_button: gtk::CheckButton, + stack: gtk::Stack, + list: Rc>, selected_cb: RefCell ()>>>, add_cb: RefCell ()>>>, navigator: Rc, @@ -29,10 +33,15 @@ impl RecordingSelector { get_widget!(builder, libhandy::Leaflet, widget); get_widget!(builder, gtk::Button, add_button); get_widget!(builder, gtk::Box, sidebar_box); + 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); get_widget!(builder, gtk::Box, empty_screen); - let person_list = PersonList::new(backend.clone()); - sidebar_box.pack_start(&person_list.widget, true, true, 0); + let list = List::::new(&gettext("No persons found.")); + scroll.add(&list.widget); let navigator = Navigator::new(&empty_screen); widget.add(&navigator.widget); @@ -41,6 +50,9 @@ impl RecordingSelector { widget, backend, sidebar_box, + server_check_button, + stack, + list, selected_cb: RefCell::new(None), add_cb: RefCell::new(None), navigator, @@ -54,26 +66,99 @@ impl RecordingSelector { } })); - person_list.set_selected(clone!(@strong this => move |person| { - let person_screen = RecordingSelectorPersonScreen::new( - this.backend.clone(), - person.clone(), - ); + search_entry.connect_search_changed(clone!(@strong this => move |_| { + this.list.invalidate_filter(); + })); - person_screen.set_selected_cb(clone!(@strong this => move |recording| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(recording); + 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(); + } + }), + ); + + 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.navigator.clone().push(person_screen); - this.widget.set_visible_child(&this.navigator.widget); + this.list + .set_selected(clone!(@strong this => move |person| { + let online = this.server_check_button.get_active(); + + let person_screen = RecordingSelectorPersonScreen::new( + this.backend.clone(), + person.clone(), + online, + ); + + person_screen.set_selected_cb(clone!(@strong this => move |work| { + if let Some(cb) = &*this.selected_cb.borrow() { + cb(work); + } + })); + + this.navigator.clone().replace(person_screen); + this.widget.set_visible_child(&this.navigator.widget); + })); + + try_again_button.connect_clicked(clone!(@strong load_online => move |_| { + load_online(); })); this.navigator.set_back_cb(clone!(@strong this => move || { this.widget.set_visible_child(&this.sidebar_box); })); + // Initialize + load_online(); + this } diff --git a/musicus/src/dialogs/recording/recording_selector_person_screen.rs b/musicus/src/dialogs/recording/recording_selector_person_screen.rs index 648b57c..ba64d2a 100644 --- a/musicus/src/dialogs/recording/recording_selector_person_screen.rs +++ b/musicus/src/dialogs/recording/recording_selector_person_screen.rs @@ -14,6 +14,8 @@ use std::rc::Rc; /// screen on selection. pub struct RecordingSelectorPersonScreen { backend: Rc, + person: Person, + online: bool, widget: gtk::Box, stack: gtk::Stack, work_list: Rc>, @@ -23,7 +25,7 @@ pub struct RecordingSelectorPersonScreen { impl RecordingSelectorPersonScreen { /// Create a new recording selector person screen. - pub fn new(backend: Rc, person: Person) -> Rc { + pub fn new(backend: Rc, person: Person, online: bool) -> Rc { // Create UI let builder = @@ -33,14 +35,18 @@ impl RecordingSelectorPersonScreen { get_widget!(builder, libhandy::HeaderBar, header); get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::ScrolledWindow, scroll); + get_widget!(builder, gtk::Button, try_again_button); header.set_title(Some(&person.name_fl())); let work_list = List::new(&gettext("No works found.")); - stack.add_named(&work_list.widget, "content"); + scroll.add(&work_list.widget); let this = Rc::new(Self { backend, + person, + online, widget, stack, work_list, @@ -57,6 +63,37 @@ impl RecordingSelectorPersonScreen { } })); + 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_works(&clone.person.id).await { + Ok(works) => { + clone.work_list.show_items(works); + clone.stack.set_visible_child_name("content"); + } + Err(_) => { + clone.work_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 works = clone.backend.db().get_works(&clone.person.id).await.unwrap(); + clone.work_list.show_items(works); + clone.stack.set_visible_child_name("content"); + }); + })); + this.work_list.set_make_widget(|work: &Work| { let label = gtk::Label::new(Some(&work.title)); label.set_ellipsize(pango::EllipsizeMode::End); @@ -75,6 +112,7 @@ impl RecordingSelectorPersonScreen { let work_screen = RecordingSelectorWorkScreen::new( this.backend.clone(), work.clone(), + this.online, ); work_screen.set_selected_cb(clone!(@strong this => move |recording| { @@ -87,16 +125,17 @@ impl RecordingSelectorPersonScreen { } })); + try_again_button.connect_clicked(clone!(@strong load_online => move |_| { + load_online(); + })); + // Initialize - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let works = clone.backend.db().get_works(&person.id).await.unwrap(); - - clone.work_list.show_items(works); - clone.stack.set_visible_child_name("content"); - }); + if this.online { + load_online(); + } else { + load_local(); + } this } diff --git a/musicus/src/dialogs/recording/recording_selector_work_screen.rs b/musicus/src/dialogs/recording/recording_selector_work_screen.rs index 31ee47b..3e23660 100644 --- a/musicus/src/dialogs/recording/recording_selector_work_screen.rs +++ b/musicus/src/dialogs/recording/recording_selector_work_screen.rs @@ -12,6 +12,8 @@ use std::rc::Rc; /// A screen within the recording selector presenting a list of recordings for a work. pub struct RecordingSelectorWorkScreen { backend: Rc, + work: Work, + online: bool, widget: gtk::Box, stack: gtk::Stack, recording_list: Rc>, @@ -21,7 +23,7 @@ pub struct RecordingSelectorWorkScreen { impl RecordingSelectorWorkScreen { /// Create a new recording selector work screen. - pub fn new(backend: Rc, work: Work) -> Rc { + pub fn new(backend: Rc, work: Work, online: bool) -> Rc { // Create UI let builder = @@ -31,15 +33,19 @@ impl RecordingSelectorWorkScreen { get_widget!(builder, libhandy::HeaderBar, header); get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::ScrolledWindow, scroll); + get_widget!(builder, gtk::Button, try_again_button); header.set_title(Some(&work.title)); header.set_subtitle(Some(&work.composer.name_fl())); let recording_list = List::new(&gettext("No recordings found.")); - stack.add_named(&recording_list.widget, "content"); + scroll.add(&recording_list.widget); let this = Rc::new(Self { backend, + work, + online, widget, stack, recording_list, @@ -56,6 +62,37 @@ impl RecordingSelectorWorkScreen { } })); + 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_recordings_for_work(&clone.work.id).await { + Ok(recordings) => { + clone.recording_list.show_items(recordings); + clone.stack.set_visible_child_name("content"); + } + Err(_) => { + clone.recording_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 recordings = clone.backend.db().get_recordings_for_work(&clone.work.id).await.unwrap(); + clone.recording_list.show_items(recordings); + clone.stack.set_visible_child_name("content"); + }); + })); + this.recording_list .set_make_widget(|recording: &Recording| { let work_label = gtk::Label::new(Some(&recording.work.get_title())); @@ -82,21 +119,17 @@ impl RecordingSelectorWorkScreen { } })); + try_again_button.connect_clicked(clone!(@strong load_online => move |_| { + load_online(); + })); + // Initialize - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let recordings = clone - .backend - .db() - .get_recordings_for_work(&work.id) - .await - .unwrap(); - - clone.recording_list.show_items(recordings); - clone.stack.set_visible_child_name("content"); - }); + if this.online { + load_online(); + } else { + load_local(); + } this } diff --git a/musicus/src/dialogs/work/work_selector.rs b/musicus/src/dialogs/work/work_selector.rs index 2685ceb..080f0d9 100644 --- a/musicus/src/dialogs/work/work_selector.rs +++ b/musicus/src/dialogs/work/work_selector.rs @@ -152,13 +152,13 @@ impl WorkSelector { load_online(); })); - // Initialize - load_online(); - this.navigator.set_back_cb(clone!(@strong this => move || { this.widget.set_visible_child(&this.sidebar_box); })); + // Initialize + load_online(); + this } diff --git a/musicus/src/meson.build b/musicus/src/meson.build index bf0ad72..53abb39 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -37,6 +37,7 @@ sources = files( 'backend/client/ensembles.rs', 'backend/client/instruments.rs', 'backend/client/persons.rs', + 'backend/client/recordings.rs', 'backend/client/works.rs', 'backend/library.rs', 'backend/mod.rs', diff --git a/musicus_server/src/main.rs b/musicus_server/src/main.rs index efd35e7..04039aa 100644 --- a/musicus_server/src/main.rs +++ b/musicus_server/src/main.rs @@ -43,6 +43,10 @@ async fn main() -> std::io::Result<()> { .service(update_work) .service(delete_work) .service(get_works) + .service(get_recording) + .service(update_recording) + .service(delete_recording) + .service(get_recordings_for_work) }); server.bind("127.0.0.1:8087")?.run().await diff --git a/musicus_server/src/routes/recordings.rs b/musicus_server/src/routes/recordings.rs index e69de29..17f5710 100644 --- a/musicus_server/src/routes/recordings.rs +++ b/musicus_server/src/routes/recordings.rs @@ -0,0 +1,74 @@ +use super::authenticate; +use crate::database; +use crate::database::{DbPool, Recording}; +use crate::error::ServerError; +use actix_web::{delete, get, post, web, HttpResponse}; +use actix_web_httpauth::extractors::bearer::BearerAuth; + +/// Get an existing recording. +#[get("/recordings/{id}")] +pub async fn get_recording( + db: web::Data, + id: web::Path, +) -> Result { + let data = web::block(move || { + let conn = db.into_inner().get()?; + database::get_recording(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) + }) + .await?; + + Ok(HttpResponse::Ok().json(data)) +} + +/// Add a new recording or update an existin one. The user must be authorized to do that. +#[post("/recordings")] +pub async fn update_recording( + 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_recording(&conn, &data.into_inner(), &user)?; + + Ok(()) + }) + .await?; + + Ok(HttpResponse::Ok().finish()) +} + +#[get("/works/{id}/recordings")] +pub async fn get_recordings_for_work( + db: web::Data, + work_id: web::Path, +) -> Result { + let data = web::block(move || { + let conn = db.into_inner().get()?; + Ok(database::get_recordings_for_work(&conn, &work_id.into_inner())?) + }) + .await?; + + Ok(HttpResponse::Ok().json(data)) +} + +#[delete("/recordings/{id}")] +pub async fn delete_recording( + 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_recording(&conn, &id.into_inner(), &user)?; + + Ok(()) + }) + .await?; + + Ok(HttpResponse::Ok().finish()) +}