mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Restructure backend and database
This commit is contained in:
parent
d0c25531d3
commit
a93c7276d2
49 changed files with 1705 additions and 1920 deletions
|
|
@ -8,8 +8,6 @@ DROP TABLE instrumentations;
|
||||||
|
|
||||||
DROP TABLE work_parts;
|
DROP TABLE work_parts;
|
||||||
|
|
||||||
DROP TABLE part_instrumentations;
|
|
||||||
|
|
||||||
DROP TABLE work_sections;
|
DROP TABLE work_sections;
|
||||||
|
|
||||||
DROP TABLE ensembles;
|
DROP TABLE ensembles;
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,15 @@ CREATE TABLE works (
|
||||||
CREATE TABLE instrumentations (
|
CREATE TABLE instrumentations (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
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 (
|
CREATE TABLE work_parts (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
||||||
part_index BIGINT NOT NULL,
|
part_index BIGINT NOT NULL,
|
||||||
composer BIGINT REFERENCES persons(id),
|
title TEXT NOT NULL,
|
||||||
title TEXT NOT NULL
|
composer BIGINT REFERENCES persons(id)
|
||||||
);
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE work_sections (
|
CREATE TABLE work_sections (
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@
|
||||||
<object class="HdyWindow" id="window">
|
<object class="HdyWindow" id="window">
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
<property name="default-width">450</property>
|
<property name="default-width">350</property>
|
||||||
<property name="default-height">300</property>
|
|
||||||
<property name="destroy-with-parent">True</property>
|
<property name="destroy-with-parent">True</property>
|
||||||
<property name="type-hint">dialog</property>
|
<property name="type-hint">dialog</property>
|
||||||
<child>
|
<child>
|
||||||
|
|
@ -51,182 +50,84 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkNotebook">
|
<!-- n-columns=2 n-rows=2 -->
|
||||||
|
<object class="GtkGrid">
|
||||||
<property name="visible">True</property>
|
<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>
|
<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">
|
<object class="GtkLabel">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</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>
|
</object>
|
||||||
<packing>
|
<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>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="border-width">18</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="spacing">6</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="scroll">
|
<object class="GtkButton" id="composer_button">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">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>
|
<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>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="position">0</property>
|
<property name="position">0</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkButton" id="reset_composer_button">
|
||||||
<property name="visible">True</property>
|
<property name="can-focus">True</property>
|
||||||
<property name="can-focus">False</property>
|
<property name="receives-default">True</property>
|
||||||
<property name="border-width">0</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">6</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="add_instrument_button">
|
<object class="GtkImage">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can-focus">True</property>
|
<property name="can-focus">False</property>
|
||||||
<property name="receives-default">True</property>
|
<property name="icon-name">user-trash-symbolic</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>
|
|
||||||
</object>
|
</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>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
|
|
@ -235,25 +136,18 @@
|
||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<style>
|
||||||
|
<class name="linked"/>
|
||||||
|
</style>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="position">1</property>
|
<property name="left-attach">1</property>
|
||||||
</packing>
|
<property name="top-attach">1</property>
|
||||||
</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>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,69 @@
|
||||||
|
use super::secure;
|
||||||
use super::Backend;
|
use super::Backend;
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use gio::prelude::*;
|
||||||
use isahc::http::StatusCode;
|
use isahc::http::StatusCode;
|
||||||
use isahc::prelude::*;
|
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 {
|
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.
|
/// Try to login a user with the provided credentials and return, wether the login suceeded.
|
||||||
pub async fn login(&self) -> Result<bool> {
|
pub async fn login(&self) -> Result<bool> {
|
||||||
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
|
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
|
||||||
|
|
|
||||||
73
musicus/src/backend/library.rs
Normal file
73
musicus/src/backend/library.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,76 @@
|
||||||
pub mod backend;
|
use crate::database::DbThread;
|
||||||
pub use backend::*;
|
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 mod client;
|
||||||
pub use client::*;
|
pub use client::*;
|
||||||
|
|
||||||
|
pub mod library;
|
||||||
|
pub use library::*;
|
||||||
|
|
||||||
mod secure;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
96
musicus/src/database/ensembles.rs
Normal file
96
musicus/src/database/ensembles.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
96
musicus/src/database/instruments.rs
Normal file
96
musicus/src/database/instruments.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,51 @@
|
||||||
pub mod database;
|
use anyhow::Result;
|
||||||
pub use database::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
pub mod models;
|
pub mod ensembles;
|
||||||
pub use models::*;
|
pub use ensembles::*;
|
||||||
|
|
||||||
pub mod schema;
|
pub mod instruments;
|
||||||
|
pub use instruments::*;
|
||||||
|
|
||||||
pub mod tables;
|
pub mod persons;
|
||||||
pub use tables::*;
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
111
musicus/src/database/persons.rs
Normal file
111
musicus/src/database/persons.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
252
musicus/src/database/recordings.rs
Normal file
252
musicus/src/database/recordings.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,14 +20,6 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table! {
|
|
||||||
part_instrumentations (id) {
|
|
||||||
id -> BigInt,
|
|
||||||
work_part -> BigInt,
|
|
||||||
instrument -> BigInt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
performances (id) {
|
performances (id) {
|
||||||
id -> BigInt,
|
id -> BigInt,
|
||||||
|
|
@ -69,8 +61,8 @@ table! {
|
||||||
id -> BigInt,
|
id -> BigInt,
|
||||||
work -> BigInt,
|
work -> BigInt,
|
||||||
part_index -> BigInt,
|
part_index -> BigInt,
|
||||||
composer -> Nullable<BigInt>,
|
|
||||||
title -> Text,
|
title -> Text,
|
||||||
|
composer -> Nullable<BigInt>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,8 +85,6 @@ table! {
|
||||||
|
|
||||||
joinable!(instrumentations -> instruments (instrument));
|
joinable!(instrumentations -> instruments (instrument));
|
||||||
joinable!(instrumentations -> works (work));
|
joinable!(instrumentations -> works (work));
|
||||||
joinable!(part_instrumentations -> instruments (instrument));
|
|
||||||
joinable!(part_instrumentations -> works (work_part));
|
|
||||||
joinable!(performances -> ensembles (ensemble));
|
joinable!(performances -> ensembles (ensemble));
|
||||||
joinable!(performances -> instruments (role));
|
joinable!(performances -> instruments (role));
|
||||||
joinable!(performances -> persons (person));
|
joinable!(performances -> persons (person));
|
||||||
|
|
@ -110,7 +100,6 @@ allow_tables_to_appear_in_same_query!(
|
||||||
ensembles,
|
ensembles,
|
||||||
instrumentations,
|
instrumentations,
|
||||||
instruments,
|
instruments,
|
||||||
part_instrumentations,
|
|
||||||
performances,
|
performances,
|
||||||
persons,
|
persons,
|
||||||
recordings,
|
recordings,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
327
musicus/src/database/thread.rs
Normal file
327
musicus/src/database/thread.rs
Normal 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?)
|
||||||
|
}
|
||||||
|
}
|
||||||
94
musicus/src/database/tracks.rs
Normal file
94
musicus/src/database/tracks.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
262
musicus/src/database/works.rs
Normal file
262
musicus/src/database/works.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ where
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
window: libhandy::Window,
|
window: libhandy::Window,
|
||||||
callback: F,
|
callback: F,
|
||||||
id: i64,
|
id: u32,
|
||||||
name_entry: gtk::Entry,
|
name_entry: gtk::Entry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,8 +26,7 @@ where
|
||||||
ensemble: Option<Ensemble>,
|
ensemble: Option<Ensemble>,
|
||||||
callback: F,
|
callback: F,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
let builder =
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui");
|
||||||
gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui");
|
|
||||||
|
|
||||||
get_widget!(builder, libhandy::Window, window);
|
get_widget!(builder, libhandy::Window, window);
|
||||||
get_widget!(builder, gtk::Button, cancel_button);
|
get_widget!(builder, gtk::Button, cancel_button);
|
||||||
|
|
@ -63,7 +62,7 @@ where
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
let c = glib::MainContext::default();
|
let c = glib::MainContext::default();
|
||||||
c.spawn_local(async move {
|
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.window.close();
|
||||||
(clone.callback)(ensemble.clone());
|
(clone.callback)(ensemble.clone());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@ where
|
||||||
F: Fn(Ensemble) -> () + 'static,
|
F: Fn(Ensemble) -> () + 'static,
|
||||||
{
|
{
|
||||||
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
|
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
|
||||||
let builder =
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui");
|
||||||
gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui");
|
|
||||||
|
|
||||||
get_widget!(builder, libhandy::Window, window);
|
get_widget!(builder, libhandy::Window, window);
|
||||||
get_widget!(builder, gtk::Button, add_button);
|
get_widget!(builder, gtk::Button, add_button);
|
||||||
|
|
@ -44,7 +43,7 @@ where
|
||||||
let c = glib::MainContext::default();
|
let c = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
c.spawn_local(async move {
|
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() {
|
for (index, ensemble) in ensembles.iter().enumerate() {
|
||||||
let label = gtk::Label::new(Some(&ensemble.name));
|
let label = gtk::Label::new(Some(&ensemble.name));
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ where
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
window: libhandy::Window,
|
window: libhandy::Window,
|
||||||
callback: F,
|
callback: F,
|
||||||
id: i64,
|
id: u32,
|
||||||
name_entry: gtk::Entry,
|
name_entry: gtk::Entry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ where
|
||||||
let c = glib::MainContext::default();
|
let c = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
c.spawn_local(async move {
|
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.window.close();
|
||||||
(clone.callback)(instrument.clone());
|
(clone.callback)(instrument.clone());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@ where
|
||||||
F: Fn(Instrument) -> () + 'static,
|
F: Fn(Instrument) -> () + 'static,
|
||||||
{
|
{
|
||||||
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
|
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
|
||||||
let builder =
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui");
|
||||||
gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui");
|
|
||||||
|
|
||||||
get_widget!(builder, libhandy::Window, window);
|
get_widget!(builder, libhandy::Window, window);
|
||||||
get_widget!(builder, gtk::Button, add_button);
|
get_widget!(builder, gtk::Button, add_button);
|
||||||
|
|
@ -44,7 +43,7 @@ where
|
||||||
let c = glib::MainContext::default();
|
let c = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
c.spawn_local(async move {
|
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() {
|
for (index, instrument) in instruments.iter().enumerate() {
|
||||||
let label = gtk::Label::new(Some(&instrument.name));
|
let label = gtk::Label::new(Some(&instrument.name));
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ where
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
window: libhandy::Window,
|
window: libhandy::Window,
|
||||||
callback: F,
|
callback: F,
|
||||||
id: i64,
|
id: u32,
|
||||||
first_name_entry: gtk::Entry,
|
first_name_entry: gtk::Entry,
|
||||||
last_name_entry: gtk::Entry,
|
last_name_entry: gtk::Entry,
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ where
|
||||||
let c = glib::MainContext::default();
|
let c = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
c.spawn_local(async move {
|
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.window.close();
|
||||||
(clone.callback)(person.clone());
|
(clone.callback)(person.clone());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ pub struct PerformanceEditor {
|
||||||
person: RefCell<Option<Person>>,
|
person: RefCell<Option<Person>>,
|
||||||
ensemble: RefCell<Option<Ensemble>>,
|
ensemble: RefCell<Option<Ensemble>>,
|
||||||
role: RefCell<Option<Instrument>>,
|
role: RefCell<Option<Instrument>>,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(PerformanceDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Performance) -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerformanceEditor {
|
impl PerformanceEditor {
|
||||||
|
|
@ -28,7 +28,7 @@ impl PerformanceEditor {
|
||||||
pub fn new<P: IsA<gtk::Window>>(
|
pub fn new<P: IsA<gtk::Window>>(
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
parent: &P,
|
parent: &P,
|
||||||
performance: Option<PerformanceDescription>,
|
performance: Option<Performance>,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ impl PerformanceEditor {
|
||||||
this.save_button
|
this.save_button
|
||||||
.connect_clicked(clone!(@strong this => move |_| {
|
.connect_clicked(clone!(@strong this => move |_| {
|
||||||
if let Some(cb) = &*this.selected_cb.borrow() {
|
if let Some(cb) = &*this.selected_cb.borrow() {
|
||||||
cb(PerformanceDescription {
|
cb(Performance {
|
||||||
person: this.person.borrow().clone(),
|
person: this.person.borrow().clone(),
|
||||||
ensemble: this.ensemble.borrow().clone(),
|
ensemble: this.ensemble.borrow().clone(),
|
||||||
role: this.role.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.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct RecordingDialog {
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
selector: Rc<RecordingSelector>,
|
selector: Rc<RecordingSelector>,
|
||||||
editor: Rc<RecordingEditor>,
|
editor: Rc<RecordingEditor>,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordingDialog {
|
impl RecordingDialog {
|
||||||
|
|
@ -75,7 +75,7 @@ impl RecordingDialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called when the user has selected or created a recording.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ pub struct RecordingEditor {
|
||||||
save_button: gtk::Button,
|
save_button: gtk::Button,
|
||||||
work_label: gtk::Label,
|
work_label: gtk::Label,
|
||||||
comment_entry: gtk::Entry,
|
comment_entry: gtk::Entry,
|
||||||
performance_list: Rc<List<PerformanceDescription>>,
|
performance_list: Rc<List<Performance>>,
|
||||||
id: i64,
|
id: u32,
|
||||||
work: RefCell<Option<WorkDescription>>,
|
work: RefCell<Option<Work>>,
|
||||||
performances: RefCell<Vec<PerformanceDescription>>,
|
performances: RefCell<Vec<Performance>>,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
|
||||||
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ impl RecordingEditor {
|
||||||
pub fn new<W: IsA<gtk::Window>>(
|
pub fn new<W: IsA<gtk::Window>>(
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
parent: &W,
|
parent: &W,
|
||||||
recording: Option<RecordingDescription>,
|
recording: Option<Recording>,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ impl RecordingEditor {
|
||||||
|
|
||||||
this.save_button
|
this.save_button
|
||||||
.connect_clicked(clone!(@strong this => move |_| {
|
.connect_clicked(clone!(@strong this => move |_| {
|
||||||
let recording = RecordingDescription {
|
let recording = Recording {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
work: this.work.borrow().clone().expect("Tried to create recording without work!"),
|
work: this.work.borrow().clone().expect("Tried to create recording without work!"),
|
||||||
comment: this.comment_entry.get_text().to_string(),
|
comment: this.comment_entry.get_text().to_string(),
|
||||||
|
|
@ -97,7 +97,7 @@ impl RecordingEditor {
|
||||||
let c = glib::MainContext::default();
|
let c = glib::MainContext::default();
|
||||||
let clone = this.clone();
|
let clone = this.clone();
|
||||||
c.spawn_local(async move {
|
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() {
|
if let Some(cb) = &*clone.selected_cb.borrow() {
|
||||||
cb(recording.clone());
|
cb(recording.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -192,12 +192,12 @@ impl RecordingEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called if the recording was created.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the UI according to work.
|
/// 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.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title));
|
||||||
self.save_button.set_sensitive(true);
|
self.save_button.set_sensitive(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use std::rc::Rc;
|
||||||
/// A dialog for creating or editing a recording.
|
/// A dialog for creating or editing a recording.
|
||||||
pub struct RecordingEditorDialog {
|
pub struct RecordingEditorDialog {
|
||||||
pub window: libhandy::Window,
|
pub window: libhandy::Window,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordingEditorDialog {
|
impl RecordingEditorDialog {
|
||||||
|
|
@ -17,7 +17,7 @@ impl RecordingEditorDialog {
|
||||||
pub fn new<W: IsA<gtk::Window>>(
|
pub fn new<W: IsA<gtk::Window>>(
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
parent: &W,
|
parent: &W,
|
||||||
recording: Option<RecordingDescription>,
|
recording: Option<Recording>,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl RecordingEditorDialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called when the user edited or created a recording.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub struct RecordingSelector {
|
||||||
pub widget: libhandy::Leaflet,
|
pub widget: libhandy::Leaflet,
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
sidebar_box: gtk::Box,
|
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() -> ()>>>,
|
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||||
navigator: Rc<Navigator>,
|
navigator: Rc<Navigator>,
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +83,7 @@ impl RecordingSelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called when the user has selected a recording.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ pub struct RecordingSelectorPersonScreen {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
work_list: Rc<List<WorkDescription>>,
|
work_list: Rc<List<Work>>,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
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));
|
let label = gtk::Label::new(Some(&work.title));
|
||||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
label.set_halign(gtk::Align::Start);
|
label.set_halign(gtk::Align::Start);
|
||||||
|
|
@ -92,11 +92,7 @@ impl RecordingSelectorPersonScreen {
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = this.clone();
|
let clone = this.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let works = clone
|
let works = clone.backend.db().get_works(person.id).await.unwrap();
|
||||||
.backend
|
|
||||||
.get_work_descriptions(person.id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
clone.work_list.show_items(works);
|
clone.work_list.show_items(works);
|
||||||
clone.stack.set_visible_child_name("content");
|
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.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ pub struct RecordingSelectorWorkScreen {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
recording_list: Rc<List<RecordingDescription>>,
|
recording_list: Rc<List<Recording>>,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Recording) -> ()>>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordingSelectorWorkScreen {
|
impl RecordingSelectorWorkScreen {
|
||||||
/// Create a new recording selector work screen.
|
/// 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
|
// Create UI
|
||||||
|
|
||||||
let builder =
|
let builder =
|
||||||
|
|
@ -56,23 +56,24 @@ impl RecordingSelectorWorkScreen {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.recording_list.set_make_widget(|recording: &RecordingDescription| {
|
this.recording_list
|
||||||
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
.set_make_widget(|recording: &Recording| {
|
||||||
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
||||||
work_label.set_halign(gtk::Align::Start);
|
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
work_label.set_halign(gtk::Align::Start);
|
||||||
|
|
||||||
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
|
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
|
||||||
performers_label.set_ellipsize(pango::EllipsizeMode::End);
|
performers_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
performers_label.set_opacity(0.5);
|
performers_label.set_opacity(0.5);
|
||||||
performers_label.set_halign(gtk::Align::Start);
|
performers_label.set_halign(gtk::Align::Start);
|
||||||
|
|
||||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
vbox.set_border_width(6);
|
vbox.set_border_width(6);
|
||||||
vbox.add(&work_label);
|
vbox.add(&work_label);
|
||||||
vbox.add(&performers_label);
|
vbox.add(&performers_label);
|
||||||
|
|
||||||
vbox.upcast()
|
vbox.upcast()
|
||||||
});
|
});
|
||||||
|
|
||||||
this.recording_list
|
this.recording_list
|
||||||
.set_selected(clone!(@strong this => move |recording| {
|
.set_selected(clone!(@strong this => move |recording| {
|
||||||
|
|
@ -88,6 +89,7 @@ impl RecordingSelectorWorkScreen {
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let recordings = clone
|
let recordings = clone
|
||||||
.backend
|
.backend
|
||||||
|
.db()
|
||||||
.get_recordings_for_work(work.id)
|
.get_recordings_for_work(work.id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -100,7 +102,7 @@ impl RecordingSelectorWorkScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a closure to be called when the user has selected a recording.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ pub struct TrackEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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
|
where
|
||||||
W: IsA<gtk::Window>,
|
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");
|
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();
|
let mut work_parts = work_parts.borrow_mut();
|
||||||
work_parts.sort();
|
work_parts.sort();
|
||||||
|
|
||||||
callback(TrackDescription {
|
callback(Track {
|
||||||
work_parts: work_parts.clone(),
|
work_parts: work_parts.clone(),
|
||||||
file_name: file_name.clone(),
|
file_name: file_name.clone(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ pub struct TracksEditor {
|
||||||
recording_stack: gtk::Stack,
|
recording_stack: gtk::Stack,
|
||||||
work_label: gtk::Label,
|
work_label: gtk::Label,
|
||||||
performers_label: gtk::Label,
|
performers_label: gtk::Label,
|
||||||
track_list: Rc<List<TrackDescription>>,
|
track_list: Rc<List<Track>>,
|
||||||
recording: RefCell<Option<RecordingDescription>>,
|
recording: RefCell<Option<Recording>>,
|
||||||
tracks: RefCell<Vec<TrackDescription>>,
|
tracks: RefCell<Vec<Track>>,
|
||||||
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,8 +30,8 @@ impl TracksEditor {
|
||||||
pub fn new<P: IsA<gtk::Window>>(
|
pub fn new<P: IsA<gtk::Window>>(
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
parent: &P,
|
parent: &P,
|
||||||
recording: Option<RecordingDescription>,
|
recording: Option<Recording>,
|
||||||
tracks: Vec<TrackDescription>,
|
tracks: Vec<Track>,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
// UI setup
|
// UI setup
|
||||||
|
|
||||||
|
|
@ -80,8 +80,8 @@ impl TracksEditor {
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let this = this.clone();
|
let this = this.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
this.backend.update_tracks(
|
this.backend.db().update_tracks(
|
||||||
this.recording.borrow().as_ref().unwrap().id,
|
this.recording.borrow().as_ref().unwrap().id as u32,
|
||||||
this.tracks.borrow().clone(),
|
this.tracks.borrow().clone(),
|
||||||
).await.unwrap();
|
).await.unwrap();
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ impl TracksEditor {
|
||||||
let mut tracks = this.tracks.borrow_mut();
|
let mut tracks = this.tracks.borrow_mut();
|
||||||
for file_name in dialog.get_filenames() {
|
for file_name in dialog.get_filenames() {
|
||||||
let file_name = file_name.strip_prefix(&music_library_path).unwrap();
|
let file_name = file_name.strip_prefix(&music_library_path).unwrap();
|
||||||
tracks.insert(index, TrackDescription {
|
tracks.insert(index, Track {
|
||||||
work_parts: Vec::new(),
|
work_parts: Vec::new(),
|
||||||
file_name: String::from(file_name.to_str().unwrap()),
|
file_name: String::from(file_name.to_str().unwrap()),
|
||||||
});
|
});
|
||||||
|
|
@ -224,7 +224,7 @@ impl TracksEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a widget representing a track.
|
/// 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();
|
let mut title_parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
if let Some(recording) = &*self.recording.borrow() {
|
if let Some(recording) = &*self.recording.borrow() {
|
||||||
|
|
@ -256,7 +256,7 @@ impl TracksEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set everything up after selecting a recording.
|
/// 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.work_label.set_text(&recording.work.get_title());
|
||||||
self.performers_label.set_text(&recording.get_performers());
|
self.performers_label.set_text(&recording.get_performers());
|
||||||
self.recording_stack.set_visible_child_name("selected");
|
self.recording_stack.set_visible_child_name("selected");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::backend::*;
|
use crate::backend::*;
|
||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::dialogs::*;
|
use crate::dialogs::*;
|
||||||
use crate::widgets::*;
|
|
||||||
use gettextrs::gettext;
|
use gettextrs::gettext;
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
@ -16,10 +15,8 @@ pub struct PartEditor {
|
||||||
title_entry: gtk::Entry,
|
title_entry: gtk::Entry,
|
||||||
composer_label: gtk::Label,
|
composer_label: gtk::Label,
|
||||||
reset_composer_button: gtk::Button,
|
reset_composer_button: gtk::Button,
|
||||||
instrument_list: Rc<List<Instrument>>,
|
|
||||||
composer: RefCell<Option<Person>>,
|
composer: RefCell<Option<Person>>,
|
||||||
instruments: RefCell<Vec<Instrument>>,
|
ready_cb: RefCell<Option<Box<dyn Fn(WorkPart) -> ()>>>,
|
||||||
ready_cb: RefCell<Option<Box<dyn Fn(WorkPartDescription) -> ()>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartEditor {
|
impl PartEditor {
|
||||||
|
|
@ -27,7 +24,7 @@ impl PartEditor {
|
||||||
pub fn new<P: IsA<gtk::Window>>(
|
pub fn new<P: IsA<gtk::Window>>(
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
parent: &P,
|
parent: &P,
|
||||||
part: Option<WorkPartDescription>,
|
part: Option<WorkPart>,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
|
|
@ -40,21 +37,15 @@ impl PartEditor {
|
||||||
get_widget!(builder, gtk::Button, composer_button);
|
get_widget!(builder, gtk::Button, composer_button);
|
||||||
get_widget!(builder, gtk::Label, composer_label);
|
get_widget!(builder, gtk::Label, composer_label);
|
||||||
get_widget!(builder, gtk::Button, reset_composer_button);
|
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));
|
window.set_transient_for(Some(parent));
|
||||||
|
|
||||||
let instrument_list = List::new(&gettext("No instruments added."));
|
let composer = match part {
|
||||||
scroll.add(&instrument_list.widget);
|
|
||||||
|
|
||||||
let (composer, instruments) = match part {
|
|
||||||
Some(part) => {
|
Some(part) => {
|
||||||
title_entry.set_text(&part.title);
|
title_entry.set_text(&part.title);
|
||||||
(part.composer, part.instruments)
|
part.composer
|
||||||
}
|
}
|
||||||
None => (None, Vec::new()),
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let this = Rc::new(Self {
|
let this = Rc::new(Self {
|
||||||
|
|
@ -63,9 +54,7 @@ impl PartEditor {
|
||||||
title_entry,
|
title_entry,
|
||||||
composer_label,
|
composer_label,
|
||||||
reset_composer_button,
|
reset_composer_button,
|
||||||
instrument_list,
|
|
||||||
composer: RefCell::new(composer),
|
composer: RefCell::new(composer),
|
||||||
instruments: RefCell::new(instruments),
|
|
||||||
ready_cb: RefCell::new(None),
|
ready_cb: RefCell::new(None),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -77,10 +66,9 @@ impl PartEditor {
|
||||||
|
|
||||||
save_button.connect_clicked(clone!(@strong this => move |_| {
|
save_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
if let Some(cb) = &*this.ready_cb.borrow() {
|
if let Some(cb) = &*this.ready_cb.borrow() {
|
||||||
cb(WorkPartDescription {
|
cb(WorkPart {
|
||||||
title: this.title_entry.get_text().to_string(),
|
title: this.title_entry.get_text().to_string(),
|
||||||
composer: this.composer.borrow().clone(),
|
composer: this.composer.borrow().clone(),
|
||||||
instruments: this.instruments.borrow().clone(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,55 +88,17 @@ impl PartEditor {
|
||||||
this.show_composer(None);
|
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
|
// Initialize
|
||||||
|
|
||||||
if let Some(composer) = &*this.composer.borrow() {
|
if let Some(composer) = &*this.composer.borrow() {
|
||||||
this.show_composer(Some(composer));
|
this.show_composer(Some(composer));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.instrument_list
|
|
||||||
.show_items(this.instruments.borrow().clone());
|
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called when the user wants to save the part.
|
/// 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)));
|
self.ready_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,12 @@ use std::rc::Rc;
|
||||||
pub struct SectionEditor {
|
pub struct SectionEditor {
|
||||||
window: libhandy::Window,
|
window: libhandy::Window,
|
||||||
title_entry: gtk::Entry,
|
title_entry: gtk::Entry,
|
||||||
ready_cb: RefCell<Option<Box<dyn Fn(WorkSectionDescription) -> ()>>>,
|
ready_cb: RefCell<Option<Box<dyn Fn(WorkSection) -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SectionEditor {
|
impl SectionEditor {
|
||||||
/// Create a new section editor and optionally initialize it.
|
/// Create a new section editor and optionally initialize it.
|
||||||
pub fn new<P: IsA<gtk::Window>>(
|
pub fn new<P: IsA<gtk::Window>>(parent: &P, section: Option<WorkSection>) -> Rc<Self> {
|
||||||
parent: &P,
|
|
||||||
section: Option<WorkSectionDescription>,
|
|
||||||
) -> Rc<Self> {
|
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section_editor.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 |_| {
|
save_button.connect_clicked(clone!(@strong this => move |_| {
|
||||||
if let Some(cb) = &*this.ready_cb.borrow() {
|
if let Some(cb) = &*this.ready_cb.borrow() {
|
||||||
cb(WorkSectionDescription {
|
cb(WorkSection {
|
||||||
before_index: 0,
|
before_index: 0,
|
||||||
title: this.title_entry.get_text().to_string(),
|
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
|
/// 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
|
/// 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.
|
/// 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)));
|
self.ready_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct WorkDialog {
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
selector: Rc<WorkSelector>,
|
selector: Rc<WorkSelector>,
|
||||||
editor: Rc<WorkEditor>,
|
editor: Rc<WorkEditor>,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkDialog {
|
impl WorkDialog {
|
||||||
|
|
@ -75,7 +75,7 @@ impl WorkDialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called when the user has selected or created a work.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ use std::rc::Rc;
|
||||||
/// Either a work part or a work section.
|
/// Either a work part or a work section.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum PartOrSection {
|
enum PartOrSection {
|
||||||
Part(WorkPartDescription),
|
Part(WorkPart),
|
||||||
Section(WorkSectionDescription),
|
Section(WorkSection),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget for editing and creating works.
|
/// A widget for editing and creating works.
|
||||||
|
|
@ -29,12 +29,12 @@ pub struct WorkEditor {
|
||||||
composer_label: gtk::Label,
|
composer_label: gtk::Label,
|
||||||
instrument_list: Rc<List<Instrument>>,
|
instrument_list: Rc<List<Instrument>>,
|
||||||
part_list: Rc<List<PartOrSection>>,
|
part_list: Rc<List<PartOrSection>>,
|
||||||
id: i64,
|
id: u32,
|
||||||
composer: RefCell<Option<Person>>,
|
composer: RefCell<Option<Person>>,
|
||||||
instruments: RefCell<Vec<Instrument>>,
|
instruments: RefCell<Vec<Instrument>>,
|
||||||
structure: RefCell<Vec<PartOrSection>>,
|
structure: RefCell<Vec<PartOrSection>>,
|
||||||
cancel_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
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 {
|
impl WorkEditor {
|
||||||
|
|
@ -43,7 +43,7 @@ impl WorkEditor {
|
||||||
pub fn new<P: IsA<gtk::Window>>(
|
pub fn new<P: IsA<gtk::Window>>(
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
parent: &P,
|
parent: &P,
|
||||||
work: Option<WorkDescription>,
|
work: Option<Work>,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ impl WorkEditor {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.save_button.connect_clicked(clone!(@strong this => move |_| {
|
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 parts = Vec::new();
|
||||||
let mut sections = Vec::new();
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
|
|
@ -129,7 +129,6 @@ impl WorkEditor {
|
||||||
PartOrSection::Part(part) => parts.push(part.clone()),
|
PartOrSection::Part(part) => parts.push(part.clone()),
|
||||||
PartOrSection::Section(section) => {
|
PartOrSection::Section(section) => {
|
||||||
let mut section = section.clone();
|
let mut section = section.clone();
|
||||||
let index: i64 = index.try_into().unwrap();
|
|
||||||
section.before_index = index - section_count;
|
section.before_index = index - section_count;
|
||||||
sections.push(section);
|
sections.push(section);
|
||||||
section_count += 1;
|
section_count += 1;
|
||||||
|
|
@ -137,7 +136,7 @@ impl WorkEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let work = WorkDescription {
|
let work = Work {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
title: this.title_entry.get_text().to_string(),
|
title: this.title_entry.get_text().to_string(),
|
||||||
composer: this.composer.borrow().clone().expect("Tried to create work without composer!"),
|
composer: this.composer.borrow().clone().expect("Tried to create work without composer!"),
|
||||||
|
|
@ -149,7 +148,7 @@ impl WorkEditor {
|
||||||
let c = glib::MainContext::default();
|
let c = glib::MainContext::default();
|
||||||
let clone = this.clone();
|
let clone = this.clone();
|
||||||
c.spawn_local(async move {
|
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() {
|
if let Some(cb) = &*clone.saved_cb.borrow() {
|
||||||
cb(work);
|
cb(work);
|
||||||
}
|
}
|
||||||
|
|
@ -333,7 +332,8 @@ impl WorkEditor {
|
||||||
this.show_composer(composer);
|
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.part_list.show_items(this.structure.borrow().clone());
|
||||||
|
|
||||||
this
|
this
|
||||||
|
|
@ -345,7 +345,7 @@ impl WorkEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The closure to call when a work was created.
|
/// 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)));
|
self.saved_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use std::rc::Rc;
|
||||||
/// A dialog for creating or editing a work.
|
/// A dialog for creating or editing a work.
|
||||||
pub struct WorkEditorDialog {
|
pub struct WorkEditorDialog {
|
||||||
pub window: libhandy::Window,
|
pub window: libhandy::Window,
|
||||||
saved_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
|
saved_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkEditorDialog {
|
impl WorkEditorDialog {
|
||||||
|
|
@ -17,7 +17,7 @@ impl WorkEditorDialog {
|
||||||
pub fn new<W: IsA<gtk::Window>>(
|
pub fn new<W: IsA<gtk::Window>>(
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
parent: &W,
|
parent: &W,
|
||||||
work: Option<WorkDescription>,
|
work: Option<Work>,
|
||||||
) -> Rc<Self> {
|
) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl WorkEditorDialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called when the user edited or created a work.
|
/// 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)));
|
self.saved_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub struct WorkSelector {
|
||||||
pub widget: libhandy::Leaflet,
|
pub widget: libhandy::Leaflet,
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
sidebar_box: gtk::Box,
|
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() -> ()>>>,
|
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
|
||||||
navigator: Rc<Navigator>,
|
navigator: Rc<Navigator>,
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +83,7 @@ impl WorkSelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the closure to be called when the user has selected a work.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ pub struct WorkSelectorPersonScreen {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
work_list: Rc<List<WorkDescription>>,
|
work_list: Rc<List<Work>>,
|
||||||
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
|
selected_cb: RefCell<Option<Box<dyn Fn(Work) -> ()>>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
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));
|
let label = gtk::Label::new(Some(&work.title));
|
||||||
label.set_ellipsize(pango::EllipsizeMode::End);
|
label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
label.set_halign(gtk::Align::Start);
|
label.set_halign(gtk::Align::Start);
|
||||||
|
|
@ -80,11 +80,7 @@ impl WorkSelectorPersonScreen {
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = this.clone();
|
let clone = this.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let works = clone
|
let works = clone.backend.db().get_works(person.id).await.unwrap();
|
||||||
.backend
|
|
||||||
.get_work_descriptions(person.id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
clone.work_list.show_items(works);
|
clone.work_list.show_items(works);
|
||||||
clone.stack.set_visible_child_name("content");
|
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.
|
/// 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)));
|
self.selected_cb.replace(Some(Box::new(cb)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,15 +33,19 @@ run_command(
|
||||||
)
|
)
|
||||||
|
|
||||||
sources = files(
|
sources = files(
|
||||||
'backend/backend.rs',
|
|
||||||
'backend/client.rs',
|
'backend/client.rs',
|
||||||
|
'backend/library.rs',
|
||||||
'backend/mod.rs',
|
'backend/mod.rs',
|
||||||
'backend/secure.rs',
|
'backend/secure.rs',
|
||||||
'database/database.rs',
|
'database/ensembles.rs',
|
||||||
|
'database/instruments.rs',
|
||||||
'database/mod.rs',
|
'database/mod.rs',
|
||||||
'database/models.rs',
|
'database/persons.rs',
|
||||||
|
'database/recordings.rs',
|
||||||
'database/schema.rs',
|
'database/schema.rs',
|
||||||
'database/tables.rs',
|
'database/thread.rs',
|
||||||
|
'database/tracks.rs',
|
||||||
|
'database/works.rs',
|
||||||
'dialogs/about.rs',
|
'dialogs/about.rs',
|
||||||
'dialogs/ensemble_editor.rs',
|
'dialogs/ensemble_editor.rs',
|
||||||
'dialogs/ensemble_selector.rs',
|
'dialogs/ensemble_selector.rs',
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ use std::rc::Rc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlaylistItem {
|
pub struct PlaylistItem {
|
||||||
pub recording: RecordingDescription,
|
pub recording: Recording,
|
||||||
pub tracks: Vec<TrackDescription>,
|
pub tracks: Vec<Track>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub struct EnsembleScreen {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
recording_list: Rc<List<RecordingDescription>>,
|
recording_list: Rc<List<Recording>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl EnsembleScreen {
|
||||||
|
|
||||||
let recording_list = List::new(&gettext("No recordings found."));
|
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()));
|
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
||||||
|
|
||||||
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
|
@ -72,7 +72,7 @@ impl EnsembleScreen {
|
||||||
});
|
});
|
||||||
|
|
||||||
recording_list.set_filter(
|
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 search = search_entry.get_text().to_string().to_lowercase();
|
||||||
let text = recording.work.get_title() + &recording.get_performers();
|
let text = recording.work.get_title() + &recording.get_performers();
|
||||||
search.is_empty() || text.contains(&search)
|
search.is_empty() || text.contains(&search)
|
||||||
|
|
@ -114,7 +114,8 @@ impl EnsembleScreen {
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let recordings = clone
|
let recordings = clone
|
||||||
.backend
|
.backend
|
||||||
.get_recordings_for_ensemble(ensemble.id)
|
.db()
|
||||||
|
.get_recordings_for_ensemble(ensemble.id as u32)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ pub struct PersonScreen {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
work_list: Rc<List<WorkDescription>>,
|
work_list: Rc<List<Work>>,
|
||||||
recording_list: Rc<List<RecordingDescription>>,
|
recording_list: Rc<List<Recording>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ impl PersonScreen {
|
||||||
|
|
||||||
let work_list = List::new(&gettext("No works found."));
|
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));
|
let label = gtk::Label::new(Some(&work.title));
|
||||||
label.set_halign(gtk::Align::Start);
|
label.set_halign(gtk::Align::Start);
|
||||||
label.set_margin_start(6);
|
label.set_margin_start(6);
|
||||||
|
|
@ -66,17 +66,15 @@ impl PersonScreen {
|
||||||
label.upcast()
|
label.upcast()
|
||||||
});
|
});
|
||||||
|
|
||||||
work_list.set_filter(
|
work_list.set_filter(clone!(@strong search_entry => move |work: &Work| {
|
||||||
clone!(@strong search_entry => move |work: &WorkDescription| {
|
let search = search_entry.get_text().to_string().to_lowercase();
|
||||||
let search = search_entry.get_text().to_string().to_lowercase();
|
let title = work.title.to_lowercase();
|
||||||
let title = work.title.to_lowercase();
|
search.is_empty() || title.contains(&search)
|
||||||
search.is_empty() || title.contains(&search)
|
}));
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let recording_list = List::new(&gettext("No recordings found."));
|
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()));
|
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
||||||
|
|
||||||
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
|
@ -96,7 +94,7 @@ impl PersonScreen {
|
||||||
});
|
});
|
||||||
|
|
||||||
recording_list.set_filter(
|
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 search = search_entry.get_text().to_string().to_lowercase();
|
||||||
let text = recording.work.get_title() + &recording.get_performers();
|
let text = recording.work.get_title() + &recording.get_performers();
|
||||||
search.is_empty() || text.contains(&search)
|
search.is_empty() || text.contains(&search)
|
||||||
|
|
@ -152,12 +150,14 @@ impl PersonScreen {
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let works = clone
|
let works = clone
|
||||||
.backend
|
.backend
|
||||||
.get_work_descriptions(person.id)
|
.db()
|
||||||
|
.get_works(person.id as u32)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let recordings = clone
|
let recordings = clone
|
||||||
.backend
|
.backend
|
||||||
.get_recordings_for_person(person.id)
|
.db()
|
||||||
|
.get_recordings_for_person(person.id as u32)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ pub struct RecordingScreen {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
tracks: RefCell<Vec<TrackDescription>>,
|
tracks: RefCell<Vec<Track>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordingScreen {
|
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");
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui");
|
||||||
|
|
||||||
get_widget!(builder, gtk::Box, widget);
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
|
@ -69,7 +69,7 @@ impl RecordingScreen {
|
||||||
let list = List::new(&gettext("No tracks found."));
|
let list = List::new(&gettext("No tracks found."));
|
||||||
|
|
||||||
list.set_make_widget(
|
list.set_make_widget(
|
||||||
clone!(@strong recording => move |track: &TrackDescription| {
|
clone!(@strong recording => move |track: &Track| {
|
||||||
let mut title_parts = Vec::<String>::new();
|
let mut title_parts = Vec::<String>::new();
|
||||||
for part in &track.work_parts {
|
for part in &track.work_parts {
|
||||||
title_parts.push(recording.work.parts[*part].title.clone());
|
title_parts.push(recording.work.parts[*part].title.clone());
|
||||||
|
|
@ -131,7 +131,7 @@ impl RecordingScreen {
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
let id = recording.id;
|
let id = recording.id;
|
||||||
context.spawn_local(async move {
|
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());
|
list.show_items(tracks.clone());
|
||||||
clone.stack.set_visible_child_name("content");
|
clone.stack.set_visible_child_name("content");
|
||||||
clone.tracks.replace(tracks);
|
clone.tracks.replace(tracks);
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ pub struct WorkScreen {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
stack: gtk::Stack,
|
stack: gtk::Stack,
|
||||||
recording_list: Rc<List<RecordingDescription>>,
|
recording_list: Rc<List<Recording>>,
|
||||||
navigator: RefCell<Option<Rc<Navigator>>>,
|
navigator: RefCell<Option<Rc<Navigator>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkScreen {
|
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");
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui");
|
||||||
|
|
||||||
get_widget!(builder, gtk::Box, widget);
|
get_widget!(builder, gtk::Box, widget);
|
||||||
|
|
@ -53,7 +53,7 @@ impl WorkScreen {
|
||||||
|
|
||||||
let recording_list = List::new(&gettext("No recordings found."));
|
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()));
|
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
|
||||||
|
|
||||||
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
work_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
|
@ -72,7 +72,7 @@ impl WorkScreen {
|
||||||
vbox.upcast()
|
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 search = search_entry.get_text().to_string().to_lowercase();
|
||||||
let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase();
|
let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase();
|
||||||
search.is_empty() || text.contains(&search)
|
search.is_empty() || text.contains(&search)
|
||||||
|
|
@ -113,7 +113,8 @@ impl WorkScreen {
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let recordings = clone
|
let recordings = clone
|
||||||
.backend
|
.backend
|
||||||
.get_recordings_for_work(work.id)
|
.db()
|
||||||
|
.get_recordings_for_work(work.id as u32)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ impl PersonList {
|
||||||
let list = self.list.clone();
|
let list = self.list.clone();
|
||||||
|
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let persons = backend.get_persons().await.unwrap();
|
let persons = backend.db().get_persons().await.unwrap();
|
||||||
list.show_items(persons);
|
list.show_items(persons);
|
||||||
self.stack.set_visible_child_name("content");
|
self.stack.set_visible_child_name("content");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ impl PoeList {
|
||||||
let list = self.list.clone();
|
let list = self.list.clone();
|
||||||
|
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
let persons = backend.get_persons().await.unwrap();
|
let persons = backend.db().get_persons().await.unwrap();
|
||||||
let ensembles = backend.get_ensembles().await.unwrap();
|
let ensembles = backend.db().get_ensembles().await.unwrap();
|
||||||
let mut poes: Vec<PersonOrEnsemble> = Vec::new();
|
let mut poes: Vec<PersonOrEnsemble> = Vec::new();
|
||||||
|
|
||||||
for person in persons {
|
for person in persons {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ impl Window {
|
||||||
get_widget!(builder, gtk::Box, empty_screen);
|
get_widget!(builder, gtk::Box, empty_screen);
|
||||||
|
|
||||||
let backend = Rc::new(Backend::new());
|
let backend = Rc::new(Backend::new());
|
||||||
backend.clone().init();
|
|
||||||
|
|
||||||
let player_screen = PlayerScreen::new();
|
let player_screen = PlayerScreen::new();
|
||||||
stack.add_named(&player_screen.widget, "player_screen");
|
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 context = glib::MainContext::default();
|
||||||
let clone = result.clone();
|
let clone = result.clone();
|
||||||
context.spawn_local(async move {
|
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.leaflet.add(&result.navigator.widget);
|
||||||
|
|
||||||
result
|
result
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue