From c72bc71432bf8b121c6a381a18a66519fc2afec3 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 3 Feb 2021 15:46:48 +0100 Subject: [PATCH] Add new work and recording selectors --- src/editors/recording.rs | 7 +- src/import/track_set_editor.rs | 9 +- src/selectors/mod.rs | 12 +- src/selectors/recording.rs | 260 +++++++++++++++++++++++---------- src/selectors/work.rs | 179 ++++++++++++++--------- 5 files changed, 311 insertions(+), 156 deletions(-) diff --git a/src/editors/recording.rs b/src/editors/recording.rs index a187f13..e5d66c0 100644 --- a/src/editors/recording.rs +++ b/src/editors/recording.rs @@ -1,7 +1,7 @@ use super::performance::PerformanceEditor; use crate::backend::Backend; use crate::database::*; -use crate::selectors::PersonSelector; +use crate::selectors::{PersonSelector, WorkSelector}; use crate::widgets::{List, Widget}; use crate::navigator::{NavigationHandle, Screen}; use anyhow::Result; @@ -94,7 +94,10 @@ impl Screen, Recording> for RecordingEditor { work_button.connect_clicked(clone!(@weak this => move |_| { spawn!(@clone this, async move { - // TODO: We need the pushed screen to return a work here. + if let Some(work) = push!(this.handle, WorkSelector).await { + this.work_selected(&work); + this.work.replace(Some(work)); + } }); })); diff --git a/src/import/track_set_editor.rs b/src/import/track_set_editor.rs index 95febd4..60f07d2 100644 --- a/src/import/track_set_editor.rs +++ b/src/import/track_set_editor.rs @@ -4,7 +4,7 @@ use super::track_selector::TrackSelector; use crate::backend::Backend; use crate::database::Recording; use crate::navigator::{NavigationHandle, Screen}; -use crate::selectors::PersonSelector; +use crate::selectors::{PersonSelector, RecordingSelector}; use crate::widgets::{List, Widget}; use gettextrs::gettext; use glib::clone; @@ -88,7 +88,12 @@ impl Screen>, TrackSetData> for TrackSetEditor { })); select_recording_button.connect_clicked(clone!(@weak this => move |_| { - // TODO: We need to push a screen returning a recording here. + spawn!(@clone this, async move { + if let Some(recording) = push!(this.handle, RecordingSelector).await { + this.recording.replace(Some(recording)); + this.recording_selected(); + } + }); })); edit_tracks_button.connect_clicked(clone!(@weak this => move |_| { diff --git a/src/selectors/mod.rs b/src/selectors/mod.rs index e42d9ab..d4000d0 100644 --- a/src/selectors/mod.rs +++ b/src/selectors/mod.rs @@ -7,12 +7,10 @@ pub use instrument::*; pub mod person; pub use person::*; -// TODO: Readd a better version of these. -// -// pub mod recording; -// pub use recording::*; -// -// pub mod work; -// pub use work::*; +pub mod recording; +pub use recording::*; + +pub mod work; +pub use work::*; mod selector; diff --git a/src/selectors/recording.rs b/src/selectors/recording.rs index 8b8e400..86c6722 100644 --- a/src/selectors/recording.rs +++ b/src/selectors/recording.rs @@ -1,8 +1,9 @@ use super::selector::Selector; use crate::backend::Backend; -use crate::database::{Recording, Work}; -use crate::editors::RecordingEditor; -use crate::widgets::{Navigator, NavigatorScreen}; +use crate::database::{Person, Work, Recording}; +use crate::editors::{PersonEditor, WorkEditor, RecordingEditor}; +use crate::navigator::{NavigationHandle, Screen}; +use crate::widgets::Widget; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -12,111 +13,222 @@ use std::rc::Rc; /// A screen for selecting a recording. pub struct RecordingSelector { - backend: Rc, - work: Work, - selector: Rc>, - selected_cb: RefCell ()>>>, - navigator: RefCell>>, + handle: NavigationHandle, + selector: Rc>, } -impl RecordingSelector { - /// Create a new recording selector for recordings of a specific work. - pub fn new(backend: Rc, work: Work) -> Rc { +impl Screen<(), Recording> for RecordingSelector { + fn new(_: (), handle: NavigationHandle) -> Rc { // Create UI - let selector = Selector::::new(); - selector.set_title(&gettext("Select recording")); - selector.set_subtitle(&work.get_title()); + let selector = Selector::::new(); + selector.set_title(&gettext("Select composer")); let this = Rc::new(Self { - backend, - work, + handle, selector, - selected_cb: RefCell::new(None), - navigator: RefCell::new(None), }); // Connect signals and callbacks - this.selector.set_back_cb(clone!(@strong this => move || { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } + this.selector.set_back_cb(clone!(@weak this => move || { + this.handle.pop(None); })); - this.selector.set_add_cb(clone!(@strong this => move || { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let recording = Recording::new(this.work.clone()); + this.selector.set_add_cb(clone!(@weak this => move || { + spawn!(@clone this, async move { + if let Some(person) = push!(this.handle, PersonEditor, None).await { + // We can assume that there are no existing works of this composer and + // immediately show the work editor. Going back from the work editor will + // correctly show the person selector again. - let editor = RecordingEditor::new(this.backend.clone(), Some(recording)); - - editor - .set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { - navigator.clone().pop(); - this.select(&recording); - })); - - navigator.push(editor); - } + let work = Work::new(person); + if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { + // There will also be no existing recordings, so we show the recording + // editor next. + + let recording = Recording::new(work); + if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await { + this.handle.pop(Some(recording)); + } + } + } + }); })); - this.selector - .set_load_online(clone!(@strong this => move || { - let clone = this.clone(); - async move { clone.backend.get_recordings_for_work(&clone.work.id).await } - })); + this.selector.set_load_online(clone!(@weak this => move || { + async move { this.handle.backend.get_persons().await } + })); - this.selector - .set_load_local(clone!(@strong this => move || { - let clone = this.clone(); - async move { clone.backend.db().get_recordings_for_work(&clone.work.id).await.unwrap() } - })); + this.selector.set_load_local(clone!(@weak this => move || { + async move { this.handle.backend.db().get_persons().await.unwrap() } + })); - this.selector.set_make_widget(clone!(@strong this => move |recording| { + this.selector.set_make_widget(clone!(@weak this => move |person| { let row = libadwaita::ActionRow::new(); row.set_activatable(true); - row.set_title(Some(&recording.get_performers())); + row.set_title(Some(&person.name_lf())); - let recording = recording.to_owned(); - row.connect_activated(clone!(@strong this => move |_| { - this.select(&recording); + let person = person.to_owned(); + row.connect_activated(clone!(@weak this => move |_| { + // Instead of returning the person from here, like the person selector does, we + // show a second selector for choosing the work. + + let person = person.clone(); + spawn!(@clone this, async move { + if let Some(work) = push!(this.handle, RecordingSelectorWorkScreen, person).await { + // Now the user can select a recording for that work. + + if let Some(recording) = push!(this.handle, RecordingSelectorRecordingScreen, work).await { + this.handle.pop(Some(recording)); + } + } + }); })); row.upcast() })); - this.selector.set_filter(|search, recording| { - recording.get_performers().to_lowercase().contains(search) - }); + this.selector + .set_filter(|search, person| person.name_fl().to_lowercase().contains(search)); this } - - /// Set the closure to be called when an item is selected. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Select a recording. - fn select(&self, recording: &Recording) { - if let Some(cb) = &*self.selected_cb.borrow() { - cb(&recording); - } - } } -impl NavigatorScreen for RecordingSelector { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - +impl Widget for RecordingSelector { fn get_widget(&self) -> gtk::Widget { self.selector.widget.clone().upcast() } +} - fn detach_navigator(&self) { - self.navigator.replace(None); +/// The work selector within the recording selector. +struct RecordingSelectorWorkScreen { + handle: NavigationHandle, + person: Person, + selector: Rc>, +} + +impl Screen for RecordingSelectorWorkScreen { + fn new(person: Person, handle: NavigationHandle) -> Rc { + let selector = Selector::::new(); + selector.set_title(&gettext("Select work")); + selector.set_subtitle(&person.name_fl()); + + let this = Rc::new(Self { + handle, + person, + selector, + }); + + this.selector.set_back_cb(clone!(@weak this => move || { + this.handle.pop(None); + })); + + this.selector.set_add_cb(clone!(@weak this => move || { + spawn!(@clone this, async move { + let work = Work::new(this.person.clone()); + if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { + this.handle.pop(Some(work)); + } + }); + })); + + this.selector.set_load_online(clone!(@weak this => move || { + async move { this.handle.backend.get_works(&this.person.id).await } + })); + + this.selector.set_load_local(clone!(@weak this => move || { + async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() } + })); + + this.selector.set_make_widget(clone!(@weak this => move |work| { + let row = libadwaita::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&work.title)); + + let work = work.to_owned(); + row.connect_activated(clone!(@weak this => move |_| { + this.handle.pop(Some(work.clone())); + })); + + row.upcast() + })); + + this.selector.set_filter(|search, work| work.title.to_lowercase().contains(search)); + + this + } +} + +impl Widget for RecordingSelectorWorkScreen { + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() + } +} + +/// The actual recording selector within the recording selector. +struct RecordingSelectorRecordingScreen { + handle: NavigationHandle, + work: Work, + selector: Rc>, +} + +impl Screen for RecordingSelectorRecordingScreen { + fn new(work: Work, handle: NavigationHandle) -> Rc { + let selector = Selector::::new(); + selector.set_title(&gettext("Select recording")); + selector.set_subtitle(&work.get_title()); + + let this = Rc::new(Self { + handle, + work, + selector, + }); + + this.selector.set_back_cb(clone!(@weak this => move || { + this.handle.pop(None); + })); + + this.selector.set_add_cb(clone!(@weak this => move || { + spawn!(@clone this, async move { + let recording = Recording::new(this.work.clone()); + if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await { + this.handle.pop(Some(recording)); + } + }); + })); + + this.selector.set_load_online(clone!(@weak this => move || { + async move { this.handle.backend.get_recordings_for_work(&this.work.id).await } + })); + + this.selector.set_load_local(clone!(@weak this => move || { + async move { this.handle.backend.db().get_recordings_for_work(&this.work.id).await.unwrap() } + })); + + this.selector.set_make_widget(clone!(@weak this => move |recording| { + let row = libadwaita::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&recording.get_performers())); + + let recording = recording.to_owned(); + row.connect_activated(clone!(@weak this => move |_| { + this.handle.pop(Some(recording.clone())); + })); + + row.upcast() + })); + + this.selector + .set_filter(|search, recording| recording.get_performers().to_lowercase().contains(search)); + + this + } +} + +impl Widget for RecordingSelectorRecordingScreen { + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() } } diff --git a/src/selectors/work.rs b/src/selectors/work.rs index b163134..43ad346 100644 --- a/src/selectors/work.rs +++ b/src/selectors/work.rs @@ -1,8 +1,9 @@ use super::selector::Selector; use crate::backend::Backend; use crate::database::{Person, Work}; -use crate::editors::WorkEditor; -use crate::widgets::{Navigator, NavigatorScreen}; +use crate::editors::{PersonEditor, WorkEditor}; +use crate::navigator::{NavigationHandle, Screen}; +use crate::widgets::Widget; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -12,110 +13,146 @@ use std::rc::Rc; /// A screen for selecting a work. pub struct WorkSelector { - backend: Rc, - person: Person, - selector: Rc>, - selected_cb: RefCell ()>>>, - navigator: RefCell>>, + handle: NavigationHandle, + selector: Rc>, } -impl WorkSelector { - /// Create a new work selector for works by a specific composer. - pub fn new(backend: Rc, person: Person) -> Rc { +impl Screen<(), Work> for WorkSelector { + fn new(_: (), handle: NavigationHandle) -> Rc { // Create UI - let selector = Selector::::new(); - selector.set_title(&gettext("Select work")); - selector.set_subtitle(&person.name_fl()); + let selector = Selector::::new(); + selector.set_title(&gettext("Select composer")); let this = Rc::new(Self { - backend, - person, + handle, selector, - selected_cb: RefCell::new(None), - navigator: RefCell::new(None), }); // Connect signals and callbacks - this.selector.set_back_cb(clone!(@strong this => move || { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } + this.selector.set_back_cb(clone!(@weak this => move || { + this.handle.pop(None); })); - this.selector.set_add_cb(clone!(@strong this => move || { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let work = Work::new(this.person.clone()); + this.selector.set_add_cb(clone!(@weak this => move || { + spawn!(@clone this, async move { + if let Some(person) = push!(this.handle, PersonEditor, None).await { + // We can assume that there are no existing works of this composer and + // immediately show the work editor. Going back from the work editor will + // correctly show the person selector again. - let editor = WorkEditor::new(this.backend.clone(), Some(work)); - - editor - .set_saved_cb(clone!(@strong this, @strong navigator => move |work| { - navigator.clone().pop(); - this.select(&work); - })); - - navigator.push(editor); - } + let work = Work::new(person); + if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { + this.handle.pop(Some(work)); + } + } + }); })); - this.selector - .set_load_online(clone!(@strong this => move || { - let clone = this.clone(); - async move { clone.backend.get_works(&clone.person.id).await } - })); + this.selector.set_load_online(clone!(@weak this => move || { + async move { this.handle.backend.get_persons().await } + })); - this.selector - .set_load_local(clone!(@strong this => move || { - let clone = this.clone(); - async move { clone.backend.db().get_works(&clone.person.id).await.unwrap() } - })); + this.selector.set_load_local(clone!(@weak this => move || { + async move { this.handle.backend.db().get_persons().await.unwrap() } + })); - this.selector.set_make_widget(clone!(@strong this => move |work| { + this.selector.set_make_widget(clone!(@weak this => move |person| { let row = libadwaita::ActionRow::new(); row.set_activatable(true); - row.set_title(Some(&work.title)); + row.set_title(Some(&person.name_lf())); - let work = work.to_owned(); - row.connect_activated(clone!(@strong this => move |_| { - this.select(&work); + let person = person.to_owned(); + row.connect_activated(clone!(@weak this => move |_| { + // Instead of returning the person from here, like the person selector does, we + // show a second selector for choosing the work. + + let person = person.clone(); + spawn!(@clone this, async move { + if let Some(work) = push!(this.handle, WorkSelectorWorkScreen, person).await { + this.handle.pop(Some(work)); + } + }); })); row.upcast() })); this.selector - .set_filter(|search, work| work.title.to_lowercase().contains(search)); + .set_filter(|search, person| person.name_fl().to_lowercase().contains(search)); this } - - /// Set the closure to be called when an item is selected. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Select a work. - fn select(&self, work: &Work) { - if let Some(cb) = &*self.selected_cb.borrow() { - cb(&work); - } - } } -impl NavigatorScreen for WorkSelector { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - +impl Widget for WorkSelector { fn get_widget(&self) -> gtk::Widget { self.selector.widget.clone().upcast() } +} - fn detach_navigator(&self) { - self.navigator.replace(None); +/// The actual work selector that is displayed after the user has selected a composer. +struct WorkSelectorWorkScreen { + handle: NavigationHandle, + person: Person, + selector: Rc>, +} + +impl Screen for WorkSelectorWorkScreen { + fn new(person: Person, handle: NavigationHandle) -> Rc { + let selector = Selector::::new(); + selector.set_title(&gettext("Select work")); + selector.set_subtitle(&person.name_fl()); + + let this = Rc::new(Self { + handle, + person, + selector, + }); + + this.selector.set_back_cb(clone!(@weak this => move || { + this.handle.pop(None); + })); + + this.selector.set_add_cb(clone!(@weak this => move || { + spawn!(@clone this, async move { + let work = Work::new(this.person.clone()); + if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { + this.handle.pop(Some(work)); + } + }); + })); + + this.selector.set_load_online(clone!(@weak this => move || { + async move { this.handle.backend.get_works(&this.person.id).await } + })); + + this.selector.set_load_local(clone!(@weak this => move || { + async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() } + })); + + this.selector.set_make_widget(clone!(@weak this => move |work| { + let row = libadwaita::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&work.title)); + + let work = work.to_owned(); + row.connect_activated(clone!(@weak this => move |_| { + this.handle.pop(Some(work.clone())); + })); + + row.upcast() + })); + + this.selector.set_filter(|search, work| work.title.to_lowercase().contains(search)); + + this + } +} + +impl Widget for WorkSelectorWorkScreen { + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() } }