Restructure backend and database

This commit is contained in:
Elias Projahn 2020-11-17 15:52:47 +01:00
parent d0c25531d3
commit a93c7276d2
49 changed files with 1705 additions and 1920 deletions

View file

@ -8,8 +8,6 @@ DROP TABLE instrumentations;
DROP TABLE work_parts;
DROP TABLE part_instrumentations;
DROP TABLE work_sections;
DROP TABLE ensembles;

View file

@ -18,21 +18,15 @@ CREATE TABLE works (
CREATE TABLE instrumentations (
id BIGINT NOT NULL PRIMARY KEY,
work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
instrument BIGINT NOT NULL REFERENCES instruments(id)
instrument BIGINT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE
);
CREATE TABLE work_parts (
id BIGINT NOT NULL PRIMARY KEY,
work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
part_index BIGINT NOT NULL,
composer BIGINT REFERENCES persons(id),
title TEXT NOT NULL
);
CREATE TABLE part_instrumentations (
id BIGINT NOT NULL PRIMARY KEY,
work_part BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
instrument BIGINT NOT NULL REFERENCES instruments(id)
title TEXT NOT NULL,
composer BIGINT REFERENCES persons(id)
);
CREATE TABLE work_sections (

View file

@ -6,8 +6,7 @@
<object class="HdyWindow" id="window">
<property name="can-focus">False</property>
<property name="modal">True</property>
<property name="default-width">450</property>
<property name="default-height">300</property>
<property name="default-width">350</property>
<property name="destroy-with-parent">True</property>
<property name="type-hint">dialog</property>
<child>
@ -51,182 +50,84 @@
</packing>
</child>
<child>
<object class="GtkNotebook">
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Composer</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="title_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="composer_labe">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Title</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkButton" id="composer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel" id="composer_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="reset_composer_button">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Overview</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Composer</property>
</object>
<packing>
<property name="tab-fill">False</property>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="title_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="composer_labe">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Title</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">18</property>
<property name="spacing">6</property>
<property name="hexpand">True</property>
<child>
<object class="GtkScrolledWindow" id="scroll">
<object class="GtkButton" id="composer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<property name="receives-default">True</property>
<property name="hexpand">True</property>
<child>
<placeholder/>
<object class="GtkLabel" id="composer_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">0</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<object class="GtkButton" id="reset_composer_button">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkButton" id="add_instrument_button">
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
<property name="can-focus">False</property>
<property name="icon-name">user-trash-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_instrument_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-remove-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
@ -235,25 +136,18 @@
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Instruments</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab-fill">False</property>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>

View file

@ -1,528 +0,0 @@
use super::secure;
use crate::database::*;
use crate::player::*;
use anyhow::{anyhow, Result};
use futures_channel::oneshot::Sender;
use futures_channel::{mpsc, oneshot};
use gio::prelude::*;
use serde::Serialize;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
/// Credentials used for login.
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LoginData {
pub username: String,
pub password: String,
}
pub enum BackendState {
NoMusicLibrary,
Loading,
Ready,
}
enum BackendAction {
UpdatePerson(Person, Sender<Result<()>>),
GetPerson(i64, Sender<Result<Person>>),
DeletePerson(i64, Sender<Result<()>>),
GetPersons(Sender<Result<Vec<Person>>>),
UpdateInstrument(Instrument, Sender<Result<()>>),
GetInstrument(i64, Sender<Result<Instrument>>),
DeleteInstrument(i64, Sender<Result<()>>),
GetInstruments(Sender<Result<Vec<Instrument>>>),
UpdateWork(WorkInsertion, Sender<Result<()>>),
GetWorkDescription(i64, Sender<Result<WorkDescription>>),
DeleteWork(i64, Sender<Result<()>>),
GetWorkDescriptions(i64, Sender<Result<Vec<WorkDescription>>>),
UpdateEnsemble(Ensemble, Sender<Result<()>>),
GetEnsemble(i64, Sender<Result<Ensemble>>),
DeleteEnsemble(i64, Sender<Result<()>>),
GetEnsembles(Sender<Result<Vec<Ensemble>>>),
UpdateRecording(RecordingInsertion, Sender<Result<()>>),
GetRecordingDescription(i64, Sender<Result<RecordingDescription>>),
DeleteRecording(i64, Sender<Result<()>>),
GetRecordingsForPerson(i64, Sender<Result<Vec<RecordingDescription>>>),
GetRecordingsForEnsemble(i64, Sender<Result<Vec<RecordingDescription>>>),
GetRecordingsForWork(i64, Sender<Result<Vec<RecordingDescription>>>),
UpdateTracks(i64, Vec<TrackDescription>, Sender<Result<()>>),
DeleteTracks(i64, Sender<Result<()>>),
GetTracks(i64, Sender<Result<Vec<TrackDescription>>>),
Stop,
}
use BackendAction::*;
pub struct Backend {
pub state_stream: RefCell<mpsc::Receiver<BackendState>>,
state_sender: RefCell<mpsc::Sender<BackendState>>,
action_sender: RefCell<Option<std::sync::mpsc::Sender<BackendAction>>>,
settings: gio::Settings,
secrets: secret_service::SecretService,
server_url: RefCell<Option<String>>,
login_data: RefCell<Option<LoginData>>,
token: RefCell<Option<String>>,
music_library_path: RefCell<Option<PathBuf>>,
player: RefCell<Option<Rc<Player>>>,
}
impl Backend {
pub fn new() -> Self {
let (state_sender, state_stream) = mpsc::channel(1024);
let secrets = secret_service::SecretService::new(secret_service::EncryptionType::Dh)
.expect("Failed to connect to SecretsService!");
Backend {
state_stream: RefCell::new(state_stream),
state_sender: RefCell::new(state_sender),
action_sender: RefCell::new(None),
settings: gio::Settings::new("de.johrpan.musicus"),
secrets,
music_library_path: RefCell::new(None),
server_url: RefCell::new(None),
login_data: RefCell::new(None),
token: RefCell::new(None),
player: RefCell::new(None),
}
}
pub fn init(self: Rc<Backend>) {
if let Some(path) = self.settings.get_string("music-library-path") {
if !path.is_empty() {
let context = glib::MainContext::default();
let clone = self.clone();
context.spawn_local(async move {
clone
.set_music_library_path_priv(PathBuf::from(path.to_string()))
.await
.unwrap();
});
}
}
if let Some(data) = secure::load_login_data().unwrap() {
self.login_data.replace(Some(data));
}
if let Some(url) = self.settings.get_string("server-url") {
if !url.is_empty() {
self.server_url.replace(Some(url.to_string()));
}
}
}
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<Person> {
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<Vec<Person>> {
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<Instrument> {
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<Vec<Instrument>> {
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<WorkDescription> {
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<Vec<WorkDescription>> {
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<Ensemble> {
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<Vec<Ensemble>> {
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<RecordingDescription> {
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<Vec<RecordingDescription>> {
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<Vec<RecordingDescription>> {
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<Vec<RecordingDescription>> {
let (sender, receiver) = oneshot::channel();
self.unwrap_action_sender()?
.send(GetRecordingsForWork(work_id, sender))?;
receiver.await?
}
pub async fn update_tracks(
&self,
recording_id: i64,
tracks: Vec<TrackDescription>,
) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.unwrap_action_sender()?
.send(UpdateTracks(recording_id, tracks, sender))?;
receiver.await?
}
pub async fn delete_tracks(&self, recording_id: i64) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.unwrap_action_sender()?
.send(DeleteTracks(recording_id, sender))?;
receiver.await?
}
pub async fn get_tracks(&self, recording_id: i64) -> Result<Vec<TrackDescription>> {
let (sender, receiver) = oneshot::channel();
self.unwrap_action_sender()?
.send(GetTracks(recording_id, sender))?;
receiver.await?
}
pub async fn set_music_library_path(&self, path: PathBuf) -> Result<()> {
self.settings
.set_string("music-library-path", path.to_str().unwrap())?;
self.set_music_library_path_priv(path).await
}
pub fn get_music_library_path(&self) -> Option<PathBuf> {
self.music_library_path.borrow().clone()
}
/// Get the currently stored login credentials.
pub fn get_login_data(&self) -> Option<LoginData> {
self.login_data.borrow().clone()
}
/// Set the URL of the Musicus server to connect to.
pub fn set_server_url(&self, url: &str) -> Result<()> {
self.settings.set_string("server-url", url)?;
self.server_url.replace(Some(url.to_string()));
Ok(())
}
/// Get the currently used login token.
pub fn get_token(&self) -> Option<String> {
self.token.borrow().clone()
}
/// Set the login token to use. This will be done automatically by the login method.
pub fn set_token(&self, token: &str) {
self.token.replace(Some(token.to_string()));
}
/// Get the currently set server URL.
pub fn get_server_url(&self) -> Option<String> {
self.server_url.borrow().clone()
}
/// Set the user credentials to use.
pub async fn set_login_data(&self, data: LoginData) -> Result<()> {
secure::store_login_data(data.clone()).await?;
self.login_data.replace(Some(data));
Ok(())
}
pub fn get_player(&self) -> Option<Rc<Player>> {
self.player.borrow().clone()
}
async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> {
self.set_state(BackendState::Loading);
if let Some(player) = &*self.player.borrow() {
player.clear();
}
self.music_library_path.replace(Some(path.clone()));
self.player.replace(Some(Player::new(path.clone())));
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(())
}
fn set_state(&self, state: BackendState) {
self.state_sender.borrow_mut().try_send(state).unwrap();
}
fn unwrap_action_sender(&self) -> Result<std::sync::mpsc::Sender<BackendAction>> {
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::<BackendAction>();
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) => {
sender
.send(db.update_person(person))
.expect("Failed to send result from database thread!");
}
GetPerson(id, sender) => {
sender
.send(db.get_person(id))
.expect("Failed to send result from database thread!");
}
DeletePerson(id, sender) => {
sender
.send(db.delete_person(id))
.expect("Failed to send result from database thread!");
}
GetPersons(sender) => {
sender
.send(db.get_persons())
.expect("Failed to send result from database thread!");
}
UpdateInstrument(instrument, sender) => {
sender
.send(db.update_instrument(instrument))
.expect("Failed to send result from database thread!");
}
GetInstrument(id, sender) => {
sender
.send(db.get_instrument(id))
.expect("Failed to send result from database thread!");
}
DeleteInstrument(id, sender) => {
sender
.send(db.delete_instrument(id))
.expect("Failed to send result from database thread!");
}
GetInstruments(sender) => {
sender
.send(db.get_instruments())
.expect("Failed to send result from database thread!");
}
UpdateWork(work, sender) => {
sender
.send(db.update_work(work))
.expect("Failed to send result from database thread!");
}
GetWorkDescription(id, sender) => {
sender
.send(db.get_work_description(id))
.expect("Failed to send result from database thread!");
}
DeleteWork(id, sender) => {
sender
.send(db.delete_work(id))
.expect("Failed to send result from database thread!");
}
GetWorkDescriptions(id, sender) => {
sender
.send(db.get_work_descriptions(id))
.expect("Failed to send result from database thread!");
}
UpdateEnsemble(ensemble, sender) => {
sender
.send(db.update_ensemble(ensemble))
.expect("Failed to send result from database thread!");
}
GetEnsemble(id, sender) => {
sender
.send(db.get_ensemble(id))
.expect("Failed to send result from database thread!");
}
DeleteEnsemble(id, sender) => {
sender
.send(db.delete_ensemble(id))
.expect("Failed to send result from database thread!");
}
GetEnsembles(sender) => {
sender
.send(db.get_ensembles())
.expect("Failed to send result from database thread!");
}
UpdateRecording(recording, sender) => {
sender
.send(db.update_recording(recording))
.expect("Failed to send result from database thread!");
}
GetRecordingDescription(id, sender) => {
sender
.send(db.get_recording_description(id))
.expect("Failed to send result from database thread!");
}
DeleteRecording(id, sender) => {
sender
.send(db.delete_recording(id))
.expect("Failed to send result from database thread!");
}
GetRecordingsForPerson(id, sender) => {
sender
.send(db.get_recordings_for_person(id))
.expect("Failed to send result from database thread!");
}
GetRecordingsForEnsemble(id, sender) => {
sender
.send(db.get_recordings_for_ensemble(id))
.expect("Failed to send result from database thread!");
}
GetRecordingsForWork(id, sender) => {
sender
.send(db.get_recordings_for_work(id))
.expect("Failed to send result from database thread!");
}
UpdateTracks(recording_id, tracks, sender) => {
sender
.send(db.update_tracks(recording_id, tracks))
.expect("Failed to send result from database thread!");
}
DeleteTracks(recording_id, sender) => {
sender
.send(db.delete_tracks(recording_id))
.expect("Failed to send result from database thread!");
}
GetTracks(recording_id, sender) => {
sender
.send(db.get_tracks(recording_id))
.expect("Failed to send result from database thread!");
}
Stop => {
break;
}
}
}
});
ready_receiver.await?;
self.action_sender.replace(Some(action_sender));
Ok(())
}
}

View file

@ -1,9 +1,69 @@
use super::secure;
use super::Backend;
use anyhow::{anyhow, bail, Result};
use gio::prelude::*;
use isahc::http::StatusCode;
use isahc::prelude::*;
use serde::Serialize;
/// Credentials used for login.
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LoginData {
pub username: String,
pub password: String,
}
impl Backend {
/// Initialize the client.
pub(super) fn init_client(&self) -> Result<()> {
if let Some(data) = secure::load_login_data()? {
self.login_data.replace(Some(data));
}
if let Some(url) = self.settings.get_string("server-url") {
if !url.is_empty() {
self.server_url.replace(Some(url.to_string()));
}
}
Ok(())
}
/// Set the URL of the Musicus server to connect to.
pub fn set_server_url(&self, url: &str) -> Result<()> {
self.settings.set_string("server-url", url)?;
self.server_url.replace(Some(url.to_string()));
Ok(())
}
/// Get the currently used login token.
pub fn get_token(&self) -> Option<String> {
self.token.borrow().clone()
}
/// Set the login token to use. This will be done automatically by the login method.
pub fn set_token(&self, token: &str) {
self.token.replace(Some(token.to_string()));
}
/// Get the currently set server URL.
pub fn get_server_url(&self) -> Option<String> {
self.server_url.borrow().clone()
}
/// Get the currently stored login credentials.
pub fn get_login_data(&self) -> Option<LoginData> {
self.login_data.borrow().clone()
}
/// Set the user credentials to use.
pub async fn set_login_data(&self, data: LoginData) -> Result<()> {
secure::store_login_data(data.clone()).await?;
self.login_data.replace(Some(data));
Ok(())
}
/// Try to login a user with the provided credentials and return, wether the login suceeded.
pub async fn login(&self) -> Result<bool> {
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;

View file

@ -0,0 +1,73 @@
use super::{Backend, BackendState};
use crate::database::DbThread;
use crate::player::Player;
use anyhow::Result;
use gio::prelude::*;
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<()> {
if let Some(path) = self.settings.get_string("music-library-path") {
if !path.is_empty() {
self.set_music_library_path_priv(PathBuf::from(path.to_string()))
.await?;
}
}
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<()> {
self.settings
.set_string("music-library-path", path.to_str().unwrap())?;
self.set_music_library_path_priv(path).await
}
/// 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<()> {
self.set_state(BackendState::Loading);
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?;
self.database.replace(Some(Rc::new(database)));
let player = Player::new(path);
self.player.replace(Some(player));
self.set_state(BackendState::Ready);
Ok(())
}
/// Get the currently set music library path.
pub fn get_music_library_path(&self) -> Option<PathBuf> {
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()
}
/// Get an interface to the playback service.
pub fn get_player(&self) -> Option<Rc<Player>> {
self.player.borrow().clone()
}
/// Get an interface to the player and panic if there is none.
pub fn pl(&self) -> Rc<Player> {
self.get_player().unwrap()
}
}

View file

@ -1,7 +1,76 @@
pub mod backend;
pub use backend::*;
use crate::database::DbThread;
use crate::player::Player;
use anyhow::Result;
use futures_channel::mpsc;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
pub mod client;
pub use client::*;
mod secure;
pub mod library;
pub use library::*;
mod secure;
/// General states the application can be in.
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
/// option.
NoMusicLibrary,
/// The backend is loading the music library. No methods should be called. The user interface
/// should represent that state by prohibiting all interaction.
Loading,
/// The backend is ready and all methods may be called.
Ready,
}
/// A collection of all backend state and functionality.
pub struct Backend {
pub state_stream: RefCell<mpsc::Receiver<BackendState>>,
state_sender: RefCell<mpsc::Sender<BackendState>>,
settings: gio::Settings,
music_library_path: RefCell<Option<PathBuf>>,
database: RefCell<Option<Rc<DbThread>>>,
player: RefCell<Option<Rc<Player>>>,
server_url: RefCell<Option<String>>,
login_data: RefCell<Option<LoginData>>,
token: RefCell<Option<String>>,
}
impl Backend {
/// Create a new backend initerface. The user interface should subscribe to the state stream
/// and call init() afterwards.
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),
settings: gio::Settings::new("de.johrpan.musicus"),
music_library_path: RefCell::new(None),
database: RefCell::new(None),
player: RefCell::new(None),
server_url: RefCell::new(None),
login_data: RefCell::new(None),
token: RefCell::new(None),
}
}
/// Initialize the backend updating the state accordingly.
pub async fn init(self: Rc<Backend>) -> Result<()> {
self.init_library().await?;
self.init_client()?;
Ok(())
}
/// Set the current state and notify the user interface.
fn set_state(&self, state: BackendState) {
self.state_sender.borrow_mut().try_send(state).unwrap();
}
}

View file

@ -1,448 +0,0 @@
use super::models::*;
use super::schema::*;
use super::tables::*;
use anyhow::{anyhow, Error, Result};
use diesel::prelude::*;
use std::convert::TryInto;
embed_migrations!();
pub struct Database {
c: SqliteConnection,
}
impl Database {
pub fn new(path: &str) -> Result<Database> {
let c = SqliteConnection::establish(path)?;
diesel::sql_query("PRAGMA foreign_keys = ON;").execute(&c)?;
embedded_migrations::run(&c)?;
Ok(Database { c: c })
}
pub fn update_person(&self, person: Person) -> Result<()> {
self.defer_foreign_keys();
self.c.transaction(|| {
diesel::replace_into(persons::table)
.values(person)
.execute(&self.c)
})?;
Ok(())
}
pub fn get_person(&self, id: i64) -> Result<Person> {
persons::table
.filter(persons::id.eq(id))
.load::<Person>(&self.c)?
.first()
.cloned()
.ok_or(anyhow!("No person with ID: {}", id))
}
pub fn delete_person(&self, id: i64) -> Result<()> {
diesel::delete(persons::table.filter(persons::id.eq(id))).execute(&self.c)?;
Ok(())
}
pub fn get_persons(&self) -> Result<Vec<Person>> {
let persons = persons::table.load::<Person>(&self.c)?;
Ok(persons)
}
pub fn update_instrument(&self, instrument: Instrument) -> Result<()> {
self.defer_foreign_keys();
self.c.transaction(|| {
diesel::replace_into(instruments::table)
.values(instrument)
.execute(&self.c)
})?;
Ok(())
}
pub fn get_instrument(&self, id: i64) -> Result<Instrument> {
instruments::table
.filter(instruments::id.eq(id))
.load::<Instrument>(&self.c)?
.first()
.cloned()
.ok_or(anyhow!("No instrument with ID: {}", id))
}
pub fn delete_instrument(&self, id: i64) -> Result<()> {
diesel::delete(instruments::table.filter(instruments::id.eq(id))).execute(&self.c)?;
Ok(())
}
pub fn get_instruments(&self) -> Result<Vec<Instrument>> {
let instruments = instruments::table.load::<Instrument>(&self.c)?;
Ok(instruments)
}
pub fn update_work(&self, work_insertion: WorkInsertion) -> Result<()> {
let id = work_insertion.work.id;
self.defer_foreign_keys();
self.c.transaction::<(), Error, _>(|| {
self.delete_work(id)?;
diesel::insert_into(works::table)
.values(work_insertion.work)
.execute(&self.c)?;
for instrument_id in work_insertion.instrument_ids {
diesel::insert_into(instrumentations::table)
.values(Instrumentation {
id: rand::random(),
work: id,
instrument: instrument_id,
})
.execute(&self.c)?;
}
for part_insertion in work_insertion.parts {
let part_id = part_insertion.part.id;
diesel::insert_into(work_parts::table)
.values(part_insertion.part)
.execute(&self.c)?;
for instrument_id in part_insertion.instrument_ids {
diesel::insert_into(part_instrumentations::table)
.values(PartInstrumentation {
id: rand::random(),
work_part: part_id,
instrument: instrument_id,
})
.execute(&self.c)?;
}
}
for section in work_insertion.sections {
diesel::insert_into(work_sections::table)
.values(section)
.execute(&self.c)?;
}
Ok(())
})?;
Ok(())
}
pub fn get_work(&self, id: i64) -> Result<Work> {
works::table
.filter(works::id.eq(id))
.load::<Work>(&self.c)?
.first()
.cloned()
.ok_or(anyhow!("No work with ID: {}", id))
}
pub fn get_work_description_for_work(&self, work: &Work) -> Result<WorkDescription> {
let mut instruments: Vec<Instrument> = Vec::new();
let instrumentations = instrumentations::table
.filter(instrumentations::work.eq(work.id))
.load::<Instrumentation>(&self.c)?;
for instrumentation in instrumentations {
instruments.push(self.get_instrument(instrumentation.instrument)?);
}
let mut part_descriptions: Vec<WorkPartDescription> = Vec::new();
let work_parts = work_parts::table
.filter(work_parts::work.eq(work.id))
.load::<WorkPart>(&self.c)?;
for work_part in work_parts {
let mut part_instruments: Vec<Instrument> = Vec::new();
let part_instrumentations = part_instrumentations::table
.filter(part_instrumentations::work_part.eq(work_part.id))
.load::<PartInstrumentation>(&self.c)?;
for part_instrumentation in part_instrumentations {
part_instruments.push(self.get_instrument(part_instrumentation.instrument)?);
}
part_descriptions.push(WorkPartDescription {
composer: match work_part.composer {
Some(composer) => Some(self.get_person(composer)?),
None => None,
},
title: work_part.title.clone(),
instruments: part_instruments,
});
}
let mut section_descriptions: Vec<WorkSectionDescription> = Vec::new();
let sections = work_sections::table
.filter(work_sections::work.eq(work.id))
.load::<WorkSection>(&self.c)?;
for section in sections {
section_descriptions.push(WorkSectionDescription {
title: section.title.clone(),
before_index: section.before_index,
});
}
let work_description = WorkDescription {
id: work.id,
composer: self.get_person(work.composer)?,
title: work.title.clone(),
instruments: instruments,
parts: part_descriptions,
sections: section_descriptions,
};
Ok(work_description)
}
pub fn get_work_description(&self, id: i64) -> Result<WorkDescription> {
let work = self.get_work(id)?;
let work_description = self.get_work_description_for_work(&work)?;
Ok(work_description)
}
pub fn delete_work(&self, id: i64) -> Result<()> {
diesel::delete(works::table.filter(works::id.eq(id))).execute(&self.c)?;
Ok(())
}
pub fn get_works(&self, composer_id: i64) -> Result<Vec<Work>> {
let works = works::table
.filter(works::composer.eq(composer_id))
.load::<Work>(&self.c)?;
Ok(works)
}
pub fn get_work_descriptions(&self, composer_id: i64) -> Result<Vec<WorkDescription>> {
let mut work_descriptions: Vec<WorkDescription> = Vec::new();
let works = self.get_works(composer_id)?;
for work in works {
work_descriptions.push(self.get_work_description_for_work(&work)?);
}
Ok(work_descriptions)
}
pub fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> {
self.defer_foreign_keys();
self.c.transaction(|| {
diesel::replace_into(ensembles::table)
.values(ensemble)
.execute(&self.c)
})?;
Ok(())
}
pub fn get_ensemble(&self, id: i64) -> Result<Ensemble> {
ensembles::table
.filter(ensembles::id.eq(id))
.load::<Ensemble>(&self.c)?
.first()
.cloned()
.ok_or(anyhow!("No ensemble with ID: {}", id))
}
pub fn delete_ensemble(&self, id: i64) -> Result<()> {
diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(&self.c)?;
Ok(())
}
pub fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
let ensembles = ensembles::table.load::<Ensemble>(&self.c)?;
Ok(ensembles)
}
pub fn update_recording(&self, recording_insertion: RecordingInsertion) -> Result<()> {
let id = recording_insertion.recording.id;
self.defer_foreign_keys();
self.c.transaction::<(), Error, _>(|| {
self.delete_recording(id)?;
diesel::insert_into(recordings::table)
.values(recording_insertion.recording)
.execute(&self.c)?;
for performance in recording_insertion.performances {
diesel::insert_into(performances::table)
.values(performance)
.execute(&self.c)?;
}
Ok(())
})?;
Ok(())
}
pub fn get_recording(&self, id: i64) -> Result<Recording> {
recordings::table
.filter(recordings::id.eq(id))
.load::<Recording>(&self.c)?
.first()
.cloned()
.ok_or(anyhow!("No recording with ID: {}", id))
}
pub fn get_recording_description_for_recording(
&self,
recording: &Recording,
) -> Result<RecordingDescription> {
let mut performance_descriptions: Vec<PerformanceDescription> = Vec::new();
let performances = performances::table
.filter(performances::recording.eq(recording.id))
.load::<Performance>(&self.c)?;
for performance in performances {
performance_descriptions.push(PerformanceDescription {
person: match performance.person {
Some(id) => Some(self.get_person(id)?),
None => None,
},
ensemble: match performance.ensemble {
Some(id) => Some(self.get_ensemble(id)?),
None => None,
},
role: match performance.role {
Some(id) => Some(self.get_instrument(id)?),
None => None,
},
});
}
Ok(RecordingDescription {
id: recording.id,
work: self.get_work_description(recording.work)?,
comment: recording.comment.clone(),
performances: performance_descriptions,
})
}
pub fn get_recording_description(&self, id: i64) -> Result<RecordingDescription> {
let recording = self.get_recording(id)?;
let recording_description = self.get_recording_description_for_recording(&recording)?;
Ok(recording_description)
}
pub fn get_recordings_for_person(&self, id: i64) -> Result<Vec<RecordingDescription>> {
let mut recording_descriptions: Vec<RecordingDescription> = Vec::new();
let recordings = recordings::table
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
.inner_join(persons::table.on(persons::id.nullable().eq(performances::person)))
.filter(persons::id.eq(id))
.select(recordings::table::all_columns())
.load::<Recording>(&self.c)?;
for recording in recordings {
recording_descriptions.push(self.get_recording_description_for_recording(&recording)?);
}
Ok(recording_descriptions)
}
pub fn get_recordings_for_ensemble(&self, id: i64) -> Result<Vec<RecordingDescription>> {
let mut recording_descriptions: Vec<RecordingDescription> = Vec::new();
let recordings = recordings::table
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
.inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble)))
.filter(ensembles::id.eq(id))
.select(recordings::table::all_columns())
.load::<Recording>(&self.c)?;
for recording in recordings {
recording_descriptions.push(self.get_recording_description_for_recording(&recording)?);
}
Ok(recording_descriptions)
}
pub fn get_recordings_for_work(&self, id: i64) -> Result<Vec<RecordingDescription>> {
let mut recording_descriptions: Vec<RecordingDescription> = Vec::new();
let recordings = recordings::table
.inner_join(works::table.on(works::id.eq(recordings::work)))
.filter(works::id.eq(id))
.select(recordings::table::all_columns())
.load::<Recording>(&self.c)?;
for recording in recordings {
recording_descriptions.push(self.get_recording_description_for_recording(&recording)?);
}
Ok(recording_descriptions)
}
pub fn delete_recording(&self, id: i64) -> Result<()> {
self.delete_tracks(id)?;
diesel::delete(recordings::table.filter(recordings::id.eq(id))).execute(&self.c)?;
Ok(())
}
pub fn get_recordings(&self, work_id: i64) -> Result<Vec<Recording>> {
let recordings = recordings::table
.filter(recordings::work.eq(work_id))
.load::<Recording>(&self.c)?;
Ok(recordings)
}
pub fn update_tracks(&self, recording_id: i64, tracks: Vec<TrackDescription>) -> Result<()> {
self.delete_tracks(recording_id)?;
for (index, track_description) in tracks.iter().enumerate() {
let track = Track {
id: rand::random(),
file_name: track_description.file_name.clone(),
recording: recording_id,
track_index: index.try_into().unwrap(),
work_parts: track_description
.work_parts
.iter()
.map(|i| i.to_string())
.collect::<Vec<String>>()
.join(","),
};
diesel::insert_into(tracks::table)
.values(track)
.execute(&self.c)?;
}
Ok(())
}
pub fn delete_tracks(&self, recording_id: i64) -> Result<()> {
diesel::delete(tracks::table.filter(tracks::recording.eq(recording_id))).execute(&self.c)?;
Ok(())
}
pub fn get_tracks(&self, recording_id: i64) -> Result<Vec<TrackDescription>> {
let tracks = tracks::table
.filter(tracks::recording.eq(recording_id))
.order_by(tracks::track_index)
.load::<Track>(&self.c)?;
Ok(tracks.iter().map(|track| track.clone().into()).collect())
}
fn defer_foreign_keys(&self) {
diesel::sql_query("PRAGMA defer_foreign_keys = ON;")
.execute(&self.c)
.expect("Failed to enable defer_foreign_keys_pragma!");
}
}

View file

@ -0,0 +1,96 @@
use super::schema::ensembles;
use super::Database;
use anyhow::{Error, Result};
use diesel::prelude::*;
use diesel::{Insertable, Queryable};
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
/// Database table data for an ensemble.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "ensembles"]
struct EnsembleRow {
pub id: i64,
pub name: String,
}
impl From<Ensemble> for EnsembleRow {
fn from(ensemble: Ensemble) -> Self {
EnsembleRow {
id: ensemble.id as i64,
name: ensemble.name,
}
}
}
/// An ensemble that takes part in recordings.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Ensemble {
pub id: u32,
pub name: String,
}
impl TryFrom<EnsembleRow> for Ensemble {
type Error = Error;
fn try_from(row: EnsembleRow) -> Result<Self> {
let ensemble = Ensemble {
id: row.id.try_into()?,
name: row.name,
};
Ok(ensemble)
}
}
impl Database {
/// Update an existing ensemble or insert a new one.
pub fn update_ensemble(&self, ensemble: Ensemble) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
let row: EnsembleRow = ensemble.into();
diesel::replace_into(ensembles::table)
.values(row)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing ensemble.
pub fn get_ensemble(&self, id: u32) -> Result<Option<Ensemble>> {
let row = ensembles::table
.filter(ensembles::id.eq(id as i64))
.load::<EnsembleRow>(&self.connection)?
.first()
.cloned();
let ensemble = match row {
Some(row) => Some(row.try_into()?),
None => None,
};
Ok(ensemble)
}
/// Delete an existing ensemble.
pub fn delete_ensemble(&self, id: u32) -> Result<()> {
diesel::delete(ensembles::table.filter(ensembles::id.eq(id as i64)))
.execute(&self.connection)?;
Ok(())
}
/// Get all existing ensembles.
pub fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
let mut ensembles = Vec::<Ensemble>::new();
let rows = ensembles::table.load::<EnsembleRow>(&self.connection)?;
for row in rows {
ensembles.push(row.try_into()?);
}
Ok(ensembles)
}
}

View file

@ -0,0 +1,96 @@
use super::schema::instruments;
use super::Database;
use anyhow::{Error, Result};
use diesel::prelude::*;
use diesel::{Insertable, Queryable};
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
/// Table row data for an instrument.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "instruments"]
struct InstrumentRow {
pub id: i64,
pub name: String,
}
impl From<Instrument> for InstrumentRow {
fn from(instrument: Instrument) -> Self {
InstrumentRow {
id: instrument.id as i64,
name: instrument.name,
}
}
}
/// An instrument or any other possible role within a recording.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Instrument {
pub id: u32,
pub name: String,
}
impl TryFrom<InstrumentRow> for Instrument {
type Error = Error;
fn try_from(row: InstrumentRow) -> Result<Self> {
let instrument = Instrument {
id: row.id.try_into()?,
name: row.name,
};
Ok(instrument)
}
}
impl Database {
/// Update an existing instrument or insert a new one.
pub fn update_instrument(&self, instrument: Instrument) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
let row: InstrumentRow = instrument.into();
diesel::replace_into(instruments::table)
.values(row)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing instrument.
pub fn get_instrument(&self, id: u32) -> Result<Option<Instrument>> {
let row = instruments::table
.filter(instruments::id.eq(id as i64))
.load::<InstrumentRow>(&self.connection)?
.first()
.cloned();
let instrument = match row {
Some(row) => Some(row.try_into()?),
None => None,
};
Ok(instrument)
}
/// Delete an existing instrument.
pub fn delete_instrument(&self, id: u32) -> Result<()> {
diesel::delete(instruments::table.filter(instruments::id.eq(id as i64)))
.execute(&self.connection)?;
Ok(())
}
/// Get all existing instruments.
pub fn get_instruments(&self) -> Result<Vec<Instrument>> {
let mut instruments = Vec::<Instrument>::new();
let rows = instruments::table.load::<InstrumentRow>(&self.connection)?;
for row in rows {
instruments.push(row.try_into()?);
}
Ok(instruments)
}
}

View file

@ -1,10 +1,51 @@
pub mod database;
pub use database::*;
use anyhow::Result;
use diesel::prelude::*;
pub mod models;
pub use models::*;
pub mod ensembles;
pub use ensembles::*;
pub mod schema;
pub mod instruments;
pub use instruments::*;
pub mod tables;
pub use tables::*;
pub mod persons;
pub use persons::*;
pub mod recordings;
pub use recordings::*;
pub mod thread;
pub use thread::*;
pub mod tracks;
pub use tracks::*;
pub mod works;
pub use works::*;
mod schema;
// This makes the SQL migration scripts accessible from the code.
embed_migrations!();
/// Interface to a Musicus database.
pub struct Database {
connection: SqliteConnection,
}
impl Database {
/// Create a new database interface and run migrations if necessary.
pub fn new(file_name: &str) -> Result<Database> {
let connection = SqliteConnection::establish(file_name)?;
diesel::sql_query("PRAGMA foreign_keys = ON").execute(&connection)?;
embedded_migrations::run(&connection)?;
Ok(Database { connection })
}
/// Defer all foreign keys for the next transaction.
fn defer_foreign_keys(&self) -> Result<()> {
diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(&self.connection)?;
Ok(())
}
}

View file

@ -1,205 +0,0 @@
use super::tables::*;
use std::convert::TryInto;
#[derive(Debug, Clone)]
pub struct WorkPartDescription {
pub title: String,
pub composer: Option<Person>,
pub instruments: Vec<Instrument>,
}
#[derive(Debug, Clone)]
pub struct WorkSectionDescription {
pub title: String,
pub before_index: i64,
}
#[derive(Debug, Clone)]
pub struct WorkDescription {
pub id: i64,
pub title: String,
pub composer: Person,
pub instruments: Vec<Instrument>,
pub parts: Vec<WorkPartDescription>,
pub sections: Vec<WorkSectionDescription>,
}
impl WorkDescription {
pub fn get_title(&self) -> String {
format!("{}: {}", self.composer.name_fl(), self.title)
}
}
#[derive(Debug, Clone)]
pub struct WorkPartInsertion {
pub part: WorkPart,
pub instrument_ids: Vec<i64>,
}
#[derive(Debug, Clone)]
pub struct WorkInsertion {
pub work: Work,
pub instrument_ids: Vec<i64>,
pub parts: Vec<WorkPartInsertion>,
pub sections: Vec<WorkSection>,
}
impl From<WorkDescription> for WorkInsertion {
fn from(description: WorkDescription) -> Self {
WorkInsertion {
work: Work {
id: description.id,
composer: description.composer.id,
title: description.title.clone(),
},
instrument_ids: description
.instruments
.iter()
.map(|instrument| instrument.id)
.collect(),
parts: description
.parts
.iter()
.enumerate()
.map(|(index, part)| WorkPartInsertion {
part: WorkPart {
id: rand::random(),
work: description.id,
part_index: index.try_into().expect("Part index didn't fit into u32!"),
composer: part.composer.as_ref().map(|person| person.id),
title: part.title.clone(),
},
instrument_ids: part
.instruments
.iter()
.map(|instrument| instrument.id)
.collect(),
})
.collect(),
sections: description
.sections
.iter()
.map(|section| WorkSection {
id: rand::random(),
work: description.id,
title: section.title.clone(),
before_index: section.before_index,
})
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct PerformanceDescription {
pub person: Option<Person>,
pub ensemble: Option<Ensemble>,
pub role: Option<Instrument>,
}
impl PerformanceDescription {
pub fn get_title(&self) -> String {
let mut text = String::from(if self.is_person() {
self.unwrap_person().name_fl()
} else {
self.unwrap_ensemble().name
});
if self.has_role() {
text = text + " (" + &self.unwrap_role().name + ")";
}
text
}
pub fn is_person(&self) -> bool {
self.person.is_some()
}
pub fn unwrap_person(&self) -> Person {
self.person.clone().unwrap()
}
pub fn unwrap_ensemble(&self) -> Ensemble {
self.ensemble.clone().unwrap()
}
pub fn has_role(&self) -> bool {
self.role.clone().is_some()
}
pub fn unwrap_role(&self) -> Instrument {
self.role.clone().unwrap()
}
}
#[derive(Debug, Clone)]
pub struct RecordingDescription {
pub id: i64,
pub work: WorkDescription,
pub comment: String,
pub performances: Vec<PerformanceDescription>,
}
impl RecordingDescription {
pub fn get_performers(&self) -> String {
let texts: Vec<String> = self
.performances
.iter()
.map(|performance| performance.get_title())
.collect();
texts.join(", ")
}
}
#[derive(Debug, Clone)]
pub struct RecordingInsertion {
pub recording: Recording,
pub performances: Vec<Performance>,
}
impl From<RecordingDescription> for RecordingInsertion {
fn from(description: RecordingDescription) -> Self {
RecordingInsertion {
recording: Recording {
id: description.id,
work: description.work.id,
comment: description.comment.clone(),
},
performances: description
.performances
.iter()
.map(|performance| Performance {
id: rand::random(),
recording: description.id,
person: performance.person.as_ref().map(|person| person.id),
ensemble: performance.ensemble.as_ref().map(|ensemble| ensemble.id),
role: performance.role.as_ref().map(|role| role.id),
})
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct TrackDescription {
pub work_parts: Vec<usize>,
pub file_name: String,
}
impl From<Track> for TrackDescription {
fn from(track: Track) -> Self {
let mut work_parts = Vec::<usize>::new();
for part in track.work_parts.split(",") {
if !part.is_empty() {
work_parts.push(part.parse().unwrap());
}
}
TrackDescription {
work_parts,
file_name: track.file_name,
}
}
}

View file

@ -0,0 +1,111 @@
use super::schema::persons;
use super::Database;
use anyhow::{Error, Result};
use diesel::prelude::*;
use diesel::{Insertable, Queryable};
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
/// Database table data for a person.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "persons"]
struct PersonRow {
pub id: i64,
pub first_name: String,
pub last_name: String,
}
impl From<Person> for PersonRow {
fn from(person: Person) -> Self {
PersonRow {
id: person.id as i64,
first_name: person.first_name,
last_name: person.last_name,
}
}
}
/// A person that is a composer, an interpret or both.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Person {
pub id: u32,
pub first_name: String,
pub last_name: String,
}
impl TryFrom<PersonRow> for Person {
type Error = Error;
fn try_from(row: PersonRow) -> Result<Self> {
let person = Person {
id: row.id.try_into()?,
first_name: row.first_name,
last_name: row.last_name,
};
Ok(person)
}
}
impl Person {
/// Get the full name in the form "First Last".
pub fn name_fl(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
/// Get the full name in the form "Last, First".
pub fn name_lf(&self) -> String {
format!("{}, {}", self.last_name, self.first_name)
}
}
impl Database {
/// Update an existing person or insert a new one.
pub fn update_person(&self, person: Person) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
let row: PersonRow = person.into();
diesel::replace_into(persons::table)
.values(row)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing person.
pub fn get_person(&self, id: u32) -> Result<Option<Person>> {
let row = persons::table
.filter(persons::id.eq(id as i64))
.load::<PersonRow>(&self.connection)?
.first()
.cloned();
let person = match row {
Some(row) => Some(row.try_into()?),
None => None,
};
Ok(person)
}
/// Delete an existing person.
pub fn delete_person(&self, id: u32) -> Result<()> {
diesel::delete(persons::table.filter(persons::id.eq(id as i64)))
.execute(&self.connection)?;
Ok(())
}
/// Get all existing persons.
pub fn get_persons(&self) -> Result<Vec<Person>> {
let mut persons = Vec::<Person>::new();
let rows = persons::table.load::<PersonRow>(&self.connection)?;
for row in rows {
persons.push(row.try_into()?);
}
Ok(persons)
}
}

View file

@ -0,0 +1,252 @@
use super::schema::{ensembles, performances, persons, recordings};
use super::{Database, Ensemble, Instrument, Person, Work};
use anyhow::{anyhow, Error, Result};
use diesel::prelude::*;
use diesel::{Insertable, Queryable};
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
/// Database table data for a recording.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "recordings"]
struct RecordingRow {
pub id: i64,
pub work: i64,
pub comment: String,
}
impl From<Recording> for RecordingRow {
fn from(recording: Recording) -> Self {
RecordingRow {
id: recording.id as i64,
work: recording.work.id as i64,
comment: recording.comment,
}
}
}
/// Database table data for a performance.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "performances"]
struct PerformanceRow {
pub id: i64,
pub recording: i64,
pub person: Option<i64>,
pub ensemble: Option<i64>,
pub role: Option<i64>,
}
/// How a person or ensemble was involved in a recording.
// TODO: Replace person/ensemble with an enum.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Performance {
pub person: Option<Person>,
pub ensemble: Option<Ensemble>,
pub role: Option<Instrument>,
}
impl Performance {
/// Get a string representation of the performance.
// TODO: Replace with impl Display.
pub fn get_title(&self) -> String {
let mut text = String::from(if self.is_person() {
self.unwrap_person().name_fl()
} else {
self.unwrap_ensemble().name
});
if self.has_role() {
text = text + " (" + &self.unwrap_role().name + ")";
}
text
}
pub fn is_person(&self) -> bool {
self.person.is_some()
}
pub fn unwrap_person(&self) -> Person {
self.person.clone().unwrap()
}
pub fn unwrap_ensemble(&self) -> Ensemble {
self.ensemble.clone().unwrap()
}
pub fn has_role(&self) -> bool {
self.role.clone().is_some()
}
pub fn unwrap_role(&self) -> Instrument {
self.role.clone().unwrap()
}
}
/// A specific recording of a work.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Recording {
pub id: u32,
pub work: Work,
pub comment: String,
pub performances: Vec<Performance>,
}
impl Recording {
/// Get a string representation of the performances in this recording.
// TODO: Maybe replace with impl Display?
pub fn get_performers(&self) -> String {
let texts: Vec<String> = self
.performances
.iter()
.map(|performance| performance.get_title())
.collect();
texts.join(", ")
}
}
impl Database {
/// Update an existing recording or insert a new one.
// TODO: Think about whether to also insert the other items.
pub fn update_recording(&self, recording: Recording) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), Error, _>(|| {
self.delete_recording(recording.id)?;
let recording_id = recording.id as i64;
let row: RecordingRow = recording.clone().into();
diesel::insert_into(recordings::table)
.values(row)
.execute(&self.connection)?;
for performance in recording.performances {
let row = PerformanceRow {
id: rand::random(),
recording: recording_id,
person: performance.person.map(|person| person.id as i64),
ensemble: performance.ensemble.map(|ensemble| ensemble.id as i64),
role: performance.role.map(|role| role.id as i64),
};
diesel::insert_into(performances::table)
.values(row)
.execute(&self.connection)?;
}
Ok(())
})?;
Ok(())
}
/// Retrieve all available information on a recording from related tables.
fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> {
let mut performance_descriptions: Vec<Performance> = Vec::new();
let performance_rows = performances::table
.filter(performances::recording.eq(row.id))
.load::<PerformanceRow>(&self.connection)?;
for row in performance_rows {
performance_descriptions.push(Performance {
person: match row.person {
Some(id) => Some(
self.get_person(id.try_into()?)?
.ok_or(anyhow!("No person with ID: {}", id))?,
),
None => None,
},
ensemble: match row.ensemble {
Some(id) => Some(
self.get_ensemble(id.try_into()?)?
.ok_or(anyhow!("No ensemble with ID: {}", id))?,
),
None => None,
},
role: match row.role {
Some(id) => Some(
self.get_instrument(id.try_into()?)?
.ok_or(anyhow!("No instrument with ID: {}", id))?,
),
None => None,
},
});
}
let work_id: u32 = row.work.try_into()?;
let work = self
.get_work(work_id)?
.ok_or(anyhow!("Work doesn't exist: {}", work_id))?;
let recording_description = Recording {
id: row.id.try_into()?,
work,
comment: row.comment.clone(),
performances: performance_descriptions,
};
Ok(recording_description)
}
/// Get all available information on all recordings where a person is performing.
pub fn get_recordings_for_person(&self, person_id: u32) -> Result<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
.inner_join(persons::table.on(persons::id.nullable().eq(performances::person)))
.filter(persons::id.eq(person_id as i64))
.select(recordings::table::all_columns())
.load::<RecordingRow>(&self.connection)?;
for row in rows {
recordings.push(self.get_recording_data(row)?);
}
Ok(recordings)
}
/// Get all available information on all recordings where an ensemble is performing.
pub fn get_recordings_for_ensemble(&self, ensemble_id: u32) -> Result<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.inner_join(performances::table.on(performances::recording.eq(recordings::id)))
.inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble)))
.filter(ensembles::id.eq(ensemble_id as i64))
.select(recordings::table::all_columns())
.load::<RecordingRow>(&self.connection)?;
for row in rows {
recordings.push(self.get_recording_data(row)?);
}
Ok(recordings)
}
/// Get allavailable information on all recordings of a work.
pub fn get_recordings_for_work(&self, work_id: u32) -> Result<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.filter(recordings::work.eq(work_id as i64))
.load::<RecordingRow>(&self.connection)?;
for row in rows {
recordings.push(self.get_recording_data(row)?);
}
Ok(recordings)
}
/// Delete an existing recording. This will fail if there are still references to this
/// recording from other tables that are not directly part of the recording data.
pub fn delete_recording(&self, id: u32) -> Result<()> {
diesel::delete(recordings::table.filter(recordings::id.eq(id as i64)))
.execute(&self.connection)?;
Ok(())
}
}

View file

@ -20,14 +20,6 @@ table! {
}
}
table! {
part_instrumentations (id) {
id -> BigInt,
work_part -> BigInt,
instrument -> BigInt,
}
}
table! {
performances (id) {
id -> BigInt,
@ -69,8 +61,8 @@ table! {
id -> BigInt,
work -> BigInt,
part_index -> BigInt,
composer -> Nullable<BigInt>,
title -> Text,
composer -> Nullable<BigInt>,
}
}
@ -93,8 +85,6 @@ table! {
joinable!(instrumentations -> instruments (instrument));
joinable!(instrumentations -> works (work));
joinable!(part_instrumentations -> instruments (instrument));
joinable!(part_instrumentations -> works (work_part));
joinable!(performances -> ensembles (ensemble));
joinable!(performances -> instruments (role));
joinable!(performances -> persons (person));
@ -110,7 +100,6 @@ allow_tables_to_appear_in_same_query!(
ensembles,
instrumentations,
instruments,
part_instrumentations,
performances,
persons,
recordings,

View file

@ -1,94 +0,0 @@
use super::schema::*;
use diesel::Queryable;
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Person {
pub id: i64,
pub first_name: String,
pub last_name: String,
}
impl Person {
pub fn name_fl(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
pub fn name_lf(&self) -> String {
format!("{}, {}", self.last_name, self.first_name)
}
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Instrument {
pub id: i64,
pub name: String,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Work {
pub id: i64,
pub composer: i64,
pub title: String,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Instrumentation {
pub id: i64,
pub work: i64,
pub instrument: i64,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct WorkPart {
pub id: i64,
pub work: i64,
pub part_index: i64,
pub composer: Option<i64>,
pub title: String,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct PartInstrumentation {
pub id: i64,
pub work_part: i64,
pub instrument: i64,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct WorkSection {
pub id: i64,
pub work: i64,
pub title: String,
pub before_index: i64,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Ensemble {
pub id: i64,
pub name: String,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Recording {
pub id: i64,
pub work: i64,
pub comment: String,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Performance {
pub id: i64,
pub recording: i64,
pub person: Option<i64>,
pub ensemble: Option<i64>,
pub role: Option<i64>,
}
#[derive(Insertable, Queryable, Debug, Clone)]
pub struct Track {
pub id: i64,
pub file_name: String,
pub recording: i64,
pub track_index: i32,
pub work_parts: String,
}

View file

@ -0,0 +1,327 @@
use super::*;
use anyhow::Result;
use futures_channel::oneshot;
use futures_channel::oneshot::Sender;
use std::sync::mpsc;
use std::thread;
/// An action the database thread can perform.
enum Action {
UpdatePerson(Person, Sender<Result<()>>),
GetPerson(u32, Sender<Result<Option<Person>>>),
DeletePerson(u32, Sender<Result<()>>),
GetPersons(Sender<Result<Vec<Person>>>),
UpdateInstrument(Instrument, Sender<Result<()>>),
GetInstrument(u32, Sender<Result<Option<Instrument>>>),
DeleteInstrument(u32, Sender<Result<()>>),
GetInstruments(Sender<Result<Vec<Instrument>>>),
UpdateWork(Work, Sender<Result<()>>),
DeleteWork(u32, Sender<Result<()>>),
GetWorks(u32, Sender<Result<Vec<Work>>>),
UpdateEnsemble(Ensemble, Sender<Result<()>>),
GetEnsemble(u32, Sender<Result<Option<Ensemble>>>),
DeleteEnsemble(u32, Sender<Result<()>>),
GetEnsembles(Sender<Result<Vec<Ensemble>>>),
UpdateRecording(Recording, Sender<Result<()>>),
DeleteRecording(u32, Sender<Result<()>>),
GetRecordingsForPerson(u32, Sender<Result<Vec<Recording>>>),
GetRecordingsForEnsemble(u32, Sender<Result<Vec<Recording>>>),
GetRecordingsForWork(u32, Sender<Result<Vec<Recording>>>),
UpdateTracks(u32, Vec<Track>, Sender<Result<()>>),
DeleteTracks(u32, Sender<Result<()>>),
GetTracks(u32, 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 || {
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 {
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();
}
UpdateTracks(recording_id, tracks, sender) => {
sender.send(db.update_tracks(recording_id, tracks)).unwrap();
}
DeleteTracks(recording_id, sender) => {
sender.send(db.delete_tracks(recording_id)).unwrap();
}
GetTracks(recording_id, sender) => {
sender.send(db.get_tracks(recording_id)).unwrap();
}
Stop(sender) => {
sender.send(()).unwrap();
break;
}
}
}
});
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: u32) -> Result<Option<Person>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetPerson(id, 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: u32) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(DeletePerson(id, 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: u32) -> Result<Option<Instrument>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetInstrument(id, 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: u32) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(DeleteInstrument(id, 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: u32) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(DeleteWork(id, sender))?;
receiver.await?
}
/// Get information on all existing works by a composer.
pub async fn get_works(&self, person_id: u32) -> Result<Vec<Work>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetWorks(person_id, 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: u32) -> Result<Option<Ensemble>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetEnsemble(id, 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: u32) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(DeleteEnsemble(id, 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: u32) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(DeleteRecording(id, sender))?;
receiver.await?
}
/// Get information on all recordings in which a person performs.
pub async fn get_recordings_for_person(&self, person_id: u32) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForPerson(person_id, sender))?;
receiver.await?
}
/// Get information on all recordings in which an ensemble performs.
pub async fn get_recordings_for_ensemble(&self, ensemble_id: u32) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForEnsemble(ensemble_id, sender))?;
receiver.await?
}
/// Get information on all recordings of a work.
pub async fn get_recordings_for_work(&self, work_id: u32) -> Result<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForWork(work_id, sender))?;
receiver.await?
}
/// Add or change the tracks associated with a recording. This will fail, if there are still
/// other items referencing this recording.
pub async fn update_tracks(
&self,
recording_id: u32,
tracks: Vec<Track>,
) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(UpdateTracks(recording_id, tracks, sender))?;
receiver.await?
}
/// Delete all tracks associated with a recording.
pub async fn delete_tracks(&self, recording_id: u32) -> Result<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteTracks(recording_id, sender))?;
receiver.await?
}
/// Get all tracks associated with a recording.
pub async fn get_tracks(&self, recording_id: u32) -> Result<Vec<Track>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetTracks(recording_id, 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

@ -0,0 +1,94 @@
use super::schema::tracks;
use super::Database;
use anyhow::{Error, Result};
use diesel::prelude::*;
use std::convert::{TryFrom, TryInto};
/// Table row data for a track.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "tracks"]
struct TrackRow {
pub id: i64,
pub file_name: String,
pub recording: i64,
pub track_index: i32,
pub work_parts: String,
}
/// A structure representing one playable audio file.
#[derive(Debug, Clone)]
pub struct Track {
pub work_parts: Vec<usize>,
pub file_name: String,
}
impl TryFrom<TrackRow> for Track {
type Error = Error;
fn try_from(row: TrackRow) -> Result<Self> {
let mut work_parts = Vec::<usize>::new();
for part in row.work_parts.split(",") {
if !part.is_empty() {
work_parts.push(part.parse()?);
}
}
let track = Track {
work_parts,
file_name: row.file_name,
};
Ok(track)
}
}
impl Database {
/// Insert or update tracks for the specified recording.
pub fn update_tracks(&self, recording_id: u32, tracks: Vec<Track>) -> Result<()> {
self.delete_tracks(recording_id)?;
for (index, track) in tracks.iter().enumerate() {
let row = TrackRow {
id: rand::random(),
file_name: track.file_name.clone(),
recording: recording_id as i64,
track_index: index.try_into()?,
work_parts: track
.work_parts
.iter()
.map(|i| i.to_string())
.collect::<Vec<String>>()
.join(","),
};
diesel::insert_into(tracks::table)
.values(row)
.execute(&self.connection)?;
}
Ok(())
}
/// Delete all tracks for the specified recording.
pub fn delete_tracks(&self, recording_id: u32) -> Result<()> {
diesel::delete(tracks::table.filter(tracks::recording.eq(recording_id as i64)))
.execute(&self.connection)?;
Ok(())
}
/// Get all tracks of the specified recording.
pub fn get_tracks(&self, recording_id: u32) -> Result<Vec<Track>> {
let mut tracks = Vec::<Track>::new();
let rows = tracks::table
.filter(tracks::recording.eq(recording_id as i64))
.order_by(tracks::track_index)
.load::<TrackRow>(&self.connection)?;
for row in rows {
tracks.push(row.try_into()?);
}
Ok(tracks)
}
}

View file

@ -0,0 +1,262 @@
use super::schema::{instrumentations, work_parts, work_sections, works};
use super::{Database, Instrument, Person};
use anyhow::{anyhow, Error, Result};
use diesel::prelude::*;
use diesel::{Insertable, Queryable};
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
/// Table row data for a work.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "works"]
struct WorkRow {
pub id: i64,
pub composer: i64,
pub title: String,
}
impl From<Work> for WorkRow {
fn from(work: Work) -> Self {
WorkRow {
id: work.id as i64,
composer: work.composer.id as i64,
title: work.title,
}
}
}
/// Definition that a work uses an instrument.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "instrumentations"]
struct InstrumentationRow {
pub id: i64,
pub work: i64,
pub instrument: i64,
}
/// Table row data for a work part.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "work_parts"]
struct WorkPartRow {
pub id: i64,
pub work: i64,
pub part_index: i64,
pub title: String,
pub composer: Option<i64>,
}
/// Table row data for a work section.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "work_sections"]
struct WorkSectionRow {
pub id: i64,
pub work: i64,
pub title: String,
pub before_index: i64,
}
/// A concrete work part that can be recorded.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct WorkPart {
pub title: String,
pub composer: Option<Person>,
}
/// A heading between work parts.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct WorkSection {
pub title: String,
pub before_index: usize,
}
/// A specific work by a composer.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Work {
pub id: u32,
pub title: String,
pub composer: Person,
pub instruments: Vec<Instrument>,
pub parts: Vec<WorkPart>,
pub sections: Vec<WorkSection>,
}
impl Work {
/// Get a string including the composer and title of the work.
// TODO: Replace with impl Display.
pub fn get_title(&self) -> String {
format!("{}: {}", self.composer.name_fl(), self.title)
}
}
impl Database {
/// Update an existing work or insert a new one.
// TODO: Think about also inserting related items.
pub fn update_work(&self, work: Work) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), Error, _>(|| {
self.delete_work(work.id)?;
let work_id = work.id as i64;
let row: WorkRow = work.clone().into();
diesel::insert_into(works::table)
.values(row)
.execute(&self.connection)?;
match work {
Work {
instruments,
parts,
sections,
..
} => {
for instrument in instruments {
let row = InstrumentationRow {
id: rand::random(),
work: work_id,
instrument: instrument.id as i64,
};
diesel::insert_into(instrumentations::table)
.values(row)
.execute(&self.connection)?;
}
for (index, part) in parts.into_iter().enumerate() {
let row = WorkPartRow {
id: rand::random(),
work: work_id,
part_index: index.try_into()?,
title: part.title,
composer: part.composer.map(|person| person.id as i64),
};
diesel::insert_into(work_parts::table)
.values(row)
.execute(&self.connection)?;
}
for section in sections {
let row = WorkSectionRow {
id: rand::random(),
work: work_id,
title: section.title,
before_index: section.before_index.try_into()?,
};
diesel::insert_into(work_sections::table)
.values(row)
.execute(&self.connection)?;
}
}
}
Ok(())
})?;
Ok(())
}
/// Get an existing work.
pub fn get_work(&self, id: u32) -> Result<Option<Work>> {
let row = works::table
.filter(works::id.eq(id as i64))
.load::<WorkRow>(&self.connection)?
.first()
.cloned();
let work = match row {
Some(row) => Some(self.get_work_data(row)?),
None => None,
};
Ok(work)
}
/// Retrieve all available information on a work from related tables.
fn get_work_data(&self, row: WorkRow) -> Result<Work> {
let mut instruments: Vec<Instrument> = Vec::new();
let instrumentations = instrumentations::table
.filter(instrumentations::work.eq(row.id))
.load::<InstrumentationRow>(&self.connection)?;
for instrumentation in instrumentations {
let id: u32 = instrumentation.instrument.try_into()?;
instruments.push(
self.get_instrument(id)?
.ok_or(anyhow!("No instrument with ID: {}", id))?,
);
}
let mut parts: Vec<WorkPart> = Vec::new();
let part_rows = work_parts::table
.filter(work_parts::work.eq(row.id))
.load::<WorkPartRow>(&self.connection)?;
for part_row in part_rows {
parts.push(WorkPart {
title: part_row.title,
composer: match part_row.composer {
Some(composer) => Some(
self.get_person(composer.try_into()?)?
.ok_or(anyhow!("No person with ID: {}", composer))?,
),
None => None,
},
});
}
let mut sections: Vec<WorkSection> = Vec::new();
let section_rows = work_sections::table
.filter(work_sections::work.eq(row.id))
.load::<WorkSectionRow>(&self.connection)?;
for section_row in section_rows {
sections.push(WorkSection {
title: section_row.title,
before_index: section_row.before_index.try_into()?,
});
}
let person_id = row.composer.try_into()?;
let person = self
.get_person(person_id)?
.ok_or(anyhow!("Person doesn't exist: {}", person_id))?;
Ok(Work {
id: row.id.try_into()?,
composer: person,
title: row.title,
instruments,
parts,
sections,
})
}
/// Delete an existing work. This will fail if there are still other tables that relate to
/// this work except for the things that are part of the information on the work it
pub fn delete_work(&self, id: u32) -> Result<()> {
diesel::delete(works::table.filter(works::id.eq(id as i64))).execute(&self.connection)?;
Ok(())
}
/// Get all existing works by a composer and related information from other tables.
pub fn get_works(&self, composer_id: u32) -> Result<Vec<Work>> {
let mut works: Vec<Work> = Vec::new();
let rows = works::table
.filter(works::composer.eq(composer_id as i64))
.load::<WorkRow>(&self.connection)?;
for row in rows {
works.push(self.get_work_data(row)?);
}
Ok(works)
}
}

View file

@ -12,7 +12,7 @@ where
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
id: i64,
id: u32,
name_entry: gtk::Entry,
}
@ -26,8 +26,7 @@ where
ensemble: Option<Ensemble>,
callback: F,
) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui");
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
@ -63,7 +62,7 @@ where
let clone = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
clone.backend.update_ensemble(ensemble.clone()).await.unwrap();
clone.backend.db().update_ensemble(ensemble.clone()).await.unwrap();
clone.window.close();
(clone.callback)(ensemble.clone());
});

View file

@ -25,8 +25,7 @@ where
F: Fn(Ensemble) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui");
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, add_button);
@ -44,7 +43,7 @@ where
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
let ensembles = clone.backend.get_ensembles().await.unwrap();
let ensembles = clone.backend.db().get_ensembles().await.unwrap();
for (index, ensemble) in ensembles.iter().enumerate() {
let label = gtk::Label::new(Some(&ensemble.name));

View file

@ -12,7 +12,7 @@ where
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
id: i64,
id: u32,
name_entry: gtk::Entry,
}
@ -63,7 +63,7 @@ where
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
clone.backend.update_instrument(instrument.clone()).await.unwrap();
clone.backend.db().update_instrument(instrument.clone()).await.unwrap();
clone.window.close();
(clone.callback)(instrument.clone());
});

View file

@ -25,8 +25,7 @@ where
F: Fn(Instrument) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui");
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, add_button);
@ -44,7 +43,7 @@ where
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
let instruments = clone.backend.get_instruments().await.unwrap();
let instruments = clone.backend.db().get_instruments().await.unwrap();
for (index, instrument) in instruments.iter().enumerate() {
let label = gtk::Label::new(Some(&instrument.name));

View file

@ -12,7 +12,7 @@ where
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
id: i64,
id: u32,
first_name_entry: gtk::Entry,
last_name_entry: gtk::Entry,
}
@ -67,7 +67,7 @@ where
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
clone.backend.update_person(person.clone()).await.unwrap();
clone.backend.db().update_person(person.clone()).await.unwrap();
clone.window.close();
(clone.callback)(person.clone());
});

View file

@ -20,7 +20,7 @@ pub struct PerformanceEditor {
person: RefCell<Option<Person>>,
ensemble: RefCell<Option<Ensemble>>,
role: RefCell<Option<Instrument>>,
selected_cb: RefCell<Option<Box<dyn Fn(PerformanceDescription) -> ()>>>,
selected_cb: RefCell<Option<Box<dyn Fn(Performance) -> ()>>>,
}
impl PerformanceEditor {
@ -28,7 +28,7 @@ impl PerformanceEditor {
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
performance: Option<PerformanceDescription>,
performance: Option<Performance>,
) -> Rc<Self> {
// Create UI
@ -70,7 +70,7 @@ impl PerformanceEditor {
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(PerformanceDescription {
cb(Performance {
person: this.person.borrow().clone(),
ensemble: this.ensemble.borrow().clone(),
role: this.role.borrow().clone(),
@ -132,7 +132,7 @@ impl PerformanceEditor {
}
/// Set a closure to be called when the user has chosen to save the performance.
pub fn set_selected_cb<F: Fn(PerformanceDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Performance) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}

View file

@ -13,7 +13,7 @@ pub struct RecordingDialog {
stack: gtk::Stack,
selector: Rc<RecordingSelector>,
editor: Rc<RecordingEditor>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
}
impl RecordingDialog {
@ -75,7 +75,7 @@ impl RecordingDialog {
}
/// Set the closure to be called when the user has selected or created a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}

View file

@ -19,11 +19,11 @@ pub struct RecordingEditor {
save_button: gtk::Button,
work_label: gtk::Label,
comment_entry: gtk::Entry,
performance_list: Rc<List<PerformanceDescription>>,
id: i64,
work: RefCell<Option<WorkDescription>>,
performances: RefCell<Vec<PerformanceDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
performance_list: Rc<List<Performance>>,
id: u32,
work: RefCell<Option<Work>>,
performances: RefCell<Vec<Performance>>,
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
}
@ -33,7 +33,7 @@ impl RecordingEditor {
pub fn new<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
recording: Option<RecordingDescription>,
recording: Option<Recording>,
) -> Rc<Self> {
// Create UI
@ -87,7 +87,7 @@ impl RecordingEditor {
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
let recording = RecordingDescription {
let recording = Recording {
id: this.id,
work: this.work.borrow().clone().expect("Tried to create recording without work!"),
comment: this.comment_entry.get_text().to_string(),
@ -97,7 +97,7 @@ impl RecordingEditor {
let c = glib::MainContext::default();
let clone = this.clone();
c.spawn_local(async move {
clone.backend.update_recording(recording.clone().into()).await.unwrap();
clone.backend.db().update_recording(recording.clone().into()).await.unwrap();
if let Some(cb) = &*clone.selected_cb.borrow() {
cb(recording.clone());
}
@ -192,12 +192,12 @@ impl RecordingEditor {
}
/// Set the closure to be called if the recording was created.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to work.
fn work_selected(&self, work: &WorkDescription) {
fn work_selected(&self, work: &Work) {
self.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title));
self.save_button.set_sensitive(true);
}

View file

@ -9,7 +9,7 @@ use std::rc::Rc;
/// A dialog for creating or editing a recording.
pub struct RecordingEditorDialog {
pub window: libhandy::Window,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
}
impl RecordingEditorDialog {
@ -17,7 +17,7 @@ impl RecordingEditorDialog {
pub fn new<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
recording: Option<RecordingDescription>,
recording: Option<Recording>,
) -> Rc<Self> {
// Create UI
@ -52,7 +52,7 @@ impl RecordingEditorDialog {
}
/// Set the closure to be called when the user edited or created a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}

View file

@ -14,7 +14,7 @@ pub struct RecordingSelector {
pub widget: libhandy::Leaflet,
backend: Rc<Backend>,
sidebar_box: gtk::Box,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
navigator: Rc<Navigator>,
}
@ -83,7 +83,7 @@ impl RecordingSelector {
}
/// Set the closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -16,8 +16,8 @@ pub struct RecordingSelectorPersonScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
work_list: Rc<List<Work>>,
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
@ -57,7 +57,7 @@ impl RecordingSelectorPersonScreen {
}
}));
this.work_list.set_make_widget(|work: &WorkDescription| {
this.work_list.set_make_widget(|work: &Work| {
let label = gtk::Label::new(Some(&work.title));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
@ -92,11 +92,7 @@ impl RecordingSelectorPersonScreen {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.await
.unwrap();
let works = clone.backend.db().get_works(person.id).await.unwrap();
clone.work_list.show_items(works);
clone.stack.set_visible_child_name("content");
@ -106,7 +102,7 @@ impl RecordingSelectorPersonScreen {
}
/// Sets a closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -14,14 +14,14 @@ pub struct RecordingSelectorWorkScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
recording_list: Rc<List<Recording>>,
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingSelectorWorkScreen {
/// Create a new recording selector work screen.
pub fn new(backend: Rc<Backend>, work: WorkDescription) -> Rc<Self> {
pub fn new(backend: Rc<Backend>, work: Work) -> Rc<Self> {
// Create UI
let builder =
@ -56,23 +56,24 @@ impl RecordingSelectorWorkScreen {
}
}));
this.recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
work_label.set_halign(gtk::Align::Start);
this.recording_list
.set_make_widget(|recording: &Recording| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
work_label.set_halign(gtk::Align::Start);
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
performers_label.set_ellipsize(pango::EllipsizeMode::End);
performers_label.set_opacity(0.5);
performers_label.set_halign(gtk::Align::Start);
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
performers_label.set_ellipsize(pango::EllipsizeMode::End);
performers_label.set_opacity(0.5);
performers_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&work_label);
vbox.add(&performers_label);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&work_label);
vbox.add(&performers_label);
vbox.upcast()
});
vbox.upcast()
});
this.recording_list
.set_selected(clone!(@strong this => move |recording| {
@ -88,6 +89,7 @@ impl RecordingSelectorWorkScreen {
context.spawn_local(async move {
let recordings = clone
.backend
.db()
.get_recordings_for_work(work.id)
.await
.unwrap();
@ -100,7 +102,7 @@ impl RecordingSelectorWorkScreen {
}
/// Sets a closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Recording) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -11,10 +11,10 @@ pub struct TrackEditor {
}
impl TrackEditor {
pub fn new<W, F>(parent: &W, track: TrackDescription, work: WorkDescription, callback: F) -> Self
pub fn new<W, F>(parent: &W, track: Track, work: Work, callback: F) -> Self
where
W: IsA<gtk::Window>,
F: Fn(TrackDescription) -> () + 'static,
F: Fn(Track) -> () + 'static,
{
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
@ -37,7 +37,7 @@ impl TrackEditor {
let mut work_parts = work_parts.borrow_mut();
work_parts.sort();
callback(TrackDescription {
callback(Track {
work_parts: work_parts.clone(),
file_name: file_name.clone(),
});

View file

@ -18,9 +18,9 @@ pub struct TracksEditor {
recording_stack: gtk::Stack,
work_label: gtk::Label,
performers_label: gtk::Label,
track_list: Rc<List<TrackDescription>>,
recording: RefCell<Option<RecordingDescription>>,
tracks: RefCell<Vec<TrackDescription>>,
track_list: Rc<List<Track>>,
recording: RefCell<Option<Recording>>,
tracks: RefCell<Vec<Track>>,
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
}
@ -30,8 +30,8 @@ impl TracksEditor {
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
recording: Option<RecordingDescription>,
tracks: Vec<TrackDescription>,
recording: Option<Recording>,
tracks: Vec<Track>,
) -> Rc<Self> {
// UI setup
@ -80,8 +80,8 @@ impl TracksEditor {
let context = glib::MainContext::default();
let this = this.clone();
context.spawn_local(async move {
this.backend.update_tracks(
this.recording.borrow().as_ref().unwrap().id,
this.backend.db().update_tracks(
this.recording.borrow().as_ref().unwrap().id as u32,
this.tracks.borrow().clone(),
).await.unwrap();
@ -135,7 +135,7 @@ impl TracksEditor {
let mut tracks = this.tracks.borrow_mut();
for file_name in dialog.get_filenames() {
let file_name = file_name.strip_prefix(&music_library_path).unwrap();
tracks.insert(index, TrackDescription {
tracks.insert(index, Track {
work_parts: Vec::new(),
file_name: String::from(file_name.to_str().unwrap()),
});
@ -224,7 +224,7 @@ impl TracksEditor {
}
/// Create a widget representing a track.
fn build_track_row(&self, track: &TrackDescription) -> gtk::Widget {
fn build_track_row(&self, track: &Track) -> gtk::Widget {
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
if let Some(recording) = &*self.recording.borrow() {
@ -256,7 +256,7 @@ impl TracksEditor {
}
/// Set everything up after selecting a recording.
fn recording_selected(&self, recording: &RecordingDescription) {
fn recording_selected(&self, recording: &Recording) {
self.work_label.set_text(&recording.work.get_title());
self.performers_label.set_text(&recording.get_performers());
self.recording_stack.set_visible_child_name("selected");

View file

@ -1,7 +1,6 @@
use crate::backend::*;
use crate::database::*;
use crate::dialogs::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
@ -16,10 +15,8 @@ pub struct PartEditor {
title_entry: gtk::Entry,
composer_label: gtk::Label,
reset_composer_button: gtk::Button,
instrument_list: Rc<List<Instrument>>,
composer: RefCell<Option<Person>>,
instruments: RefCell<Vec<Instrument>>,
ready_cb: RefCell<Option<Box<dyn Fn(WorkPartDescription) -> ()>>>,
ready_cb: RefCell<Option<Box<dyn Fn(WorkPart) -> ()>>>,
}
impl PartEditor {
@ -27,7 +24,7 @@ impl PartEditor {
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
part: Option<WorkPartDescription>,
part: Option<WorkPart>,
) -> Rc<Self> {
// Create UI
@ -40,21 +37,15 @@ impl PartEditor {
get_widget!(builder, gtk::Button, composer_button);
get_widget!(builder, gtk::Label, composer_label);
get_widget!(builder, gtk::Button, reset_composer_button);
get_widget!(builder, gtk::ScrolledWindow, scroll);
get_widget!(builder, gtk::Button, add_instrument_button);
get_widget!(builder, gtk::Button, remove_instrument_button);
window.set_transient_for(Some(parent));
let instrument_list = List::new(&gettext("No instruments added."));
scroll.add(&instrument_list.widget);
let (composer, instruments) = match part {
let composer = match part {
Some(part) => {
title_entry.set_text(&part.title);
(part.composer, part.instruments)
part.composer
}
None => (None, Vec::new()),
None => None,
};
let this = Rc::new(Self {
@ -63,9 +54,7 @@ impl PartEditor {
title_entry,
composer_label,
reset_composer_button,
instrument_list,
composer: RefCell::new(composer),
instruments: RefCell::new(instruments),
ready_cb: RefCell::new(None),
});
@ -77,10 +66,9 @@ impl PartEditor {
save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.ready_cb.borrow() {
cb(WorkPartDescription {
cb(WorkPart {
title: this.title_entry.get_text().to_string(),
composer: this.composer.borrow().clone(),
instruments: this.instruments.borrow().clone(),
});
}
@ -100,55 +88,17 @@ impl PartEditor {
this.show_composer(None);
}));
this.instrument_list.set_make_widget(|instrument| {
let label = gtk::Label::new(Some(&instrument.name));
label.set_ellipsize(pango::EllipsizeMode::End);
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()
});
add_instrument_button.connect_clicked(clone!(@strong this => move |_| {
InstrumentSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |instrument| {
let mut instruments = this.instruments.borrow_mut();
let index = match this.instrument_list.get_selected_index() {
Some(index) => index + 1,
None => instruments.len(),
};
instruments.insert(index, instrument);
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
})).show();
}));
remove_instrument_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.instrument_list.get_selected_index() {
let mut instruments = this.instruments.borrow_mut();
instruments.remove(index);
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
}
}));
// Initialize
if let Some(composer) = &*this.composer.borrow() {
this.show_composer(Some(composer));
}
this.instrument_list
.show_items(this.instruments.borrow().clone());
this
}
/// Set the closure to be called when the user wants to save the part.
pub fn set_ready_cb<F: Fn(WorkPartDescription) -> () + 'static>(&self, cb: F) {
pub fn set_ready_cb<F: Fn(WorkPart) -> () + 'static>(&self, cb: F) {
self.ready_cb.replace(Some(Box::new(cb)));
}

View file

@ -9,15 +9,12 @@ use std::rc::Rc;
pub struct SectionEditor {
window: libhandy::Window,
title_entry: gtk::Entry,
ready_cb: RefCell<Option<Box<dyn Fn(WorkSectionDescription) -> ()>>>,
ready_cb: RefCell<Option<Box<dyn Fn(WorkSection) -> ()>>>,
}
impl SectionEditor {
/// Create a new section editor and optionally initialize it.
pub fn new<P: IsA<gtk::Window>>(
parent: &P,
section: Option<WorkSectionDescription>,
) -> Rc<Self> {
pub fn new<P: IsA<gtk::Window>>(parent: &P, section: Option<WorkSection>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section_editor.ui");
@ -47,7 +44,7 @@ impl SectionEditor {
save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.ready_cb.borrow() {
cb(WorkSectionDescription {
cb(WorkSection {
before_index: 0,
title: this.title_entry.get_text().to_string(),
});
@ -62,7 +59,7 @@ impl SectionEditor {
/// Set the closure to be called when the user wants to save the section. Note that the
/// resulting object will always have `before_index` set to 0. The caller is expected to
/// change that later before adding the section to the database.
pub fn set_ready_cb<F: Fn(WorkSectionDescription) -> () + 'static>(&self, cb: F) {
pub fn set_ready_cb<F: Fn(WorkSection) -> () + 'static>(&self, cb: F) {
self.ready_cb.replace(Some(Box::new(cb)));
}

View file

@ -13,7 +13,7 @@ pub struct WorkDialog {
stack: gtk::Stack,
selector: Rc<WorkSelector>,
editor: Rc<WorkEditor>,
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
}
impl WorkDialog {
@ -75,7 +75,7 @@ impl WorkDialog {
}
/// Set the closure to be called when the user has selected or created a work.
pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}

View file

@ -15,8 +15,8 @@ use std::rc::Rc;
/// Either a work part or a work section.
#[derive(Clone)]
enum PartOrSection {
Part(WorkPartDescription),
Section(WorkSectionDescription),
Part(WorkPart),
Section(WorkSection),
}
/// A widget for editing and creating works.
@ -29,12 +29,12 @@ pub struct WorkEditor {
composer_label: gtk::Label,
instrument_list: Rc<List<Instrument>>,
part_list: Rc<List<PartOrSection>>,
id: i64,
id: u32,
composer: RefCell<Option<Person>>,
instruments: RefCell<Vec<Instrument>>,
structure: RefCell<Vec<PartOrSection>>,
cancel_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
saved_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
saved_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
}
impl WorkEditor {
@ -43,7 +43,7 @@ impl WorkEditor {
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
work: Option<WorkDescription>,
work: Option<Work>,
) -> Rc<Self> {
// Create UI
@ -120,7 +120,7 @@ impl WorkEditor {
}));
this.save_button.connect_clicked(clone!(@strong this => move |_| {
let mut section_count = 0;
let mut section_count: usize = 0;
let mut parts = Vec::new();
let mut sections = Vec::new();
@ -129,7 +129,6 @@ impl WorkEditor {
PartOrSection::Part(part) => parts.push(part.clone()),
PartOrSection::Section(section) => {
let mut section = section.clone();
let index: i64 = index.try_into().unwrap();
section.before_index = index - section_count;
sections.push(section);
section_count += 1;
@ -137,7 +136,7 @@ impl WorkEditor {
}
}
let work = WorkDescription {
let work = Work {
id: this.id,
title: this.title_entry.get_text().to_string(),
composer: this.composer.borrow().clone().expect("Tried to create work without composer!"),
@ -149,7 +148,7 @@ impl WorkEditor {
let c = glib::MainContext::default();
let clone = this.clone();
c.spawn_local(async move {
clone.backend.update_work(work.clone().into()).await.unwrap();
clone.backend.db().update_work(work.clone().into()).await.unwrap();
if let Some(cb) = &*clone.saved_cb.borrow() {
cb(work);
}
@ -333,7 +332,8 @@ impl WorkEditor {
this.show_composer(composer);
}
this.instrument_list.show_items(this.instruments.borrow().clone());
this.instrument_list
.show_items(this.instruments.borrow().clone());
this.part_list.show_items(this.structure.borrow().clone());
this
@ -345,7 +345,7 @@ impl WorkEditor {
}
/// The closure to call when a work was created.
pub fn set_saved_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
pub fn set_saved_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}

View file

@ -9,7 +9,7 @@ use std::rc::Rc;
/// A dialog for creating or editing a work.
pub struct WorkEditorDialog {
pub window: libhandy::Window,
saved_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
saved_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
}
impl WorkEditorDialog {
@ -17,7 +17,7 @@ impl WorkEditorDialog {
pub fn new<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
work: Option<WorkDescription>,
work: Option<Work>,
) -> Rc<Self> {
// Create UI
@ -52,7 +52,7 @@ impl WorkEditorDialog {
}
/// Set the closure to be called when the user edited or created a work.
pub fn set_saved_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
pub fn set_saved_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}

View file

@ -14,7 +14,7 @@ pub struct WorkSelector {
pub widget: libhandy::Leaflet,
backend: Rc<Backend>,
sidebar_box: gtk::Box,
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
navigator: Rc<Navigator>,
}
@ -83,7 +83,7 @@ impl WorkSelector {
}
/// Set the closure to be called when the user has selected a work.
pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -14,8 +14,8 @@ pub struct WorkSelectorPersonScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
work_list: Rc<List<Work>>,
selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
@ -54,7 +54,7 @@ impl WorkSelectorPersonScreen {
}
}));
this.work_list.set_make_widget(|work: &WorkDescription| {
this.work_list.set_make_widget(|work: &Work| {
let label = gtk::Label::new(Some(&work.title));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
@ -80,11 +80,7 @@ impl WorkSelectorPersonScreen {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.await
.unwrap();
let works = clone.backend.db().get_works(person.id).await.unwrap();
clone.work_list.show_items(works);
clone.stack.set_visible_child_name("content");
@ -94,7 +90,7 @@ impl WorkSelectorPersonScreen {
}
/// Sets a closure to be called when the user has selected a work.
pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
pub fn set_selected_cb<F: Fn(Work) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -33,15 +33,19 @@ run_command(
)
sources = files(
'backend/backend.rs',
'backend/client.rs',
'backend/library.rs',
'backend/mod.rs',
'backend/secure.rs',
'database/database.rs',
'database/ensembles.rs',
'database/instruments.rs',
'database/mod.rs',
'database/models.rs',
'database/persons.rs',
'database/recordings.rs',
'database/schema.rs',
'database/tables.rs',
'database/thread.rs',
'database/tracks.rs',
'database/works.rs',
'dialogs/about.rs',
'dialogs/ensemble_editor.rs',
'dialogs/ensemble_selector.rs',

View file

@ -8,8 +8,8 @@ use std::rc::Rc;
#[derive(Clone)]
pub struct PlaylistItem {
pub recording: RecordingDescription,
pub tracks: Vec<TrackDescription>,
pub recording: Recording,
pub tracks: Vec<Track>,
}
pub struct Player {

View file

@ -14,7 +14,7 @@ pub struct EnsembleScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
recording_list: Rc<List<Recording>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
@ -52,7 +52,7 @@ impl EnsembleScreen {
let recording_list = List::new(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &RecordingDescription| {
recording_list.set_make_widget(|recording: &Recording| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
@ -72,7 +72,7 @@ impl EnsembleScreen {
});
recording_list.set_filter(
clone!(@strong search_entry => move |recording: &RecordingDescription| {
clone!(@strong search_entry => move |recording: &Recording| {
let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search)
@ -114,7 +114,8 @@ impl EnsembleScreen {
context.spawn_local(async move {
let recordings = clone
.backend
.get_recordings_for_ensemble(ensemble.id)
.db()
.get_recordings_for_ensemble(ensemble.id as u32)
.await
.unwrap();

View file

@ -14,8 +14,8 @@ pub struct PersonScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
recording_list: Rc<List<RecordingDescription>>,
work_list: Rc<List<Work>>,
recording_list: Rc<List<Recording>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
@ -56,7 +56,7 @@ impl PersonScreen {
let work_list = List::new(&gettext("No works found."));
work_list.set_make_widget(|work: &WorkDescription| {
work_list.set_make_widget(|work: &Work| {
let label = gtk::Label::new(Some(&work.title));
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
@ -66,17 +66,15 @@ impl PersonScreen {
label.upcast()
});
work_list.set_filter(
clone!(@strong search_entry => move |work: &WorkDescription| {
let search = search_entry.get_text().to_string().to_lowercase();
let title = work.title.to_lowercase();
search.is_empty() || title.contains(&search)
}),
);
work_list.set_filter(clone!(@strong search_entry => move |work: &Work| {
let search = search_entry.get_text().to_string().to_lowercase();
let title = work.title.to_lowercase();
search.is_empty() || title.contains(&search)
}));
let recording_list = List::new(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &RecordingDescription| {
recording_list.set_make_widget(|recording: &Recording| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
@ -96,7 +94,7 @@ impl PersonScreen {
});
recording_list.set_filter(
clone!(@strong search_entry => move |recording: &RecordingDescription| {
clone!(@strong search_entry => move |recording: &Recording| {
let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search)
@ -152,12 +150,14 @@ impl PersonScreen {
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.db()
.get_works(person.id as u32)
.await
.unwrap();
let recordings = clone
.backend
.get_recordings_for_person(person.id)
.db()
.get_recordings_for_person(person.id as u32)
.await
.unwrap();

View file

@ -14,12 +14,12 @@ pub struct RecordingScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
tracks: RefCell<Vec<TrackDescription>>,
tracks: RefCell<Vec<Track>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingScreen {
pub fn new(backend: Rc<Backend>, recording: RecordingDescription) -> Rc<Self> {
pub fn new(backend: Rc<Backend>, recording: Recording) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui");
get_widget!(builder, gtk::Box, widget);
@ -69,7 +69,7 @@ impl RecordingScreen {
let list = List::new(&gettext("No tracks found."));
list.set_make_widget(
clone!(@strong recording => move |track: &TrackDescription| {
clone!(@strong recording => move |track: &Track| {
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
title_parts.push(recording.work.parts[*part].title.clone());
@ -131,7 +131,7 @@ impl RecordingScreen {
let clone = result.clone();
let id = recording.id;
context.spawn_local(async move {
let tracks = clone.backend.get_tracks(id).await.unwrap();
let tracks = clone.backend.db().get_tracks(id as u32).await.unwrap();
list.show_items(tracks.clone());
clone.stack.set_visible_child_name("content");
clone.tracks.replace(tracks);

View file

@ -14,12 +14,12 @@ pub struct WorkScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
recording_list: Rc<List<Recording>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl WorkScreen {
pub fn new(backend: Rc<Backend>, work: WorkDescription) -> Rc<Self> {
pub fn new(backend: Rc<Backend>, work: Work) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui");
get_widget!(builder, gtk::Box, widget);
@ -53,7 +53,7 @@ impl WorkScreen {
let recording_list = List::new(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &RecordingDescription| {
recording_list.set_make_widget(|recording: &Recording| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
@ -72,7 +72,7 @@ impl WorkScreen {
vbox.upcast()
});
recording_list.set_filter(clone!(@strong search_entry => move |recording: &RecordingDescription| {
recording_list.set_filter(clone!(@strong search_entry => move |recording: &Recording| {
let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase();
search.is_empty() || text.contains(&search)
@ -113,7 +113,8 @@ impl WorkScreen {
context.spawn_local(async move {
let recordings = clone
.backend
.get_recordings_for_work(work.id)
.db()
.get_recordings_for_work(work.id as u32)
.await
.unwrap();

View file

@ -74,7 +74,7 @@ impl PersonList {
let list = self.list.clone();
context.spawn_local(async move {
let persons = backend.get_persons().await.unwrap();
let persons = backend.db().get_persons().await.unwrap();
list.show_items(persons);
self.stack.set_visible_child_name("content");
});

View file

@ -89,8 +89,8 @@ impl PoeList {
let list = self.list.clone();
context.spawn_local(async move {
let persons = backend.get_persons().await.unwrap();
let ensembles = backend.get_ensembles().await.unwrap();
let persons = backend.db().get_persons().await.unwrap();
let ensembles = backend.db().get_ensembles().await.unwrap();
let mut poes: Vec<PersonOrEnsemble> = Vec::new();
for person in persons {

View file

@ -37,7 +37,6 @@ impl Window {
get_widget!(builder, gtk::Box, empty_screen);
let backend = Rc::new(Backend::new());
backend.clone().init();
let player_screen = PlayerScreen::new();
stack.add_named(&player_screen.widget, "player_screen");
@ -122,252 +121,6 @@ impl Window {
})
);
action!(
result.window,
"add-person",
clone!(@strong result => move |_, _| {
PersonEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |_| {
result.reload();
})).show();
})
);
action!(
result.window,
"add-instrument",
clone!(@strong result => move |_, _| {
InstrumentEditor::new(result.backend.clone(), &result.window, None, |instrument| {
println!("{:?}", instrument);
}).show();
})
);
action!(
result.window,
"add-work",
clone!(@strong result => move |_, _| {
let dialog = WorkDialog::new(result.backend.clone(), &result.window);
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
})
);
action!(
result.window,
"add-ensemble",
clone!(@strong result => move |_, _| {
EnsembleEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |_| {
result.reload();
})).show();
})
);
action!(
result.window,
"add-recording",
clone!(@strong result => move |_, _| {
let dialog = RecordingDialog::new(result.backend.clone(), &result.window);
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
})
);
action!(
result.window,
"add-tracks",
clone!(@strong result => move |_, _| {
let editor = TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new());
editor.set_callback(clone!(@strong result => move || {
result.reload();
}));
editor.show();
})
);
action!(
result.window,
"edit-person",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let person = result.backend.get_person(id).await.unwrap();
PersonEditor::new(result.backend.clone(), &result.window, Some(person), clone!(@strong result => move |_| {
result.reload();
})).show();
});
})
);
action!(
result.window,
"delete-person",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_person(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-ensemble",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let ensemble = result.backend.get_ensemble(id).await.unwrap();
EnsembleEditor::new(result.backend.clone(), &result.window, Some(ensemble), clone!(@strong result => move |_| {
result.reload();
})).show();
});
})
);
action!(
result.window,
"delete-ensemble",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_ensemble(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-work",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let work = result.backend.get_work_description(id).await.unwrap();
let dialog = WorkEditorDialog::new(result.backend.clone(), &result.window, Some(work));
dialog.set_saved_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
});
})
);
action!(
result.window,
"delete-work",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_work(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-recording",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let recording = result.backend.get_recording_description(id).await.unwrap();
let dialog = RecordingEditorDialog::new(result.backend.clone(), &result.window, Some(recording));
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
});
})
);
action!(
result.window,
"delete-recording",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_recording(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-tracks",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let recording = result.backend.get_recording_description(id).await.unwrap();
let tracks = result.backend.get_tracks(id).await.unwrap();
let editor = TracksEditor::new(result.backend.clone(), &result.window, Some(recording), tracks);
editor.set_callback(clone!(@strong result => move || {
result.reload();
}));
editor.show();
});
})
);
action!(
result.window,
"delete-tracks",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_tracks(id).await.unwrap();
result.reload();
});
})
);
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
@ -393,6 +146,13 @@ impl Window {
}
});
let clone = result.clone();
context.spawn_local(async move {
// This is not done in the async block below, because backend state changes may happen
// while this method is running.
clone.backend.clone().init().await.unwrap();
});
result.leaflet.add(&result.navigator.widget);
result