diff --git a/res/ui/window.ui b/res/ui/window.ui index d8076d9..02f95d8 100644 --- a/res/ui/window.ui +++ b/res/ui/window.ui @@ -88,57 +88,21 @@ 800 566 - + True False - sidebar_box - True + crossfade - - 250 + True False - False vertical True False - False - - - True - True - True - - - True - False - list-add-symbolic - - - - - - - True - True - False - True - menu - - - True - False - open-menu-symbolic - - - - - end - 1 - - + Musicus Editor + True False @@ -146,26 +110,206 @@ 0 + + + True + False + center + center + vertical + 18 + 18 + + + True + False + 0.5019607843137255 + 80 + folder-music-symbolic + + + False + True + 0 + + + + + True + False + 0.5019607843137255 + Welcome to Musicus Editor! + + + + + + False + True + 1 + + + + + True + False + 0.5019607843137255 + Get startet by selecting the folder containing your music files! Musicus will create a new database there or open one that already exists. + center + True + 40 + + + False + True + 2 + + + + + Select folder + True + True + True + center + + + + False + True + 3 + + + + + True + True + 1 + + - sidebar + empty - + True False vertical - + + + True + False + Musicus Editor + True + + + False + True + 0 + + + + + True + False + True + + + True + True + 1 + + - False + loading + 1 - + + True + False + True + + + 250 + True + False + False + vertical + + + True + False + + + True + True + True + + + True + False + list-add-symbolic + + + + + + + True + True + False + True + menu + + + True + False + open-menu-symbolic + + + + + end + 1 + + + + + False + True + 0 + + + + + sidebar + + + + + True + False + vertical + + + + False + + + + + content + 2 + diff --git a/src/backend.rs b/src/backend.rs index 4713ad8..a9db728 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,10 +1,16 @@ use super::database::*; -use anyhow::Result; -use futures_channel::oneshot; +use anyhow::{anyhow, Result}; use futures_channel::oneshot::Sender; +use futures_channel::{mpsc, oneshot}; use std::cell::RefCell; use std::path::PathBuf; +pub enum BackendState { + NoMusicLibrary, + Loading, + Ready, +} + enum BackendAction { UpdatePerson(Person, Sender>), GetPerson(i64, Sender>), @@ -28,24 +34,229 @@ enum BackendAction { GetRecordingsForPerson(i64, Sender>>), GetRecordingsForEnsemble(i64, Sender>>), GetRecordingsForWork(i64, Sender>>), + Stop, } use BackendAction::*; pub struct Backend { - action_sender: std::sync::mpsc::Sender, + pub state_stream: RefCell>, + state_sender: RefCell>, + action_sender: RefCell>>, music_library_path: RefCell>, } impl Backend { - pub fn new(url: &str, music_library_path: PathBuf) -> Self { - let url = url.to_string(); + pub fn new() -> Self { + let (state_sender, state_stream) = mpsc::channel(1024); + Backend { + state_stream: RefCell::new(state_stream), + state_sender: RefCell::new(state_sender), + action_sender: RefCell::new(None), + music_library_path: RefCell::new(None), + } + } + + pub async fn update_person(&self, person: Person) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(UpdatePerson(person, sender))?; + receiver.await? + } + + pub async fn get_person(&self, id: i64) -> Result { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()?.send(GetPerson(id, sender))?; + receiver.await? + } + + pub async fn delete_person(&self, id: i64) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(DeletePerson(id, sender))?; + receiver.await? + } + + pub async fn get_persons(&self) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()?.send(GetPersons(sender))?; + receiver.await? + } + + pub async fn update_instrument(&self, instrument: Instrument) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(UpdateInstrument(instrument, sender))?; + receiver.await? + } + + pub async fn get_instrument(&self, id: i64) -> Result { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(GetInstrument(id, sender))?; + receiver.await? + } + + pub async fn delete_instrument(&self, id: i64) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(DeleteInstrument(id, sender))?; + receiver.await? + } + + pub async fn get_instruments(&self) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()?.send(GetInstruments(sender))?; + receiver.await? + } + + pub async fn update_work(&self, work_insertion: WorkInsertion) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(UpdateWork(work_insertion, sender))?; + receiver.await? + } + + pub async fn get_work_description(&self, id: i64) -> Result { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(GetWorkDescription(id, sender))?; + receiver.await? + } + + pub async fn delete_work(&self, id: i64) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()?.send(DeleteWork(id, sender))?; + receiver.await? + } + + pub async fn get_work_descriptions(&self, person_id: i64) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(GetWorkDescriptions(person_id, sender))?; + receiver.await? + } + + pub async fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(UpdateEnsemble(ensemble, sender))?; + receiver.await? + } + + pub async fn get_ensemble(&self, id: i64) -> Result { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()?.send(GetEnsemble(id, sender))?; + receiver.await? + } + + pub async fn delete_ensemble(&self, id: i64) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(DeleteEnsemble(id, sender))?; + receiver.await? + } + + pub async fn get_ensembles(&self) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()?.send(GetEnsembles(sender))?; + receiver.await? + } + + pub async fn update_recording(&self, recording_insertion: RecordingInsertion) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(UpdateRecording(recording_insertion, sender))?; + receiver.await? + } + + pub async fn get_recording_description(&self, id: i64) -> Result { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(GetRecordingDescription(id, sender))?; + receiver.await? + } + + pub async fn delete_recording(&self, id: i64) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(DeleteRecording(id, sender))?; + receiver.await? + } + + pub async fn get_recordings_for_person( + &self, + person_id: i64, + ) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(GetRecordingsForPerson(person_id, sender))?; + receiver.await? + } + + pub async fn get_recordings_for_ensemble( + &self, + ensemble_id: i64, + ) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(GetRecordingsForEnsemble(ensemble_id, sender))?; + receiver.await? + } + + pub async fn get_recordings_for_work(&self, work_id: i64) -> Result> { + let (sender, receiver) = oneshot::channel(); + self.unwrap_action_sender()? + .send(GetRecordingsForWork(work_id, sender))?; + receiver.await? + } + + pub async fn set_music_library_path(&self, path: PathBuf) -> Result<()> { + self.music_library_path.replace(Some(path.clone())); + self.set_state(BackendState::Loading); + + if let Some(action_sender) = self.action_sender.borrow_mut().take() { + action_sender.send(Stop)?; + } + + let mut db_path = path.clone(); + db_path.push("musicus.db"); + + self.start_db_thread(String::from(db_path.to_str().unwrap())) + .await?; + + self.set_state(BackendState::Ready); + + Ok(()) + } + + pub fn get_music_library_path(&self) -> Option { + self.music_library_path.borrow().clone() + } + + fn set_state(&self, state: BackendState) { + self.state_sender.borrow_mut().try_send(state).unwrap(); + } + + fn unwrap_action_sender(&self) -> Result> { + match &*self.action_sender.borrow() { + Some(action_sender) => Ok(action_sender.clone()), + None => Err(anyhow!("Database thread is not running!")), + } + } + + async fn start_db_thread(&self, url: String) -> Result<()> { + let (ready_sender, ready_receiver) = oneshot::channel(); let (action_sender, action_receiver) = std::sync::mpsc::channel::(); std::thread::spawn(move || { let db = Database::new(&url).expect("Failed to open database!"); + ready_sender + .send(()) + .expect("Failed to communicate to main thread!"); + for action in action_receiver { match action { UpdatePerson(person, sender) => { @@ -158,167 +369,15 @@ impl Backend { .send(db.get_recordings_for_work(id)) .expect("Failed to send result from database thread!"); } + Stop => { + break; + } } } }); - Backend { - action_sender: action_sender, - music_library_path: RefCell::new(Some(music_library_path)), - } - } - - pub async fn update_person(&self, person: Person) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(UpdatePerson(person, sender))?; - receiver.await? - } - - pub async fn get_person(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetPerson(id, sender))?; - receiver.await? - } - - pub async fn delete_person(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(DeletePerson(id, sender))?; - receiver.await? - } - - pub async fn get_persons(&self) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetPersons(sender))?; - receiver.await? - } - - pub async fn update_instrument(&self, instrument: Instrument) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(UpdateInstrument(instrument, sender))?; - receiver.await? - } - - pub async fn get_instrument(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetInstrument(id, sender))?; - receiver.await? - } - - pub async fn delete_instrument(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(DeleteInstrument(id, sender))?; - receiver.await? - } - - pub async fn get_instruments(&self) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetInstruments(sender))?; - receiver.await? - } - - pub async fn update_work(&self, work_insertion: WorkInsertion) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(UpdateWork(work_insertion, sender))?; - receiver.await? - } - - pub async fn get_work_description(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetWorkDescription(id, sender))?; - receiver.await? - } - - pub async fn delete_work(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(DeleteWork(id, sender))?; - receiver.await? - } - - pub async fn get_work_descriptions(&self, person_id: i64) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(GetWorkDescriptions(person_id, sender))?; - receiver.await? - } - - pub async fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(UpdateEnsemble(ensemble, sender))?; - receiver.await? - } - - pub async fn get_ensemble(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetEnsemble(id, sender))?; - receiver.await? - } - - pub async fn delete_ensemble(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(DeleteEnsemble(id, sender))?; - receiver.await? - } - - pub async fn get_ensembles(&self) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetEnsembles(sender))?; - receiver.await? - } - - pub async fn update_recording(&self, recording_insertion: RecordingInsertion) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(UpdateRecording(recording_insertion, sender))?; - receiver.await? - } - - pub async fn get_recording_description(&self, id: i64) -> Result { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(GetRecordingDescription(id, sender))?; - receiver.await? - } - - pub async fn delete_recording(&self, id: i64) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.action_sender.send(DeleteRecording(id, sender))?; - receiver.await? - } - - pub async fn get_recordings_for_person( - &self, - person_id: i64, - ) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(GetRecordingsForPerson(person_id, sender))?; - receiver.await? - } - - pub async fn get_recordings_for_ensemble( - &self, - ensemble_id: i64, - ) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(GetRecordingsForEnsemble(ensemble_id, sender))?; - receiver.await? - } - - pub async fn get_recordings_for_work(&self, work_id: i64) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(GetRecordingsForWork(work_id, sender))?; - receiver.await? - } - - pub fn set_music_library_path(&self, path: PathBuf) { - self.music_library_path.replace(Some(path.clone())); - } - - pub fn get_music_library_path(&self) -> Option { - self.music_library_path.borrow().clone() + ready_receiver.await?; + self.action_sender.replace(Some(action_sender)); + Ok(()) } } diff --git a/src/dialogs/preferences.rs b/src/dialogs/preferences.rs index 3a64521..8a16771 100644 --- a/src/dialogs/preferences.rs +++ b/src/dialogs/preferences.rs @@ -29,7 +29,12 @@ impl Preferences { if let gtk::ResponseType::Accept = dialog.run() { if let Some(path) = dialog.get_filename() { music_library_path_row.set_subtitle(Some(path.to_str().unwrap())); - backend.set_music_library_path(path); + + let context = glib::MainContext::default(); + let backend = backend.clone(); + context.spawn_local(async move { + backend.set_music_library_path(path).await.unwrap(); + }); } } })); diff --git a/src/widgets/poe_list.rs b/src/widgets/poe_list.rs index 44b9cc3..4d82f8a 100644 --- a/src/widgets/poe_list.rs +++ b/src/widgets/poe_list.rs @@ -64,8 +64,6 @@ impl PoeList { result.list.invalidate_filter(); })); - result.clone().reload(); - result } diff --git a/src/window.rs b/src/window.rs index 2da9fcb..7030574 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,6 +2,7 @@ use crate::backend::*; use crate::dialogs::*; use crate::screens::*; use crate::widgets::*; +use futures::prelude::*; use gio::prelude::*; use glib::clone; use gtk::prelude::*; @@ -12,6 +13,7 @@ use std::rc::Rc; pub struct Window { backend: Rc, window: libhandy::ApplicationWindow, + stack: gtk::Stack, leaflet: libhandy::Leaflet, sidebar_box: gtk::Box, poe_list: Rc, @@ -23,47 +25,43 @@ impl Window { let builder = gtk::Builder::from_resource("/de/johrpan/musicus_editor/ui/window.ui"); get_widget!(builder, libhandy::ApplicationWindow, window); + get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::Button, select_music_library_path_button); get_widget!(builder, libhandy::Leaflet, leaflet); get_widget!(builder, gtk::Button, add_button); get_widget!(builder, gtk::Box, sidebar_box); get_widget!(builder, gtk::Box, empty_screen); - let backend = Rc::new(Backend::new( - "test.sqlite", - std::env::current_dir().unwrap(), - )); + let backend = Rc::new(Backend::new()); let poe_list = PoeList::new(backend.clone()); let navigator = Navigator::new(&empty_screen); let result = Rc::new(Self { backend, window, + stack, leaflet, sidebar_box, poe_list, navigator, }); - result - .poe_list - .set_selected(clone!(@strong result => move |poe| { - result.leaflet.set_visible_child(&result.navigator.widget); - match poe { - PersonOrEnsemble::Person(person) => { - result.navigator.clone().replace(PersonScreen::new(result.backend.clone(), person.clone())); - } - PersonOrEnsemble::Ensemble(ensemble) => { - result.navigator.clone().replace(EnsembleScreen::new(result.backend.clone(), ensemble.clone())); - } - } - })); - - result.leaflet.add(&result.navigator.widget); - result - .sidebar_box - .pack_start(&result.poe_list.widget, true, true, 0); result.window.set_application(Some(app)); + select_music_library_path_button.connect_clicked(clone!(@strong result => move |_| { + let dialog = gtk::FileChooserNative::new(Some("Select music library folder"), Some(&result.window), gtk::FileChooserAction::SelectFolder, None, None); + + if let gtk::ResponseType::Accept = dialog.run() { + if let Some(path) = dialog.get_filename() { + let context = glib::MainContext::default(); + let backend = result.backend.clone(); + context.spawn_local(async move { + backend.set_music_library_path(path).await.unwrap(); + }); + } + } + })); + add_button.connect_clicked(clone!(@strong result => move |_| { TracksEditor::new(result.backend.clone(), &result.window, clone!(@strong result => move || { result.reload(); @@ -266,6 +264,46 @@ impl Window { }) ); + let context = glib::MainContext::default(); + let clone = result.clone(); + context.spawn_local(async move { + let mut state_stream = clone.backend.state_stream.borrow_mut(); + while let Some(state) = state_stream.next().await { + match state { + BackendState::NoMusicLibrary => { + clone.stack.set_visible_child_name("empty"); + } + BackendState::Loading => { + clone.stack.set_visible_child_name("loading"); + } + BackendState::Ready => { + clone.stack.set_visible_child_name("content"); + clone.poe_list.clone().reload(); + } + } + } + }); + + result.leaflet.add(&result.navigator.widget); + + result + .poe_list + .set_selected(clone!(@strong result => move |poe| { + result.leaflet.set_visible_child(&result.navigator.widget); + match poe { + PersonOrEnsemble::Person(person) => { + result.navigator.clone().replace(PersonScreen::new(result.backend.clone(), person.clone())); + } + PersonOrEnsemble::Ensemble(ensemble) => { + result.navigator.clone().replace(EnsembleScreen::new(result.backend.clone(), ensemble.clone())); + } + } + })); + + result + .sidebar_box + .pack_start(&result.poe_list.widget, true, true, 0); + result }