Split into multiple crates

This commit is contained in:
Elias Projahn 2021-02-04 21:47:22 +01:00
parent d7fb996183
commit 5d06ec9faf
88 changed files with 501 additions and 528 deletions

View file

@ -1,16 +0,0 @@
use crate::backend::{Backend, Ensemble, Result};
impl Backend {
/// Get all available ensembles from the server.
pub async fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
let body = self.get("ensembles").await?;
let ensembles: Vec<Ensemble> = serde_json::from_str(&body)?;
Ok(ensembles)
}
/// Post a new ensemble to the server.
pub async fn post_ensemble(&self, data: &Ensemble) -> Result<()> {
self.post("ensembles", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,16 +0,0 @@
use crate::backend::{Backend, Instrument, Result};
impl Backend {
/// Get all available instruments from the server.
pub async fn get_instruments(&self) -> Result<Vec<Instrument>> {
let body = self.get("instruments").await?;
let instruments: Vec<Instrument> = serde_json::from_str(&body)?;
Ok(instruments)
}
/// Post a new instrument to the server.
pub async fn post_instrument(&self, data: &Instrument) -> Result<()> {
self.post("instruments", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,25 +0,0 @@
use crate::backend::{Backend, Medium, Result};
impl Backend {
/// Get all available mediums from the server, that contain the specified
/// recording.
pub async fn get_mediums_for_recording(&self, recording_id: &str) -> Result<Vec<Medium>> {
let body = self.get(&format!("recordings/{}/mediums", recording_id)).await?;
let mediums: Vec<Medium> = serde_json::from_str(&body)?;
Ok(mediums)
}
/// Get all available mediums from the server, that match the specified
/// DiscID.
pub async fn get_mediums_by_discid(&self, discid: &str) -> Result<Vec<Medium>> {
let body = self.get(&format!("discids/{}/mediums", discid)).await?;
let mediums: Vec<Medium> = serde_json::from_str(&body)?;
Ok(mediums)
}
/// Post a new medium to the server.
pub async fn post_medium(&self, data: &Medium) -> Result<()> {
self.post("mediums", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,183 +0,0 @@
use crate::backend::{Backend, Error, Result, secure};
use gio::prelude::*;
use isahc::http::StatusCode;
use isahc::prelude::*;
use serde::Serialize;
use std::time::Duration;
pub mod ensembles;
pub use ensembles::*;
pub mod instruments;
pub use instruments::*;
pub mod mediums;
pub use mediums::*;
pub mod persons;
pub use persons::*;
pub mod recordings;
pub use recordings::*;
pub mod register;
pub use register::*;
pub mod works;
pub use works::*;
/// Credentials used for login.
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LoginData {
pub username: String,
pub password: String,
}
impl Backend {
/// Initialize the client.
pub(super) fn init_client(&self) -> Result<()> {
if let Some(data) = secure::load_login_data()? {
self.login_data.replace(Some(data));
}
if let Some(url) = self.settings.get_string("server-url") {
if !url.is_empty() {
self.server_url.replace(Some(url.to_string()));
}
}
Ok(())
}
/// Set the URL of the Musicus server to connect to.
pub fn set_server_url(&self, url: &str) -> Result<()> {
self.settings.set_string("server-url", url)?;
self.server_url.replace(Some(url.to_string()));
Ok(())
}
/// Get the currently used login token.
pub fn get_token(&self) -> Option<String> {
self.token.borrow().clone()
}
/// Set the login token to use. This will be done automatically by the login method.
pub fn set_token(&self, token: &str) {
self.token.replace(Some(token.to_string()));
}
/// Get the currently set server URL.
pub fn get_server_url(&self) -> Option<String> {
self.server_url.borrow().clone()
}
/// Require the server URL to be set.
fn server_url(&self) -> Result<String> {
self.get_server_url().ok_or(Error::Other("The server URL is not available!"))
}
/// Get the currently stored login credentials.
pub fn get_login_data(&self) -> Option<LoginData> {
self.login_data.borrow().clone()
}
fn login_data(&self) -> Result<LoginData> {
self.get_login_data().ok_or(Error::Other("The login data is unset!"))
}
/// 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));
self.token.replace(None);
Ok(())
}
/// Try to login a user with the provided credentials and return, wether the login suceeded.
pub async fn login(&self) -> Result<bool> {
let server_url = self.server_url()?;
let data = self.login_data()?;
let request = Request::post(format!("{}/login", server_url))
.timeout(Duration::from_secs(10))
.header("Content-Type", "application/json")
.body(serde_json::to_string(&data)?)?;
let mut response = isahc::send_async(request).await?;
let success = match response.status() {
StatusCode::OK => {
let token = response.text_async().await?;
self.set_token(&token);
true
}
StatusCode::UNAUTHORIZED => false,
status_code => Err(Error::UnexpectedResponse(status_code))?,
};
Ok(success)
}
/// Make an unauthenticated get request to the server.
async fn get(&self, url: &str) -> Result<String> {
let server_url = self.server_url()?;
let mut response = Request::get(format!("{}/{}", server_url, url))
.timeout(Duration::from_secs(10))
.body(())?
.send_async()
.await?;
let body = response.text_async().await?;
Ok(body)
}
/// Make an authenticated post request to the server.
async fn post(&self, url: &str, body: String) -> Result<String> {
let body = match self.get_token() {
Some(_) => {
let mut response = self.post_priv(url, body.clone()).await?;
// Try one more time (maybe the token was expired)
if response.status() == StatusCode::UNAUTHORIZED {
if self.login().await? {
response = self.post_priv(url, body).await?;
} else {
Err(Error::LoginFailed)?;
}
}
response.text_async().await?
}
None => {
let mut response = if self.login().await? {
self.post_priv(url, body).await?
} else {
Err(Error::LoginFailed)?
};
response.text_async().await?
}
};
Ok(body)
}
/// Post something to the server assuming there is a valid login token.
async fn post_priv(&self, url: &str, body: String) -> Result<Response<Body>> {
let server_url = self.server_url()?;
let token = self.get_token().ok_or(Error::Other("No login token found!"))?;
let response = Request::post(format!("{}/{}", server_url, url))
.timeout(Duration::from_secs(10))
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.body(body)?
.send_async()
.await?;
Ok(response)
}
}

View file

@ -1,16 +0,0 @@
use crate::backend::{Backend, Person, Result};
impl Backend {
/// Get all available persons from the server.
pub async fn get_persons(&self) -> Result<Vec<Person>> {
let body = self.get("persons").await?;
let persons: Vec<Person> = serde_json::from_str(&body)?;
Ok(persons)
}
/// Post a new person to the server.
pub async fn post_person(&self, data: &Person) -> Result<()> {
self.post("persons", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,16 +0,0 @@
use crate::backend::{Backend, Recording, Result};
impl Backend {
/// Get all available recordings from the server.
pub async fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> {
let body = self.get(&format!("works/{}/recordings", work_id)).await?;
let recordings: Vec<Recording> = serde_json::from_str(&body)?;
Ok(recordings)
}
/// Post a new recording to the server.
pub async fn post_recording(&self, data: &Recording) -> Result<()> {
self.post("recordings", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,48 +0,0 @@
use crate::backend::{Backend, Result};
use isahc::http::StatusCode;
use isahc::prelude::*;
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Response body data for captcha requests.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Captcha {
pub id: String,
pub question: String,
}
/// Request body data for user registration.
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct UserRegistration {
pub username: String,
pub password: String,
pub email: Option<String>,
pub captcha_id: String,
pub answer: String,
}
impl Backend {
/// Request a new captcha for registration.
pub async fn get_captcha(&self) -> Result<Captcha> {
let body = self.get("captcha").await?;
let captcha = serde_json::from_str(&body)?;
Ok(captcha)
}
/// Register a new user and return whether the process suceeded. This will
/// not store the new login credentials.
pub async fn register(&self, data: UserRegistration) -> Result<bool> {
let server_url = self.server_url()?;
let mut response = Request::post(format!("{}/users", server_url))
.timeout(Duration::from_secs(10))
.header("Content-Type", "application/json")
.body(serde_json::to_string(&data)?)?
.send_async()
.await?;
Ok(response.status() == StatusCode::OK)
}
}

View file

@ -1,16 +0,0 @@
use crate::backend::{Backend, Result, Work};
impl Backend {
/// Get all available works from the server.
pub async fn get_works(&self, composer_id: &str) -> Result<Vec<Work>> {
let body = self.get(&format!("persons/{}/works", composer_id)).await?;
let works: Vec<Work> = serde_json::from_str(&body)?;
Ok(works)
}
/// Post a new work to the server.
pub async fn post_work(&self, data: &Work) -> Result<()> {
self.post("works", serde_json::to_string(data)?).await?;
Ok(())
}
}

View file

@ -1,52 +0,0 @@
use super::schema::ensembles;
use super::{Database, DatabaseResult};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// An ensemble that takes part in recordings.
#[derive(Serialize, Deserialize, Insertable, Queryable, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Ensemble {
pub id: String,
pub name: String,
}
impl Database {
/// Update an existing ensemble or insert a new one.
pub fn update_ensemble(&self, ensemble: Ensemble) -> DatabaseResult<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
diesel::replace_into(ensembles::table)
.values(ensemble)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing ensemble.
pub fn get_ensemble(&self, id: &str) -> DatabaseResult<Option<Ensemble>> {
let ensemble = ensembles::table
.filter(ensembles::id.eq(id))
.load::<Ensemble>(&self.connection)?
.into_iter()
.next();
Ok(ensemble)
}
/// Delete an existing ensemble.
pub fn delete_ensemble(&self, id: &str) -> DatabaseResult<()> {
diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all existing ensembles.
pub fn get_ensembles(&self) -> DatabaseResult<Vec<Ensemble>> {
let ensembles = ensembles::table.load::<Ensemble>(&self.connection)?;
Ok(ensembles)
}
}

View file

@ -1,26 +0,0 @@
use thiserror::Error;
/// Error that happens within the database module.
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error(transparent)]
ConnectionError(#[from] diesel::result::ConnectionError),
#[error(transparent)]
MigrationsError(#[from] diesel_migrations::RunMigrationsError),
#[error(transparent)]
QueryError(#[from] diesel::result::Error),
#[error(transparent)]
SendError(#[from] std::sync::mpsc::SendError<super::thread::Action>),
#[error(transparent)]
ReceiveError(#[from] futures_channel::oneshot::Canceled),
#[error("Database error: {0}")]
Other(String),
}
/// Return type for database methods.
pub type DatabaseResult<T> = Result<T, DatabaseError>;

View file

@ -1,53 +0,0 @@
use super::schema::instruments;
use super::{Database, DatabaseResult};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// An instrument or any other possible role within a recording.
#[derive(Serialize, Deserialize, Insertable, Queryable, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Instrument {
pub id: String,
pub name: String,
}
impl Database {
/// Update an existing instrument or insert a new one.
pub fn update_instrument(&self, instrument: Instrument) -> DatabaseResult<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
diesel::replace_into(instruments::table)
.values(instrument)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing instrument.
pub fn get_instrument(&self, id: &str) -> DatabaseResult<Option<Instrument>> {
let instrument = instruments::table
.filter(instruments::id.eq(id))
.load::<Instrument>(&self.connection)?
.into_iter()
.next();
Ok(instrument)
}
/// Delete an existing instrument.
pub fn delete_instrument(&self, id: &str) -> DatabaseResult<()> {
diesel::delete(instruments::table.filter(instruments::id.eq(id)))
.execute(&self.connection)?;
Ok(())
}
/// Get all existing instruments.
pub fn get_instruments(&self) -> DatabaseResult<Vec<Instrument>> {
let instruments = instruments::table.load::<Instrument>(&self.connection)?;
Ok(instruments)
}
}

View file

@ -1,263 +0,0 @@
use super::generate_id;
use super::schema::{mediums, recordings, track_sets, tracks};
use super::{Database, DatabaseError, Recording, DatabaseResult};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// Representation of someting like a physical audio disc or a folder with
/// audio files (i.e. a collection of tracks for one or more recordings).
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Medium {
/// An unique ID for the medium.
pub id: String,
/// The human identifier for the medium.
pub name: String,
/// If applicable, the MusicBrainz DiscID.
pub discid: Option<String>,
/// The tracks of the medium, grouped by recording.
pub tracks: Vec<TrackSet>,
}
/// A set of tracks of one recording within a medium.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TrackSet {
/// The recording to which the tracks belong.
pub recording: Recording,
/// The actual tracks.
pub tracks: Vec<Track>,
}
/// A track within a recording on a medium.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Track {
/// The work parts that are played on this track. They are indices to the
/// work parts of the work that is associated with the recording.
pub work_parts: Vec<usize>,
/// The path to the audio file containing this track. This will not be
/// included when communicating with the server.
#[serde(skip)]
pub path: String,
}
/// Table data for a [`Medium`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "mediums"]
struct MediumRow {
pub id: String,
pub name: String,
pub discid: Option<String>,
}
/// Table data for a [`TrackSet`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "track_sets"]
struct TrackSetRow {
pub id: String,
pub medium: String,
pub index: i32,
pub recording: String,
}
/// Table data for a [`Track`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "tracks"]
struct TrackRow {
pub id: String,
pub track_set: String,
pub index: i32,
pub work_parts: String,
pub path: String,
}
impl Database {
/// Update an existing medium or insert a new one.
pub fn update_medium(&self, medium: Medium) -> DatabaseResult<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), DatabaseError, _>(|| {
let medium_id = &medium.id;
// This will also delete the track sets and tracks.
self.delete_medium(medium_id)?;
// Add the new medium.
let medium_row = MediumRow {
id: medium_id.to_owned(),
name: medium.name.clone(),
discid: medium.discid.clone(),
};
diesel::insert_into(mediums::table)
.values(medium_row)
.execute(&self.connection)?;
for (index, track_set) in medium.tracks.iter().enumerate() {
// Add associated items from the server, if they don't already
// exist.
if self.get_recording(&track_set.recording.id)?.is_none() {
self.update_recording(track_set.recording.clone())?;
}
// Add the actual track set data.
let track_set_id = generate_id();
let track_set_row = TrackSetRow {
id: track_set_id.clone(),
medium: medium_id.to_owned(),
index: index as i32,
recording: track_set.recording.id.clone(),
};
diesel::insert_into(track_sets::table)
.values(track_set_row)
.execute(&self.connection)?;
for (index, track) in track_set.tracks.iter().enumerate() {
let work_parts = track
.work_parts
.iter()
.map(|part_index| part_index.to_string())
.collect::<Vec<String>>()
.join(",");
let track_row = TrackRow {
id: generate_id(),
track_set: track_set_id.clone(),
index: index as i32,
work_parts,
path: track.path.clone(),
};
diesel::insert_into(tracks::table)
.values(track_row)
.execute(&self.connection)?;
}
}
Ok(())
})?;
Ok(())
}
/// Get an existing medium.
pub fn get_medium(&self, id: &str) -> DatabaseResult<Option<Medium>> {
let row = mediums::table
.filter(mediums::id.eq(id))
.load::<MediumRow>(&self.connection)?
.into_iter()
.next();
let medium = match row {
Some(row) => Some(self.get_medium_data(row)?),
None => None,
};
Ok(medium)
}
/// Delete a medium and all of its tracks. This will fail, if the music
/// library contains audio files referencing any of those tracks.
pub fn delete_medium(&self, id: &str) -> DatabaseResult<()> {
diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all available track sets for a recording.
pub fn get_track_sets(&self, recording_id: &str) -> DatabaseResult<Vec<TrackSet>> {
let mut track_sets: Vec<TrackSet> = Vec::new();
let rows = track_sets::table
.inner_join(recordings::table.on(recordings::id.eq(track_sets::recording)))
.filter(recordings::id.eq(recording_id))
.select(track_sets::table::all_columns())
.load::<TrackSetRow>(&self.connection)?;
for row in rows {
let track_set = self.get_track_set_from_row(row)?;
track_sets.push(track_set);
}
Ok(track_sets)
}
/// Retrieve all available information on a medium from related tables.
fn get_medium_data(&self, row: MediumRow) -> DatabaseResult<Medium> {
let track_set_rows = track_sets::table
.filter(track_sets::medium.eq(&row.id))
.order_by(track_sets::index)
.load::<TrackSetRow>(&self.connection)?;
let mut track_sets = Vec::new();
for track_set_row in track_set_rows {
let track_set = self.get_track_set_from_row(track_set_row)?;
track_sets.push(track_set);
}
let medium = Medium {
id: row.id,
name: row.name,
discid: row.discid,
tracks: track_sets,
};
Ok(medium)
}
/// Convert a track set row from the database to an actual track set.
fn get_track_set_from_row(&self, row: TrackSetRow) -> DatabaseResult<TrackSet> {
let recording_id = row.recording;
let recording = self
.get_recording(&recording_id)?
.ok_or(DatabaseError::Other(format!(
"Failed to get recording ({}) for track set ({}).",
recording_id,
row.id,
)))?;
let track_rows = tracks::table
.filter(tracks::track_set.eq(row.id))
.order_by(tracks::index)
.load::<TrackRow>(&self.connection)?;
let mut tracks = Vec::new();
for track_row in track_rows {
let work_parts = track_row
.work_parts
.split(',')
.map(|part_index| {
str::parse(part_index)
.or(Err(DatabaseError::Other(format!(
"Failed to parse part index from '{}'.",
track_row.work_parts,
)))?)
})
.collect::<DatabaseResult<Vec<usize>>>()?;
let track = Track {
work_parts,
path: track_row.path,
};
tracks.push(track);
}
let track_set = TrackSet { recording, tracks };
Ok(track_set)
}
}

View file

@ -1,61 +0,0 @@
use diesel::prelude::*;
pub mod ensembles;
pub use ensembles::*;
pub mod error;
pub use error::*;
pub mod instruments;
pub use instruments::*;
pub mod medium;
pub use medium::*;
pub mod persons;
pub use persons::*;
pub mod recordings;
pub use recordings::*;
pub mod thread;
pub use thread::*;
pub mod works;
pub use works::*;
mod schema;
// This makes the SQL migration scripts accessible from the code.
embed_migrations!();
/// Generate a random string suitable as an item ID.
pub fn generate_id() -> String {
let mut buffer = uuid::Uuid::encode_buffer();
let id = uuid::Uuid::new_v4().to_simple().encode_lower(&mut buffer);
id.to_string()
}
/// 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) -> DatabaseResult<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) -> DatabaseResult<()> {
diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(&self.connection)?;
Ok(())
}
}

View file

@ -1,65 +0,0 @@
use super::schema::persons;
use super::{Database, DatabaseResult};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// A person that is a composer, an interpret or both.
#[derive(Serialize, Deserialize, Insertable, Queryable, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Person {
pub id: String,
pub first_name: String,
pub last_name: String,
}
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) -> DatabaseResult<()> {
self.defer_foreign_keys()?;
self.connection.transaction(|| {
diesel::replace_into(persons::table)
.values(person)
.execute(&self.connection)
})?;
Ok(())
}
/// Get an existing person.
pub fn get_person(&self, id: &str) -> DatabaseResult<Option<Person>> {
let person = persons::table
.filter(persons::id.eq(id))
.load::<Person>(&self.connection)?
.into_iter()
.next();
Ok(person)
}
/// Delete an existing person.
pub fn delete_person(&self, id: &str) -> DatabaseResult<()> {
diesel::delete(persons::table.filter(persons::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all existing persons.
pub fn get_persons(&self) -> DatabaseResult<Vec<Person>> {
let persons = persons::table.load::<Person>(&self.connection)?;
Ok(persons)
}
}

View file

@ -1,331 +0,0 @@
use super::generate_id;
use super::schema::{ensembles, performances, persons, recordings};
use super::{Database, Ensemble, DatabaseError, Instrument, Person, DatabaseResult, Work};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// Database table data for a recording.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "recordings"]
struct RecordingRow {
pub id: String,
pub work: String,
pub comment: String,
}
impl From<Recording> for RecordingRow {
fn from(recording: Recording) -> Self {
RecordingRow {
id: recording.id,
work: recording.work.id,
comment: recording.comment,
}
}
}
/// Database table data for a performance.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "performances"]
struct PerformanceRow {
pub id: i64,
pub recording: String,
pub person: Option<String>,
pub ensemble: Option<String>,
pub role: Option<String>,
}
/// 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: String,
pub work: Work,
pub comment: String,
pub performances: Vec<Performance>,
}
impl Recording {
/// Initialize a new recording with a work.
pub fn new(work: Work) -> Self {
Self {
id: generate_id(),
work,
comment: String::new(),
performances: Vec::new(),
}
}
/// 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) -> DatabaseResult<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), DatabaseError, _>(|| {
let recording_id = &recording.id;
self.delete_recording(recording_id)?;
// Add associated items from the server, if they don't already exist.
if self.get_work(&recording.work.id)?.is_none() {
self.update_work(recording.work.clone())?;
}
for performance in &recording.performances {
if let Some(person) = &performance.person {
if self.get_person(&person.id)?.is_none() {
self.update_person(person.clone())?;
}
}
if let Some(ensemble) = &performance.ensemble {
if self.get_ensemble(&ensemble.id)?.is_none() {
self.update_ensemble(ensemble.clone())?;
}
}
if let Some(role) = &performance.role {
if self.get_instrument(&role.id)?.is_none() {
self.update_instrument(role.clone())?;
}
}
}
// Add the actual recording.
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.to_string(),
person: performance.person.map(|person| person.id),
ensemble: performance.ensemble.map(|ensemble| ensemble.id),
role: performance.role.map(|role| role.id),
};
diesel::insert_into(performances::table)
.values(row)
.execute(&self.connection)?;
}
Ok(())
})?;
Ok(())
}
/// Check whether the database contains a recording.
pub fn recording_exists(&self, id: &str) -> DatabaseResult<bool> {
let exists = recordings::table
.filter(recordings::id.eq(id))
.load::<RecordingRow>(&self.connection)?
.first()
.is_some();
Ok(exists)
}
/// Get an existing recording.
pub fn get_recording(&self, id: &str) -> DatabaseResult<Option<Recording>> {
let row = recordings::table
.filter(recordings::id.eq(id))
.load::<RecordingRow>(&self.connection)?
.into_iter()
.next();
let recording = match row {
Some(row) => Some(self.get_recording_data(row)?),
None => None,
};
Ok(recording)
}
/// Retrieve all available information on a recording from related tables.
fn get_recording_data(&self, row: RecordingRow) -> DatabaseResult<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)?
.ok_or(DatabaseError::Other(format!(
"Failed to get person ({}) for recording ({}).",
id,
row.id,
)))?
),
None => None,
},
ensemble: match row.ensemble {
Some(id) => Some(
self.get_ensemble(&id)?
.ok_or(DatabaseError::Other(format!(
"Failed to get ensemble ({}) for recording ({}).",
id,
row.id,
)))?
),
None => None,
},
role: match row.role {
Some(id) => Some(
self.get_instrument(&id)?
.ok_or(DatabaseError::Other(format!(
"Failed to get instrument ({}) for recording ({}).",
id,
row.id,
)))?
),
None => None,
},
});
}
let work_id = &row.work;
let work = self
.get_work(work_id)?
.ok_or(DatabaseError::Other(format!(
"Failed to get work ({}) for recording ({}).",
work_id,
row.id,
)))?;
let recording_description = Recording {
id: row.id,
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: &str) -> DatabaseResult<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))
.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: &str) -> DatabaseResult<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))
.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: &str) -> DatabaseResult<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.filter(recordings::work.eq(work_id))
.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: &str) -> DatabaseResult<()> {
diesel::delete(recordings::table.filter(recordings::id.eq(id)))
.execute(&self.connection)?;
Ok(())
}
}

View file

@ -1,131 +0,0 @@
table! {
ensembles (id) {
id -> Text,
name -> Text,
}
}
table! {
instrumentations (id) {
id -> BigInt,
work -> Text,
instrument -> Text,
}
}
table! {
instruments (id) {
id -> Text,
name -> Text,
}
}
table! {
mediums (id) {
id -> Text,
name -> Text,
discid -> Nullable<Text>,
}
}
table! {
performances (id) {
id -> BigInt,
recording -> Text,
person -> Nullable<Text>,
ensemble -> Nullable<Text>,
role -> Nullable<Text>,
}
}
table! {
persons (id) {
id -> Text,
first_name -> Text,
last_name -> Text,
}
}
table! {
recordings (id) {
id -> Text,
work -> Text,
comment -> Text,
}
}
table! {
track_sets (id) {
id -> Text,
medium -> Text,
index -> Integer,
recording -> Text,
}
}
table! {
tracks (id) {
id -> Text,
track_set -> Text,
index -> Integer,
work_parts -> Text,
path -> Text,
}
}
table! {
work_parts (id) {
id -> BigInt,
work -> Text,
part_index -> BigInt,
title -> Text,
composer -> Nullable<Text>,
}
}
table! {
work_sections (id) {
id -> BigInt,
work -> Text,
title -> Text,
before_index -> BigInt,
}
}
table! {
works (id) {
id -> Text,
composer -> Text,
title -> Text,
}
}
joinable!(instrumentations -> instruments (instrument));
joinable!(instrumentations -> works (work));
joinable!(performances -> ensembles (ensemble));
joinable!(performances -> instruments (role));
joinable!(performances -> persons (person));
joinable!(performances -> recordings (recording));
joinable!(recordings -> works (work));
joinable!(track_sets -> mediums (medium));
joinable!(track_sets -> recordings (recording));
joinable!(tracks -> track_sets (track_set));
joinable!(work_parts -> persons (composer));
joinable!(work_parts -> works (work));
joinable!(work_sections -> works (work));
joinable!(works -> persons (composer));
allow_tables_to_appear_in_same_query!(
ensembles,
instrumentations,
instruments,
mediums,
performances,
persons,
recordings,
track_sets,
tracks,
work_parts,
work_sections,
works,
);

View file

@ -1,354 +0,0 @@
use super::*;
use futures_channel::oneshot;
use futures_channel::oneshot::Sender;
use std::sync::mpsc;
use std::thread;
/// An action the database thread can perform.
pub enum Action {
UpdatePerson(Person, Sender<DatabaseResult<()>>),
GetPerson(String, Sender<DatabaseResult<Option<Person>>>),
DeletePerson(String, Sender<DatabaseResult<()>>),
GetPersons(Sender<DatabaseResult<Vec<Person>>>),
UpdateInstrument(Instrument, Sender<DatabaseResult<()>>),
GetInstrument(String, Sender<DatabaseResult<Option<Instrument>>>),
DeleteInstrument(String, Sender<DatabaseResult<()>>),
GetInstruments(Sender<DatabaseResult<Vec<Instrument>>>),
UpdateWork(Work, Sender<DatabaseResult<()>>),
DeleteWork(String, Sender<DatabaseResult<()>>),
GetWorks(String, Sender<DatabaseResult<Vec<Work>>>),
UpdateEnsemble(Ensemble, Sender<DatabaseResult<()>>),
GetEnsemble(String, Sender<DatabaseResult<Option<Ensemble>>>),
DeleteEnsemble(String, Sender<DatabaseResult<()>>),
GetEnsembles(Sender<DatabaseResult<Vec<Ensemble>>>),
UpdateRecording(Recording, Sender<DatabaseResult<()>>),
DeleteRecording(String, Sender<DatabaseResult<()>>),
GetRecordingsForPerson(String, Sender<DatabaseResult<Vec<Recording>>>),
GetRecordingsForEnsemble(String, Sender<DatabaseResult<Vec<Recording>>>),
GetRecordingsForWork(String, Sender<DatabaseResult<Vec<Recording>>>),
RecordingExists(String, Sender<DatabaseResult<bool>>),
UpdateMedium(Medium, Sender<DatabaseResult<()>>),
GetMedium(String, Sender<DatabaseResult<Option<Medium>>>),
DeleteMedium(String, Sender<DatabaseResult<()>>),
GetTrackSets(String, Sender<DatabaseResult<Vec<TrackSet>>>),
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) -> DatabaseResult<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();
}
RecordingExists(id, sender) => {
sender.send(db.recording_exists(&id)).unwrap();
}
UpdateMedium(medium, sender) => {
sender.send(db.update_medium(medium)).unwrap();
}
GetMedium(id, sender) => {
sender.send(db.get_medium(&id)).unwrap();
}
DeleteMedium(id, sender) => {
sender.send(db.delete_medium(&id)).unwrap();
}
GetTrackSets(recording_id, sender) => {
sender.send(db.get_track_sets(&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) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdatePerson(person, sender))?;
receiver.await?
}
/// Get an existing person.
pub async fn get_person(&self, id: &str) -> DatabaseResult<Option<Person>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetPerson(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing person. This will fail, if there are still other items referencing
/// this person.
pub async fn delete_person(&self, id: &str) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeletePerson(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing persons.
pub async fn get_persons(&self) -> DatabaseResult<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) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(UpdateInstrument(instrument, sender))?;
receiver.await?
}
/// Get an existing instrument.
pub async fn get_instrument(&self, id: &str) -> DatabaseResult<Option<Instrument>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetInstrument(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing instrument. This will fail, if there are still other items referencing
/// this instrument.
pub async fn delete_instrument(&self, id: &str) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteInstrument(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing instruments.
pub async fn get_instruments(&self) -> DatabaseResult<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) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateWork(work, sender))?;
receiver.await?
}
/// Delete an existing work. This will fail, if there are still other items referencing
/// this work.
pub async fn delete_work(&self, id: &str) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteWork(id.to_string(), sender))?;
receiver.await?
}
/// Get information on all existing works by a composer.
pub async fn get_works(&self, person_id: &str) -> DatabaseResult<Vec<Work>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetWorks(person_id.to_string(), sender))?;
receiver.await?
}
/// Update an existing ensemble or insert a new one.
pub async fn update_ensemble(&self, ensemble: Ensemble) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateEnsemble(ensemble, sender))?;
receiver.await?
}
/// Get an existing ensemble.
pub async fn get_ensemble(&self, id: &str) -> DatabaseResult<Option<Ensemble>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetEnsemble(id.to_string(), sender))?;
receiver.await?
}
/// Delete an existing ensemble. This will fail, if there are still other items referencing
/// this ensemble.
pub async fn delete_ensemble(&self, id: &str) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteEnsemble(id.to_string(), sender))?;
receiver.await?
}
/// Get all existing ensembles.
pub async fn get_ensembles(&self) -> DatabaseResult<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) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(UpdateRecording(recording, sender))?;
receiver.await?
}
/// Delete an existing recording.
pub async fn delete_recording(&self, id: &str) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteRecording(id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings in which a person performs.
pub async fn get_recordings_for_person(&self, person_id: &str) -> DatabaseResult<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForPerson(person_id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings in which an ensemble performs.
pub async fn get_recordings_for_ensemble(&self, ensemble_id: &str) -> DatabaseResult<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForEnsemble(ensemble_id.to_string(), sender))?;
receiver.await?
}
/// Get information on all recordings of a work.
pub async fn get_recordings_for_work(&self, work_id: &str) -> DatabaseResult<Vec<Recording>> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(GetRecordingsForWork(work_id.to_string(), sender))?;
receiver.await?
}
/// Check whether a recording exists within the database.
pub async fn recording_exists(&self, id: &str) -> DatabaseResult<bool> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(RecordingExists(id.to_string(), sender))?;
receiver.await?
}
/// Update an existing medium or insert a new one.
pub async fn update_medium(&self, medium: Medium) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(UpdateMedium(medium, sender))?;
receiver.await?
}
/// Delete an existing medium. This will fail, if there are still other
/// items referencing this medium.
pub async fn delete_medium(&self, id: &str) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender
.send(DeleteMedium(id.to_owned(), sender))?;
receiver.await?
}
/// Get an existing medium.
pub async fn get_medium(&self, id: &str) -> DatabaseResult<Option<Medium>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetMedium(id.to_owned(), sender))?;
receiver.await?
}
/// Get all track sets for a recording.
pub async fn get_track_sets(&self, recording_id: &str) -> DatabaseResult<Vec<TrackSet>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetTrackSets(recording_id.to_owned(), sender))?;
receiver.await?
}
/// Stop the database thread. Any future access to the database will fail.
pub async fn stop(&self) -> DatabaseResult<()> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(Stop(sender))?;
Ok(receiver.await?)
}
}

View file

@ -1,308 +0,0 @@
use super::generate_id;
use super::schema::{instrumentations, work_parts, work_sections, works};
use super::{Database, DatabaseError, Instrument, Person, DatabaseResult};
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: String,
pub composer: String,
pub title: String,
}
impl From<Work> for WorkRow {
fn from(work: Work) -> Self {
WorkRow {
id: work.id,
composer: work.composer.id,
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: String,
pub instrument: String,
}
/// Table row data for a work part.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "work_parts"]
struct WorkPartRow {
pub id: i64,
pub work: String,
pub part_index: i64,
pub title: String,
pub composer: Option<String>,
}
/// Table row data for a work section.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "work_sections"]
struct WorkSectionRow {
pub id: i64,
pub work: String,
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: String,
pub title: String,
pub composer: Person,
pub instruments: Vec<Instrument>,
pub parts: Vec<WorkPart>,
pub sections: Vec<WorkSection>,
}
impl Work {
/// Initialize a new work with a composer.
pub fn new(composer: Person) -> Self {
Self {
id: generate_id(),
title: String::new(),
composer,
instruments: Vec::new(),
parts: Vec::new(),
sections: Vec::new(),
}
}
/// 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) -> DatabaseResult<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), DatabaseError, _>(|| {
let work_id = &work.id;
self.delete_work(work_id)?;
// Add associated items from the server, if they don't already exist.
if self.get_person(&work.composer.id)?.is_none() {
self.update_person(work.composer.clone())?;
}
for instrument in &work.instruments {
if self.get_instrument(&instrument.id)?.is_none() {
self.update_instrument(instrument.clone())?;
}
}
for part in &work.parts {
if let Some(person) = &part.composer {
if self.get_person(&person.id)?.is_none() {
self.update_person(person.clone())?;
}
}
}
// Add the actual work.
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.to_string(),
instrument: instrument.id,
};
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.to_string(),
part_index: index as i64,
title: part.title,
composer: part.composer.map(|person| person.id),
};
diesel::insert_into(work_parts::table)
.values(row)
.execute(&self.connection)?;
}
for section in sections {
let row = WorkSectionRow {
id: rand::random(),
work: work_id.to_string(),
title: section.title,
before_index: section.before_index as i64,
};
diesel::insert_into(work_sections::table)
.values(row)
.execute(&self.connection)?;
}
}
}
Ok(())
})?;
Ok(())
}
/// Get an existing work.
pub fn get_work(&self, id: &str) -> DatabaseResult<Option<Work>> {
let row = works::table
.filter(works::id.eq(id))
.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) -> DatabaseResult<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 = &instrumentation.instrument;
instruments.push(
self.get_instrument(id)?
.ok_or(DatabaseError::Other(format!(
"Failed to get instrument ({}) for work ({}).",
id,
row.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)?
.ok_or(DatabaseError::Other(format!(
"Failed to get person ({}) for work ({}).",
composer,
row.id,
)))?
),
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 as usize,
});
}
let person_id = &row.composer;
let person = self
.get_person(person_id)?
.ok_or(DatabaseError::Other(format!(
"Failed to get person ({}) for work ({}).",
person_id,
row.id,
)))?;
Ok(Work {
id: row.id,
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: &str) -> DatabaseResult<()> {
diesel::delete(works::table.filter(works::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all existing works by a composer and related information from other tables.
pub fn get_works(&self, composer_id: &str) -> DatabaseResult<Vec<Work>> {
let mut works: Vec<Work> = Vec::new();
let rows = works::table
.filter(works::composer.eq(composer_id))
.load::<WorkRow>(&self.connection)?;
for row in rows {
works.push(self.get_work_data(row)?);
}
Ok(works)
}
}

View file

@ -1,55 +0,0 @@
use isahc::http::StatusCode;
/// An error that can happen within the backend.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("The users login credentials were wrong.")]
LoginFailed,
#[error("The user has to be logged in to perform this action.")]
Unauthorized,
#[error("The user is not allowed to perform this action.")]
Forbidden,
#[error("The server returned an unexpected status code: {0}.")]
UnexpectedResponse(StatusCode),
#[error("A networking error happened.")]
NetworkError(#[from] isahc::Error),
#[error("A networking error happened.")]
HttpError(#[from] isahc::http::Error),
#[error(transparent)]
DatabaseError(#[from] crate::backend::DatabaseError),
#[error("An IO error happened.")]
IoError(#[from] std::io::Error),
#[error("An error happened using the SecretService.")]
SecretServiceError(#[from] secret_service::Error),
#[error("An error happened while serializing or deserializing.")]
SerdeError(#[from] serde_json::Error),
#[error("An error happened in GLib.")]
GlibError(#[from] glib::BoolError),
#[error("A channel was canceled.")]
ChannelError(#[from] futures_channel::oneshot::Canceled),
#[error("Error decoding to UTF8.")]
Utf8Error(#[from] std::str::Utf8Error),
#[error("An error happened: {0}")]
Other(&'static str),
// TODO: Remove this once anyhow has been dropped as a dependency.
#[error("An unkown error happened.")]
Unknown(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;

View file

@ -1,80 +0,0 @@
use crate::backend::{Backend, BackendState, DbThread, Player, 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);
if let Some(db) = &*self.database.borrow() {
db.stop().await?;
}
self.music_library_path.replace(Some(path.clone()));
let mut db_path = path.clone();
db_path.push("musicus.db");
let database = DbThread::new(db_path.to_str().unwrap().to_string()).await?;
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()
}
/// Notify the frontend that the library was changed.
pub fn library_changed(&self) {
self.set_state(BackendState::Loading);
self.set_state(BackendState::Ready);
}
/// Get an interface to the player and panic if there is none.
pub fn pl(&self) -> Rc<Player> {
self.get_player().unwrap()
}
}

View file

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

View file

@ -1,286 +0,0 @@
use crate::backend::{Error, Result, TrackSet};
use gstreamer_player::prelude::*;
use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::rc::Rc;
#[derive(Clone)]
pub struct PlaylistItem {
pub track_set: TrackSet,
pub indices: Vec<usize>,
}
pub struct Player {
music_library_path: PathBuf,
player: gstreamer_player::Player,
playlist: RefCell<Vec<PlaylistItem>>,
current_item: Cell<Option<usize>>,
current_track: Cell<Option<usize>>,
playing: Cell<bool>,
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>,
track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize)>>>,
duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>,
position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
}
impl Player {
pub fn new(music_library_path: PathBuf) -> Rc<Self> {
let dispatcher = gstreamer_player::PlayerGMainContextSignalDispatcher::new(None);
let player = gstreamer_player::Player::new(None, Some(&dispatcher.upcast()));
let mut config = player.get_config();
config.set_position_update_interval(250);
player.set_config(config).unwrap();
player.set_video_track_enabled(false);
let result = Rc::new(Self {
music_library_path,
player: player.clone(),
playlist: RefCell::new(Vec::new()),
current_item: Cell::new(None),
current_track: Cell::new(None),
playing: Cell::new(false),
playlist_cbs: RefCell::new(Vec::new()),
track_cbs: RefCell::new(Vec::new()),
duration_cbs: RefCell::new(Vec::new()),
playing_cbs: RefCell::new(Vec::new()),
position_cbs: RefCell::new(Vec::new()),
});
let clone = fragile::Fragile::new(result.clone());
player.connect_end_of_stream(move |_| {
let clone = clone.get();
if clone.has_next() {
clone.next().unwrap();
} else {
clone.player.stop();
clone.playing.replace(false);
for cb in &*clone.playing_cbs.borrow() {
cb(false);
}
}
});
let clone = fragile::Fragile::new(result.clone());
player.connect_position_updated(move |_, position| {
for cb in &*clone.get().position_cbs.borrow() {
cb(position.mseconds().unwrap());
}
});
let clone = fragile::Fragile::new(result.clone());
player.connect_duration_changed(move |_, duration| {
for cb in &*clone.get().duration_cbs.borrow() {
cb(duration.mseconds().unwrap());
}
});
result
}
pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) + 'static>(&self, cb: F) {
self.playlist_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_track_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) {
self.track_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_duration_cb<F: Fn(u64) + 'static>(&self, cb: F) {
self.duration_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_playing_cb<F: Fn(bool) + 'static>(&self, cb: F) {
self.playing_cbs.borrow_mut().push(Box::new(cb));
}
pub fn add_position_cb<F: Fn(u64) + 'static>(&self, cb: F) {
self.position_cbs.borrow_mut().push(Box::new(cb));
}
pub fn get_playlist(&self) -> Vec<PlaylistItem> {
self.playlist.borrow().clone()
}
pub fn get_current_item(&self) -> Option<usize> {
self.current_item.get()
}
pub fn get_current_track(&self) -> Option<usize> {
self.current_track.get()
}
pub fn get_duration(&self) -> gstreamer::ClockTime {
self.player.get_duration()
}
pub fn is_playing(&self) -> bool {
self.playing.get()
}
pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
if item.indices.is_empty() {
Err(Error::Other("Tried to add an empty playlist item!"))
} else {
let was_empty = {
let mut playlist = self.playlist.borrow_mut();
let was_empty = playlist.is_empty();
playlist.push(item);
was_empty
};
for cb in &*self.playlist_cbs.borrow() {
cb(self.playlist.borrow().clone());
}
if was_empty {
self.set_track(0, 0)?;
self.player.play();
self.playing.set(true);
for cb in &*self.playing_cbs.borrow() {
cb(true);
}
}
Ok(())
}
}
pub fn play_pause(&self) {
if self.is_playing() {
self.player.pause();
self.playing.set(false);
for cb in &*self.playing_cbs.borrow() {
cb(false);
}
} else {
self.player.play();
self.playing.set(true);
for cb in &*self.playing_cbs.borrow() {
cb(true);
}
}
}
pub fn seek(&self, ms: u64) {
self.player.seek(gstreamer::ClockTime::from_mseconds(ms));
}
pub fn has_previous(&self) -> bool {
if let Some(current_item) = self.current_item.get() {
if let Some(current_track) = self.current_track.get() {
current_track > 0 || current_item > 0
} else {
false
}
} else {
false
}
}
pub fn previous(&self) -> Result<()> {
let mut current_item = self.current_item.get()
.ok_or(Error::Other("Player tried to access non existant current item."))?;
let mut current_track = self
.current_track
.get()
.ok_or(Error::Other("Player tried to access non existant current track."))?;
let playlist = self.playlist.borrow();
if current_track > 0 {
current_track -= 1;
} else if current_item > 0 {
current_item -= 1;
current_track = playlist[current_item].indices.len() - 1;
} else {
return Err(Error::Other("No existing previous track."));
}
self.set_track(current_item, current_track)
}
pub fn has_next(&self) -> bool {
if let Some(current_item) = self.current_item.get() {
if let Some(current_track) = self.current_track.get() {
let playlist = self.playlist.borrow();
let item = &playlist[current_item];
current_track + 1 < item.indices.len() || current_item + 1 < playlist.len()
} else {
false
}
} else {
false
}
}
pub fn next(&self) -> Result<()> {
let mut current_item = self.current_item.get()
.ok_or(Error::Other("Player tried to access non existant current item."))?;
let mut current_track = self
.current_track
.get()
.ok_or(Error::Other("Player tried to access non existant current track."))?;
let playlist = self.playlist.borrow();
let item = &playlist[current_item];
if current_track + 1 < item.indices.len() {
current_track += 1;
} else if current_item + 1 < playlist.len() {
current_item += 1;
current_track = 0;
} else {
return Err(Error::Other("No existing previous track."));
}
self.set_track(current_item, current_track)
}
pub fn set_track(&self, current_item: usize, current_track: usize) -> Result<()> {
let uri = format!(
"file://{}",
self.music_library_path
.join(
self.playlist.borrow()[current_item].track_set.tracks[current_track].path.clone(),
)
.to_str()
.unwrap(),
);
self.player.set_uri(&uri);
if self.is_playing() {
self.player.play();
}
self.current_item.set(Some(current_item));
self.current_track.set(Some(current_track));
for cb in &*self.track_cbs.borrow() {
cb(current_item, current_track);
}
Ok(())
}
pub fn clear(&self) {
self.player.stop();
self.playing.set(false);
self.current_item.set(None);
self.current_track.set(None);
self.playlist.replace(Vec::new());
for cb in &*self.playing_cbs.borrow() {
cb(false);
}
for cb in &*self.playlist_cbs.borrow() {
cb(Vec::new());
}
}
}

View file

@ -1,70 +0,0 @@
use crate::backend::{LoginData, Result};
use futures_channel::oneshot;
use secret_service::{Collection, EncryptionType, SecretService};
use std::collections::HashMap;
use std::thread;
/// Savely store the user's current login credentials.
pub async fn store_login_data(data: LoginData) -> Result<()> {
let (sender, receiver) = oneshot::channel();
thread::spawn(move || sender.send(store_login_data_priv(data)).unwrap());
receiver.await?
}
/// Savely store the user's current login credentials.
fn store_login_data_priv(data: LoginData) -> Result<()> {
let ss = SecretService::new(EncryptionType::Dh)?;
let collection = get_collection(&ss)?;
let key = "musicus-login-data";
delete_secrets(&collection, key)?;
let mut attributes = HashMap::new();
attributes.insert("username", data.username.as_str());
collection.create_item(key, attributes, data.password.as_bytes(), true, "text/plain")?;
Ok(())
}
/// Get the login credentials from secret storage.
pub fn load_login_data() -> Result<Option<LoginData>> {
let ss = SecretService::new(EncryptionType::Dh)?;
let collection = get_collection(&ss)?;
let items = collection.get_all_items()?;
let key = "musicus-login-data";
let item = items.iter().find(|item| item.get_label().unwrap_or_default() == key);
Ok(match item {
Some(item) => {
// TODO: Delete the item when malformed.
let username = item.get_attributes()?.get("username").unwrap().to_owned();
let password = std::str::from_utf8(&item.get_secret()?)?.to_owned();
Some(LoginData { username, password })
}
None => None,
})
}
/// Delete all stored secrets for the provided key.
fn delete_secrets(collection: &Collection, key: &str) -> Result<()> {
let items = collection.get_all_items()?;
for item in items {
if item.get_label().unwrap_or_default() == key {
item.delete()?;
}
}
Ok(())
}
/// Get the default SecretService collection and unlock it.
fn get_collection<'a>(ss: &'a SecretService) -> Result<Collection<'a>> {
let collection = ss.get_default_collection()?;
collection.unlock()?;
Ok(collection)
}

View file

@ -1,2 +0,0 @@
pub static VERSION: &str = @VERSION@;
pub static LOCALEDIR: &str = @LOCALEDIR@;

View file

@ -1,110 +0,0 @@
use crate::backend::generate_id;
use crate::backend::{Backend, Ensemble};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
use anyhow::Result;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a ensemble.
pub struct EnsembleEditor {
handle: NavigationHandle<Ensemble>,
/// The ID of the ensemble that is edited or a newly generated one.
id: String,
editor: Editor,
name: EntryRow,
upload: UploadSection,
}
impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
/// Create a new ensemble editor and optionally initialize it.
fn new(ensemble: Option<Ensemble>, handle: NavigationHandle<Ensemble>) -> Rc<Self> {
let editor = Editor::new();
editor.set_title("Ensemble/Role");
let list = gtk::ListBoxBuilder::new()
.selection_mode(gtk::SelectionMode::None)
.build();
let name = EntryRow::new(&gettext("Name"));
list.append(&name.widget);
let section = Section::new(&gettext("General"), &list);
let upload = UploadSection::new();
editor.add_content(&section.widget);
editor.add_content(&upload.widget);
let id = match ensemble {
Some(ensemble) => {
name.set_text(&ensemble.name);
ensemble.id
}
None => generate_id(),
};
let this = Rc::new(Self {
handle,
id,
editor,
name,
upload,
});
// Connect signals and callbacks
this.editor.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.editor.set_save_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
this.editor.loading();
match this.save().await {
Ok(ensemble) => {
this.handle.pop(Some(ensemble));
}
Err(err) => {
let description = gettext!("Cause: {}", err);
this.editor.error(&gettext("Failed to save ensemble!"), &description);
}
}
});
}));
this
}
}
impl EnsembleEditor {
/// Save the ensemble and possibly upload it to the server.
async fn save(&self) -> Result<Ensemble> {
let name = self.name.get_text();
let ensemble = Ensemble {
id: self.id.clone(),
name,
};
if self.upload.get_active() {
self.handle.backend.post_ensemble(&ensemble).await?;
}
self.handle.backend.db().update_ensemble(ensemble.clone()).await?;
self.handle.backend.library_changed();
Ok(ensemble)
}
}
impl Widget for EnsembleEditor {
fn get_widget(&self) -> gtk::Widget {
self.editor.widget.clone().upcast()
}
}

View file

@ -1,110 +0,0 @@
use crate::backend::generate_id;
use crate::backend::{Backend, Instrument};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
use anyhow::Result;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a instrument.
pub struct InstrumentEditor {
handle: NavigationHandle<Instrument>,
/// The ID of the instrument that is edited or a newly generated one.
id: String,
editor: Editor,
name: EntryRow,
upload: UploadSection,
}
impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
/// Create a new instrument editor and optionally initialize it.
fn new(instrument: Option<Instrument>, handle: NavigationHandle<Instrument>) -> Rc<Self> {
let editor = Editor::new();
editor.set_title("Instrument/Role");
let list = gtk::ListBoxBuilder::new()
.selection_mode(gtk::SelectionMode::None)
.build();
let name = EntryRow::new(&gettext("Name"));
list.append(&name.widget);
let section = Section::new(&gettext("General"), &list);
let upload = UploadSection::new();
editor.add_content(&section.widget);
editor.add_content(&upload.widget);
let id = match instrument {
Some(instrument) => {
name.set_text(&instrument.name);
instrument.id
}
None => generate_id(),
};
let this = Rc::new(Self {
handle,
id,
editor,
name,
upload,
});
// Connect signals and callbacks
this.editor.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.editor.set_save_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
this.editor.loading();
match this.save().await {
Ok(instrument) => {
this.handle.pop(Some(instrument));
}
Err(err) => {
let description = gettext!("Cause: {}", err);
this.editor.error(&gettext("Failed to save instrument!"), &description);
}
}
});
}));
this
}
}
impl InstrumentEditor {
/// Save the instrument and possibly upload it to the server.
async fn save(&self) -> Result<Instrument> {
let name = self.name.get_text();
let instrument = Instrument {
id: self.id.clone(),
name,
};
if self.upload.get_active() {
self.handle.backend.post_instrument(&instrument).await?;
}
self.handle.backend.db().update_instrument(instrument.clone()).await?;
self.handle.backend.library_changed();
Ok(instrument)
}
}
impl Widget for InstrumentEditor {
fn get_widget(&self) -> gtk::Widget {
self.editor.widget.clone().upcast()
}
}

View file

@ -1,18 +0,0 @@
pub mod ensemble;
pub use ensemble::*;
pub mod instrument;
pub use instrument::*;
pub mod person;
pub use person::*;
pub mod recording;
pub use recording::*;
pub mod work;
pub use work::*;
mod performance;
mod work_part;
mod work_section;

View file

@ -1,188 +0,0 @@
use crate::backend::{Backend, Performance, Person, Ensemble, Instrument};
use crate::navigator::{NavigationHandle, Screen};
use crate::selectors::{EnsembleSelector, InstrumentSelector, PersonSelector};
use crate::widgets::{Editor, Section, ButtonRow, Widget};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing a performance within a recording.
pub struct PerformanceEditor {
handle: NavigationHandle<Performance>,
editor: Editor,
person_row: ButtonRow,
ensemble_row: ButtonRow,
role_row: ButtonRow,
reset_role_button: gtk::Button,
person: RefCell<Option<Person>>,
ensemble: RefCell<Option<Ensemble>>,
role: RefCell<Option<Instrument>>,
}
impl Screen<Option<Performance>, Performance> for PerformanceEditor {
/// Create a new performance editor.
fn new(performance: Option<Performance>, handle: NavigationHandle<Performance>) -> Rc<Self> {
let editor = Editor::new();
editor.set_title("Performance");
editor.set_may_save(false);
let performer_list = gtk::ListBoxBuilder::new()
.selection_mode(gtk::SelectionMode::None)
.build();
let person_row = ButtonRow::new("Person", "Select");
let ensemble_row = ButtonRow::new("Ensemble", "Select");
performer_list.append(&person_row.get_widget());
performer_list.append(&ensemble_row.get_widget());
let performer_section = Section::new(&gettext("Performer"), &performer_list);
performer_section.set_subtitle(
&gettext("Select either a person or an ensemble as a performer."));
let role_list = gtk::ListBoxBuilder::new()
.selection_mode(gtk::SelectionMode::None)
.build();
let reset_role_button = gtk::ButtonBuilder::new()
.icon_name("user-trash-symbolic")
.valign(gtk::Align::Center)
.visible(false)
.build();
let role_row = ButtonRow::new("Role", "Select");
role_row.widget.add_suffix(&reset_role_button);
role_list.append(&role_row.get_widget());
let role_section = Section::new(&gettext("Role"), &role_list);
role_section.set_subtitle(
&gettext("Optionally, choose a role to specify what the performer does."));
editor.add_content(&performer_section);
editor.add_content(&role_section);
let this = Rc::new(PerformanceEditor {
handle,
editor,
person_row,
ensemble_row,
role_row,
reset_role_button,
person: RefCell::new(None),
ensemble: RefCell::new(None),
role: RefCell::new(None),
});
this.editor.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.editor.set_save_cb(clone!(@weak this => move || {
let performance = Performance {
person: this.person.borrow().clone(),
ensemble: this.ensemble.borrow().clone(),
role: this.role.borrow().clone(),
};
this.handle.pop(Some(performance));
}));
this.person_row.set_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(person) = push!(this.handle, PersonSelector).await {
this.show_person(Some(&person));
this.person.replace(Some(person.clone()));
this.show_ensemble(None);
this.ensemble.replace(None);
}
});
}));
this.ensemble_row.set_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(ensemble) = push!(this.handle, EnsembleSelector).await {
this.show_person(None);
this.person.replace(None);
this.show_ensemble(Some(&ensemble));
this.ensemble.replace(None);
}
});
}));
this.role_row.set_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(role) = push!(this.handle, InstrumentSelector).await {
this.show_role(Some(&role));
this.role.replace(Some(role));
}
});
}));
this.reset_role_button.connect_clicked(clone!(@weak this => move |_| {
this.show_role(None);
this.role.replace(None);
}));
// Initialize
if let Some(performance) = performance {
if let Some(person) = performance.person {
this.show_person(Some(&person));
this.person.replace(Some(person));
} else if let Some(ensemble) = performance.ensemble {
this.show_ensemble(Some(&ensemble));
this.ensemble.replace(Some(ensemble));
}
if let Some(role) = performance.role {
this.show_role(Some(&role));
this.role.replace(Some(role));
}
}
this
}
}
impl PerformanceEditor {
/// Update the UI according to person.
fn show_person(&self, person: Option<&Person>) {
if let Some(person) = person {
self.person_row.set_subtitle(Some(&person.name_fl()));
self.editor.set_may_save(true);
} else {
self.person_row.set_subtitle(None);
}
}
/// Update the UI according to ensemble.
fn show_ensemble(&self, ensemble: Option<&Ensemble>) {
if let Some(ensemble) = ensemble {
self.ensemble_row.set_subtitle(Some(&ensemble.name));
self.editor.set_may_save(true);
} else {
self.ensemble_row.set_subtitle(None);
}
}
/// Update the UI according to role.
fn show_role(&self, role: Option<&Instrument>) {
if let Some(role) = role {
self.role_row.set_subtitle(Some(&role.name));
self.reset_role_button.show();
} else {
self.role_row.set_subtitle(None);
self.reset_role_button.hide();
}
}
}
impl Widget for PerformanceEditor {
fn get_widget(&self) -> gtk::Widget {
self.editor.widget.clone().upcast()
}
}

View file

@ -1,119 +0,0 @@
use crate::backend::generate_id;
use crate::backend::{Backend, Person};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
use anyhow::Result;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a person.
pub struct PersonEditor {
handle: NavigationHandle<Person>,
/// The ID of the person that is edited or a newly generated one.
id: String,
editor: Editor,
first_name: EntryRow,
last_name: EntryRow,
upload: UploadSection,
}
impl Screen<Option<Person>, Person> for PersonEditor {
/// Create a new person editor and optionally initialize it.
fn new(person: Option<Person>, handle: NavigationHandle<Person>) -> Rc<Self> {
let editor = Editor::new();
editor.set_title("Person");
let list = gtk::ListBoxBuilder::new()
.selection_mode(gtk::SelectionMode::None)
.build();
let first_name = EntryRow::new(&gettext("First name"));
let last_name = EntryRow::new(&gettext("Last name"));
list.append(&first_name.widget);
list.append(&last_name.widget);
let section = Section::new(&gettext("General"), &list);
let upload = UploadSection::new();
editor.add_content(&section.widget);
editor.add_content(&upload.widget);
let id = match person {
Some(person) => {
first_name.set_text(&person.first_name);
last_name.set_text(&person.last_name);
person.id
}
None => generate_id(),
};
let this = Rc::new(Self {
handle,
id,
editor,
first_name,
last_name,
upload,
});
// Connect signals and callbacks
this.editor.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.editor.set_save_cb(clone!(@strong this => move || {
spawn!(@clone this, async move {
this.editor.loading();
match this.save().await {
Ok(person) => {
this.handle.pop(Some(person));
}
Err(err) => {
let description = gettext!("Cause: {}", err);
this.editor.error(&gettext("Failed to save person!"), &description);
}
}
});
}));
this
}
}
impl PersonEditor {
/// Save the person and possibly upload it to the server.
async fn save(self: &Rc<Self>) -> Result<Person> {
let first_name = self.first_name.get_text();
let last_name = self.last_name.get_text();
let person = Person {
id: self.id.clone(),
first_name,
last_name,
};
if self.upload.get_active() {
self.handle.backend.post_person(&person).await?;
}
self.handle.backend.db().update_person(person.clone()).await?;
self.handle.backend.library_changed();
Ok(person)
}
}
impl Widget for PersonEditor {
fn get_widget(&self) -> gtk::Widget {
self.editor.widget.clone().upcast()
}
}

View file

@ -1,217 +0,0 @@
use super::performance::PerformanceEditor;
use crate::backend::generate_id;
use crate::backend::{Backend, Performance, Person, Recording, Work};
use crate::selectors::{PersonSelector, WorkSelector};
use crate::widgets::{List, Widget};
use crate::navigator::{NavigationHandle, Screen};
use anyhow::Result;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A widget for creating or editing a recording.
pub struct RecordingEditor {
handle: NavigationHandle<Recording>,
widget: gtk::Stack,
save_button: gtk::Button,
info_bar: gtk::InfoBar,
work_row: libadwaita::ActionRow,
comment_entry: gtk::Entry,
upload_switch: gtk::Switch,
performance_list: Rc<List>,
id: String,
work: RefCell<Option<Work>>,
performances: RefCell<Vec<Performance>>,
}
impl Screen<Option<Recording>, Recording> for RecordingEditor {
/// Create a new recording editor widget and optionally initialize it.
fn new(recording: Option<Recording>, handle: NavigationHandle<Recording>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, libadwaita::ActionRow, work_row);
get_widget!(builder, gtk::Button, work_button);
get_widget!(builder, gtk::Entry, comment_entry);
get_widget!(builder, gtk::Switch, upload_switch);
get_widget!(builder, gtk::Frame, performance_frame);
get_widget!(builder, gtk::Button, add_performer_button);
let performance_list = List::new();
performance_frame.set_child(Some(&performance_list.widget));
let (id, work, performances) = match recording {
Some(recording) => {
comment_entry.set_text(&recording.comment);
(recording.id, Some(recording.work), recording.performances)
}
None => (generate_id(), None, Vec::new()),
};
let this = Rc::new(RecordingEditor {
handle,
widget,
save_button,
info_bar,
work_row,
comment_entry,
upload_switch,
performance_list,
id,
work: RefCell::new(work),
performances: RefCell::new(performances),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
this.save_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
this.widget.set_visible_child_name("loading");
match this.save().await {
Ok(recording) => {
this.handle.pop(Some(recording));
}
Err(_) => {
this.info_bar.set_revealed(true);
this.widget.set_visible_child_name("content");
}
}
});
}));
work_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(work) = push!(this.handle, WorkSelector).await {
this.work_selected(&work);
this.work.replace(Some(work));
}
});
}));
this.performance_list.set_make_widget_cb(clone!(@weak this => move |index| {
let performance = &this.performances.borrow()[index];
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
delete_button.set_valign(gtk::Align::Center);
delete_button.connect_clicked(clone!(@weak this => move |_| {
let length = {
let mut performances = this.performances.borrow_mut();
performances.remove(index);
performances.len()
};
this.performance_list.update(length);
}));
let edit_button = gtk::Button::from_icon_name(Some("document-edit-symbolic"));
edit_button.set_valign(gtk::Align::Center);
edit_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
let performance = &this.performances.borrow()[index];
if let Some(performance) = push!(this.handle, PerformanceEditor, Some(performance.to_owned())).await {
let length = {
let mut performances = this.performances.borrow_mut();
performances[index] = performance;
performances.len()
};
this.performance_list.update(length);
}
});
}));
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&performance.get_title()));
row.add_suffix(&delete_button);
row.add_suffix(&edit_button);
row.set_activatable_widget(Some(&edit_button));
row.upcast()
}));
add_performer_button.connect_clicked(clone!(@strong this => move |_| {
spawn!(@clone this, async move {
if let Some(performance) = push!(this.handle, PerformanceEditor, None).await {
let length = {
let mut performances = this.performances.borrow_mut();
performances.push(performance);
performances.len()
};
this.performance_list.update(length);
}
});
}));
// Initialize
if let Some(work) = &*this.work.borrow() {
this.work_selected(work);
}
let length = this.performances.borrow().len();
this.performance_list.update(length);
this
}
}
impl RecordingEditor {
/// Update the UI according to work.
fn work_selected(&self, work: &Work) {
self.work_row.set_title(Some(&gettext("Work")));
self.work_row.set_subtitle(Some(&work.get_title()));
self.save_button.set_sensitive(true);
}
/// Save the recording and possibly upload it to the server.
async fn save(self: &Rc<Self>) -> Result<Recording> {
let recording = Recording {
id: self.id.clone(),
work: self
.work
.borrow()
.clone()
.expect("Tried to create recording without work!"),
comment: self.comment_entry.get_text().unwrap().to_string(),
performances: self.performances.borrow().clone(),
};
let upload = self.upload_switch.get_active();
if upload {
self.handle.backend.post_recording(&recording).await?;
}
self.handle.backend
.db()
.update_recording(recording.clone().into())
.await
.unwrap();
self.handle.backend.library_changed();
Ok(recording)
}
}
impl Widget for RecordingEditor {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,358 +0,0 @@
use super::work_part::WorkPartEditor;
use crate::backend::generate_id;
use crate::backend::{Instrument, Person, Work, WorkPart, WorkSection};
use super::work_section::WorkSectionEditor;
use crate::selectors::{InstrumentSelector, PersonSelector};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{List, Widget};
use anyhow::Result;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::convert::TryInto;
use std::rc::Rc;
/// Either a work part or a work section.
#[derive(Clone)]
enum PartOrSection {
Part(WorkPart),
Section(WorkSection),
}
impl PartOrSection {
pub fn get_title(&self) -> String {
match self {
PartOrSection::Part(part) => part.title.clone(),
PartOrSection::Section(section) => section.title.clone(),
}
}
}
/// A widget for editing and creating works.
pub struct WorkEditor {
handle: NavigationHandle<Work>,
widget: gtk::Stack,
save_button: gtk::Button,
title_entry: gtk::Entry,
info_bar: gtk::InfoBar,
composer_row: libadwaita::ActionRow,
upload_switch: gtk::Switch,
instrument_list: Rc<List>,
part_list: Rc<List>,
id: String,
composer: RefCell<Option<Person>>,
instruments: RefCell<Vec<Instrument>>,
structure: RefCell<Vec<PartOrSection>>,
}
impl Screen<Option<Work>, Work> for WorkEditor {
/// Create a new work editor widget and optionally initialize it.
fn new(work: Option<Work>, handle: NavigationHandle<Work>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Entry, title_entry);
get_widget!(builder, gtk::Button, composer_button);
get_widget!(builder, libadwaita::ActionRow, composer_row);
get_widget!(builder, gtk::Switch, upload_switch);
get_widget!(builder, gtk::Frame, instrument_frame);
get_widget!(builder, gtk::Button, add_instrument_button);
get_widget!(builder, gtk::Frame, structure_frame);
get_widget!(builder, gtk::Button, add_part_button);
get_widget!(builder, gtk::Button, add_section_button);
let instrument_list = List::new();
instrument_frame.set_child(Some(&instrument_list.widget));
let part_list = List::new();
part_list.set_enable_dnd(true);
structure_frame.set_child(Some(&part_list.widget));
let (id, composer, instruments, structure) = match work {
Some(work) => {
title_entry.set_text(&work.title);
let mut structure = Vec::new();
for part in work.parts {
structure.push(PartOrSection::Part(part));
}
for section in work.sections {
structure.insert(
section.before_index.try_into().unwrap(),
PartOrSection::Section(section),
);
}
(work.id, Some(work.composer), work.instruments, structure)
}
None => (generate_id(), None, Vec::new(), Vec::new()),
};
let this = Rc::new(Self {
handle,
widget,
save_button,
id,
info_bar,
title_entry,
composer_row,
upload_switch,
instrument_list,
part_list,
composer: RefCell::new(composer),
instruments: RefCell::new(instruments),
structure: RefCell::new(structure),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
this.save_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
this.widget.set_visible_child_name("loading");
match this.save().await {
Ok(work) => {
this.handle.pop(Some(work));
}
Err(_) => {
this.info_bar.set_revealed(true);
this.widget.set_visible_child_name("content");
}
}
});
}));
composer_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(person) = push!(this.handle, PersonSelector).await {
this.show_composer(&person);
this.composer.replace(Some(person.to_owned()));
}
});
}));
this.instrument_list.set_make_widget_cb(clone!(@weak this => move |index| {
let instrument = &this.instruments.borrow()[index];
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
delete_button.set_valign(gtk::Align::Center);
delete_button.connect_clicked(clone!(@strong this => move |_| {
let length = {
let mut instruments = this.instruments.borrow_mut();
instruments.remove(index);
instruments.len()
};
this.instrument_list.update(length);
}));
let row = libadwaita::ActionRow::new();
row.set_title(Some(&instrument.name));
row.add_suffix(&delete_button);
row.upcast()
}));
add_instrument_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(instrument) = push!(this.handle, InstrumentSelector).await {
let length = {
let mut instruments = this.instruments.borrow_mut();
instruments.push(instrument.clone());
instruments.len()
};
this.instrument_list.update(length);
}
});
}));
this.part_list.set_make_widget_cb(clone!(@weak this => move |index| {
let pos = &this.structure.borrow()[index];
let delete_button = gtk::Button::from_icon_name(Some("user-trash-symbolic"));
delete_button.set_valign(gtk::Align::Center);
delete_button.connect_clicked(clone!(@weak this => move |_| {
let length = {
let mut structure = this.structure.borrow_mut();
structure.remove(index);
structure.len()
};
this.part_list.update(length);
}));
let edit_button = gtk::Button::from_icon_name(Some("document-edit-symbolic"));
edit_button.set_valign(gtk::Align::Center);
edit_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
match this.structure.borrow()[index].clone() {
PartOrSection::Part(part) => {
if let Some(part) = push!(this.handle, WorkPartEditor, Some(part)).await {
let length = {
let mut structure = this.structure.borrow_mut();
structure[index] = PartOrSection::Part(part);
structure.len()
};
this.part_list.update(length);
}
}
PartOrSection::Section(section) => {
if let Some(section) = push!(this.handle, WorkSectionEditor, Some(section)).await {
let length = {
let mut structure = this.structure.borrow_mut();
structure[index] = PartOrSection::Section(section);
structure.len()
};
this.part_list.update(length);
}
}
}
});
}));
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&pos.get_title()));
row.add_suffix(&delete_button);
row.add_suffix(&edit_button);
row.set_activatable_widget(Some(&edit_button));
if let PartOrSection::Part(_) = pos {
// TODO: Replace with better solution to differentiate parts and sections.
row.set_margin_start(12);
}
row.upcast()
}));
this.part_list.set_move_cb(clone!(@weak this => move |old_index, new_index| {
let length = {
let mut structure = this.structure.borrow_mut();
structure.swap(old_index, new_index);
structure.len()
};
this.part_list.update(length);
}));
add_part_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(part) = push!(this.handle, WorkPartEditor, None).await {
let length = {
let mut structure = this.structure.borrow_mut();
structure.push(PartOrSection::Part(part));
structure.len()
};
this.part_list.update(length);
}
});
}));
add_section_button.connect_clicked(clone!(@strong this => move |_| {
spawn!(@clone this, async move {
if let Some(section) = push!(this.handle, WorkSectionEditor, None).await {
let length = {
let mut structure = this.structure.borrow_mut();
structure.push(PartOrSection::Section(section));
structure.len()
};
this.part_list.update(length);
}
});
}));
// Initialization
if let Some(composer) = &*this.composer.borrow() {
this.show_composer(composer);
}
this.instrument_list.update(this.instruments.borrow().len());
this.part_list.update(this.structure.borrow().len());
this
}
}
impl WorkEditor {
/// Update the UI according to person.
fn show_composer(&self, person: &Person) {
self.composer_row.set_title(Some(&gettext("Composer")));
self.composer_row.set_subtitle(Some(&person.name_fl()));
self.save_button.set_sensitive(true);
}
/// Save the work and possibly upload it to the server.
async fn save(self: &Rc<Self>) -> Result<Work> {
let mut section_count: usize = 0;
let mut parts = Vec::new();
let mut sections = Vec::new();
for (index, pos) in self.structure.borrow().iter().enumerate() {
match pos {
PartOrSection::Part(part) => parts.push(part.clone()),
PartOrSection::Section(section) => {
let mut section = section.clone();
section.before_index = index - section_count;
sections.push(section);
section_count += 1;
}
}
}
let work = Work {
id: self.id.clone(),
title: self.title_entry.get_text().unwrap().to_string(),
composer: self
.composer
.borrow()
.clone()
.expect("Tried to create work without composer!"),
instruments: self.instruments.borrow().clone(),
parts: parts,
sections: sections,
};
let upload = self.upload_switch.get_active();
if upload {
self.handle.backend.post_work(&work).await?;
}
self.handle.backend
.db()
.update_work(work.clone().into())
.await
.unwrap();
self.handle.backend.library_changed();
Ok(work)
}
}
impl Widget for WorkEditor {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,114 +0,0 @@
use crate::backend::{Person, WorkPart};
use crate::selectors::PersonSelector;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a work part.
pub struct WorkPartEditor {
handle: NavigationHandle<WorkPart>,
widget: gtk::Box,
title_entry: gtk::Entry,
composer_row: libadwaita::ActionRow,
reset_composer_button: gtk::Button,
composer: RefCell<Option<Person>>,
}
impl Screen<Option<WorkPart>, WorkPart> for WorkPartEditor {
/// Create a new part editor and optionally initialize it.
fn new(part: Option<WorkPart>, handle: NavigationHandle<WorkPart>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_part_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, title_entry);
get_widget!(builder, gtk::Button, composer_button);
get_widget!(builder, libadwaita::ActionRow, composer_row);
get_widget!(builder, gtk::Button, reset_composer_button);
let composer = match part {
Some(part) => {
title_entry.set_text(&part.title);
part.composer
}
None => None,
};
let this = Rc::new(Self {
handle,
widget,
title_entry,
composer_row,
reset_composer_button,
composer: RefCell::new(composer),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
save_button.connect_clicked(clone!(@weak this => move |_| {
let part = WorkPart {
title: this.title_entry.get_text().unwrap().to_string(),
composer: this.composer.borrow().clone(),
};
this.handle.pop(Some(part));
}));
composer_button.connect_clicked(clone!(@strong this => move |_| {
spawn!(@clone this, async move {
if let Some(person) = push!(this.handle, PersonSelector).await {
this.show_composer(Some(&person));
this.composer.replace(Some(person.to_owned()));
}
});
}));
this.reset_composer_button
.connect_clicked(clone!(@strong this => move |_| {
this.composer.replace(None);
this.show_composer(None);
}));
// Initialize
if let Some(composer) = &*this.composer.borrow() {
this.show_composer(Some(composer));
}
this
}
}
impl WorkPartEditor {
/// Update the UI according to person.
fn show_composer(&self, person: Option<&Person>) {
if let Some(person) = person {
self.composer_row.set_title(Some(&gettext("Composer")));
self.composer_row.set_subtitle(Some(&person.name_fl()));
self.reset_composer_button.show();
} else {
self.composer_row.set_title(Some(&gettext("Select a composer")));
self.composer_row.set_subtitle(None);
self.reset_composer_button.hide();
}
}
}
impl Widget for WorkPartEditor {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,62 +0,0 @@
use crate::backend::WorkSection;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a work section.
pub struct WorkSectionEditor {
handle: NavigationHandle<WorkSection>,
widget: gtk::Box,
title_entry: gtk::Entry,
}
impl Screen<Option<WorkSection>, WorkSection> for WorkSectionEditor {
/// Create a new section editor and optionally initialize it.
fn new(section: Option<WorkSection>, handle: NavigationHandle<WorkSection>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_section_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, title_entry);
if let Some(section) = section {
title_entry.set_text(&section.title);
}
let this = Rc::new(Self {
handle,
widget,
title_entry,
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
save_button.connect_clicked(clone!(@weak this => move |_| {
let section = WorkSection {
before_index: 0,
title: this.title_entry.get_text().unwrap().to_string(),
};
this.handle.pop(Some(section));
}));
this
}
}
impl Widget for WorkSectionEditor {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,189 +0,0 @@
use super::source::{Source, SourceTrack};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use discid::DiscId;
use futures_channel::oneshot;
use gettextrs::gettext;
use gstreamer::prelude::*;
use gstreamer::{Element, ElementFactory, Pipeline};
use once_cell::sync::OnceCell;
use std::path::{Path, PathBuf};
use std::thread;
/// Representation of an audio CD being imported as a medium.
#[derive(Clone, Debug)]
pub struct DiscSource {
/// The MusicBrainz DiscID of the CD.
pub discid: OnceCell<String>,
/// The tracks on this disc.
tracks: OnceCell<Vec<SourceTrack>>,
}
impl DiscSource {
/// Create a new disc source. The source has to be initialized by calling
/// load() afterwards.
pub fn new() -> Result<Self> {
let result = Self {
discid: OnceCell::new(),
tracks: OnceCell::new(),
};
Ok(result)
}
/// Load the disc from the default disc drive and return the MusicBrainz
/// DiscID as well as descriptions of the contained tracks.
fn load_priv() -> Result<(String, Vec<SourceTrack>)> {
let discid = DiscId::read(None)?;
let id = discid.id();
let mut tracks = Vec::new();
let first_track = discid.first_track_num() as u32;
let last_track = discid.last_track_num() as u32;
let tmp_dir = Self::create_tmp_dir()?;
for number in first_track..=last_track {
let name = gettext!("Track {}", number);
let file_name = format!("track_{:02}.flac", number);
let mut path = tmp_dir.clone();
path.push(file_name);
let track = SourceTrack {
number,
name,
path,
};
tracks.push(track);
}
Ok((id, tracks))
}
/// Create a new temporary directory and return its path.
// TODO: Move to a more appropriate place.
fn create_tmp_dir() -> Result<PathBuf> {
let mut tmp_dir = glib::get_tmp_dir()
.ok_or_else(|| {
anyhow!("Failed to get temporary directory using glib::get_tmp_dir()!")
})?;
let dir_name = format!("musicus-{}", rand::random::<u64>());
tmp_dir.push(dir_name);
std::fs::create_dir(&tmp_dir)?;
Ok(tmp_dir)
}
/// Rip one track.
fn rip_track(path: &Path, number: u32) -> Result<()> {
let pipeline = Self::build_pipeline(path, number)?;
let bus = pipeline
.get_bus()
.ok_or_else(|| anyhow!("Failed to get bus from pipeline!"))?;
pipeline.set_state(gstreamer::State::Playing)?;
for msg in bus.iter_timed(gstreamer::CLOCK_TIME_NONE) {
use gstreamer::MessageView::*;
match msg.view() {
Eos(..) => break,
Error(err) => {
pipeline.set_state(gstreamer::State::Null)?;
bail!("GStreamer error: {:?}!", err);
}
_ => (),
}
}
pipeline.set_state(gstreamer::State::Null)?;
Ok(())
}
/// Build the GStreamer pipeline to rip a track.
fn build_pipeline(path: &Path, number: u32) -> Result<Pipeline> {
let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?;
cdparanoiasrc.set_property("track", &number)?;
let queue = ElementFactory::make("queue", None)?;
let audioconvert = ElementFactory::make("audioconvert", None)?;
let flacenc = ElementFactory::make("flacenc", None)?;
let path_str = path.to_str().ok_or_else(|| {
anyhow!("Failed to convert path '{:?}' to string!", path)
})?;
let filesink = gstreamer::ElementFactory::make("filesink", None)?;
filesink.set_property("location", &path_str.to_owned())?;
let pipeline = gstreamer::Pipeline::new(None);
pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Ok(pipeline)
}
}
#[async_trait]
impl Source for DiscSource {
async fn load(&self) -> Result<()> {
let (sender, receiver) = oneshot::channel();
thread::spawn(|| {
let result = Self::load_priv();
sender.send(result).unwrap();
});
let (discid, tracks) = receiver.await??;
self.discid.set(discid);
self.tracks.set(tracks);
Ok(())
}
fn tracks(&self) -> Option<&[SourceTrack]> {
match self.tracks.get() {
Some(tracks) => Some(tracks.as_slice()),
None => None,
}
}
fn discid(&self) -> Option<String> {
match self.discid.get() {
Some(discid) => Some(discid.to_owned()),
None => None,
}
}
async fn copy(&self) -> Result<()> {
let tracks = self.tracks.get()
.ok_or_else(|| anyhow!("Tried to copy disc before loading has finished!"))?;
for track in tracks {
let (sender, receiver) = oneshot::channel();
let number = track.number;
let path = track.path.clone();
thread::spawn(move || {
let result = Self::rip_track(&path, number);
sender.send(result).unwrap();
});
receiver.await??;
}
Ok(())
}
}

View file

@ -1,90 +0,0 @@
use super::source::{Source, SourceTrack};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures_channel::oneshot;
use once_cell::sync::OnceCell;
use std::path::{Path, PathBuf};
use std::thread;
/// A folder outside of the music library that contains tracks to import.
#[derive(Clone, Debug)]
pub struct FolderSource {
/// The path to the folder.
path: PathBuf,
/// The tracks within the folder.
tracks: OnceCell<Vec<SourceTrack>>,
}
impl FolderSource {
/// Create a new folder source.
pub fn new(path: PathBuf) -> Self {
Self {
path,
tracks: OnceCell::new(),
}
}
/// Load the contents of the folder as tracks.
fn load_priv(path: &Path) -> Result<Vec<SourceTrack>> {
let mut tracks = Vec::new();
let mut number = 1;
for entry in std::fs::read_dir(path)? {
let entry = entry?;
if entry.file_type()?.is_file() {
let name = entry
.file_name()
.into_string()
.or_else(|_| Err(anyhow!("Failed to convert OsString to String!")))?;
let path = entry.path();
let track = SourceTrack {
number,
name,
path,
};
tracks.push(track);
number += 1;
}
}
Ok(tracks)
}
}
#[async_trait]
impl Source for FolderSource {
async fn load(&self) -> Result<()> {
let (sender, receiver) = oneshot::channel();
let path = self.path.clone();
thread::spawn(move || {
let result = Self::load_priv(&path);
sender.send(result).unwrap();
});
let tracks = receiver.await??;
self.tracks.set(tracks);
Ok(())
}
fn tracks(&self) -> Option<&[SourceTrack]> {
match self.tracks.get() {
Some(tracks) => Some(tracks.as_slice()),
None => None,
}
}
fn discid(&self) -> Option<String> {
None
}
async fn copy(&self) -> Result<()> {
Ok(())
}
}

View file

@ -1,213 +0,0 @@
use super::source::Source;
use super::track_set_editor::{TrackSetData, TrackSetEditor};
use crate::backend::generate_id;
use crate::backend::{Backend, Medium, Track, TrackSet};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{List, Widget};
use anyhow::{anyhow, Result};
use glib::clone;
use glib::prelude::*;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing metadata while importing music into the music library.
pub struct MediumEditor {
handle: NavigationHandle<()>,
source: Rc<Box<dyn Source>>,
widget: gtk::Stack,
done_button: gtk::Button,
done_stack: gtk::Stack,
done: gtk::Image,
name_entry: gtk::Entry,
publish_switch: gtk::Switch,
track_set_list: Rc<List>,
track_sets: RefCell<Vec<TrackSetData>>,
}
impl Screen<Rc<Box<dyn Source>>, ()> for MediumEditor {
/// Create a new medium editor.
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<()>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, done_button);
get_widget!(builder, gtk::Stack, done_stack);
get_widget!(builder, gtk::Image, done);
get_widget!(builder, gtk::Entry, name_entry);
get_widget!(builder, gtk::Switch, publish_switch);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Frame, frame);
let list = List::new();
frame.set_child(Some(&list.widget));
let this = Rc::new(Self {
handle,
source,
widget,
done_button,
done_stack,
done,
name_entry,
publish_switch,
track_set_list: list,
track_sets: RefCell::new(Vec::new()),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
this.done_button.connect_clicked(clone!(@weak this => move |_| {
this.widget.set_visible_child_name("loading");
spawn!(@clone this, async move {
match this.save().await {
Ok(_) => (),
Err(err) => {
// TODO: Display errors.
println!("{:?}", err);
}
}
});
}));
add_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(track_set) = push!(this.handle, TrackSetEditor, Rc::clone(&this.source)).await {
let length = {
let mut track_sets = this.track_sets.borrow_mut();
track_sets.push(track_set);
track_sets.len()
};
this.track_set_list.update(length);
}
});
}));
this.track_set_list.set_make_widget_cb(clone!(@weak this => move |index| {
let track_set = &this.track_sets.borrow()[index];
let title = track_set.recording.work.get_title();
let subtitle = track_set.recording.get_performers();
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
let edit_button = gtk::Button::new();
edit_button.set_has_frame(false);
edit_button.set_valign(gtk::Align::Center);
edit_button.set_child(Some(&edit_image));
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&title));
row.set_subtitle(Some(&subtitle));
row.add_suffix(&edit_button);
row.set_activatable_widget(Some(&edit_button));
edit_button.connect_clicked(clone!(@weak this => move |_| {
// TODO: Implement editing.
}));
row.upcast()
}));
spawn!(@clone this, async move {
match this.source.copy().await {
Err(error) => {
// TODO: Present error.
println!("Failed to copy source: {}", error);
},
Ok(_) => {
this.done_stack.set_visible_child(&this.done);
this.done_button.set_sensitive(true);
}
}
});
this
}
}
impl MediumEditor {
/// Save the medium and possibly upload it to the server.
async fn save(&self) -> Result<()> {
let name = self.name_entry.get_text().unwrap().to_string();
// Create a new directory in the music library path for the imported medium.
let mut path = self.handle.backend.get_music_library_path().unwrap().clone();
path.push(&name);
std::fs::create_dir(&path)?;
// Convert the track set data to real track sets.
let mut track_sets = Vec::new();
let source_tracks = self.source.tracks().ok_or_else(|| anyhow!("Tracks not loaded!"))?;
for track_set_data in &*self.track_sets.borrow() {
let mut tracks = Vec::new();
for track_data in &track_set_data.tracks {
// Copy the corresponding audio file to the music library.
let track_source = &source_tracks[track_data.track_source];
let mut track_path = path.clone();
track_path.push(track_source.path.file_name().unwrap());
std::fs::copy(&track_source.path, &track_path)?;
// Create the real track.
let track = Track {
work_parts: track_data.work_parts.clone(),
path: track_path.to_str().unwrap().to_owned(),
};
tracks.push(track);
}
let track_set = TrackSet {
recording: track_set_data.recording.clone(),
tracks,
};
track_sets.push(track_set);
}
let medium = Medium {
id: generate_id(),
name: self.name_entry.get_text().unwrap().to_string(),
discid: self.source.discid(),
tracks: track_sets,
};
let upload = self.publish_switch.get_active();
if upload {
self.handle.backend.post_medium(&medium).await?;
}
self.handle.backend
.db()
.update_medium(medium.clone())
.await?;
self.handle.backend.library_changed();
Ok(())
}
}
impl Widget for MediumEditor {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,10 +0,0 @@
mod disc_source;
mod folder_source;
mod medium_editor;
mod source;
mod source_selector;
mod track_editor;
mod track_selector;
mod track_set_editor;
pub use source_selector::SourceSelector;

View file

@ -1,39 +0,0 @@
use anyhow::Result;
use async_trait::async_trait;
use std::path::PathBuf;
/// A source for tracks that can be imported into the music library.
#[async_trait]
pub trait Source {
/// Load the source and discover the contained tracks.
async fn load(&self) -> Result<()>;
/// Get a reference to the tracks within this source, if they are ready.
fn tracks(&self) -> Option<&[SourceTrack]>;
/// Get the DiscID of the corresponging medium, if possible.
fn discid(&self) -> Option<String>;
/// Asynchronously copy the tracks to the files that are advertised within
/// their corresponding objects.
async fn copy(&self) -> Result<()>;
}
/// Representation of a single track on a source.
#[derive(Clone, Debug)]
pub struct SourceTrack {
/// The track number. This is different from the index in the disc
/// source's tracks list, because it is not defined from which number the
/// the track numbers start.
pub number: u32,
/// A human readable identifier for the track. This will be used to
/// present the track for selection.
pub name: String,
/// The path to the file where the corresponding audio file is. This file
/// is only required to exist, once the source's copy method has finished.
/// This will not be the actual file within the user's music library, but
/// the location from which it can be copied to the music library.
pub path: PathBuf,
}

View file

@ -1,119 +0,0 @@
use super::medium_editor::MediumEditor;
use super::disc_source::DiscSource;
use super::folder_source::FolderSource;
use super::source::Source;
use crate::backend::Backend;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
/// A dialog for starting to import music.
pub struct SourceSelector {
handle: NavigationHandle<()>,
widget: gtk::Box,
stack: gtk::Stack,
info_bar: gtk::InfoBar,
}
impl Screen<(), ()> for SourceSelector {
/// Create a new source selector.
fn new(_: (), handle: NavigationHandle<()>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, folder_button);
get_widget!(builder, gtk::Button, disc_button);
let this = Rc::new(Self {
handle,
widget,
stack,
info_bar,
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
folder_button.connect_clicked(clone!(@weak this => move |_| {
let dialog = gtk::FileChooserDialog::new(
Some(&gettext("Select folder")),
Some(&this.handle.window),
gtk::FileChooserAction::SelectFolder,
&[
(&gettext("Cancel"), gtk::ResponseType::Cancel),
(&gettext("Select"), gtk::ResponseType::Accept),
]);
dialog.connect_response(clone!(@weak this => move |dialog, response| {
this.stack.set_visible_child_name("loading");
dialog.hide();
if let gtk::ResponseType::Accept = response {
if let Some(file) = dialog.get_file() {
if let Some(path) = file.get_path() {
spawn!(@clone this, async move {
let folder = FolderSource::new(PathBuf::from(path));
match folder.load().await {
Ok(_) => {
let source = Rc::new(Box::new(folder) as Box<dyn Source>);
push!(this.handle, MediumEditor, source).await;
this.handle.pop(Some(()));
}
Err(_) => {
// TODO: Present error.
this.info_bar.set_revealed(true);
this.stack.set_visible_child_name("start");
}
}
});
}
}
}
}));
dialog.show();
}));
disc_button.connect_clicked(clone!(@weak this => move |_| {
this.stack.set_visible_child_name("loading");
spawn!(@clone this, async move {
let disc = DiscSource::new().unwrap();
match disc.load().await {
Ok(_) => {
let source = Rc::new(Box::new(disc) as Box<dyn Source>);
push!(this.handle, MediumEditor, source).await;
this.handle.pop(Some(()));
}
Err(_) => {
// TODO: Present error.
this.info_bar.set_revealed(true);
this.stack.set_visible_child_name("start");
}
}
});
}));
this
}
}
impl Widget for SourceSelector {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,84 +0,0 @@
use crate::backend::Recording;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for editing a single track.
pub struct TrackEditor {
handle: NavigationHandle<Vec<usize>>,
widget: gtk::Box,
selection: RefCell<Vec<usize>>,
}
impl Screen<(Recording, Vec<usize>), Vec<usize>> for TrackEditor {
/// Create a new track editor.
fn new((recording, selection): (Recording, Vec<usize>), handle: NavigationHandle<Vec<usize>>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, select_button);
get_widget!(builder, gtk::Frame, parts_frame);
let parts_list = gtk::ListBox::new();
parts_list.set_selection_mode(gtk::SelectionMode::None);
parts_list.set_vexpand(false);
parts_list.show();
parts_frame.set_child(Some(&parts_list));
let this = Rc::new(Self {
handle,
widget,
selection: RefCell::new(selection),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
select_button.connect_clicked(clone!(@weak this => move |_| {
let selection = this.selection.borrow().clone();
this.handle.pop(Some(selection));
}));
for (index, part) in recording.work.parts.iter().enumerate() {
let check = gtk::CheckButton::new();
check.set_active(this.selection.borrow().contains(&index));
check.connect_toggled(clone!(@weak this => move |check| {
let mut selection = this.selection.borrow_mut();
if check.get_active() {
selection.push(index);
} else {
if let Some(pos) = selection.iter().position(|part| *part == index) {
selection.remove(pos);
}
}
}));
let row = libadwaita::ActionRow::new();
row.add_prefix(&check);
row.set_activatable_widget(Some(&check));
row.set_title(Some(&part.title));
parts_list.append(&row);
}
this
}
}
impl Widget for TrackEditor {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,96 +0,0 @@
use super::source::Source;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting tracks from a source.
pub struct TrackSelector {
handle: NavigationHandle<Vec<usize>>,
source: Rc<Box<dyn Source>>,
widget: gtk::Box,
select_button: gtk::Button,
selection: RefCell<Vec<usize>>,
}
impl Screen<Rc<Box<dyn Source>>, Vec<usize>> for TrackSelector {
/// Create a new track selector.
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<Vec<usize>>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, select_button);
get_widget!(builder, gtk::Frame, tracks_frame);
let track_list = gtk::ListBox::new();
track_list.set_selection_mode(gtk::SelectionMode::None);
track_list.set_vexpand(false);
track_list.show();
tracks_frame.set_child(Some(&track_list));
let this = Rc::new(Self {
handle,
source,
widget,
select_button,
selection: RefCell::new(Vec::new()),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
this.select_button.connect_clicked(clone!(@weak this => move |_| {
let selection = this.selection.borrow().clone();
this.handle.pop(Some(selection));
}));
let tracks = this.source.tracks().unwrap();
for (index, track) in tracks.iter().enumerate() {
let check = gtk::CheckButton::new();
check.connect_toggled(clone!(@weak this => move |check| {
let mut selection = this.selection.borrow_mut();
if check.get_active() {
selection.push(index);
} else {
if let Some(pos) = selection.iter().position(|part| *part == index) {
selection.remove(pos);
}
}
if selection.is_empty() {
this.select_button.set_sensitive(false);
} else {
this.select_button.set_sensitive(true);
}
}));
let row = libadwaita::ActionRow::new();
row.add_prefix(&check);
row.set_activatable_widget(Some(&check));
row.set_activatable(true);
row.set_title(Some(&track.name));
track_list.append(&row);
}
this
}
}
impl Widget for TrackSelector {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,220 +0,0 @@
use super::source::Source;
use super::track_editor::TrackEditor;
use super::track_selector::TrackSelector;
use crate::backend::{Backend, Recording};
use crate::navigator::{NavigationHandle, Screen};
use crate::selectors::{PersonSelector, RecordingSelector};
use crate::widgets::{List, Widget};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A track set before being imported.
#[derive(Clone, Debug)]
pub struct TrackSetData {
pub recording: Recording,
pub tracks: Vec<TrackData>,
}
/// A track before being imported.
#[derive(Clone, Debug)]
pub struct TrackData {
/// Index of the track source within the medium source's tracks.
pub track_source: usize,
/// Actual track data.
pub work_parts: Vec<usize>,
}
/// A screen for editing a set of tracks for one recording.
pub struct TrackSetEditor {
handle: NavigationHandle<TrackSetData>,
source: Rc<Box<dyn Source>>,
widget: gtk::Box,
save_button: gtk::Button,
recording_row: libadwaita::ActionRow,
track_list: Rc<List>,
recording: RefCell<Option<Recording>>,
tracks: RefCell<Vec<TrackData>>,
}
impl Screen<Rc<Box<dyn Source>>, TrackSetData> for TrackSetEditor {
/// Create a new track set editor.
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<TrackSetData>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, libadwaita::ActionRow, recording_row);
get_widget!(builder, gtk::Button, select_recording_button);
get_widget!(builder, gtk::Button, edit_tracks_button);
get_widget!(builder, gtk::Frame, tracks_frame);
let track_list = List::new();
tracks_frame.set_child(Some(&track_list.widget));
let this = Rc::new(Self {
handle,
source,
widget,
save_button,
recording_row,
track_list,
recording: RefCell::new(None),
tracks: RefCell::new(Vec::new()),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
this.save_button.connect_clicked(clone!(@weak this => move |_| {
let data = TrackSetData {
recording: this.recording.borrow().clone().unwrap(),
tracks: this.tracks.borrow().clone(),
};
this.handle.pop(Some(data));
}));
select_recording_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(recording) = push!(this.handle, RecordingSelector).await {
this.recording.replace(Some(recording));
this.recording_selected();
}
});
}));
edit_tracks_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(selection) = push!(this.handle, TrackSelector, Rc::clone(&this.source)).await {
let mut tracks = Vec::new();
for index in selection {
let data = TrackData {
track_source: index,
work_parts: Vec::new(),
};
tracks.push(data);
}
let length = tracks.len();
this.tracks.replace(tracks);
this.track_list.update(length);
this.autofill_parts();
}
});
}));
this.track_list.set_make_widget_cb(clone!(@weak this => move |index| {
let track = &this.tracks.borrow()[index];
let mut title_parts = Vec::<String>::new();
if let Some(recording) = &*this.recording.borrow() {
for part in &track.work_parts {
title_parts.push(recording.work.parts[*part].title.clone());
}
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let tracks = this.source.tracks().unwrap();
let track_name = &tracks[track.track_source].name;
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
let edit_button = gtk::Button::new();
edit_button.set_has_frame(false);
edit_button.set_valign(gtk::Align::Center);
edit_button.set_child(Some(&edit_image));
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&title));
row.set_subtitle(Some(track_name));
row.add_suffix(&edit_button);
row.set_activatable_widget(Some(&edit_button));
edit_button.connect_clicked(clone!(@weak this => move |_| {
let recording = this.recording.borrow().clone();
if let Some(recording) = recording {
spawn!(@clone this, async move {
let track = &this.tracks.borrow()[index];
if let Some(selection) = push!(this.handle, TrackEditor, (recording, track.work_parts.clone())).await {
{
let mut tracks = this.tracks.borrow_mut();
let mut track = &mut tracks[index];
track.work_parts = selection;
};
this.update_tracks();
}
});
}
}));
row.upcast()
}));
this
}
}
impl TrackSetEditor {
/// Set everything up after selecting a recording.
fn recording_selected(&self) {
if let Some(recording) = &*self.recording.borrow() {
self.recording_row.set_title(Some(&recording.work.get_title()));
self.recording_row.set_subtitle(Some(&recording.get_performers()));
self.save_button.set_sensitive(true);
}
self.autofill_parts();
}
/// Automatically try to put work part information from the selected recording into the
/// selected tracks.
fn autofill_parts(&self) {
if let Some(recording) = &*self.recording.borrow() {
let mut tracks = self.tracks.borrow_mut();
for (index, _) in recording.work.parts.iter().enumerate() {
if let Some(mut track) = tracks.get_mut(index) {
track.work_parts = vec![index];
} else {
break;
}
}
}
self.update_tracks();
}
/// Update the track list.
fn update_tracks(&self) {
let length = self.tracks.borrow().len();
self.track_list.update(length);
}
}
impl Widget for TrackSetEditor {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,86 +0,0 @@
/// Simplification for pushing new screens.
///
/// This macro can be invoked in two forms.
///
/// 1. To push screens without an input value:
///
/// ```
/// let result = push!(handle, ScreenType).await;
/// ```
///
/// 2. To push screens with an input value:
///
/// ```
/// let result = push!(handle, ScreenType, input).await;
/// ```
#[macro_export]
macro_rules! push {
($handle:expr, $screen:ty) => {
$handle.push::<_, _, $screen>(())
};
($handle:expr, $screen:ty, $input:expr) => {
$handle.push::<_, _, $screen>($input)
};
}
/// Simplification for replacing the current navigator screen.
///
/// This macro can be invoked in two forms.
///
/// 1. To replace with screens without an input value:
///
/// ```
/// let result = replace!(navigator, ScreenType).await;
/// ```
///
/// 2. To replace with screens with an input value:
///
/// ```
/// let result = replace!(navigator, ScreenType, input).await;
/// ```
#[macro_export]
macro_rules! replace {
($navigator:expr, $screen:ty) => {
$navigator.replace::<_, _, $screen>(())
};
($navigator:expr, $screen:ty, $input:expr) => {
$navigator.replace::<_, _, $screen>($input)
};
}
/// Spawn a future on the GLib MainContext.
///
/// This can be invoked in the following forms:
///
/// 1. For spawning a future and nothing more:
///
/// ```
/// spawn!(async {
/// // Some code
/// });
///
/// 2. For spawning a future and cloning some data, that will be accessible
/// from the async code:
///
/// ```
/// spawn!(@clone data: Rc<_>, async move {
/// // Some code
/// });
#[macro_export]
macro_rules! spawn {
($future:expr) => {
{
let context = glib::MainContext::default();
context.spawn_local($future);
}
};
(@clone $data:ident, $future:expr) => {
{
let context = glib::MainContext::default();
let $data = Rc::clone(&$data);
context.spawn_local($future);
}
};
}

View file

@ -1,57 +0,0 @@
// Required for database/schema.rs
#[macro_use]
extern crate diesel;
// Required for embed_migrations macro in database/database.rs
#[macro_use]
extern crate diesel_migrations;
use gio::prelude::*;
use glib::clone;
use std::cell::RefCell;
use std::rc::Rc;
#[macro_use]
mod macros;
mod backend;
mod config;
mod editors;
mod import;
mod navigator;
mod preferences;
mod screens;
mod selectors;
mod widgets;
mod window;
use window::Window;
mod resources;
fn main() {
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
gettextrs::bindtextdomain("musicus", config::LOCALEDIR);
gettextrs::textdomain("musicus");
gstreamer::init().expect("Failed to initialize GStreamer!");
gtk::init().expect("Failed to initialize GTK!");
libadwaita::init();
resources::init().expect("Failed to initialize resources!");
let app = gtk::Application::new(Some("de.johrpan.musicus"), gio::ApplicationFlags::empty())
.expect("Failed to initialize GTK application!");
let window: RefCell<Option<Rc<Window>>> = RefCell::new(None);
app.connect_activate(clone!(@strong app => move |_| {
let mut window = window.borrow_mut();
if window.is_none() {
window.replace(Window::new(&app));
}
window.as_ref().unwrap().present();
}));
let args = std::env::args().collect::<Vec<String>>();
app.run(&args);
}

View file

@ -1,134 +0,0 @@
prefix = get_option('prefix')
localedir = join_paths(prefix, get_option('localedir'))
global_conf = configuration_data()
global_conf.set_quoted('LOCALEDIR', localedir)
global_conf.set_quoted('VERSION', meson.project_version())
config_rs = configure_file(
input: 'config.rs.in',
output: 'config.rs',
configuration: global_conf
)
run_command(
'cp',
config_rs,
meson.current_source_dir(),
check: true
)
resource_conf = configuration_data()
resource_conf.set_quoted('RESOURCEFILE', resources.full_path())
resource_rs = configure_file(
input: 'resources.rs.in',
output: 'resources.rs',
configuration: resource_conf
)
run_command(
'cp',
resource_rs,
meson.current_source_dir(),
check: true
)
sources = files(
'backend/client/ensembles.rs',
'backend/client/instruments.rs',
'backend/client/mediums.rs',
'backend/client/mod.rs',
'backend/client/persons.rs',
'backend/client/recordings.rs',
'backend/client/register.rs',
'backend/client/works.rs',
'backend/database/ensembles.rs',
'backend/database/error.rs',
'backend/database/instruments.rs',
'backend/database/medium.rs',
'backend/database/mod.rs',
'backend/database/persons.rs',
'backend/database/recordings.rs',
'backend/database/schema.rs',
'backend/database/thread.rs',
'backend/database/works.rs',
'backend/error.rs',
'backend/library.rs',
'backend/mod.rs',
'backend/player.rs',
'backend/secure.rs',
'editors/ensemble.rs',
'editors/instrument.rs',
'editors/mod.rs',
'editors/performance.rs',
'editors/person.rs',
'editors/recording.rs',
'editors/work.rs',
'editors/work_part.rs',
'editors/work_section.rs',
'import/disc_source.rs',
'import/folder_source.rs',
'import/medium_editor.rs',
'import/mod.rs',
'import/source.rs',
'import/source_selector.rs',
'import/track_editor.rs',
'import/track_selector.rs',
'import/track_set_editor.rs',
'navigator/mod.rs',
'navigator/window.rs',
'preferences/login.rs',
'preferences/mod.rs',
'preferences/register.rs',
'preferences/server.rs',
'screens/ensemble.rs',
'screens/mod.rs',
'screens/person.rs',
'screens/player_screen.rs',
'screens/recording.rs',
'screens/work.rs',
'selectors/ensemble.rs',
'selectors/instrument.rs',
'selectors/mod.rs',
'selectors/person.rs',
'selectors/recording.rs',
'selectors/selector.rs',
'selectors/work.rs',
'widgets/button_row.rs',
'widgets/editor.rs',
'widgets/entry_row.rs',
'widgets/upload_section.rs',
'widgets/indexed_list_model.rs',
'widgets/list.rs',
'widgets/mod.rs',
'widgets/player_bar.rs',
'widgets/poe_list.rs',
'widgets/screen.rs',
'widgets/section.rs',
'config.rs',
'config.rs.in',
'macros.rs',
'main.rs',
'resources.rs',
'resources.rs.in',
'window.rs',
)
cargo_script = find_program(join_paths(meson.source_root(), 'build-aux/cargo.sh'))
cargo_release = custom_target(
'cargo-build',
build_by_default: true,
input: sources,
depends: resources,
output: meson.project_name(),
console: true,
install: true,
install_dir: get_option('bindir'),
command: [
cargo_script,
meson.build_root(),
meson.source_root(),
'@OUTPUT@',
get_option('buildtype'),
meson.project_name(),
]
)

View file

@ -1,220 +0,0 @@
use crate::backend::Backend;
use crate::widgets::Widget;
use futures_channel::oneshot;
use futures_channel::oneshot::{Receiver, Sender};
use glib::clone;
use gtk::prelude::*;
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};
pub mod window;
pub use window::*;
/// A widget that represents a logical unit of transient user interaction and
/// that optionally resolves to a specific return value.
pub trait Screen<I, O>: Widget {
/// Create a new screen and initialize it with the provided input value.
fn new(input: I, navigation_handle: NavigationHandle<O>) -> Rc<Self> where Self: Sized;
}
/// An accessor to navigation functionality for screens.
pub struct NavigationHandle<O> {
/// The backend, in case the screen needs it.
pub backend: Rc<Backend>,
/// The toplevel window, in case the screen needs it.
pub window: gtk::Window,
/// The navigator that created this navigation handle.
navigator: Weak<Navigator>,
/// The sender through which the result should be sent.
sender: Cell<Option<Sender<Option<O>>>>,
}
impl<O> NavigationHandle<O> {
/// Switch to another screen and wait for that screen's result.
pub async fn push<I, R, S: Screen<I, R> + 'static>(&self, input: I) -> Option<R> {
let navigator = self.unwrap_navigator();
let receiver = navigator.push::<I, R, S>(input);
// If the sender is dropped, return None.
receiver.await.unwrap_or(None)
}
/// Go back to the previous screen optionally returning something.
pub fn pop(&self, output: Option<O>) {
self.unwrap_navigator().pop();
let sender = self.sender.take()
.expect("Tried to send result from screen through a dropped sender.");
if sender.send(output).is_err() {
panic!("Tried to send result from screen to non-existing previous screen.");
}
}
/// Get the navigator and panic if it doesn't exist.
fn unwrap_navigator(&self) -> Rc<Navigator> {
Weak::upgrade(&self.navigator)
.expect("Tried to access non-existing navigator from a screen.")
}
}
/// A toplevel widget for managing screens.
pub struct Navigator {
/// The underlying GTK widget.
pub widget: gtk::Stack,
/// The backend, in case screens need it.
backend: Rc<Backend>,
/// The toplevel window of the navigator, in case screens need it.
window: gtk::Window,
/// The currently active screens. The last screen in this vector is the one
/// that is currently visible.
screens: RefCell<Vec<Rc<dyn Widget>>>,
/// A vector holding the widgets of the old screens that are waiting to be
/// removed after the animation has finished.
old_widgets: RefCell<Vec<gtk::Widget>>,
/// A closure that will be called when the last screen is popped.
back_cb: RefCell<Option<Box<dyn Fn()>>>,
}
impl Navigator {
/// Create a new navigator which will display the provided widget
/// initially.
pub fn new<W, E>(backend: Rc<Backend>, window: &W, empty_screen: &E) -> Rc<Self>
where
W: IsA<gtk::Window>,
E: IsA<gtk::Widget>,
{
let widget = gtk::StackBuilder::new()
.hhomogeneous(false)
.vhomogeneous(false)
.interpolate_size(true)
.transition_type(gtk::StackTransitionType::Crossfade)
.hexpand(true)
.vexpand(true)
.build();
widget.add_named(empty_screen, Some("empty_screen"));
let this = Rc::new(Self {
widget,
backend,
window: window.to_owned().upcast(),
screens: RefCell::new(Vec::new()),
old_widgets: RefCell::new(Vec::new()),
back_cb: RefCell::new(None),
});
this.widget.connect_property_transition_running_notify(clone!(@strong this => move |_| {
if !this.widget.get_transition_running() {
this.clear_old_widgets();
}
}));
this
}
/// Set the closure to be called when the last screen is popped so that
/// the navigator shows its empty state.
pub fn set_back_cb<F: Fn() + 'static>(&self, cb: F) {
self.back_cb.replace(Some(Box::new(cb)));
}
/// Drop all screens and show the provided screen instead.
pub async fn replace<I, O, S: Screen<I, O> + 'static>(self: &Rc<Self>, input: I) -> Option<O> {
for screen in self.screens.replace(Vec::new()) {
self.old_widgets.borrow_mut().push(screen.get_widget());
}
let receiver = self.push::<I, O, S>(input);
if !self.widget.get_transition_running() {
self.clear_old_widgets();
}
// We ignore the case, if a sender is dropped.
receiver.await.unwrap_or(None)
}
/// Drop all screens and go back to the initial screen. The back callback
/// will not be called.
pub fn reset(&self) {
self.widget.set_visible_child_name("empty_screen");
for screen in self.screens.replace(Vec::new()) {
self.old_widgets.borrow_mut().push(screen.get_widget());
}
if !self.widget.get_transition_running() {
self.clear_old_widgets();
}
}
/// Show a screen with the provided input. This should only be called from
/// within a navigation handle.
fn push<I, O, S: Screen<I, O> + 'static>(self: &Rc<Self>, input: I) -> Receiver<Option<O>> {
let (sender, receiver) = oneshot::channel();
let handle = NavigationHandle {
backend: Rc::clone(&self.backend),
window: self.window.clone(),
navigator: Rc::downgrade(self),
sender: Cell::new(Some(sender)),
};
let screen = S::new(input, handle);
let widget = screen.get_widget();
self.widget.add_child(&widget);
self.widget.set_visible_child(&widget);
self.screens.borrow_mut().push(screen);
receiver
}
/// Pop the last screen from the list of screens.
fn pop(&self) {
let popped = if let Some(screen) = self.screens.borrow_mut().pop() {
let widget = screen.get_widget();
self.old_widgets.borrow_mut().push(widget);
true
} else {
false
};
if popped {
if let Some(screen) = self.screens.borrow().last() {
let widget = screen.get_widget();
self.widget.set_visible_child(&widget);
} else {
self.widget.set_visible_child_name("empty_screen");
if let Some(cb) = &*self.back_cb.borrow() {
cb()
}
}
if !self.widget.get_transition_running() {
self.clear_old_widgets();
}
}
}
/// Drop the old widgets.
fn clear_old_widgets(&self) {
for widget in self.old_widgets.borrow().iter() {
self.widget.remove(widget);
}
self.old_widgets.borrow_mut().clear();
}
}

View file

@ -1,43 +0,0 @@
use crate::backend::Backend;
use super::{Navigator, Screen};
use glib::clone;
use gtk::prelude::*;
use std::rc::Rc;
/// A window hosting a navigator.
pub struct NavigatorWindow {
pub navigator: Rc<Navigator>,
window: libadwaita::Window,
}
impl NavigatorWindow {
/// Create a new navigator window and show it.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
let window = libadwaita::Window::new();
window.set_default_size(600, 424);
let placeholder = gtk::Label::new(None);
let navigator = Navigator::new(backend, &window, &placeholder);
libadwaita::WindowExt::set_child(&window, Some(&navigator.widget));
let this = Rc::new(Self { navigator, window });
this.navigator.set_back_cb(clone!(@strong this => move || {
this.window.close();
}));
this.window.show();
this
}
/// Make the wrapped window transient. This will make the window modal.
pub fn set_transient_for<W: IsA<gtk::Window>>(&self, window: &W) {
self.window.set_modal(true);
self.window.set_transient_for(Some(window));
}
/// Show the navigator window.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,83 +0,0 @@
use super::register::RegisterDialog;
use crate::push;
use crate::backend::{Backend, LoginData};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for entering login credentials.
pub struct LoginDialog {
handle: NavigationHandle<LoginData>,
widget: gtk::Stack,
info_bar: gtk::InfoBar,
username_entry: gtk::Entry,
password_entry: gtk::Entry,
}
impl Screen<(), LoginData> for LoginDialog {
fn new(_: (), handle: NavigationHandle<LoginData>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/login_dialog.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, login_button);
get_widget!(builder, gtk::Entry, username_entry);
get_widget!(builder, gtk::Entry, password_entry);
get_widget!(builder, gtk::Button, register_button);
let this = Rc::new(Self {
handle,
widget,
info_bar,
username_entry,
password_entry,
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
login_button.connect_clicked(clone!(@weak this => move |_| {
this.widget.set_visible_child_name("loading");
let data = LoginData {
username: this.username_entry.get_text().unwrap().to_string(),
password: this.password_entry.get_text().unwrap().to_string(),
};
spawn!(@clone this, async move {
this.handle.backend.set_login_data(data.clone()).await.unwrap();
if this.handle.backend.login().await.unwrap() {
this.handle.pop(Some(data));
} else {
this.widget.set_visible_child_name("content");
this.info_bar.set_revealed(true);
}
});
}));
register_button.connect_clicked(clone!(@weak this => move |_| {
spawn!(@clone this, async move {
if let Some(data) = push!(this.handle, RegisterDialog).await {
this.handle.pop(Some(data));
}
});
}));
this
}
}
impl Widget for LoginDialog {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,125 +0,0 @@
use crate::backend::Backend;
use crate::navigator::NavigatorWindow;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::rc::Rc;
mod login;
use login::LoginDialog;
mod server;
use server::ServerDialog;
mod register;
/// A dialog for configuring the app.
pub struct Preferences {
backend: Rc<Backend>,
window: libadwaita::Window,
music_library_path_row: libadwaita::ActionRow,
url_row: libadwaita::ActionRow,
login_row: libadwaita::ActionRow,
}
impl Preferences {
/// Create a new preferences dialog.
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/preferences.ui");
get_widget!(builder, libadwaita::Window, window);
get_widget!(builder, libadwaita::ActionRow, music_library_path_row);
get_widget!(builder, gtk::Button, select_music_library_path_button);
get_widget!(builder, libadwaita::ActionRow, url_row);
get_widget!(builder, gtk::Button, url_button);
get_widget!(builder, libadwaita::ActionRow, login_row);
get_widget!(builder, gtk::Button, login_button);
window.set_transient_for(Some(parent));
let this = Rc::new(Self {
backend,
window,
music_library_path_row,
url_row,
login_row,
});
// Connect signals and callbacks
select_music_library_path_button.connect_clicked(clone!(@strong this => move |_| {
let dialog = gtk::FileChooserDialog::new(
Some(&gettext("Select music library folder")),
Some(&this.window),
gtk::FileChooserAction::SelectFolder,
&[
(&gettext("Cancel"), gtk::ResponseType::Cancel),
(&gettext("Select"), gtk::ResponseType::Accept),
]);
dialog.connect_response(clone!(@strong this => move |dialog, response| {
if let gtk::ResponseType::Accept = response {
if let Some(file) = dialog.get_file() {
if let Some(path) = file.get_path() {
this.music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
spawn!(@clone this, async move {
this.backend.set_music_library_path(path).await.unwrap();
});
}
}
}
dialog.hide();
}));
dialog.show();
}));
url_button.connect_clicked(clone!(@strong this => move |_| {
let dialog = ServerDialog::new(this.backend.clone(), &this.window);
dialog.set_selected_cb(clone!(@strong this => move |url| {
this.url_row.set_subtitle(Some(&url));
}));
dialog.show();
}));
login_button.connect_clicked(clone!(@strong this => move |_| {
let window = NavigatorWindow::new(this.backend.clone());
window.set_transient_for(&this.window);
spawn!(@clone this, async move {
if let Some(data) = replace!(window.navigator, LoginDialog).await {
this.login_row.set_subtitle(Some(&data.username));
}
});
}));
// Initialize
if let Some(path) = this.backend.get_music_library_path() {
this.music_library_path_row
.set_subtitle(Some(path.to_str().unwrap()));
}
if let Some(url) = this.backend.get_server_url() {
this.url_row.set_subtitle(Some(&url));
}
if let Some(data) = this.backend.get_login_data() {
this.login_row.set_subtitle(Some(&data.username));
}
this
}
/// Show the preferences dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,119 +0,0 @@
use crate::backend::{Backend, LoginData, UserRegistration};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating a new user account.
pub struct RegisterDialog {
handle: NavigationHandle<LoginData>,
widget: gtk::Stack,
username_entry: gtk::Entry,
email_entry: gtk::Entry,
password_entry: gtk::Entry,
repeat_password_entry: gtk::Entry,
captcha_row: libadwaita::ActionRow,
captcha_entry: gtk::Entry,
captcha_id: RefCell<Option<String>>,
}
impl Screen<(), LoginData> for RegisterDialog {
/// Create a new register dialog.
fn new(_: (), handle: NavigationHandle<LoginData>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/register_dialog.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, register_button);
get_widget!(builder, gtk::Entry, username_entry);
get_widget!(builder, gtk::Entry, email_entry);
get_widget!(builder, gtk::Entry, password_entry);
get_widget!(builder, gtk::Entry, repeat_password_entry);
get_widget!(builder, libadwaita::ActionRow, captcha_row);
get_widget!(builder, gtk::Entry, captcha_entry);
let this = Rc::new(Self {
handle,
widget,
username_entry,
email_entry,
password_entry,
repeat_password_entry,
captcha_row,
captcha_entry,
captcha_id: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@weak this => move |_| {
this.handle.pop(None);
}));
register_button.connect_clicked(clone!(@weak this => move |_| {
let password = this.password_entry.get_text().unwrap().to_string();
let repeat = this.repeat_password_entry.get_text().unwrap().to_string();
if (password != repeat) {
// TODO: Show error and validate other input.
} else {
this.widget.set_visible_child_name("loading");
spawn!(@clone this, async move {
let username = this.username_entry.get_text().unwrap().to_string();
let email = this.email_entry.get_text().unwrap().to_string();
let captcha_id = this.captcha_id.borrow().clone().unwrap();
let answer = this.captcha_entry.get_text().unwrap().to_string();
let email = if email.len() == 0 {
None
} else {
Some(email)
};
let registration = UserRegistration {
username: username.clone(),
password: password.clone(),
email,
captcha_id,
answer,
};
// TODO: Handle errors.
if this.handle.backend.register(registration).await.unwrap() {
let data = LoginData {
username,
password,
};
this.handle.pop(Some(data));
} else {
this.widget.set_visible_child_name("content");
}
});
}
}));
// Initialize
spawn!(@clone this, async move {
let captcha = this.handle.backend.get_captcha().await.unwrap();
this.captcha_row.set_title(Some(&captcha.question));
this.captcha_id.replace(Some(captcha.id));
this.widget.set_visible_child_name("content");
});
this
}
}
impl Widget for RegisterDialog {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,65 +0,0 @@
use crate::backend::Backend;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for setting up the server.
pub struct ServerDialog {
backend: Rc<Backend>,
window: libadwaita::Window,
url_entry: gtk::Entry,
selected_cb: RefCell<Option<Box<dyn Fn(String) -> ()>>>,
}
impl ServerDialog {
/// Create a new server dialog.
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/server_dialog.ui");
get_widget!(builder, libadwaita::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, set_button);
get_widget!(builder, gtk::Entry, url_entry);
window.set_transient_for(Some(parent));
let this = Rc::new(Self {
backend,
window,
url_entry,
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
set_button.connect_clicked(clone!(@strong this => move |_| {
let url = this.url_entry.get_text().unwrap().to_string();
this.backend.set_server_url(&url).unwrap();
if let Some(cb) = &*this.selected_cb.borrow() {
cb(url);
}
this.window.close();
}));
this
}
/// The closure to call when the server was set.
pub fn set_selected_cb<F: Fn(String) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the server dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,9 +0,0 @@
use anyhow::Result;
pub fn init() -> Result<()> {
let bytes = glib::Bytes::from(include_bytes!(@RESOURCEFILE@).as_ref());
let resource = gio::Resource::from_data(&bytes)?;
gio::resources_register(&resource);
Ok(())
}

View file

@ -1,119 +0,0 @@
use super::RecordingScreen;
use crate::backend::{Backend, Ensemble, Recording};
use crate::editors::EnsembleEditor;
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
use crate::widgets;
use crate::widgets::{List, Section, Widget};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for showing recordings with a ensemble.
pub struct EnsembleScreen {
handle: NavigationHandle<()>,
ensemble: Ensemble,
widget: widgets::Screen,
recording_list: Rc<List>,
recordings: RefCell<Vec<Recording>>,
}
impl Screen<Ensemble, ()> for EnsembleScreen {
/// Create a new ensemble screen for the specified ensemble and load the
/// contents asynchronously.
fn new(ensemble: Ensemble, handle: NavigationHandle<()>) -> Rc<Self> {
let widget = widgets::Screen::new();
widget.set_title(&ensemble.name);
let recording_list = List::new();
let this = Rc::new(Self {
handle,
ensemble,
widget,
recording_list,
recordings: RefCell::new(Vec::new()),
});
this.widget.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.widget.add_action(&gettext("Edit ensemble"), clone!(@weak this => move || {
spawn!(@clone this, async move {
let window = NavigatorWindow::new(this.handle.backend.clone());
replace!(window.navigator, EnsembleEditor, Some(this.ensemble.clone())).await;
});
}));
this.widget.add_action(&gettext("Delete ensemble"), clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_ensemble(&this.ensemble.id).await.unwrap();
this.handle.backend.library_changed();
});
}));
this.widget.set_search_cb(clone!(@weak this => move || {
this.recording_list.invalidate_filter();
}));
this.recording_list.set_make_widget_cb(clone!(@weak this => move |index| {
let recording = &this.recordings.borrow()[index];
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&recording.work.get_title()));
row.set_subtitle(Some(&recording.get_performers()));
let recording = recording.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
let recording = recording.clone();
spawn!(@clone this, async move {
push!(this.handle, RecordingScreen, recording.clone()).await;
});
}));
row.upcast()
}));
this.recording_list.set_filter_cb(clone!(@weak this => move |index| {
let recording = &this.recordings.borrow()[index];
let search = this.widget.get_search();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.to_lowercase().contains(&search)
}));
// Load the content asynchronously.
spawn!(@clone this, async move {
let recordings = this.handle
.backend
.db()
.get_recordings_for_ensemble(&this.ensemble.id)
.await
.unwrap();
if !recordings.is_empty() {
let length = recordings.len();
this.recordings.replace(recordings);
this.recording_list.update(length);
let section = Section::new("Recordings", &this.recording_list.widget);
this.widget.add_content(&section.widget);
}
this.widget.ready();
});
this
}
}
impl Widget for EnsembleScreen {
fn get_widget(&self) -> gtk::Widget {
self.widget.widget.clone().upcast()
}
}

View file

@ -1,14 +0,0 @@
pub mod ensemble;
pub use ensemble::*;
pub mod person;
pub use person::*;
pub mod player_screen;
pub use player_screen::*;
pub mod work;
pub use work::*;
pub mod recording;
pub use recording::*;

View file

@ -1,166 +0,0 @@
use super::{WorkScreen, RecordingScreen};
use crate::backend::{Backend, Person, Recording, Work};
use crate::editors::PersonEditor;
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
use crate::widgets;
use crate::widgets::{List, Section, Widget};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for showing works by and recordings with a person.
pub struct PersonScreen {
handle: NavigationHandle<()>,
person: Person,
widget: widgets::Screen,
work_list: Rc<List>,
recording_list: Rc<List>,
works: RefCell<Vec<Work>>,
recordings: RefCell<Vec<Recording>>,
}
impl Screen<Person, ()> for PersonScreen {
/// Create a new person screen for the specified person and load the
/// contents asynchronously.
fn new(person: Person, handle: NavigationHandle<()>) -> Rc<Self> {
let widget = widgets::Screen::new();
widget.set_title(&person.name_fl());
let work_list = List::new();
let recording_list = List::new();
let this = Rc::new(Self {
handle,
person,
widget,
work_list,
recording_list,
works: RefCell::new(Vec::new()),
recordings: RefCell::new(Vec::new()),
});
this.widget.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.widget.add_action(&gettext("Edit person"), clone!(@weak this => move || {
spawn!(@clone this, async move {
let window = NavigatorWindow::new(this.handle.backend.clone());
replace!(window.navigator, PersonEditor, Some(this.person.clone())).await;
});
}));
this.widget.add_action(&gettext("Delete person"), clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_person(&this.person.id).await.unwrap();
this.handle.backend.library_changed();
});
}));
this.widget.set_search_cb(clone!(@weak this => move || {
this.work_list.invalidate_filter();
this.recording_list.invalidate_filter();
}));
this.work_list.set_make_widget_cb(clone!(@weak this => move |index| {
let work = &this.works.borrow()[index];
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&work.title));
let work = work.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
let work = work.clone();
spawn!(@clone this, async move {
push!(this.handle, WorkScreen, work.clone()).await;
});
}));
row.upcast()
}));
this.work_list.set_filter_cb(clone!(@weak this => move |index| {
let work = &this.works.borrow()[index];
let search = this.widget.get_search();
let title = work.title.to_lowercase();
search.is_empty() || title.contains(&search)
}));
this.recording_list.set_make_widget_cb(clone!(@weak this => move |index| {
let recording = &this.recordings.borrow()[index];
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&recording.work.get_title()));
row.set_subtitle(Some(&recording.get_performers()));
let recording = recording.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
let recording = recording.clone();
spawn!(@clone this, async move {
push!(this.handle, RecordingScreen, recording.clone()).await;
});
}));
row.upcast()
}));
this.recording_list.set_filter_cb(clone!(@weak this => move |index| {
let recording = &this.recordings.borrow()[index];
let search = this.widget.get_search();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.to_lowercase().contains(&search)
}));
// Load the content asynchronously.
spawn!(@clone this, async move {
let works = this.handle
.backend
.db()
.get_works(&this.person.id)
.await
.unwrap();
let recordings = this.handle
.backend
.db()
.get_recordings_for_person(&this.person.id)
.await
.unwrap();
if !works.is_empty() {
let length = works.len();
this.works.replace(works);
this.work_list.update(length);
let section = Section::new("Works", &this.work_list.widget);
this.widget.add_content(&section.widget);
}
if !recordings.is_empty() {
let length = recordings.len();
this.recordings.replace(recordings);
this.recording_list.update(length);
let section = Section::new("Recordings", &this.recording_list.widget);
this.widget.add_content(&section.widget);
}
this.widget.ready();
});
this
}
}
impl Widget for PersonScreen {
fn get_widget(&self) -> gtk::Widget {
self.widget.widget.clone().upcast()
}
}

View file

@ -1,372 +0,0 @@
use crate::backend::{Player, PlaylistItem};
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
/// Elements for visually representing the playlist.
enum ListItem {
/// A header shown on top of a track set. This contains an index
/// referencing the playlist item containing this track set.
Header(usize),
/// A playable track. This contains an index to the playlist item, an
/// index to the track and whether it is the currently played one.
Track(usize, usize, bool),
/// A separator shown between track sets.
Separator,
}
pub struct PlayerScreen {
pub widget: gtk::Box,
title_label: gtk::Label,
subtitle_label: gtk::Label,
previous_button: gtk::Button,
play_button: gtk::Button,
next_button: gtk::Button,
position_label: gtk::Label,
position: gtk::Adjustment,
duration_label: gtk::Label,
play_image: gtk::Image,
pause_image: gtk::Image,
list: Rc<List>,
playlist: RefCell<Vec<PlaylistItem>>,
items: RefCell<Vec<ListItem>>,
player: RefCell<Option<Rc<Player>>>,
seeking: Cell<bool>,
current_item: Cell<usize>,
current_track: Cell<usize>,
back_cb: RefCell<Option<Box<dyn Fn()>>>,
}
impl PlayerScreen {
pub fn new() -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Label, subtitle_label);
get_widget!(builder, gtk::Button, previous_button);
get_widget!(builder, gtk::Button, play_button);
get_widget!(builder, gtk::Button, next_button);
get_widget!(builder, gtk::Button, stop_button);
get_widget!(builder, gtk::Label, position_label);
get_widget!(builder, gtk::Scale, position_scale);
get_widget!(builder, gtk::Adjustment, position);
get_widget!(builder, gtk::Label, duration_label);
get_widget!(builder, gtk::Image, play_image);
get_widget!(builder, gtk::Image, pause_image);
get_widget!(builder, gtk::Frame, frame);
let list = List::new();
frame.set_child(Some(&list.widget));
let this = Rc::new(Self {
widget,
title_label,
subtitle_label,
previous_button,
play_button,
next_button,
position_label,
position,
duration_label,
play_image,
pause_image,
list,
items: RefCell::new(Vec::new()),
playlist: RefCell::new(Vec::new()),
player: RefCell::new(None),
seeking: Cell::new(false),
current_item: Cell::new(0),
current_track: Cell::new(0),
back_cb: RefCell::new(None),
});
back_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.back_cb.borrow() {
cb();
}
}));
this.previous_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.previous().unwrap();
}
}));
this.play_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.play_pause();
}
}));
this.next_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.next().unwrap();
}
}));
stop_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
if let Some(cb) = &*this.back_cb.borrow() {
cb();
}
player.clear();
}
}));
// position_scale.connect_button_press_event(clone!(@strong seeking => move |_, _| {
// seeking.replace(true);
// Inhibit(false)
// }));
// position_scale.connect_button_release_event(
// clone!(@strong seeking, @strong position, @strong player => move |_, _| {
// if let Some(player) = &*player.borrow() {
// player.seek(position.get_value() as u64);
// }
// seeking.replace(false);
// Inhibit(false)
// }),
// );
position_scale.connect_value_changed(clone!(@strong this => move |_| {
if this.seeking.get() {
let ms = this.position.get_value() as u64;
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
this.position_label.set_text(&format!("{}:{:02}", min, sec));
}
}));
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
match this.items.borrow()[index] {
ListItem::Header(item_index) => {
let playlist_item = &this.playlist.borrow()[item_index];
let recording = &playlist_item.track_set.recording;
let row = libadwaita::ActionRow::new();
row.set_activatable(false);
row.set_selectable(false);
row.set_title(Some(&recording.work.get_title()));
row.set_subtitle(Some(&recording.get_performers()));
row.upcast()
}
ListItem::Track(item_index, track_index, playing) => {
let playlist_item = &this.playlist.borrow()[item_index];
let index = playlist_item.indices[track_index];
let track = &playlist_item.track_set.tracks[index];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(playlist_item.track_set.recording.work.parts[*part].title.clone());
}
let title = if parts.is_empty() {
gettext("Unknown")
} else {
parts.join(", ")
};
let row = libadwaita::ActionRow::new();
row.set_selectable(false);
row.set_activatable(true);
row.set_title(Some(&title));
row.connect_activated(clone!(@strong this => move |_| {
if let Some(player) = &*this.player.borrow() {
player.set_track(item_index, track_index).unwrap();
}
}));
let icon = if playing {
Some("media-playback-start-symbolic")
} else {
None
};
let image = gtk::Image::from_icon_name(icon);
row.add_prefix(&image);
row.upcast()
}
ListItem::Separator => {
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
separator.upcast()
}
}
}));
// list.set_make_widget(clone!(
// @strong current_item,
// @strong current_track
// => move |element: &PlaylistElement| {
// let title_label = gtk::Label::new(Some(&element.title));
// title_label.set_ellipsize(pango::EllipsizeMode::End);
// title_label.set_halign(gtk::Align::Start);
// let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
// vbox.append(&title_label);
// if let Some(subtitle) = &element.subtitle {
// let subtitle_label = gtk::Label::new(Some(&subtitle));
// subtitle_label.set_ellipsize(pango::EllipsizeMode::End);
// subtitle_label.set_halign(gtk::Align::Start);
// subtitle_label.set_opacity(0.5);
// vbox.append(&subtitle_label);
// }
// let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
// hbox.set_margin_top(6);
// hbox.set_margin_bottom(6);
// hbox.set_margin_start(6);
// hbox.set_margin_end(6);
// if element.playable {
// let image = gtk::Image::new();
// if element.item == current_item.get() && element.track == current_track.get() {
// image.set_from_icon_name(
// Some("media-playback-start-symbolic"),
// gtk::IconSize::Button,
// );
// }
// hbox.append(&image);
// } else if element.item > 0 {
// hbox.set_margin_top(18);
// }
// hbox.append(&vbox);
// hbox.upcast()
// }
// ));
// list.set_selected(clone!(@strong player => move |element| {
// if let Some(player) = &*player.borrow() {
// player.set_track(element.item, element.track).unwrap();
// }
// }));
this
}
pub fn set_player(self: Rc<Self>, player: Option<Rc<Player>>) {
self.player.replace(player.clone());
if let Some(player) = player {
player.add_playlist_cb(clone!(@strong self as this => move |playlist| {
this.playlist.replace(playlist);
this.show_playlist();
}));
player.add_track_cb(clone!(@strong self as this, @strong player => move |current_item, current_track| {
this.previous_button.set_sensitive(player.has_previous());
this.next_button.set_sensitive(player.has_next());
let item = &this.playlist.borrow()[current_item];
let track = &item.track_set.tracks[current_track];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.track_set.recording.work.parts[*part].title.clone());
}
let mut title = item.track_set.recording.work.get_title();
if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", "));
}
this.title_label.set_text(&title);
this.subtitle_label.set_text(&item.track_set.recording.get_performers());
this.position_label.set_text("0:00");
this.current_item.set(current_item);
this.current_track.set(current_track);
this.show_playlist();
}));
player.add_duration_cb(clone!(
@strong self.duration_label as duration_label,
@strong self.position as position
=> move |ms| {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
duration_label.set_text(&format!("{}:{:02}", min, sec));
position.set_upper(ms as f64);
}
));
player.add_playing_cb(clone!(
@strong self.play_button as play_button,
@strong self.play_image as play_image,
@strong self.pause_image as pause_image
=> move |playing| {
play_button.set_child(Some(if playing {
&pause_image
} else {
&play_image
}));
}
));
player.add_position_cb(clone!(
@strong self.position_label as position_label,
@strong self.position as position,
@strong self.seeking as seeking
=> move |ms| {
if !seeking.get() {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
position_label.set_text(&format!("{}:{:02}", min, sec));
position.set_value(ms as f64);
}
}
));
}
}
pub fn set_back_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.back_cb.replace(Some(Box::new(cb)));
}
/// Update the user interface according to the playlist.
fn show_playlist(&self) {
let playlist = self.playlist.borrow();
let current_item = self.current_item.get();
let current_track = self.current_track.get();
let mut first = true;
let mut items = Vec::new();
for (item_index, playlist_item) in playlist.iter().enumerate() {
if !first {
items.push(ListItem::Separator);
} else {
first = false;
}
items.push(ListItem::Header(item_index));
for (index, _) in playlist_item.indices.iter().enumerate() {
let playing = current_item == item_index && current_track == index;
items.push(ListItem::Track(item_index, index, playing));
}
}
let length = items.len();
self.items.replace(items);
self.list.update(length);
}
}

View file

@ -1,163 +0,0 @@
use crate::backend::{Backend, PlaylistItem, Recording, TrackSet};
use crate::editors::RecordingEditor;
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
use crate::widgets;
use crate::widgets::{List, Section, Widget};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// Representation of one entry within the track list.
enum ListItem {
/// A track row. This hold an index to the track set and an index to the
/// track within the track set.
Track(usize, usize),
/// A separator intended for use between track sets.
Separator,
}
/// A screen for showing a recording.
pub struct RecordingScreen {
handle: NavigationHandle<()>,
recording: Recording,
widget: widgets::Screen,
list: Rc<List>,
track_sets: RefCell<Vec<TrackSet>>,
items: RefCell<Vec<ListItem>>,
}
impl Screen<Recording, ()> for RecordingScreen {
/// Create a new recording screen for the specified recording and load the
/// contents asynchronously.
fn new(recording: Recording, handle: NavigationHandle<()>) -> Rc<Self> {
let widget = widgets::Screen::new();
widget.set_title(&recording.work.get_title());
widget.set_subtitle(&recording.get_performers());
let list = List::new();
let section = Section::new(&gettext("Tracks"), &list.widget);
widget.add_content(&section.widget);
let this = Rc::new(Self {
handle,
recording,
widget,
list,
track_sets: RefCell::new(Vec::new()),
items: RefCell::new(Vec::new()),
});
section.add_action("media-playback-start-symbolic", clone!(@weak this => move || {
if let Some(player) = this.handle.backend.get_player() {
if let Some(track_set) = this.track_sets.borrow().get(0).cloned() {
let indices = (0..track_set.tracks.len()).collect();
let playlist_item = PlaylistItem {
track_set,
indices,
};
player.add_item(playlist_item).unwrap();
}
}
}));
this.widget.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.widget.add_action(&gettext("Edit recording"), clone!(@weak this => move || {
spawn!(@clone this, async move {
let window = NavigatorWindow::new(this.handle.backend.clone());
replace!(window.navigator, RecordingEditor, Some(this.recording.clone())).await;
});
}));
this.widget.add_action(&gettext("Delete recording"), clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_recording(&this.recording.id).await.unwrap();
this.handle.backend.library_changed();
});
}));
this.list.set_make_widget_cb(clone!(@weak this => move |index| {
match this.items.borrow()[index] {
ListItem::Track(track_set_index, track_index) => {
let track_set = &this.track_sets.borrow()[track_set_index];
let track = &track_set.tracks[track_index];
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
title_parts.push(this.recording.work.parts[*part].title.clone());
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let row = libadwaita::ActionRow::new();
row.set_title(Some(&title));
row.upcast()
}
ListItem::Separator => {
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
separator.upcast()
}
}
}));
// Load the content asynchronously.
spawn!(@clone this, async move {
let track_sets = this.handle
.backend
.db()
.get_track_sets(&this.recording.id)
.await
.unwrap();
this.show_track_sets(track_sets);
this.widget.ready();
});
this
}
}
impl RecordingScreen {
/// Update the track sets variable as well as the user interface.
fn show_track_sets(&self, track_sets: Vec<TrackSet>) {
let mut first = true;
let mut items = Vec::new();
for (track_set_index, track_set) in track_sets.iter().enumerate() {
if !first {
items.push(ListItem::Separator);
} else {
first = false;
}
for (track_index, _) in track_set.tracks.iter().enumerate() {
items.push(ListItem::Track(track_set_index, track_index));
}
}
let length = items.len();
self.items.replace(items);
self.track_sets.replace(track_sets);
self.list.update(length);
}
}
impl Widget for RecordingScreen {
fn get_widget(&self) -> gtk::Widget {
self.widget.widget.clone().upcast()
}
}

View file

@ -1,120 +0,0 @@
use super::RecordingScreen;
use crate::backend::{Backend, Work, Recording};
use crate::editors::WorkEditor;
use crate::navigator::{NavigatorWindow, NavigationHandle, Screen};
use crate::widgets;
use crate::widgets::{List, Section, Widget};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for showing recordings of a work.
pub struct WorkScreen {
handle: NavigationHandle<()>,
work: Work,
widget: widgets::Screen,
recording_list: Rc<List>,
recordings: RefCell<Vec<Recording>>,
}
impl Screen<Work, ()> for WorkScreen {
/// Create a new work screen for the specified work and load the
/// contents asynchronously.
fn new(work: Work, handle: NavigationHandle<()>) -> Rc<Self> {
let widget = widgets::Screen::new();
widget.set_title(&work.title);
widget.set_subtitle(&work.composer.name_fl());
let recording_list = List::new();
let this = Rc::new(Self {
handle,
work,
widget,
recording_list,
recordings: RefCell::new(Vec::new()),
});
this.widget.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.widget.add_action(&gettext("Edit work"), clone!(@weak this => move || {
spawn!(@clone this, async move {
let window = NavigatorWindow::new(this.handle.backend.clone());
replace!(window.navigator, WorkEditor, Some(this.work.clone())).await;
});
}));
this.widget.add_action(&gettext("Delete work"), clone!(@weak this => move || {
spawn!(@clone this, async move {
this.handle.backend.db().delete_work(&this.work.id).await.unwrap();
this.handle.backend.library_changed();
});
}));
this.widget.set_search_cb(clone!(@weak this => move || {
this.recording_list.invalidate_filter();
}));
this.recording_list.set_make_widget_cb(clone!(@weak this => move |index| {
let recording = &this.recordings.borrow()[index];
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&recording.work.get_title()));
row.set_subtitle(Some(&recording.get_performers()));
let recording = recording.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
let recording = recording.clone();
spawn!(@clone this, async move {
push!(this.handle, RecordingScreen, recording.clone()).await;
});
}));
row.upcast()
}));
this.recording_list.set_filter_cb(clone!(@weak this => move |index| {
let recording = &this.recordings.borrow()[index];
let search = this.widget.get_search();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.to_lowercase().contains(&search)
}));
// Load the content asynchronously.
spawn!(@clone this, async move {
let recordings = this.handle
.backend
.db()
.get_recordings_for_work(&this.work.id)
.await
.unwrap();
if !recordings.is_empty() {
let length = recordings.len();
this.recordings.replace(recordings);
this.recording_list.update(length);
let section = Section::new("Recordings", &this.recording_list.widget);
this.widget.add_content(&section.widget);
}
this.widget.ready();
});
this
}
}
impl Widget for WorkScreen {
fn get_widget(&self) -> gtk::Widget {
self.widget.widget.clone().upcast()
}
}

View file

@ -1,80 +0,0 @@
use super::selector::Selector;
use crate::backend::{Backend, Ensemble};
use crate::editors::EnsembleEditor;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting a ensemble.
pub struct EnsembleSelector {
handle: NavigationHandle<Ensemble>,
selector: Rc<Selector<Ensemble>>,
}
impl Screen<(), Ensemble> for EnsembleSelector {
/// Create a new ensemble selector.
fn new(_: (), handle: NavigationHandle<Ensemble>) -> Rc<Self> {
// Create UI
let selector = Selector::<Ensemble>::new();
selector.set_title(&gettext("Select ensemble"));
let this = Rc::new(Self {
handle,
selector,
});
// Connect signals and callbacks
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(ensemble) = push!(this.handle, EnsembleEditor, None).await {
this.handle.pop(Some(ensemble));
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
let clone = this.clone();
async move { clone.handle.backend.get_ensembles().await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
let clone = this.clone();
async move { clone.handle.backend.db().get_ensembles().await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |ensemble| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&ensemble.name));
let ensemble = ensemble.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
this.handle.pop(Some(ensemble.clone()))
}));
row.upcast()
}));
this.selector
.set_filter(|search, ensemble| ensemble.name.to_lowercase().contains(search));
this
}
}
impl Widget for EnsembleSelector {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}

View file

@ -1,80 +0,0 @@
use super::selector::Selector;
use crate::backend::{Backend, Instrument};
use crate::editors::InstrumentEditor;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting a instrument.
pub struct InstrumentSelector {
handle: NavigationHandle<Instrument>,
selector: Rc<Selector<Instrument>>,
}
impl Screen<(), Instrument> for InstrumentSelector {
/// Create a new instrument selector.
fn new(_: (), handle: NavigationHandle<Instrument>) -> Rc<Self> {
// Create UI
let selector = Selector::<Instrument>::new();
selector.set_title(&gettext("Select instrument"));
let this = Rc::new(Self {
handle,
selector,
});
// Connect signals and callbacks
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(instrument) = push!(this.handle, InstrumentEditor, None).await {
this.handle.pop(Some(instrument));
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
let clone = this.clone();
async move { clone.handle.backend.get_instruments().await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
let clone = this.clone();
async move { clone.handle.backend.db().get_instruments().await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |instrument| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&instrument.name));
let instrument = instrument.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
this.handle.pop(Some(instrument.clone()))
}));
row.upcast()
}));
this.selector
.set_filter(|search, instrument| instrument.name.to_lowercase().contains(search));
this
}
}
impl Widget for InstrumentSelector {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}

View file

@ -1,16 +0,0 @@
pub mod ensemble;
pub use ensemble::*;
pub mod instrument;
pub use instrument::*;
pub mod person;
pub use person::*;
pub mod recording;
pub use recording::*;
pub mod work;
pub use work::*;
mod selector;

View file

@ -1,80 +0,0 @@
use super::selector::Selector;
use crate::backend::{Backend, Person};
use crate::editors::PersonEditor;
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting a person.
pub struct PersonSelector {
handle: NavigationHandle<Person>,
selector: Rc<Selector<Person>>,
}
impl Screen<(), Person> for PersonSelector {
/// Create a new person selector.
fn new(_: (), handle: NavigationHandle<Person>) -> Rc<Self> {
// Create UI
let selector = Selector::<Person>::new();
selector.set_title(&gettext("Select person"));
let this = Rc::new(Self {
handle,
selector,
});
// Connect signals and callbacks
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(person) = push!(this.handle, PersonEditor, None).await {
this.handle.pop(Some(person));
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
let clone = this.clone();
async move { clone.handle.backend.get_persons().await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
let clone = this.clone();
async move { clone.handle.backend.db().get_persons().await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |person| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&person.name_lf()));
let person = person.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
this.handle.pop(Some(person.clone()));
}));
row.upcast()
}));
this.selector
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
this
}
}
impl Widget for PersonSelector {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}

View file

@ -1,233 +0,0 @@
use super::selector::Selector;
use crate::backend::{Backend, Person, Work, Recording};
use crate::editors::{PersonEditor, WorkEditor, RecordingEditor};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting a recording.
pub struct RecordingSelector {
handle: NavigationHandle<Recording>,
selector: Rc<Selector<Person>>,
}
impl Screen<(), Recording> for RecordingSelector {
fn new(_: (), handle: NavigationHandle<Recording>) -> Rc<Self> {
// Create UI
let selector = Selector::<Person>::new();
selector.set_title(&gettext("Select composer"));
let this = Rc::new(Self {
handle,
selector,
});
// Connect signals and callbacks
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(person) = push!(this.handle, PersonEditor, None).await {
// We can assume that there are no existing works of this composer and
// immediately show the work editor. Going back from the work editor will
// correctly show the person selector again.
let work = Work::new(person);
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
// There will also be no existing recordings, so we show the recording
// editor next.
let recording = Recording::new(work);
if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await {
this.handle.pop(Some(recording));
}
}
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
async move { this.handle.backend.get_persons().await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
async move { this.handle.backend.db().get_persons().await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |person| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&person.name_lf()));
let person = person.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
// Instead of returning the person from here, like the person selector does, we
// show a second selector for choosing the work.
let person = person.clone();
spawn!(@clone this, async move {
if let Some(work) = push!(this.handle, RecordingSelectorWorkScreen, person).await {
// Now the user can select a recording for that work.
if let Some(recording) = push!(this.handle, RecordingSelectorRecordingScreen, work).await {
this.handle.pop(Some(recording));
}
}
});
}));
row.upcast()
}));
this.selector
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
this
}
}
impl Widget for RecordingSelector {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}
/// The work selector within the recording selector.
struct RecordingSelectorWorkScreen {
handle: NavigationHandle<Work>,
person: Person,
selector: Rc<Selector<Work>>,
}
impl Screen<Person, Work> for RecordingSelectorWorkScreen {
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> {
let selector = Selector::<Work>::new();
selector.set_title(&gettext("Select work"));
selector.set_subtitle(&person.name_fl());
let this = Rc::new(Self {
handle,
person,
selector,
});
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
let work = Work::new(this.person.clone());
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
this.handle.pop(Some(work));
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
async move { this.handle.backend.get_works(&this.person.id).await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |work| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&work.title));
let work = work.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
this.handle.pop(Some(work.clone()));
}));
row.upcast()
}));
this.selector.set_filter(|search, work| work.title.to_lowercase().contains(search));
this
}
}
impl Widget for RecordingSelectorWorkScreen {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}
/// The actual recording selector within the recording selector.
struct RecordingSelectorRecordingScreen {
handle: NavigationHandle<Recording>,
work: Work,
selector: Rc<Selector<Recording>>,
}
impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
fn new(work: Work, handle: NavigationHandle<Recording>) -> Rc<Self> {
let selector = Selector::<Recording>::new();
selector.set_title(&gettext("Select recording"));
selector.set_subtitle(&work.get_title());
let this = Rc::new(Self {
handle,
work,
selector,
});
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
let recording = Recording::new(this.work.clone());
if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await {
this.handle.pop(Some(recording));
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
async move { this.handle.backend.get_recordings_for_work(&this.work.id).await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
async move { this.handle.backend.db().get_recordings_for_work(&this.work.id).await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |recording| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&recording.get_performers()));
let recording = recording.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
this.handle.pop(Some(recording.clone()));
}));
row.upcast()
}));
this.selector
.set_filter(|search, recording| recording.get_performers().to_lowercase().contains(search));
this
}
}
impl Widget for RecordingSelectorRecordingScreen {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}

View file

@ -1,217 +0,0 @@
use crate::backend::Result;
use crate::widgets::List;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
/// A screen that presents a list of items. It allows to switch between the server and the local
/// database and to search within the list.
pub struct Selector<T: 'static> {
pub widget: gtk::Box,
title_label: gtk::Label,
subtitle_label: gtk::Label,
search_entry: gtk::SearchEntry,
server_check_button: gtk::CheckButton,
stack: gtk::Stack,
list: Rc<List>,
items: RefCell<Vec<T>>,
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
load_online: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Result<Vec<T>>>>>>>,
load_local: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Vec<T>>>>>>,
filter: RefCell<Option<Box<dyn Fn(&str, &T) -> bool>>>,
}
impl<T> Selector<T> {
/// Create a new selector.
pub fn new() -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Label, subtitle_label);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::CheckButton, server_check_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, frame);
get_widget!(builder, gtk::Button, try_again_button);
let list = List::new();
frame.set_child(Some(&list.widget));
let this = Rc::new(Self {
widget,
title_label,
subtitle_label,
search_entry,
server_check_button,
stack,
list,
items: RefCell::new(Vec::new()),
back_cb: RefCell::new(None),
add_cb: RefCell::new(None),
make_widget: RefCell::new(None),
load_online: RefCell::new(None),
load_local: RefCell::new(None),
filter: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.back_cb.borrow() {
cb();
}
}));
add_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.add_cb.borrow() {
cb();
}
}));
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
this.list.invalidate_filter();
}));
this.server_check_button
.connect_toggled(clone!(@strong this => move |_| {
if this.server_check_button.get_active() {
this.clone().load_online();
} else {
this.clone().load_local();
}
}));
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
if let Some(cb) = &*this.make_widget.borrow() {
let item = &this.items.borrow()[index];
cb(item)
} else {
gtk::Label::new(None).upcast()
}
}));
this.list.set_filter_cb(clone!(@strong this => move |index| {
match &*this.filter.borrow() {
Some(filter) => {
let item = &this.items.borrow()[index];
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
search.is_empty() || filter(&search, item)
}
None => true,
}
}));
try_again_button.connect_clicked(clone!(@strong this => move |_| {
this.clone().load_online();
}));
// Initialize
this.clone().load_online();
this
}
/// Set the title to be shown in the header.
pub fn set_title(&self, title: &str) {
self.title_label.set_label(&title);
}
/// Set the subtitle to be shown in the header.
pub fn set_subtitle(&self, subtitle: &str) {
self.subtitle_label.set_label(&subtitle);
self.subtitle_label.show();
}
/// Set the closure to be called when the user wants to go back.
pub fn set_back_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.back_cb.replace(Some(Box::new(cb)));
}
/// Set the closure to be called when the user wants to add an item.
pub fn set_add_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.add_cb.replace(Some(Box::new(cb)));
}
/// Set the async closure to be called to fetch items from the server. If that results in an
/// error, an error screen is shown allowing to try again.
pub fn set_load_online<F, R>(&self, cb: F)
where
F: (Fn() -> R) + 'static,
R: Future<Output = Result<Vec<T>>> + 'static,
{
self.load_online
.replace(Some(Box::new(move || Box::new(cb()))));
}
/// Set the async closure to be called to get local items.
pub fn set_load_local<F, R>(&self, cb: F)
where
F: (Fn() -> R) + 'static,
R: Future<Output = Vec<T>> + 'static,
{
self.load_local
.replace(Some(Box::new(move || Box::new(cb()))));
}
/// Set the closure to be called for creating a new list row.
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
self.make_widget.replace(Some(Box::new(make_widget)));
}
/// Set a closure to call when deciding whether to show an item based on a search string. The
/// search string will be converted to lowercase.
pub fn set_filter<F: Fn(&str, &T) -> bool + 'static>(&self, filter: F) {
self.filter.replace(Some(Box::new(filter)));
}
fn load_online(self: Rc<Self>) {
let context = glib::MainContext::default();
let clone = self.clone();
context.spawn_local(async move {
if let Some(cb) = &*self.load_online.borrow() {
self.stack.set_visible_child_name("loading");
match Pin::from(cb()).await {
Ok(items) => {
clone.show_items(items);
}
Err(_) => {
clone.show_items(Vec::new());
clone.stack.set_visible_child_name("error");
}
}
}
});
}
fn load_local(self: Rc<Self>) {
let context = glib::MainContext::default();
let clone = self.clone();
context.spawn_local(async move {
if let Some(cb) = &*self.load_local.borrow() {
self.stack.set_visible_child_name("loading");
let items = Pin::from(cb()).await;
clone.show_items(items);
}
});
}
fn show_items(&self, items: Vec<T>) {
let length = items.len();
self.items.replace(items);
self.list.update(length);
self.stack.set_visible_child_name("content");
}
}

View file

@ -1,157 +0,0 @@
use super::selector::Selector;
use crate::backend::{Backend, Person, Work};
use crate::editors::{PersonEditor, WorkEditor};
use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::Widget;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting a work.
pub struct WorkSelector {
handle: NavigationHandle<Work>,
selector: Rc<Selector<Person>>,
}
impl Screen<(), Work> for WorkSelector {
fn new(_: (), handle: NavigationHandle<Work>) -> Rc<Self> {
// Create UI
let selector = Selector::<Person>::new();
selector.set_title(&gettext("Select composer"));
let this = Rc::new(Self {
handle,
selector,
});
// Connect signals and callbacks
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
if let Some(person) = push!(this.handle, PersonEditor, None).await {
// We can assume that there are no existing works of this composer and
// immediately show the work editor. Going back from the work editor will
// correctly show the person selector again.
let work = Work::new(person);
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
this.handle.pop(Some(work));
}
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
async move { this.handle.backend.get_persons().await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
async move { this.handle.backend.db().get_persons().await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |person| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&person.name_lf()));
let person = person.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
// Instead of returning the person from here, like the person selector does, we
// show a second selector for choosing the work.
let person = person.clone();
spawn!(@clone this, async move {
if let Some(work) = push!(this.handle, WorkSelectorWorkScreen, person).await {
this.handle.pop(Some(work));
}
});
}));
row.upcast()
}));
this.selector
.set_filter(|search, person| person.name_fl().to_lowercase().contains(search));
this
}
}
impl Widget for WorkSelector {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}
/// The actual work selector that is displayed after the user has selected a composer.
struct WorkSelectorWorkScreen {
handle: NavigationHandle<Work>,
person: Person,
selector: Rc<Selector<Work>>,
}
impl Screen<Person, Work> for WorkSelectorWorkScreen {
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> {
let selector = Selector::<Work>::new();
selector.set_title(&gettext("Select work"));
selector.set_subtitle(&person.name_fl());
let this = Rc::new(Self {
handle,
person,
selector,
});
this.selector.set_back_cb(clone!(@weak this => move || {
this.handle.pop(None);
}));
this.selector.set_add_cb(clone!(@weak this => move || {
spawn!(@clone this, async move {
let work = Work::new(this.person.clone());
if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await {
this.handle.pop(Some(work));
}
});
}));
this.selector.set_load_online(clone!(@weak this => move || {
async move { this.handle.backend.get_works(&this.person.id).await }
}));
this.selector.set_load_local(clone!(@weak this => move || {
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
}));
this.selector.set_make_widget(clone!(@weak this => move |work| {
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&work.title));
let work = work.to_owned();
row.connect_activated(clone!(@weak this => move |_| {
this.handle.pop(Some(work.clone()));
}));
row.upcast()
}));
this.selector.set_filter(|search, work| work.title.to_lowercase().contains(search));
this
}
}
impl Widget for WorkSelectorWorkScreen {
fn get_widget(&self) -> gtk::Widget {
self.selector.widget.clone().upcast()
}
}

View file

@ -1,51 +0,0 @@
use super::Widget;
use gtk::prelude::*;
use libadwaita::prelude::*;
/// A list box row with a single button.
pub struct ButtonRow {
/// The actual GTK widget.
pub widget: libadwaita::ActionRow,
/// The managed button.
button: gtk::Button,
}
impl ButtonRow {
/// Create a new button row.
pub fn new(title: &str, label: &str) -> Self {
let button = gtk::ButtonBuilder::new()
.valign(gtk::Align::Center)
.label(label)
.build();
let widget = libadwaita::ActionRowBuilder::new()
.activatable(true)
.activatable_widget(&button)
.title(title)
.build();
widget.add_suffix(&button);
Self {
widget,
button,
}
}
/// Set the subtitle of the row.
pub fn set_subtitle(&self, subtitle: Option<&str>) {
self.widget.set_subtitle(subtitle);
}
/// Set the closure to be called on activation
pub fn set_cb<F: Fn() + 'static>(&self, cb: F) {
self.button.connect_clicked(move |_| cb());
}
}
impl Widget for ButtonRow {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,91 +0,0 @@
use super::Widget;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
/// Common UI elements for an editor.
pub struct Editor {
/// The actual GTK widget.
pub widget: gtk::Stack,
/// The button to switch to the previous screen.
back_button: gtk::Button,
/// The title widget within the header bar.
window_title: libadwaita::WindowTitle,
/// The button to save the edited item.
save_button: gtk::Button,
/// The box containing the content.
content_box: gtk::Box,
/// The status page for the error screen.
status_page: libadwaita::StatusPage,
}
impl Editor {
/// Create a new screen.
pub fn new() -> Self {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, libadwaita::WindowTitle, window_title);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Box, content_box);
get_widget!(builder, libadwaita::StatusPage, status_page);
get_widget!(builder, gtk::Button, try_again_button);
try_again_button.connect_clicked(clone!(@strong widget => move |_| {
widget.set_visible_child_name("content");
}));
Self {
widget,
back_button,
window_title,
save_button,
content_box,
status_page,
}
}
/// Set a closure to be called when the back button is pressed.
pub fn set_back_cb<F: Fn() + 'static>(&self, cb: F) {
self.back_button.connect_clicked(move |_| cb());
}
/// Show a title in the header bar.
pub fn set_title(&self, title: &str) {
self.window_title.set_title(Some(title));
}
/// Set whether the user should be able to click the save button.
pub fn set_may_save(&self, save: bool) {
self.save_button.set_sensitive(save);
}
pub fn set_save_cb<F: Fn() + 'static>(&self, cb: F) {
self.save_button.connect_clicked(move |_| cb());
}
/// Show a loading page.
pub fn loading(&self) {
self.widget.set_visible_child_name("loading");
}
/// Show an error page. The page contains a button to get back to the
/// actual editor.
pub fn error(&self, title: &str, description: &str) {
self.status_page.set_title(Some(title));
self.status_page.set_description(Some(description));
self.widget.set_visible_child_name("error");
}
/// Add content to the bottom of the content area.
pub fn add_content<W: Widget>(&self, content: &W) {
self.content_box.append(&content.get_widget());
}
}

View file

@ -1,44 +0,0 @@
use gtk::prelude::*;
use libadwaita::prelude::*;
/// A list box row with an entry.
pub struct EntryRow {
/// The actual GTK widget.
pub widget: libadwaita::ActionRow,
/// The managed entry.
entry: gtk::Entry,
}
impl EntryRow {
/// Create a new entry row.
pub fn new(title: &str) -> Self {
let entry = gtk::EntryBuilder::new()
.hexpand(true)
.valign(gtk::Align::Center)
.build();
let widget = libadwaita::ActionRowBuilder::new()
.activatable(true)
.activatable_widget(&entry)
.title(title)
.build();
widget.add_suffix(&entry);
Self {
widget,
entry,
}
}
/// Set the text of the entry.
pub fn set_text(&self, text: &str) {
self.entry.set_text(text);
}
/// Get the text that was entered by the user.
pub fn get_text(&self) -> String {
self.entry.get_text().unwrap().to_string()
}
}

View file

@ -1,180 +0,0 @@
use glib::prelude::*;
use glib::subclass;
use glib::subclass::prelude::*;
use gio::prelude::*;
use gio::subclass::prelude::*;
use once_cell::sync::Lazy;
use std::cell::Cell;
glib::wrapper! {
pub struct IndexedListModel(ObjectSubclass<indexed_list_model::IndexedListModel>)
@implements gio::ListModel;
}
impl IndexedListModel {
/// Create a new indexed list model, which will be empty initially.
pub fn new() -> Self {
glib::Object::new(&[]).unwrap()
}
/// Set the length of the list model.
pub fn set_length(&self, length: u32) {
let old_length = self.get_property("length").unwrap().get_some::<u32>().unwrap();
self.set_property("length", &length).unwrap();
self.items_changed(0, old_length, length);
}
}
mod indexed_list_model {
use super::*;
#[derive(Debug)]
pub struct IndexedListModel {
length: Cell<u32>,
}
impl ObjectSubclass for IndexedListModel {
const NAME: &'static str = "IndexedListModel";
type Type = super::IndexedListModel;
type ParentType = glib::Object;
type Interfaces = (gio::ListModel,);
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib::object_subclass!();
fn new() -> Self {
Self { length: Cell::new(0) }
}
}
impl ObjectImpl for IndexedListModel {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpec::uint(
"length",
"Length",
"Length",
0,
std::u32::MAX,
0,
glib::ParamFlags::READWRITE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.get_name() {
"length" => {
let length = value.get().unwrap().unwrap();
self.length.set(length);
}
_ => unimplemented!(),
}
}
fn get_property(&self, _obj: &Self::Type, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.get_name() {
"length" => self.length.get().to_value(),
_ => unimplemented!(),
}
}
}
impl ListModelImpl for IndexedListModel {
fn get_item_type(&self, _: &Self::Type) -> glib::Type {
ItemIndex::static_type()
}
fn get_n_items(&self, _: &Self::Type) -> u32 {
self.length.get()
}
fn get_item(&self, _: &Self::Type, position: u32) -> Option<glib::Object> {
Some(ItemIndex::new(position).upcast())
}
}
}
glib::wrapper! {
pub struct ItemIndex(ObjectSubclass<item_index::ItemIndex>);
}
impl ItemIndex {
/// Create a new item index.
pub fn new(value: u32) -> Self {
glib::Object::new(&[("value", &value)]).unwrap()
}
/// Get the value of the item index..
pub fn get(&self) -> u32 {
self.get_property("value").unwrap().get_some::<u32>().unwrap()
}
}
mod item_index {
use super::*;
#[derive(Debug)]
pub struct ItemIndex {
value: Cell<u32>,
}
impl ObjectSubclass for ItemIndex {
const NAME: &'static str = "ItemIndex";
type Type = super::ItemIndex;
type ParentType = glib::Object;
type Interfaces = ();
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib::object_subclass!();
fn new() -> Self {
Self { value: Cell::new(0) }
}
}
impl ObjectImpl for ItemIndex {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpec::uint(
"value",
"Value",
"Value",
0,
std::u32::MAX,
0,
glib::ParamFlags::READWRITE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.get_name() {
"value" => {
let value = value.get().unwrap().unwrap();
self.value.set(value);
}
_ => unimplemented!(),
}
}
fn get_property(&self, _obj: &Self::Type, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.get_name() {
"value" => self.value.get().to_value(),
_ => unimplemented!(),
}
}
}
}

View file

@ -1,140 +0,0 @@
use super::indexed_list_model::{IndexedListModel, ItemIndex};
use glib::clone;
use gtk::prelude::*;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
/// A simple list of widgets.
pub struct List {
pub widget: gtk::ListBox,
model: IndexedListModel,
filter: gtk::CustomFilter,
enable_dnd: Cell<bool>,
make_widget_cb: RefCell<Option<Box<dyn Fn(usize) -> gtk::Widget>>>,
filter_cb: RefCell<Option<Box<dyn Fn(usize) -> bool>>>,
move_cb: RefCell<Option<Box<dyn Fn(usize, usize)>>>,
}
impl List {
/// Create a new list. The list will be empty initially.
pub fn new() -> Rc<Self> {
let model = IndexedListModel::new();
let filter = gtk::CustomFilter::new(|_| true);
let filter_model = gtk::FilterListModel::new(Some(&model), Some(&filter));
// TODO: Switch to gtk::ListView.
// let selection = gtk::NoSelection::new(Some(&model));
// let factory = gtk::SignalListItemFactory::new();
// let widget = gtk::ListView::new(Some(&selection), Some(&factory));
let widget = gtk::ListBox::new();
widget.set_selection_mode(gtk::SelectionMode::None);
let this = Rc::new(Self {
widget,
model,
filter,
enable_dnd: Cell::new(false),
make_widget_cb: RefCell::new(None),
filter_cb: RefCell::new(None),
move_cb: RefCell::new(None),
});
this.filter.set_filter_func(clone!(@strong this => move |index| {
if let Some(cb) = &*this.filter_cb.borrow() {
let index = index.downcast_ref::<ItemIndex>().unwrap().get() as usize;
cb(index)
} else {
true
}
}));
this.widget.bind_model(Some(&filter_model), clone!(@strong this => move |index| {
let index = index.downcast_ref::<ItemIndex>().unwrap().get() as usize;
if let Some(cb) = &*this.make_widget_cb.borrow() {
let widget = cb(index);
if this.enable_dnd.get() {
let drag_source = gtk::DragSource::new();
drag_source.connect_drag_begin(clone!(@strong widget => move |_, drag| {
// TODO: Replace with a better solution.
let paintable = gtk::WidgetPaintable::new(Some(&widget));
gtk::DragIcon::set_from_paintable(drag, &paintable, 0, 0);
}));
let drag_value = (index as u32).to_value();
drag_source.set_content(Some(&gdk::ContentProvider::new_for_value(&drag_value)));
let drop_target = gtk::DropTarget::new(glib::Type::U32, gdk::DragAction::COPY);
drop_target.connect_drop(clone!(@strong this => move |_, value, _, _| {
if let Some(cb) = &*this.move_cb.borrow() {
let old_index: u32 = value.get_some().unwrap();
cb(old_index as usize, index);
true
} else {
false
}
}));
widget.add_controller(&drag_source);
widget.add_controller(&drop_target);
}
widget
} else {
// This shouldn't be reachable under normal circumstances.
gtk::Label::new(None).upcast()
}
}));
this
}
/// Whether the list should support drag and drop.
pub fn set_enable_dnd(&self, enable: bool) {
self.enable_dnd.set(enable);
}
/// Set the closure to be called to construct widgets for the items.
pub fn set_make_widget_cb<F: Fn(usize) -> gtk::Widget + 'static>(&self, cb: F) {
self.make_widget_cb.replace(Some(Box::new(cb)));
}
/// Set the closure to be called to filter the items. If this returns
/// false, the item will not be shown.
pub fn set_filter_cb<F: Fn(usize) -> bool + 'static>(&self, cb: F) {
self.filter_cb.replace(Some(Box::new(cb)));
}
/// Set the closure to be called to when the use has dragged an item to a
/// new position.
pub fn set_move_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) {
self.move_cb.replace(Some(Box::new(cb)));
}
/// Set the lists selection mode to single.
pub fn enable_selection(&self) {
self.widget.set_selection_mode(gtk::SelectionMode::Single);
}
/// Select an item by its index. If the index is out of range, nothing will happen.
pub fn select(&self, index: usize) {
let row = self.widget.get_row_at_index(index as i32);
if let Some(row) = row {
self.widget.select_row(Some(&row));
}
}
/// Refilter the list based on the filter callback.
pub fn invalidate_filter(&self) {
self.filter.changed(gtk::FilterChange::Different);
}
/// Call the make_widget function for each item. This will automatically
/// show all children by indices 0..length.
pub fn update(&self, length: usize) {
self.model.set_length(length as u32);
}
}

View file

@ -1,42 +0,0 @@
use gtk::prelude::*;
pub mod button_row;
pub use button_row::*;
pub mod editor;
pub use editor::*;
pub mod entry_row;
pub use entry_row::*;
pub mod list;
pub use list::*;
pub mod player_bar;
pub use player_bar::*;
pub mod poe_list;
pub use poe_list::*;
pub mod screen;
pub use screen::*;
pub mod section;
pub use section::*;
pub mod upload_section;
pub use upload_section::*;
mod indexed_list_model;
/// Something that can be represented as a GTK widget.
pub trait Widget {
/// Get the widget.
fn get_widget(&self) -> gtk::Widget;
}
impl<W: IsA<gtk::Widget>> Widget for W {
fn get_widget(&self) -> gtk::Widget {
self.clone().upcast()
}
}

View file

@ -1,171 +0,0 @@
use crate::backend::{Player, PlaylistItem};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
pub struct PlayerBar {
pub widget: gtk::Revealer,
title_label: gtk::Label,
subtitle_label: gtk::Label,
previous_button: gtk::Button,
play_button: gtk::Button,
next_button: gtk::Button,
position_label: gtk::Label,
duration_label: gtk::Label,
play_image: gtk::Image,
pause_image: gtk::Image,
player: Rc<RefCell<Option<Rc<Player>>>>,
playlist_cb: Rc<RefCell<Option<Box<dyn Fn() -> ()>>>>,
}
impl PlayerBar {
pub fn new() -> Self {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_bar.ui");
get_widget!(builder, gtk::Revealer, widget);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Label, subtitle_label);
get_widget!(builder, gtk::Button, previous_button);
get_widget!(builder, gtk::Button, play_button);
get_widget!(builder, gtk::Button, next_button);
get_widget!(builder, gtk::Label, position_label);
get_widget!(builder, gtk::Label, duration_label);
get_widget!(builder, gtk::Button, playlist_button);
get_widget!(builder, gtk::Image, play_image);
get_widget!(builder, gtk::Image, pause_image);
let player = Rc::new(RefCell::new(None::<Rc<Player>>));
let playlist_cb = Rc::new(RefCell::new(None::<Box<dyn Fn() -> ()>>));
previous_button.connect_clicked(clone!(@strong player => move |_| {
if let Some(player) = &*player.borrow() {
player.previous().unwrap();
}
}));
play_button.connect_clicked(clone!(@strong player => move |_| {
if let Some(player) = &*player.borrow() {
player.play_pause();
}
}));
next_button.connect_clicked(clone!(@strong player => move |_| {
if let Some(player) = &*player.borrow() {
player.next().unwrap();
}
}));
playlist_button.connect_clicked(clone!(@strong playlist_cb => move |_| {
if let Some(cb) = &*playlist_cb.borrow() {
cb();
}
}));
Self {
widget,
title_label,
subtitle_label,
previous_button,
play_button,
next_button,
position_label,
duration_label,
play_image,
pause_image,
player: player,
playlist_cb: playlist_cb,
}
}
pub fn set_player(&self, player: Option<Rc<Player>>) {
self.player.replace(player.clone());
if let Some(player) = player {
let playlist = Rc::new(RefCell::new(Vec::<PlaylistItem>::new()));
player.add_playlist_cb(clone!(
@strong player,
@strong self.widget as widget,
@strong self.previous_button as previous_button,
@strong self.next_button as next_button,
@strong playlist
=> move |new_playlist| {
widget.set_reveal_child(!new_playlist.is_empty());
playlist.replace(new_playlist);
previous_button.set_sensitive(player.has_previous());
next_button.set_sensitive(player.has_next());
}
));
player.add_track_cb(clone!(
@strong player,
@strong playlist,
@strong self.previous_button as previous_button,
@strong self.next_button as next_button,
@strong self.title_label as title_label,
@strong self.subtitle_label as subtitle_label,
@strong self.position_label as position_label
=> move |current_item, current_track| {
previous_button.set_sensitive(player.has_previous());
next_button.set_sensitive(player.has_next());
let item = &playlist.borrow()[current_item];
let track = &item.track_set.tracks[current_track];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.track_set.recording.work.parts[*part].title.clone());
}
let mut title = item.track_set.recording.work.get_title();
if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", "));
}
title_label.set_text(&title);
subtitle_label.set_text(&item.track_set.recording.get_performers());
position_label.set_text("0:00");
}
));
player.add_duration_cb(clone!(
@strong self.duration_label as duration_label
=> move |ms| {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
duration_label.set_text(&format!("{}:{:02}", min, sec));
}
));
player.add_playing_cb(clone!(
@strong self.play_button as play_button,
@strong self.play_image as play_image,
@strong self.pause_image as pause_image
=> move |playing| {
play_button.set_child(Some(if playing {
&pause_image
} else {
&play_image
}));
}
));
player.add_position_cb(clone!(
@strong self.position_label as position_label
=> move |ms| {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
position_label.set_text(&format!("{}:{:02}", min, sec));
}
));
} else {
self.widget.set_reveal_child(false);
}
}
pub fn set_playlist_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.playlist_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -1,121 +0,0 @@
use super::*;
use crate::backend::{Backend, Person, Ensemble};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libadwaita::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone)]
pub enum PersonOrEnsemble {
Person(Person),
Ensemble(Ensemble),
}
impl PersonOrEnsemble {
pub fn get_title(&self) -> String {
match self {
PersonOrEnsemble::Person(person) => person.name_lf(),
PersonOrEnsemble::Ensemble(ensemble) => ensemble.name.clone(),
}
}
}
pub struct PoeList {
pub widget: gtk::Box,
backend: Rc<Backend>,
stack: gtk::Stack,
search_entry: gtk::SearchEntry,
list: Rc<List>,
data: RefCell<Vec<PersonOrEnsemble>>,
selected_cb: RefCell<Option<Box<dyn Fn(&PersonOrEnsemble)>>>,
}
impl PoeList {
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/poe_list.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::ScrolledWindow, scrolled_window);
let list = List::new();
list.widget.add_css_class("navigation-sidebar");
list.enable_selection();
scrolled_window.set_child(Some(&list.widget));
let this = Rc::new(Self {
widget,
backend,
stack,
search_entry,
list,
data: RefCell::new(Vec::new()),
selected_cb: RefCell::new(None),
});
this.search_entry.connect_search_changed(clone!(@strong this => move |_| {
this.list.invalidate_filter();
}));
this.list.set_make_widget_cb(clone!(@strong this => move |index| {
let poe = &this.data.borrow()[index];
let row = libadwaita::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&poe.get_title()));
let poe = poe.to_owned();
row.connect_activated(clone!(@strong this => move |_| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(&poe);
}
}));
row.upcast()
}));
this.list.set_filter_cb(clone!(@strong this => move |index| {
let poe = &this.data.borrow()[index];
let search = this.search_entry.get_text().unwrap().to_string().to_lowercase();
let title = poe.get_title().to_lowercase();
search.is_empty() || title.contains(&search)
}));
this
}
pub fn set_selected_cb<F: Fn(&PersonOrEnsemble) + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
pub fn reload(self: Rc<Self>) {
self.stack.set_visible_child_name("loading");
let context = glib::MainContext::default();
let backend = self.backend.clone();
context.spawn_local(async move {
let persons = backend.db().get_persons().await.unwrap();
let ensembles = backend.db().get_ensembles().await.unwrap();
let mut poes: Vec<PersonOrEnsemble> = Vec::new();
for person in persons {
poes.push(PersonOrEnsemble::Person(person));
}
for ensemble in ensembles {
poes.push(PersonOrEnsemble::Ensemble(ensemble));
}
let length = poes.len();
self.data.replace(poes);
self.list.update(length);
self.stack.set_visible_child_name("content");
});
}
}

View file

@ -1,113 +0,0 @@
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
/// A general framework for screens. Screens have a header bar with at least
/// a button to go back and a scrollable content area that clamps its content.
pub struct Screen {
/// The actual GTK widget.
pub widget: gtk::Box,
/// The button to switch to the previous screen.
back_button: gtk::Button,
/// The title widget within the header bar.
window_title: libadwaita::WindowTitle,
/// The action menu.
menu: gio::Menu,
/// The entry for searching.
search_entry: gtk::SearchEntry,
/// The stack to switch to the loading page.
stack: gtk::Stack,
/// The box containing the content.
content_box: gtk::Box,
/// The actions for the menu.
actions: gio::SimpleActionGroup,
}
impl Screen {
/// Create a new screen.
pub fn new() -> Self {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, libadwaita::WindowTitle, window_title);
get_widget!(builder, gio::Menu, menu);
get_widget!(builder, gtk::ToggleButton, search_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Box, content_box);
let actions = gio::SimpleActionGroup::new();
widget.insert_action_group("widget", Some(&actions));
search_button.connect_toggled(clone!(@strong search_entry => move |search_button| {
if search_button.get_active() {
search_entry.grab_focus();
}
}));
Self {
widget,
back_button,
window_title,
menu,
search_entry,
stack,
content_box,
actions,
}
}
/// Set a closure to be called when the back button is pressed.
pub fn set_back_cb<F: Fn() + 'static>(&self, cb: F) {
self.back_button.connect_clicked(move |_| cb());
}
/// Show a title in the header bar.
pub fn set_title(&self, title: &str) {
self.window_title.set_title(Some(title));
}
/// Show a subtitle in the header bar.
pub fn set_subtitle(&self, subtitle: &str) {
self.window_title.set_subtitle(Some(subtitle));
}
/// Add a new item to the action menu and register a callback for it.
pub fn add_action<F: Fn() + 'static>(&self, label: &str, cb: F) {
let name = rand::random::<u64>().to_string();
let action = gio::SimpleAction::new(&name, None);
action.connect_activate(move |_, _| cb());
self.actions.add_action(&action);
self.menu.append(Some(label), Some(&format!("widget.{}", name)));
}
/// Set the closure to be called when the search string has changed.
pub fn set_search_cb<F: Fn() + 'static>(&self, cb: F) {
self.search_entry.connect_search_changed(move |_| cb());
}
/// Get the current search string.
pub fn get_search(&self) -> String {
self.search_entry.get_text().unwrap().to_string().to_lowercase()
}
/// Hide the loading page and switch to the content.
pub fn ready(&self) {
self.stack.set_visible_child_name("content");
}
/// Add content to the bottom of the content area.
pub fn add_content<W: IsA<gtk::Widget>>(&self, content: &W) {
self.content_box.append(content);
}
}

View file

@ -1,67 +0,0 @@
use super::Widget;
use gtk::prelude::*;
use gtk_macros::get_widget;
/// A widget displaying a title, a framed child widget and, if needed, some
/// actions.
pub struct Section {
/// The actual GTK widget.
pub widget: gtk::Box,
/// The box containing the title and action buttons.
title_box: gtk::Box,
/// An optional subtitle below the title.
subtitle_label: gtk::Label,
}
impl Section {
/// Create a new section.
pub fn new<W: Widget>(title: &str, content: &W) -> Self {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Box, title_box);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Label, subtitle_label);
get_widget!(builder, gtk::Frame, frame);
title_label.set_label(title);
frame.set_child(Some(&content.get_widget()));
Self {
widget,
title_box,
subtitle_label,
}
}
/// Add a subtitle below the title.
pub fn set_subtitle(&self, subtitle: &str) {
self.subtitle_label.set_label(subtitle);
self.subtitle_label.show();
}
/// Add an action button. This should by definition be something that is
/// doing something with the child widget that is applicable in all
/// situations where the widget is visible. The new button will be packed
/// to the end of the title box.
pub fn add_action<F: Fn() + 'static>(&self, icon_name: &str, cb: F) {
let button = gtk::ButtonBuilder::new()
.has_frame(false)
.valign(gtk::Align::Center)
.margin_top(12)
.icon_name(icon_name)
.build();
button.connect_clicked(move |_| cb());
self.title_box.append(&button);
}
}
impl Widget for Section {
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
}

View file

@ -1,53 +0,0 @@
use super::Section;
use gettextrs::gettext;
use gtk::prelude::*;
use libadwaita::prelude::*;
/// A section showing a switch to enable uploading an item.
pub struct UploadSection {
/// The GTK widget of the wrapped section.
pub widget: gtk::Box,
/// The section itself.
section: Section,
/// The upload switch.
switch: gtk::Switch,
}
impl UploadSection {
/// Create a new upload section which will be initially switched on.
pub fn new() -> Self {
let list = gtk::ListBoxBuilder::new()
.selection_mode(gtk::SelectionMode::None)
.build();
let switch = gtk::SwitchBuilder::new()
.active(true)
.valign(gtk::Align::Center)
.build();
let row = libadwaita::ActionRowBuilder::new()
.title("Upload changes to the server")
.activatable(true)
.activatable_widget(&switch)
.build();
row.add_suffix(&switch);
list.append(&row);
let section = Section::new(&gettext("Upload"), &list);
Self {
widget: section.widget.clone(),
section,
switch,
}
}
/// Return whether the user has enabled the upload switch.
pub fn get_active(&self) -> bool {
self.switch.get_active()
}
}

View file

@ -1,228 +0,0 @@
use crate::backend::*;
use crate::config;
use crate::import::SourceSelector;
use crate::preferences::Preferences;
use crate::screens::*;
use crate::widgets::*;
use crate::navigator::{Navigator, NavigatorWindow};
use futures::prelude::*;
use gettextrs::gettext;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::{action, get_widget};
use std::rc::Rc;
pub struct Window {
backend: Rc<Backend>,
window: libadwaita::ApplicationWindow,
stack: gtk::Stack,
leaflet: libadwaita::Leaflet,
sidebar_box: gtk::Box,
poe_list: Rc<PoeList>,
navigator: Rc<Navigator>,
player_bar: PlayerBar,
player_screen: Rc<PlayerScreen>,
}
impl Window {
pub fn new(app: &gtk::Application) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/window.ui");
get_widget!(builder, libadwaita::ApplicationWindow, window);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Button, select_music_library_path_button);
get_widget!(builder, gtk::Box, content_box);
get_widget!(builder, libadwaita::Leaflet, leaflet);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Box, sidebar_box);
get_widget!(builder, gtk::Box, empty_screen);
let backend = Rc::new(Backend::new());
let player_screen = PlayerScreen::new();
stack.add_named(&player_screen.widget, Some("player_screen"));
let poe_list = PoeList::new(backend.clone());
let navigator = Navigator::new(backend.clone(), &window, &empty_screen);
navigator.set_back_cb(clone!(@strong leaflet, @strong sidebar_box => move || {
leaflet.set_visible_child(&sidebar_box);
}));
let player_bar = PlayerBar::new();
content_box.append(&player_bar.widget);
let result = Rc::new(Self {
backend,
window,
stack,
leaflet,
sidebar_box,
poe_list,
navigator,
player_bar,
player_screen,
});
result.window.set_application(Some(app));
select_music_library_path_button.connect_clicked(clone!(@strong result => move |_| {
let dialog = gtk::FileChooserDialog::new(
Some(&gettext("Select music library folder")),
Some(&result.window),
gtk::FileChooserAction::SelectFolder,
&[
(&gettext("Cancel"), gtk::ResponseType::Cancel),
(&gettext("Select"), gtk::ResponseType::Accept),
]);
dialog.connect_response(clone!(@strong result => move |dialog, response| {
if let gtk::ResponseType::Accept = response {
if let Some(file) = dialog.get_file() {
if let Some(path) = file.get_path() {
let context = glib::MainContext::default();
let backend = result.backend.clone();
context.spawn_local(async move {
backend.set_music_library_path(path).await.unwrap();
});
}
}
}
dialog.hide();
}));
dialog.show();
}));
add_button.connect_clicked(clone!(@strong result => move |_| {
spawn!(@clone result, async move {
let window = NavigatorWindow::new(result.backend.clone());
replace!(window.navigator, SourceSelector).await;
});
}));
result
.player_bar
.set_playlist_cb(clone!(@strong result => move || {
result.stack.set_visible_child_name("player_screen");
}));
result
.player_screen
.set_back_cb(clone!(@strong result => move || {
result.stack.set_visible_child_name("content");
}));
// action!(
// result.window,
// "import-disc",
// clone!(@strong result => move |_, _| {
// let dialog = ImportDiscDialog::new(result.backend.clone());
// let window = NavigatorWindow::new(dialog);
// window.show();
// })
// );
action!(
result.window,
"preferences",
clone!(@strong result => move |_, _| {
Preferences::new(result.backend.clone(), &result.window).show();
})
);
action!(
result.window,
"about",
clone!(@strong result => move |_, _| {
result.show_about_dialog();
})
);
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let mut state_stream = clone.backend.state_stream.borrow_mut();
while let Some(state) = state_stream.next().await {
match state {
BackendState::NoMusicLibrary => {
clone.stack.set_visible_child_name("empty");
}
BackendState::Loading => {
clone.stack.set_visible_child_name("loading");
}
BackendState::Ready => {
clone.stack.set_visible_child_name("content");
clone.poe_list.clone().reload();
clone.navigator.reset();
let player = clone.backend.get_player().unwrap();
clone.player_bar.set_player(Some(player.clone()));
clone.player_screen.clone().set_player(Some(player));
}
}
}
});
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.append(&result.navigator.widget);
result
.poe_list
.set_selected_cb(clone!(@strong result => move |poe| {
result.leaflet.set_visible_child(&result.navigator.widget);
let poe = poe.to_owned();
spawn!(@clone result, async move {
match poe {
PersonOrEnsemble::Person(person) => {
replace!(result.navigator, PersonScreen, person.clone()).await;
}
PersonOrEnsemble::Ensemble(ensemble) => {
replace!(result.navigator, EnsembleScreen, ensemble.clone()).await;
}
}
});
}));
result
.sidebar_box
.append(&result.poe_list.widget);
result
}
pub fn present(&self) {
self.window.present();
}
fn reload(&self) {
self.poe_list.clone().reload();
self.navigator.reset();
self.leaflet.set_visible_child(&self.sidebar_box);
}
fn show_about_dialog(&self) {
let dialog = gtk::AboutDialogBuilder::new()
.transient_for(&self.window)
.modal(true)
.logo_icon_name("de.johrpan.musicus")
.program_name(&gettext("Musicus"))
.version(config::VERSION)
.comments(&gettext("The classical music player and organizer."))
.website("https://github.com/johrpan/musicus")
.website_label(&gettext("Further information and source code"))
.copyright("© 2020 Elias Projahn")
.license_type(gtk::License::Agpl30)
.authors(vec![String::from("Elias Projahn <johrpan@gmail.com>")])
.build();
dialog.show();
}
}