database: Remove wrapper thread

This commit is contained in:
Elias Projahn 2022-01-23 14:35:33 +01:00
parent 678367ec1a
commit 42d1d047e3
31 changed files with 267 additions and 826 deletions

1
Cargo.lock generated
View file

@ -1021,7 +1021,6 @@ dependencies = [
"log",
"rand",
"thiserror",
"tokio",
"uuid",
]

View file

@ -1,4 +1,4 @@
use musicus_database::DbThread;
use musicus_database::Database;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
@ -19,7 +19,7 @@ pub mod player;
pub use player::*;
/// General states the application can be in.
#[derive(Debug, Clone)]
#[derive(Debug, Copy, Clone)]
pub enum BackendState {
/// The backend is not set up yet. This means that no backend methods except for setting the
/// music library path should be called. The user interface should adapt and only present this
@ -36,8 +36,8 @@ pub enum BackendState {
/// A collection of all backend state and functionality.
pub struct Backend {
/// The internal sender to publish the state via state_stream.
state_sender: Sender<BackendState>,
/// A closure that will be called whenever the backend state changes.
state_cb: RefCell<Option<Box<dyn Fn(BackendState)>>>,
/// Access to GSettings.
settings: gio::Settings,
@ -50,7 +50,7 @@ pub struct Backend {
library_updated_sender: Sender<()>,
/// The database. This can be assumed to exist, when the state is set to BackendState::Ready.
database: RefCell<Option<Rc<DbThread>>>,
database: RefCell<Option<Rc<Database>>>,
/// The player handling playlist and playback. This can be assumed to exist, when the state is
/// set to BackendState::Ready.
@ -64,40 +64,46 @@ impl Backend {
pub fn new() -> Self {
logger::register();
let (state_sender, _) = broadcast::channel(1024);
let (library_updated_sender, _) = broadcast::channel(1024);
Backend {
state_sender,
state_cb: RefCell::new(None),
settings: gio::Settings::new("de.johrpan.musicus"),
music_library_path: RefCell::new(None),
library_updated_sender,
database: RefCell::new(None),
player: RefCell::new(None)
player: RefCell::new(None),
}
}
/// Wait for the next state change. Initially, the state should be assumed to be
/// BackendState::Loading. Changes should be awaited before calling init().
pub async fn next_state(&self) -> Result<BackendState> {
Ok(self.state_sender.subscribe().recv().await?)
/// Set the closure to be called whenever the backend state changes.
pub fn set_state_cb<F: Fn(BackendState) + 'static>(&self, cb: F) {
self.state_cb.replace(Some(Box::new(cb)));
}
/// Initialize the backend updating the state accordingly.
pub async fn init(&self) -> Result<()> {
self.init_library().await?;
/// Initialize the backend. A state callback should already have been registered using
/// [`set_state_cb()`] to react to the result.
pub fn init(&self) -> Result<()> {
self.init_library()?;
if self.get_music_library_path().is_none() {
self.set_state(BackendState::NoMusicLibrary);
} else {
self.set_state(BackendState::Ready);
}
match self.get_music_library_path() {
None => self.set_state(BackendState::NoMusicLibrary),
Some(_) => self.set_state(BackendState::Ready),
};
Ok(())
}
/// Set the current state and notify the user interface.
fn set_state(&self, state: BackendState) {
self.state_sender.send(state).unwrap();
if let Some(cb) = &*self.state_cb.borrow() {
cb(state);
}
}
}
impl Default for Backend {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,24 +1,23 @@
use crate::{Backend, BackendState, Player, Result};
use gio::prelude::*;
use log::warn;
use musicus_database::DbThread;
use musicus_database::Database;
use std::path::PathBuf;
use std::rc::Rc;
impl Backend {
/// Initialize the music library if it is set in the settings.
pub(super) async fn init_library(&self) -> Result<()> {
pub(super) fn init_library(&self) -> Result<()> {
let path = self.settings.string("music-library-path");
if !path.is_empty() {
self.set_music_library_path_priv(PathBuf::from(path.to_string()))
.await?;
self.set_music_library_path_priv(PathBuf::from(path.to_string()))?;
}
Ok(())
}
/// Set the path to the music library folder and start a database thread in the background.
pub async fn set_music_library_path(&self, path: PathBuf) -> Result<()> {
/// Set the path to the music library folder and connect to the database.
pub fn set_music_library_path(&self, path: PathBuf) -> Result<()> {
if let Err(err) = self
.settings
.set_string("music-library-path", path.to_str().unwrap())
@ -30,23 +29,19 @@ impl Backend {
);
}
self.set_music_library_path_priv(path).await
self.set_music_library_path_priv(path)
}
/// Set the path to the music library folder and start a database thread in the background.
pub async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> {
/// Set the path to the music library folder and and connect to the database.
pub 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();
db_path.push("musicus.db");
let database = DbThread::new(db_path.to_str().unwrap().to_string()).await?;
let database = Database::new(db_path.to_str().unwrap())?;
self.database.replace(Some(Rc::new(database)));
let player = Player::new(path);
@ -62,14 +57,9 @@ impl Backend {
self.music_library_path.borrow().clone()
}
/// Get an interface to the current music library database.
pub fn get_database(&self) -> Option<Rc<DbThread>> {
self.database.borrow().clone()
}
/// Get an interface to the database and panic if there is none.
pub fn db(&self) -> Rc<DbThread> {
self.get_database().unwrap()
pub fn db(&self) -> Rc<Database> {
self.database.borrow().clone().unwrap()
}
/// Get an interface to the playback service.

View file

@ -9,5 +9,4 @@ diesel_migrations = "1.4.0"
log = "0.4.14"
rand = "0.7.3"
thiserror = "1.0.23"
tokio = { version = "1.4.0", features = ["sync"] }
uuid = { version = "0.8", features = ["v4"] }

View file

@ -16,12 +16,6 @@ pub enum Error {
#[error("Failed to parse {0} from '{1}'")]
ParsingError(&'static str, String),
#[error(transparent)]
SendError(#[from] std::sync::mpsc::SendError<super::thread::Action>),
#[error(transparent)]
ReceiveError(#[from] tokio::sync::oneshot::error::RecvError),
#[error("{0}")]
Other(&'static str),
}

View file

@ -27,9 +27,6 @@ pub use persons::*;
pub mod recordings;
pub use recordings::*;
pub mod thread;
pub use thread::*;
pub mod works;
pub use works::*;

View file

@ -1,397 +0,0 @@
use super::*;
use log::debug;
use std::sync::mpsc;
use std::thread;
use tokio::sync::oneshot::{self, Sender};
/// An action the database thread can perform.
#[derive(Debug)]
pub enum Action {
UpdatePerson(Person, Sender<Result<()>>),
GetPerson(String, Sender<Result<Option<Person>>>),
DeletePerson(String, Sender<Result<()>>),
GetPersons(Sender<Result<Vec<Person>>>),
UpdateInstrument(Instrument, Sender<Result<()>>),
GetInstrument(String, Sender<Result<Option<Instrument>>>),
DeleteInstrument(String, Sender<Result<()>>),
GetInstruments(Sender<Result<Vec<Instrument>>>),
UpdateWork(Work, Sender<Result<()>>),
DeleteWork(String, Sender<Result<()>>),
GetWorks(String, Sender<Result<Vec<Work>>>),
UpdateEnsemble(Ensemble, Sender<Result<()>>),
GetEnsemble(String, Sender<Result<Option<Ensemble>>>),
DeleteEnsemble(String, Sender<Result<()>>),
GetEnsembles(Sender<Result<Vec<Ensemble>>>),
UpdateRecording(Recording, Sender<Result<()>>),
DeleteRecording(String, Sender<Result<()>>),
GetRecordingsForPerson(String, Sender<Result<Vec<Recording>>>),
GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>),
GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>),
RecordingExists(String, Sender<Result<bool>>),
UpdateMedium(Medium, Sender<Result<()>>),
GetMedium(String, Sender<Result<Option<Medium>>>),
GetMediumsBySourceId(String, Sender<Result<Vec<Medium>>>),
GetMediumsForPerson(String, Sender<Result<Vec<Medium>>>),
GetMediumsForEnsemble(String, Sender<Result<Vec<Medium>>>),
DeleteMedium(String, Sender<Result<()>>),
GetTracks(String, Sender<Result<Vec<Track>>>),
Stop(Sender<()>),
}
use Action::*;
/// A database running within a thread.
pub struct DbThread {
action_sender: mpsc::Sender<Action>,
}
impl DbThread {
/// Create a new database connection in a background thread.
pub async fn new(path: String) -> Result<Self> {
let (action_sender, action_receiver) = mpsc::channel();
let (ready_sender, ready_receiver) = oneshot::channel();
thread::spawn(move || {
debug!("Database thread for '{}' started", path);
let db = match Database::new(&path) {
Ok(db) => {
ready_sender.send(Ok(())).unwrap();
db
}
Err(error) => {
ready_sender.send(Err(error)).unwrap();
return;
}
};
for action in action_receiver {
debug!("Database thread for '{}' got action {:?}", path, action);
match action {
UpdatePerson(person, sender) => {
sender.send(db.update_person(person)).unwrap();
}
GetPerson(id, sender) => {
sender.send(db.get_person(&id)).unwrap();
}
DeletePerson(id, sender) => {
sender.send(db.delete_person(&id)).unwrap();
}
GetPersons(sender) => {
sender.send(db.get_persons()).unwrap();
}
UpdateInstrument(instrument, sender) => {
sender.send(db.update_instrument(instrument)).unwrap();
}
GetInstrument(id, sender) => {
sender.send(db.get_instrument(&id)).unwrap();
}
DeleteInstrument(id, sender) => {
sender.send(db.delete_instrument(&id)).unwrap();
}
GetInstruments(sender) => {
sender.send(db.get_instruments()).unwrap();
}
UpdateWork(work, sender) => {
sender.send(db.update_work(work)).unwrap();
}
DeleteWork(id, sender) => {
sender.send(db.delete_work(&id)).unwrap();
}
GetWorks(id, sender) => {
sender.send(db.get_works(&id)).unwrap();
}
UpdateEnsemble(ensemble, sender) => {
sender.send(db.update_ensemble(ensemble)).unwrap();
}
GetEnsemble(id, sender) => {
sender.send(db.get_ensemble(&id)).unwrap();
}
DeleteEnsemble(id, sender) => {
sender.send(db.delete_ensemble(&id)).unwrap();
}
GetEnsembles(sender) => {
sender.send(db.get_ensembles()).unwrap();
}
UpdateRecording(recording, sender) => {
sender.send(db.update_recording(recording)).unwrap();
}
DeleteRecording(id, sender) => {
sender.send(db.delete_recording(&id)).unwrap();
}
GetRecordingsForPerson(id, sender) => {
sender.send(db.get_recordings_for_person(&id)).unwrap();
}
GetRecordingsForEnsemble(id, sender) => {
sender.send(db.get_recordings_for_ensemble(&id)).unwrap();
}
GetRecordingsForWork(id, sender) => {
sender.send(db.get_recordings_for_work(&id)).unwrap();
}
RecordingExists(id, sender) => {
sender.send(db.recording_exists(&id)).unwrap();
}
UpdateMedium(medium, sender) => {
sender.send(db.update_medium(medium)).unwrap();
}
GetMedium(id, sender) => {
sender.send(db.get_medium(&id)).unwrap();
}
GetMediumsBySourceId(id, sender) => {
sender.send(db.get_mediums_by_source_id(&id)).unwrap();
}
GetMediumsForPerson(id, sender) => {
sender.send(db.get_mediums_for_person(&id)).unwrap();
}
GetMediumsForEnsemble(id, sender) => {
sender.send(db.get_mediums_for_ensemble(&id)).unwrap();
}
DeleteMedium(id, sender) => {
sender.send(db.delete_medium(&id)).unwrap();
}
GetTracks(recording_id, sender) => {
sender.send(db.get_tracks(&recording_id)).unwrap();
}
Stop(sender) => {
sender.send(()).unwrap();
break;
}
}
}
debug!("Database thread for '{}' stopped", path);
});
ready_receiver.await??;
Ok(Self { action_sender })
}
/// Update an existing person or insert a new one.
pub async fn update_person(&self, person: Person) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdatePerson(person, sender))?;
receiver.await?
}
/// Get an existing person.
pub async fn get_person(&self, id: &str) -> Result<Option<Person>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetPerson(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing person. This will fail, if there are still other items referencing
/// this person.
pub async fn delete_person(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeletePerson(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing persons.
pub async fn get_persons(&self) -> Result<Vec<Person>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetPersons(sender))?;
receiver.await?
}
/// Update an existing instrument or insert a new one.
pub async fn update_instrument(&self, instrument: Instrument) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(UpdateInstrument(instrument, sender))?;
receiver.await?
}
/// Get an existing instrument.
pub async fn get_instrument(&self, id: &str) -> Result<Option<Instrument>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetInstrument(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing instrument. This will fail, if there are still other items referencing
/// this instrument.
pub async fn delete_instrument(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteInstrument(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing instruments.
pub async fn get_instruments(&self) -> Result<Vec<Instrument>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetInstruments(sender))?;
receiver.await?
}
/// Update an existing work or insert a new one.
pub async fn update_work(&self, work: Work) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateWork(work, sender))?;
receiver.await?
}
/// Delete an existing work. This will fail, if there are still other items referencing
/// this work.
pub async fn delete_work(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteWork(id.to_string(), sender))?;
receiver.await?
}
/// Get information on all existing works by a composer.
pub async fn get_works(&self, person_id: &str) -> Result<Vec<Work>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetWorks(person_id.to_string(), sender))?;
receiver.await?
}
/// Update an existing ensemble or insert a new one.
pub async fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateEnsemble(ensemble, sender))?;
receiver.await?
}
/// Get an existing ensemble.
pub async fn get_ensemble(&self, id: &str) -> Result<Option<Ensemble>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetEnsemble(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing ensemble. This will fail, if there are still other items referencing
/// this ensemble.
pub async fn delete_ensemble(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteEnsemble(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing ensembles.
pub async fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetEnsembles(sender))?;
receiver.await?
}
/// Update an existing recording or insert a new one.
pub async fn update_recording(&self, recording: Recording) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(UpdateRecording(recording, sender))?;
receiver.await?
}
/// Delete an existing recording.
pub async fn delete_recording(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteRecording(id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings in which a person performs.
pub async fn get_recordings_for_person(&self, person_id: &str) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForPerson(person_id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings in which an ensemble performs.
pub async fn get_recordings_for_ensemble(&self, ensemble_id: &str) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForEnsemble(ensemble_id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings of a work.
pub async fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForWork(work_id.to_string(), sender))?;
receiver.await?
}
/// Check whether a recording exists within the database.
pub async fn recording_exists(&self, id: &str) -> Result<bool> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(RecordingExists(id.to_string(), sender))?;
receiver.await?
}
/// Update an existing medium or insert a new one.
pub async fn update_medium(&self, medium: Medium) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateMedium(medium, sender))?;
receiver.await?
}
/// Delete an existing medium. This will fail, if there are still other
/// items referencing this medium.
pub async fn delete_medium(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteMedium(id.to_owned(), sender))?;
receiver.await?
}
/// Get an existing medium.
pub async fn get_medium(&self, id: &str) -> Result<Option<Medium>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetMedium(id.to_owned(), sender))?;
receiver.await?
}
/// Get all mediums with the specified source ID.
pub async fn get_mediums_by_source_id(&self, id: &str) -> Result<Vec<Medium>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetMediumsBySourceId(id.to_owned(), sender))?;
receiver.await?
}
/// Get all mediums on which a person performs.
pub async fn get_mediums_for_person(&self, id: &str) -> Result<Vec<Medium>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetMediumsForPerson(id.to_owned(), sender))?;
receiver.await?
}
/// Get all mediums on which an ensemble performs.
pub async fn get_mediums_for_ensemble(&self, id: &str) -> Result<Vec<Medium>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetMediumsForEnsemble(id.to_owned(), sender))?;
receiver.await?
}
/// Get all tracks for a recording.
pub async fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetTracks(recording_id.to_owned(), sender))?;
receiver.await?
}
/// Stop the database thread. Any future access to the database will fail.
pub async fn stop(&self) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(Stop(sender))?;
Ok(receiver.await?)
}
}

View file

@ -53,38 +53,6 @@
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar">
<property name="show-start-title-buttons">false</property>
<property name="show-end-title-buttons">false</property>
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title" translatable="true">Loading</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkSpinner">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="width-request">32</property>
<property name="height-request">32</property>
<property name="spinning">true</property>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">error</property>

View file

@ -56,9 +56,7 @@ impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
}));
this.editor.set_save_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
this.editor.loading();
match this.save().await {
match this.save() {
Ok(ensemble) => {
this.handle.pop(Some(ensemble));
}
@ -67,7 +65,6 @@ impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
this.editor.error(&gettext("Failed to save ensemble!"), &description);
}
}
});
}));
this.name
@ -87,7 +84,7 @@ impl EnsembleEditor {
}
/// Save the ensemble.
async fn save(&self) -> Result<Ensemble> {
fn save(&self) -> Result<Ensemble> {
let name = self.name.get_text();
let ensemble = Ensemble {
@ -95,12 +92,7 @@ impl EnsembleEditor {
name,
};
self.handle
.backend
.db()
.update_ensemble(ensemble.clone())
.await?;
self.handle.backend.db().update_ensemble(ensemble.clone())?;
self.handle.backend.library_changed();
Ok(ensemble)

View file

@ -56,9 +56,7 @@ impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
}));
this.editor.set_save_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
this.editor.loading();
match this.save().await {
match this.save() {
Ok(instrument) => {
this.handle.pop(Some(instrument));
}
@ -67,7 +65,6 @@ impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
this.editor.error(&gettext("Failed to save instrument!"), &description);
}
}
});
}));
this.name
@ -87,7 +84,7 @@ impl InstrumentEditor {
}
/// Save the instrument.
async fn save(&self) -> Result<Instrument> {
fn save(&self) -> Result<Instrument> {
let name = self.name.get_text();
let instrument = Instrument {
@ -95,12 +92,7 @@ impl InstrumentEditor {
name,
};
self.handle
.backend
.db()
.update_instrument(instrument.clone())
.await?;
self.handle.backend.db().update_instrument(instrument.clone())?;
self.handle.backend.library_changed();
Ok(instrument)

View file

@ -63,9 +63,7 @@ impl Screen<Option<Person>, Person> for PersonEditor {
}));
this.editor.set_save_cb(clone!(@strong this => move || {
spawn!(@clone this, async move {
this.editor.loading();
match this.save().await {
match this.save() {
Ok(person) => {
this.handle.pop(Some(person));
}
@ -74,7 +72,6 @@ impl Screen<Option<Person>, Person> for PersonEditor {
this.editor.error(&gettext("Failed to save person!"), &description);
}
}
});
}));
this.first_name
@ -100,7 +97,7 @@ impl PersonEditor {
}
/// Save the person.
async fn save(self: &Rc<Self>) -> Result<Person> {
fn save(self: &Rc<Self>) -> Result<Person> {
let first_name = self.first_name.get_text();
let last_name = self.last_name.get_text();
@ -110,11 +107,7 @@ impl PersonEditor {
last_name,
};
self.handle
.backend
.db()
.update_person(person.clone())
.await?;
self.handle.backend.db().update_person(person.clone())?;
self.handle.backend.library_changed();
Ok(person)

View file

@ -74,9 +74,7 @@ impl Screen<Option<Recording>, Recording> for RecordingEditor {
this.save_button
.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
this.widget.set_visible_child_name("loading");
match this.save().await {
match this.save() {
Ok(recording) => {
this.handle.pop(Some(recording));
}
@ -85,7 +83,6 @@ impl Screen<Option<Recording>, Recording> for RecordingEditor {
this.widget.set_visible_child_name("content");
}
}
});
}));
work_button.connect_clicked(clone!(@weak this => move |_| {
@ -179,7 +176,7 @@ impl RecordingEditor {
}
/// Save the recording.
async fn save(self: &Rc<Self>) -> Result<Recording> {
fn save(self: &Rc<Self>) -> Result<Recording> {
let recording = Recording {
id: self.id.clone(),
work: self
@ -191,13 +188,7 @@ impl RecordingEditor {
performances: self.performances.borrow().clone(),
};
self.handle
.backend
.db()
.update_recording(recording.clone())
.await
.unwrap();
self.handle.backend.db().update_recording(recording.clone())?;
self.handle.backend.library_changed();
Ok(recording)

View file

@ -113,9 +113,7 @@ impl Screen<Option<Work>, Work> for WorkEditor {
this.save_button
.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
this.widget.set_visible_child_name("loading");
match this.save().await {
match this.save() {
Ok(work) => {
this.handle.pop(Some(work));
}
@ -124,7 +122,6 @@ impl Screen<Option<Work>, Work> for WorkEditor {
this.widget.set_visible_child_name("content");
}
}
});
}));
composer_button.connect_clicked(clone!(@weak this => move |_| {
@ -313,7 +310,7 @@ impl WorkEditor {
}
/// Save the work.
async fn save(self: &Rc<Self>) -> Result<Work> {
fn save(self: &Rc<Self>) -> Result<Work> {
let mut section_count: usize = 0;
let mut parts = Vec::new();
let mut sections = Vec::new();
@ -343,13 +340,7 @@ impl WorkEditor {
sections,
};
self.handle
.backend
.db()
.update_work(work.clone())
.await
.unwrap();
self.handle.backend.db().update_work(work.clone())?;
self.handle.backend.library_changed();
Ok(work)

View file

@ -28,7 +28,7 @@ impl ImportScreen {
let this = self;
spawn!(@clone this, async move {
let mediums = this.handle.backend.db().get_mediums_by_source_id(this.session.source_id()).await;
let mediums = this.handle.backend.db().get_mediums_by_source_id(this.session.source_id());
match mediums {
Ok(mediums) => {

View file

@ -256,12 +256,7 @@ impl MediumPreview {
tracks,
};
self.handle
.backend
.db()
.update_medium(medium.clone())
.await?;
self.handle.backend.db().update_medium(medium)?;
self.handle.backend.library_changed();
Ok(())

View file

@ -48,11 +48,8 @@ impl Preferences {
if let gtk::ResponseType::Accept = response {
if let Some(file) = dialog.file() {
if let Some(path) = file.path() {
this.backend.set_music_library_path(path.clone()).unwrap();
this.music_library_path_row.set_subtitle(path.to_str().unwrap());
spawn!(@clone this, async move {
this.backend.set_music_library_path(path).await.unwrap();
});
}
}
}

View file

@ -59,7 +59,7 @@ impl Screen<Ensemble, ()> for EnsembleScreen {
&gettext("Delete ensemble"),
clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_ensemble(&this.ensemble.id).await.unwrap();
this.handle.backend.db().delete_ensemble(&this.ensemble.id).unwrap();
this.handle.backend.library_changed();
});
}),
@ -128,21 +128,20 @@ impl Screen<Ensemble, ()> for EnsembleScreen {
search.is_empty() || name.contains(&search)
}));
// Load the content asynchronously.
// Load the content.
spawn!(@clone this, async move {
let recordings = this.handle
let recordings = this
.handle
.backend
.db()
.get_recordings_for_ensemble(&this.ensemble.id)
.await
.unwrap();
let mediums = this.handle
let mediums = this
.handle
.backend
.db()
.get_mediums_for_ensemble(&this.ensemble.id)
.await
.unwrap();
if !recordings.is_empty() {
@ -164,7 +163,6 @@ impl Screen<Ensemble, ()> for EnsembleScreen {
}
this.widget.ready();
});
this
}

View file

@ -136,16 +136,15 @@ impl Screen<(), ()> for MainScreen {
});
}));
// Load the content asynchronously.
// Load the content whenever there is a new library update.
spawn!(@clone this, async move {
loop {
this.navigator.reset();
let mut poes = Vec::new();
let persons = this.handle.backend.db().get_persons().await.unwrap();
let ensembles = this.handle.backend.db().get_ensembles().await.unwrap();
let persons = this.handle.backend.db().get_persons().unwrap();
let ensembles = this.handle.backend.db().get_ensembles().unwrap();
for person in persons {
poes.push(PersonOrEnsemble::Person(person));

View file

@ -64,7 +64,7 @@ impl Screen<Person, ()> for PersonScreen {
&gettext("Delete person"),
clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_person(&this.person.id).await.unwrap();
this.handle.backend.db().delete_person(&this.person.id).unwrap();
this.handle.backend.library_changed();
});
}),
@ -162,28 +162,22 @@ impl Screen<Person, ()> for PersonScreen {
search.is_empty() || name.contains(&search)
}));
// Load the content asynchronously.
// Load the content.
spawn!(@clone this, async move {
let works = this.handle
.backend
.db()
.get_works(&this.person.id)
.await
.unwrap();
let works = this.handle.backend.db().get_works(&this.person.id).unwrap();
let recordings = this.handle
let recordings = this
.handle
.backend
.db()
.get_recordings_for_person(&this.person.id)
.await
.unwrap();
let mediums = this.handle
let mediums = this
.handle
.backend
.db()
.get_mediums_for_person(&this.person.id)
.await
.unwrap();
if !works.is_empty() {
@ -214,7 +208,6 @@ impl Screen<Person, ()> for PersonScreen {
}
this.widget.ready();
});
this
}

View file

@ -65,7 +65,7 @@ impl Screen<Recording, ()> for RecordingScreen {
&gettext("Delete recording"),
clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_recording(&this.recording.id).await.unwrap();
this.handle.backend.db().delete_recording(&this.recording.id).unwrap();
this.handle.backend.library_changed();
});
}),
@ -93,19 +93,17 @@ impl Screen<Recording, ()> for RecordingScreen {
row.upcast()
}));
// Load the content asynchronously.
// Load the content.
spawn!(@clone this, async move {
let tracks = this.handle
let tracks = this
.handle
.backend
.db()
.get_tracks(&this.recording.id)
.await
.unwrap();
this.show_tracks(tracks);
this.widget.ready();
});
this
}

View file

@ -62,9 +62,7 @@ impl Screen<(), ()> for WelcomeScreen {
if let gtk::ResponseType::Accept = response {
if let Some(file) = dialog.file() {
if let Some(path) = file.path() {
spawn!(@clone this, async move {
this.handle.backend.set_music_library_path(path).await.unwrap();
});
this.handle.backend.set_music_library_path(path).unwrap();
}
}
}

View file

@ -55,7 +55,7 @@ impl Screen<Work, ()> for WorkScreen {
&gettext("Delete work"),
clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_work(&this.work.id).await.unwrap();
this.handle.backend.db().delete_work(&this.work.id).unwrap();
this.handle.backend.library_changed();
});
}),
@ -95,14 +95,13 @@ impl Screen<Work, ()> for WorkScreen {
search.is_empty() || text.to_lowercase().contains(&search)
}));
// Load the content asynchronously.
// Load the content.
spawn!(@clone this, async move {
let recordings = this.handle
let recordings = this
.handle
.backend
.db()
.get_recordings_for_work(&this.work.id)
.await
.unwrap();
if !recordings.is_empty() {
@ -115,7 +114,6 @@ impl Screen<Work, ()> for WorkScreen {
}
this.widget.ready();
});
this
}

View file

@ -38,12 +38,6 @@ impl Screen<(), Ensemble> for EnsembleSelector {
});
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
let clone = this;
async move { clone.handle.backend.db().get_ensembles().await.unwrap() }
}));
this.selector
.set_make_widget(clone!(@weak this => @default-panic, move |ensemble| {
let row = adw::ActionRowBuilder::new()
@ -62,6 +56,9 @@ impl Screen<(), Ensemble> for EnsembleSelector {
this.selector
.set_filter(|search, ensemble| ensemble.name.to_lowercase().contains(search));
this.selector
.set_items(this.handle.backend.db().get_ensembles().unwrap());
this
}
}

View file

@ -38,12 +38,6 @@ impl Screen<(), Instrument> for InstrumentSelector {
});
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
let clone = this;
async move { clone.handle.backend.db().get_instruments().await.unwrap() }
}));
this.selector
.set_make_widget(clone!(@weak this => @default-panic, move |instrument| {
let row = adw::ActionRowBuilder::new()
@ -62,6 +56,9 @@ impl Screen<(), Instrument> for InstrumentSelector {
this.selector
.set_filter(|search, instrument| instrument.name.to_lowercase().contains(search));
this.selector
.set_items(this.handle.backend.db().get_instruments().unwrap());
this
}
}

View file

@ -28,26 +28,6 @@ impl Screen<(), Medium> for MediumSelector {
this.handle.pop(None);
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
async move {
let mut poes = Vec::new();
let persons = this.handle.backend.db().get_persons().await.unwrap();
let ensembles = this.handle.backend.db().get_ensembles().await.unwrap();
for person in persons {
poes.push(PersonOrEnsemble::Person(person));
}
for ensemble in ensembles {
poes.push(PersonOrEnsemble::Ensemble(ensemble));
}
poes
}
}));
this.selector.set_make_widget(clone!(@weak this => @default-panic, move |poe| {
let row = adw::ActionRowBuilder::new()
.activatable(true)
@ -70,6 +50,23 @@ impl Screen<(), Medium> for MediumSelector {
this.selector
.set_filter(|search, poe| poe.get_title().to_lowercase().contains(search));
// Initialize items.
let mut poes = Vec::new();
let persons = this.handle.backend.db().get_persons().unwrap();
let ensembles = this.handle.backend.db().get_ensembles().unwrap();
for person in persons {
poes.push(PersonOrEnsemble::Person(person));
}
for ensemble in ensembles {
poes.push(PersonOrEnsemble::Ensemble(ensemble));
}
this.selector.set_items(poes);
this
}
}
@ -103,25 +100,6 @@ impl Screen<PersonOrEnsemble, Medium> for MediumSelectorMediumScreen {
this.handle.pop(None);
}));
match this.poe.clone() {
PersonOrEnsemble::Person(person) => {
// this.selector.set_load_online(clone!(@weak this => move || {
// async move { this.handle.backend.cl().get_mediums_for_person(&person.id).await }
// }));
this.selector.set_load_local(clone!(@weak this => @default-panic, move || {
let person = person.clone();
async move { this.handle.backend.db().get_mediums_for_person(&person.id).await.unwrap() }
}));
}
PersonOrEnsemble::Ensemble(ensemble) => {
this.selector.set_load_local(clone!(@weak this => @default-panic, move || {
let ensemble = ensemble.clone();
async move { this.handle.backend.db().get_mediums_for_ensemble(&ensemble.id).await.unwrap() }
}));
}
}
this.selector
.set_make_widget(clone!(@weak this => @default-panic, move |medium| {
let row = adw::ActionRowBuilder::new()
@ -140,6 +118,28 @@ impl Screen<PersonOrEnsemble, Medium> for MediumSelectorMediumScreen {
this.selector
.set_filter(|search, medium| medium.name.to_lowercase().contains(search));
// Initialize items.
match this.poe.clone() {
PersonOrEnsemble::Person(person) => {
this.selector.set_items(
this.handle
.backend
.db()
.get_mediums_for_person(&person.id)
.unwrap(),
);
}
PersonOrEnsemble::Ensemble(ensemble) => {
this.selector.set_items(
this.handle
.backend
.db()
.get_mediums_for_ensemble(&ensemble.id)
.unwrap(),
);
}
}
this
}
}

View file

@ -38,12 +38,6 @@ impl Screen<(), Person> for PersonSelector {
});
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
let clone = this;
async move { clone.handle.backend.db().get_persons().await.unwrap() }
}));
this.selector
.set_make_widget(clone!(@weak this => @default-panic, move |person| {
let row = adw::ActionRowBuilder::new()
@ -62,6 +56,9 @@ impl Screen<(), Person> for PersonSelector {
this.selector
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
this.selector
.set_items(this.handle.backend.db().get_persons().unwrap());
this
}
}

View file

@ -50,11 +50,6 @@ impl Screen<(), Recording> for RecordingSelector {
});
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_persons().await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => @default-panic, move |person| {
let row = adw::ActionRowBuilder::new()
.activatable(true)
@ -84,6 +79,9 @@ impl Screen<(), Recording> for RecordingSelector {
this.selector
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
this.selector
.set_items(this.handle.backend.db().get_persons().unwrap());
this
}
}
@ -126,11 +124,6 @@ impl Screen<Person, Work> for RecordingSelectorWorkScreen {
});
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
}));
this.selector
.set_make_widget(clone!(@weak this => @default-panic, move |work| {
let row = adw::ActionRowBuilder::new()
@ -149,6 +142,9 @@ impl Screen<Person, Work> for RecordingSelectorWorkScreen {
this.selector
.set_filter(|search, work| work.title.to_lowercase().contains(search));
this.selector
.set_items(this.handle.backend.db().get_works(&this.person.id).unwrap());
this
}
}
@ -191,10 +187,6 @@ impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
});
}));
this.selector.set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_recordings_for_work(&this.work.id).await.unwrap() }
}));
this.selector
.set_make_widget(clone!(@weak this => @default-panic, move |recording| {
let row = adw::ActionRowBuilder::new()
@ -214,6 +206,14 @@ impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
recording.get_performers().to_lowercase().contains(search)
});
this.selector.set_items(
this.handle
.backend
.db()
.get_recordings_for_work(&this.work.id)
.unwrap(),
);
this
}
}

View file

@ -3,8 +3,6 @@ use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
/// A screen that presents a list of items from the library.
@ -19,7 +17,6 @@ pub struct Selector<T: 'static> {
back_cb: RefCell<Option<Box<dyn Fn()>>>,
add_cb: RefCell<Option<Box<dyn Fn()>>>,
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
load_local: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Vec<T>>>>>>,
filter: RefCell<Option<Box<dyn Fn(&str, &T) -> bool>>>,
}
@ -53,7 +50,6 @@ impl<T> Selector<T> {
back_cb: RefCell::new(None),
add_cb: RefCell::new(None),
make_widget: RefCell::new(None),
load_local: RefCell::new(None),
filter: RefCell::new(None),
});
@ -98,9 +94,6 @@ impl<T> Selector<T> {
}
}));
// Initialize
this.clone().load_local();
this
}
@ -125,16 +118,6 @@ impl<T> Selector<T> {
self.add_cb.replace(Some(Box::new(cb)));
}
/// Set the async closure to be called to get local items.
pub fn set_load_local<F, R>(&self, cb: F)
where
F: (Fn() -> R) + 'static,
R: Future<Output = Vec<T>> + 'static,
{
self.load_local
.replace(Some(Box::new(move || Box::new(cb()))));
}
/// Set the closure to be called for creating a new list row.
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
self.make_widget.replace(Some(Box::new(make_widget)));
@ -146,20 +129,8 @@ impl<T> Selector<T> {
self.filter.replace(Some(Box::new(filter)));
}
fn load_local(self: Rc<Self>) {
let context = glib::MainContext::default();
let clone = self.clone();
context.spawn_local(async move {
if let Some(cb) = &*self.load_local.borrow() {
self.stack.set_visible_child_name("loading");
let items = Pin::from(cb()).await;
clone.show_items(items);
}
});
}
fn show_items(&self, items: Vec<T>) {
/// Set the list items the user may select from.
pub fn set_items(&self, items: Vec<T>) {
let length = items.len();
self.items.replace(items);
self.list.update(length);

View file

@ -44,11 +44,6 @@ impl Screen<(), Work> for WorkSelector {
});
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_persons().await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => @default-panic, move |person| {
let row = adw::ActionRowBuilder::new()
.activatable(true)
@ -74,6 +69,9 @@ impl Screen<(), Work> for WorkSelector {
this.selector
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
this.selector
.set_items(this.handle.backend.db().get_persons().unwrap());
this
}
}
@ -116,11 +114,6 @@ impl Screen<Person, Work> for WorkSelectorWorkScreen {
});
}));
this.selector
.set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
}));
this.selector
.set_make_widget(clone!(@weak this => @default-panic, move |work| {
let row = adw::ActionRowBuilder::new()
@ -139,6 +132,9 @@ impl Screen<Person, Work> for WorkSelectorWorkScreen {
this.selector
.set_filter(|search, work| work.title.to_lowercase().contains(search));
this.selector
.set_items(this.handle.backend.db().get_works(&this.person.id).unwrap());
this
}
}

View file

@ -70,11 +70,6 @@ impl Editor {
self.save_button.connect_clicked(move |_| cb());
}
/// Show a loading page.
pub fn loading(&self) {
self.widget.set_visible_child_name("loading");
}
/// Show an error page. The page contains a button to get back to the
/// actual editor.
pub fn error(&self, title: &str, description: &str) {

View file

@ -1,5 +1,6 @@
use crate::navigator::Navigator;
use crate::screens::{MainScreen, WelcomeScreen};
use glib::clone;
use gtk::prelude::*;
use musicus_backend::{Backend, BackendState};
use std::rc::Rc;
@ -50,21 +51,17 @@ impl Window {
navigator,
});
spawn!(@clone this, async move {
while let Ok(state) = this.backend.next_state().await {
// Listen for backend state changes.
this.backend.set_state_cb(clone!(@weak this => move |state| {
match state {
BackendState::Loading => this.navigator.reset(),
BackendState::NoMusicLibrary => this.show_welcome_screen(),
BackendState::Ready => this.show_main_screen(),
}
}
});
}));
spawn!(@clone this, async move {
// This is not done in the async block above, because backend state changes may happen
// while this method is running.
this.backend.init().await.unwrap();
});
// Initialize the backend.
this.backend.init().unwrap();
this
}