Move desktop app to subdirectory

This commit is contained in:
Elias Projahn 2020-11-14 23:01:51 +01:00
parent ea3bd35ffd
commit 775f3ffe90
109 changed files with 64 additions and 53 deletions

View file

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

View file

@ -1,31 +0,0 @@
use super::Backend;
use anyhow::{anyhow, bail, Result};
use isahc::http::StatusCode;
use isahc::prelude::*;
impl Backend {
/// Try to login a user with the provided credentials and return, wether the login suceeded.
pub async fn login(&self) -> Result<bool> {
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
let data = self.get_login_data().ok_or(anyhow!("No login data set!"))?;
let request = Request::post(format!("{}/login", server_url))
.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);
println!("{}", &token);
true
}
StatusCode::UNAUTHORIZED => false,
_ => bail!("Unexpected response status!"),
};
Ok(success)
}
}

View file

@ -1,7 +0,0 @@
pub mod backend;
pub use backend::*;
pub mod client;
pub use client::*;
mod secure;

View file

@ -1,108 +0,0 @@
use super::LoginData;
use anyhow::{anyhow, Result};
use futures_channel::oneshot;
use secret_service::{Collection, EncryptionType, SecretService};
/// Savely store the user's current login credentials.
pub async fn store_login_data(data: LoginData) -> Result<()> {
let (sender, receiver) = oneshot::channel::<Result<()>>();
std::thread::spawn(move || sender.send(store_login_data_priv(data)));
receiver.await?
}
/// Savely store the user's current login credentials.
fn store_login_data_priv(data: LoginData) -> Result<()> {
let ss = get_ss()?;
let collection = get_collection(&ss)?;
let key = "musicus-login-data";
delete_secrets(&collection, key)?;
collection
.create_item(
key,
vec![("username", &data.username)],
data.password.as_bytes(),
true,
"text/plain",
)
.or(Err(anyhow!(
"Failed to save login data using SecretService!"
)))?;
Ok(())
}
/// Get the login credentials from secret storage.
pub fn load_login_data() -> Result<Option<LoginData>> {
let ss = get_ss()?;
let collection = get_collection(&ss)?;
let items = collection.get_all_items().or(Err(anyhow!(
"Failed to get items from SecretService collection!"
)))?;
let key = "musicus-login-data";
let item = items
.iter()
.find(|item| item.get_label().unwrap_or_default() == key);
Ok(match item {
Some(item) => {
let attrs = item.get_attributes().or(Err(anyhow!(
"Failed to get attributes for ScretService item!"
)))?;
let username = attrs
.iter()
.find(|attr| attr.0 == "username")
.ok_or(anyhow!("No username in login data!"))?
.1
.clone();
let password = std::str::from_utf8(
&item
.get_secret()
.or(Err(anyhow!("Failed to get secret from SecretService!")))?,
)?
.to_string();
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().or(Err(anyhow!(
"Failed to get items from SecretService collection!"
)))?;
for item in items {
if item.get_label().unwrap_or_default() == key {
item.delete()
.or(Err(anyhow!("Failed to delete SecretService item!")))?;
}
}
Ok(())
}
/// Get the SecretService interface.
fn get_ss() -> Result<SecretService> {
SecretService::new(EncryptionType::Dh).or(Err(anyhow!("Failed to get SecretService!")))
}
/// Get the default SecretService collection and unlock it.
fn get_collection(ss: &SecretService) -> Result<Collection> {
let collection = ss
.get_default_collection()
.or(Err(anyhow!("Failed to get SecretService connection!")))?;
collection
.unlock()
.or(Err(anyhow!("Failed to unclock SecretService collection!")))?;
Ok(collection)
}

View file

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

View file

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

View file

@ -1,10 +0,0 @@
pub mod database;
pub use database::*;
pub mod models;
pub use models::*;
pub mod schema;
pub mod tables;
pub use tables::*;

View file

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

View file

@ -1,121 +0,0 @@
table! {
ensembles (id) {
id -> BigInt,
name -> Text,
}
}
table! {
instrumentations (id) {
id -> BigInt,
work -> BigInt,
instrument -> BigInt,
}
}
table! {
instruments (id) {
id -> BigInt,
name -> Text,
}
}
table! {
part_instrumentations (id) {
id -> BigInt,
work_part -> BigInt,
instrument -> BigInt,
}
}
table! {
performances (id) {
id -> BigInt,
recording -> BigInt,
person -> Nullable<BigInt>,
ensemble -> Nullable<BigInt>,
role -> Nullable<BigInt>,
}
}
table! {
persons (id) {
id -> BigInt,
first_name -> Text,
last_name -> Text,
}
}
table! {
recordings (id) {
id -> BigInt,
work -> BigInt,
comment -> Text,
}
}
table! {
tracks (id) {
id -> BigInt,
file_name -> Text,
recording -> BigInt,
track_index -> Integer,
work_parts -> Text,
}
}
table! {
work_parts (id) {
id -> BigInt,
work -> BigInt,
part_index -> BigInt,
composer -> Nullable<BigInt>,
title -> Text,
}
}
table! {
work_sections (id) {
id -> BigInt,
work -> BigInt,
title -> Text,
before_index -> BigInt,
}
}
table! {
works (id) {
id -> BigInt,
composer -> BigInt,
title -> Text,
}
}
joinable!(instrumentations -> instruments (instrument));
joinable!(instrumentations -> works (work));
joinable!(part_instrumentations -> instruments (instrument));
joinable!(part_instrumentations -> works (work_part));
joinable!(performances -> ensembles (ensemble));
joinable!(performances -> instruments (role));
joinable!(performances -> persons (person));
joinable!(performances -> recordings (recording));
joinable!(recordings -> works (work));
joinable!(tracks -> recordings (recording));
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,
part_instrumentations,
performances,
persons,
recordings,
tracks,
work_parts,
work_sections,
works,
);

View file

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

View file

@ -1,22 +0,0 @@
use crate::config;
use gettextrs::gettext;
use gtk::prelude::*;
pub fn show_about_dialog<W: IsA<gtk::Window>>(parent: &W) {
let dialog = gtk::AboutDialogBuilder::new()
.transient_for(parent)
.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.connect_response(|dialog, _| dialog.close());
dialog.show();
}

View file

@ -1,80 +0,0 @@
use crate::backend::*;
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::rc::Rc;
pub struct EnsembleEditor<F>
where
F: Fn(Ensemble) -> () + 'static,
{
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
id: i64,
name_entry: gtk::Entry,
}
impl<F> EnsembleEditor<F>
where
F: Fn(Ensemble) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
ensemble: Option<Ensemble>,
callback: F,
) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, name_entry);
let id = match ensemble {
Some(ensemble) => {
name_entry.set_text(&ensemble.name);
ensemble.id
}
None => rand::random::<u32>().into(),
};
let result = Rc::new(EnsembleEditor {
backend: backend,
window: window,
callback: callback,
id: id,
name_entry: name_entry,
});
cancel_button.connect_clicked(clone!(@strong result => move |_| {
result.window.close();
}));
save_button.connect_clicked(clone!(@strong result => move |_| {
let ensemble = Ensemble {
id: result.id,
name: result.name_entry.get_text().to_string(),
};
let clone = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
clone.backend.update_ensemble(ensemble.clone()).await.unwrap();
clone.window.close();
(clone.callback)(ensemble.clone());
});
}));
result.window.set_transient_for(Some(parent));
result
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,112 +0,0 @@
use super::EnsembleEditor;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::convert::TryInto;
use std::rc::Rc;
pub struct EnsembleSelector<F>
where
F: Fn(Ensemble) -> () + 'static,
{
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
list: gtk::ListBox,
search_entry: gtk::SearchEntry,
}
impl<F> EnsembleSelector<F>
where
F: Fn(Ensemble) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::ListBox, list);
let result = Rc::new(EnsembleSelector {
backend: backend,
window: window,
callback: callback,
search_entry: search_entry,
list: list,
});
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
let ensembles = clone.backend.get_ensembles().await.unwrap();
for (index, ensemble) in ensembles.iter().enumerate() {
let label = gtk::Label::new(Some(&ensemble.name));
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
let row = SelectorRow::new(index.try_into().unwrap(), &label);
row.show_all();
clone.list.insert(&row, -1);
}
clone.list.connect_row_activated(
clone!(@strong clone, @strong ensembles => move |_, row| {
clone.window.close();
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap();
(clone.callback)(ensembles[index].clone());
}),
);
clone
.list
.set_filter_func(Some(Box::new(clone!(@strong clone => move |row| {
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap();
let search = clone.search_entry.get_text().to_string().to_lowercase();
search.is_empty() || ensembles[index]
.name
.to_lowercase()
.contains(&search)
}))));
});
result
.search_entry
.connect_search_changed(clone!(@strong result => move |_| {
result.list.invalidate_filter();
}));
add_button.connect_clicked(clone!(@strong result => move |_| {
let editor = EnsembleEditor::new(
result.backend.clone(),
&result.window,
None,
clone!(@strong result => move |ensemble| {
result.window.close();
(result.callback)(ensemble);
}),
);
editor.show();
}));
result.window.set_transient_for(Some(parent));
result
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,80 +0,0 @@
use crate::backend::*;
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::rc::Rc;
pub struct InstrumentEditor<F>
where
F: Fn(Instrument) -> () + 'static,
{
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
id: i64,
name_entry: gtk::Entry,
}
impl<F> InstrumentEditor<F>
where
F: Fn(Instrument) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
instrument: Option<Instrument>,
callback: F,
) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, name_entry);
let id = match instrument {
Some(instrument) => {
name_entry.set_text(&instrument.name);
instrument.id
}
None => rand::random::<u32>().into(),
};
let result = Rc::new(InstrumentEditor {
backend: backend,
window: window,
callback: callback,
id: id,
name_entry: name_entry,
});
cancel_button.connect_clicked(clone!(@strong result => move |_| {
result.window.close();
}));
save_button.connect_clicked(clone!(@strong result => move |_| {
let instrument = Instrument {
id: result.id,
name: result.name_entry.get_text().to_string(),
};
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
clone.backend.update_instrument(instrument.clone()).await.unwrap();
clone.window.close();
(clone.callback)(instrument.clone());
});
}));
result.window.set_transient_for(Some(parent));
result
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,113 +0,0 @@
use super::InstrumentEditor;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::convert::TryInto;
use std::rc::Rc;
pub struct InstrumentSelector<F>
where
F: Fn(Instrument) -> () + 'static,
{
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
list: gtk::ListBox,
search_entry: gtk::SearchEntry,
}
impl<F> InstrumentSelector<F>
where
F: Fn(Instrument) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::ListBox, list);
let result = Rc::new(InstrumentSelector {
backend: backend,
window: window,
callback: callback,
search_entry: search_entry,
list: list,
});
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
let instruments = clone.backend.get_instruments().await.unwrap();
for (index, instrument) in instruments.iter().enumerate() {
let label = gtk::Label::new(Some(&instrument.name));
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
let row = SelectorRow::new(index.try_into().unwrap(), &label);
row.show_all();
clone.list.insert(&row, -1);
}
clone.list.connect_row_activated(
clone!(@strong clone, @strong instruments => move |_, row| {
clone.window.close();
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap();
(clone.callback)(instruments[index].clone());
}),
);
clone
.list
.set_filter_func(Some(Box::new(clone!(@strong clone => move |row| {
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap();
let search = clone.search_entry.get_text().to_string();
search.is_empty() || instruments[index]
.name
.to_lowercase()
.contains(&clone.search_entry.get_text().to_string().to_lowercase())
}))));
});
result
.search_entry
.connect_search_changed(clone!(@strong result => move |_| {
result.list.invalidate_filter();
}));
add_button.connect_clicked(clone!(@strong result => move |_| {
let editor = InstrumentEditor::new(
result.backend.clone(),
&result.window,
None,
clone!(@strong result => move |instrument| {
result.window.close();
(result.callback)(instrument);
}),
);
editor.show();
}));
result.window.set_transient_for(Some(parent));
result
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,88 +0,0 @@
use crate::backend::{Backend, LoginData};
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 {
backend: Rc<Backend>,
window: libhandy::Window,
stack: gtk::Stack,
info_bar: gtk::InfoBar,
username_entry: gtk::Entry,
password_entry: gtk::Entry,
selected_cb: RefCell<Option<Box<dyn Fn(LoginData) -> ()>>>,
}
impl LoginDialog {
/// Create a new login 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/login_dialog.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Stack, stack);
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);
window.set_transient_for(Some(parent));
let this = Rc::new(Self {
backend,
window,
stack,
info_bar,
username_entry,
password_entry,
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
login_button.connect_clicked(clone!(@strong this => move |_| {
this.stack.set_visible_child_name("loading");
let data = LoginData {
username: this.username_entry.get_text().to_string(),
password: this.password_entry.get_text().to_string(),
};
let c = glib::MainContext::default();
let clone = this.clone();
c.spawn_local(async move {
clone.backend.set_login_data(data.clone()).await.unwrap();
if clone.backend.login().await.unwrap() {
if let Some(cb) = &*clone.selected_cb.borrow() {
cb(data);
}
clone.window.close();
} else {
clone.stack.set_visible_child_name("content");
clone.info_bar.set_revealed(true);
}
});
}));
this
}
/// The closure to call when the login succeded.
pub fn set_selected_cb<F: Fn(LoginData) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the login dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,41 +0,0 @@
pub mod about;
pub use about::*;
pub mod ensemble_editor;
pub use ensemble_editor::*;
pub mod ensemble_selector;
pub use ensemble_selector::*;
pub mod instrument_editor;
pub use instrument_editor::*;
pub mod instrument_selector;
pub use instrument_selector::*;
pub mod login_dialog;
pub use login_dialog::*;
pub mod person_editor;
pub use person_editor::*;
pub mod person_selector;
pub use person_selector::*;
pub mod preferences;
pub use preferences::*;
pub mod server_dialog;
pub use server_dialog::*;
pub mod recording;
pub use recording::*;
pub mod track_editor;
pub use track_editor::*;
pub mod tracks_editor;
pub use tracks_editor::*;
pub mod work;
pub use work::*;

View file

@ -1,84 +0,0 @@
use crate::backend::Backend;
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::rc::Rc;
pub struct PersonEditor<F>
where
F: Fn(Person) -> () + 'static,
{
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
id: i64,
first_name_entry: gtk::Entry,
last_name_entry: gtk::Entry,
}
impl<F> PersonEditor<F>
where
F: Fn(Person) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
person: Option<Person>,
callback: F,
) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, first_name_entry);
get_widget!(builder, gtk::Entry, last_name_entry);
let id = match person {
Some(person) => {
first_name_entry.set_text(&person.first_name);
last_name_entry.set_text(&person.last_name);
person.id
}
None => rand::random::<u32>().into(),
};
let result = Rc::new(PersonEditor {
backend: backend,
window: window,
callback: callback,
id: id,
first_name_entry: first_name_entry,
last_name_entry: last_name_entry,
});
cancel_button.connect_clicked(clone!(@strong result => move |_| {
result.window.close();
}));
save_button.connect_clicked(clone!(@strong result => move |_| {
let person = Person {
id: result.id,
first_name: result.first_name_entry.get_text().to_string(),
last_name: result.last_name_entry.get_text().to_string(),
};
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
clone.backend.update_person(person.clone()).await.unwrap();
clone.window.close();
(clone.callback)(person.clone());
});
}));
result.window.set_transient_for(Some(parent));
result
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,61 +0,0 @@
use super::PersonEditor;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::rc::Rc;
pub struct PersonSelector {
window: libhandy::Window,
}
impl PersonSelector {
pub fn new<P, F>(backend: Rc<Backend>, parent: &P, callback: F) -> Self
where
P: IsA<gtk::Window>,
F: Fn(Person) -> () + 'static,
{
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_selector.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Box, vbox);
get_widget!(builder, gtk::Button, add_button);
let callback = Rc::new(callback);
let list = PersonList::new(backend.clone());
list.set_selected(clone!(@strong window, @strong callback => move |person| {
window.close();
callback(person.clone());
}));
vbox.pack_start(&list.widget, true, true, 0);
window.set_transient_for(Some(parent));
add_button.connect_clicked(
clone!(@strong backend, @strong window, @strong callback => move |_| {
let editor = PersonEditor::new(
backend.clone(),
&window,
None,
clone!(@strong window, @strong callback => move |person| {
window.close();
callback(person);
}),
);
editor.show();
}),
);
Self { window }
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,105 +0,0 @@
use super::{LoginDialog, ServerDialog};
use crate::backend::Backend;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::rc::Rc;
/// A dialog for configuring the app.
pub struct Preferences {
backend: Rc<Backend>,
window: libhandy::Window,
music_library_path_row: libhandy::ActionRow,
url_row: libhandy::ActionRow,
login_row: libhandy::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, libhandy::Window, window);
get_widget!(builder, libhandy::ActionRow, music_library_path_row);
get_widget!(builder, gtk::Button, select_music_library_path_button);
get_widget!(builder, libhandy::ActionRow, url_row);
get_widget!(builder, gtk::Button, url_button);
get_widget!(builder, libhandy::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::FileChooserNative::new(
Some(&gettext("Select music library folder")),
Some(&this.window), gtk::FileChooserAction::SelectFolder,None, None);
if let gtk::ResponseType::Accept = dialog.run() {
if let Some(path) = dialog.get_filename() {
this.music_library_path_row.set_subtitle(Some(path.to_str().unwrap()));
let context = glib::MainContext::default();
let backend = this.backend.clone();
context.spawn_local(async move {
backend.set_music_library_path(path).await.unwrap();
});
}
}
}));
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 dialog = LoginDialog::new(this.backend.clone(), &this.window);
dialog.set_selected_cb(clone!(@strong this => move |data| {
this.login_row.set_subtitle(Some(&data.username));
}));
dialog.show();
}));
// 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,11 +0,0 @@
pub mod recording_dialog;
pub use recording_dialog::*;
pub mod recording_editor_dialog;
pub use recording_editor_dialog::*;
mod performance_editor;
mod recording_editor;
mod recording_selector;
mod recording_selector_person_screen;
mod recording_selector_work_screen;

View file

@ -1,174 +0,0 @@
use crate::backend::*;
use crate::database::*;
use crate::dialogs::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing a performance within a recording.
pub struct PerformanceEditor {
backend: Rc<Backend>,
window: libhandy::Window,
save_button: gtk::Button,
person_label: gtk::Label,
ensemble_label: gtk::Label,
role_label: gtk::Label,
reset_role_button: gtk::Button,
person: RefCell<Option<Person>>,
ensemble: RefCell<Option<Ensemble>>,
role: RefCell<Option<Instrument>>,
selected_cb: RefCell<Option<Box<dyn Fn(PerformanceDescription) -> ()>>>,
}
impl PerformanceEditor {
/// Create a new performance editor.
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
performance: Option<PerformanceDescription>,
) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/performance_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Button, person_button);
get_widget!(builder, gtk::Button, ensemble_button);
get_widget!(builder, gtk::Button, role_button);
get_widget!(builder, gtk::Button, reset_role_button);
get_widget!(builder, gtk::Label, person_label);
get_widget!(builder, gtk::Label, ensemble_label);
get_widget!(builder, gtk::Label, role_label);
window.set_transient_for(Some(parent));
let this = Rc::new(PerformanceEditor {
backend,
window,
save_button,
person_label,
ensemble_label,
role_label,
reset_role_button,
person: RefCell::new(None),
ensemble: RefCell::new(None),
role: RefCell::new(None),
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(PerformanceDescription {
person: this.person.borrow().clone(),
ensemble: this.ensemble.borrow().clone(),
role: this.role.borrow().clone(),
});
this.window.close();
}
}));
person_button.connect_clicked(clone!(@strong this => move |_| {
PersonSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |person| {
this.show_person(Some(&person));
this.person.replace(Some(person));
this.show_ensemble(None);
this.ensemble.replace(None);
})).show();
}));
ensemble_button.connect_clicked(clone!(@strong this => move |_| {
EnsembleSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |ensemble| {
this.show_person(None);
this.person.replace(None);
this.show_ensemble(Some(&ensemble));
this.ensemble.replace(Some(ensemble));
})).show();
}));
role_button.connect_clicked(clone!(@strong this => move |_| {
InstrumentSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |role| {
this.show_role(Some(&role));
this.role.replace(Some(role));
})).show();
}));
this.reset_role_button
.connect_clicked(clone!(@strong 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
}
/// Set a closure to be called when the user has chosen to save the performance.
pub fn set_selected_cb<F: Fn(PerformanceDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the performance editor.
pub fn show(&self) {
self.window.show();
}
/// Update the UI according to person.
fn show_person(&self, person: Option<&Person>) {
if let Some(person) = person {
self.person_label.set_text(&person.name_fl());
self.save_button.set_sensitive(true);
} else {
self.person_label.set_text(&gettext("Select …"));
}
}
/// Update the UI according to ensemble.
fn show_ensemble(&self, ensemble: Option<&Ensemble>) {
if let Some(ensemble) = ensemble {
self.ensemble_label.set_text(&ensemble.name);
self.save_button.set_sensitive(true);
} else {
self.ensemble_label.set_text(&gettext("Select …"));
}
}
/// Update the UI according to role.
fn show_role(&self, role: Option<&Instrument>) {
if let Some(role) = role {
self.role_label.set_text(&role.name);
self.reset_role_button.show();
} else {
self.role_label.set_text(&gettext("Select …"));
self.reset_role_button.hide();
}
}
}

View file

@ -1,86 +0,0 @@
use super::recording_editor::*;
use super::recording_selector::*;
use crate::backend::*;
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for selecting and creating a recording.
pub struct RecordingDialog {
pub window: libhandy::Window,
stack: gtk::Stack,
selector: Rc<RecordingSelector>,
editor: Rc<RecordingEditor>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
}
impl RecordingDialog {
/// Create a new recording dialog.
pub fn new<W: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &W) -> Rc<Self> {
// Create UI
let window = libhandy::Window::new();
window.set_type_hint(gdk::WindowTypeHint::Dialog);
window.set_modal(true);
window.set_transient_for(Some(parent));
window.set_default_size(600, 424);
let selector = RecordingSelector::new(backend.clone());
let editor = RecordingEditor::new(backend.clone(), &window, None);
let stack = gtk::Stack::new();
stack.set_transition_type(gtk::StackTransitionType::Crossfade);
stack.add(&selector.widget);
stack.add(&editor.widget);
window.add(&stack);
window.show_all();
let this = Rc::new(Self {
window,
stack,
selector,
editor,
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
this.selector.set_add_cb(clone!(@strong this => move || {
this.stack.set_visible_child(&this.editor.widget);
}));
this.selector
.set_selected_cb(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording);
this.window.close();
}
}));
this.editor.set_back_cb(clone!(@strong this => move || {
this.stack.set_visible_child(&this.selector.widget);
}));
this.editor
.set_selected_cb(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording);
this.window.close();
}
}));
this
}
/// Set the closure to be called when the user has selected or created a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the recording dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,204 +0,0 @@
use super::performance_editor::*;
use crate::backend::*;
use crate::database::*;
use crate::dialogs::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A widget for creating or editing a recording.
// TODO: Disable buttons if no performance is selected.
pub struct RecordingEditor {
pub widget: gtk::Box,
backend: Rc<Backend>,
parent: gtk::Window,
save_button: gtk::Button,
work_label: gtk::Label,
comment_entry: gtk::Entry,
performance_list: Rc<List<PerformanceDescription>>,
id: i64,
work: RefCell<Option<WorkDescription>>,
performances: RefCell<Vec<PerformanceDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
}
impl RecordingEditor {
/// Create a new recording editor widget and optionally initialize it. The parent window is
/// used as the parent for newly created dialogs.
pub fn new<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
recording: Option<RecordingDescription>,
) -> Rc<Self> {
// Create UI
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Button, work_button);
get_widget!(builder, gtk::Label, work_label);
get_widget!(builder, gtk::Entry, comment_entry);
get_widget!(builder, gtk::ScrolledWindow, scroll);
get_widget!(builder, gtk::Button, add_performer_button);
get_widget!(builder, gtk::Button, edit_performer_button);
get_widget!(builder, gtk::Button, remove_performer_button);
let performance_list = List::new(&gettext("No performers added."));
scroll.add(&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 => (rand::random::<u32>().into(), None, Vec::new()),
};
let this = Rc::new(RecordingEditor {
widget,
backend,
parent: parent.clone().upcast(),
save_button,
work_label,
comment_entry,
performance_list,
id,
work: RefCell::new(work),
performances: RefCell::new(performances),
selected_cb: RefCell::new(None),
back_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.back_cb.borrow() {
cb();
}
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
let recording = RecordingDescription {
id: this.id,
work: this.work.borrow().clone().expect("Tried to create recording without work!"),
comment: this.comment_entry.get_text().to_string(),
performances: this.performances.borrow().clone(),
};
let c = glib::MainContext::default();
let clone = this.clone();
c.spawn_local(async move {
clone.backend.update_recording(recording.clone().into()).await.unwrap();
if let Some(cb) = &*clone.selected_cb.borrow() {
cb(recording.clone());
}
});
}));
work_button.connect_clicked(clone!(@strong this => move |_| {
let dialog = WorkDialog::new(this.backend.clone(), &this.parent);
dialog.set_selected_cb(clone!(@strong this => move |work| {
this.work_selected(&work);
this.work.replace(Some(work));
}));
dialog.show();
}));
this.performance_list.set_make_widget(|performance| {
let label = gtk::Label::new(Some(&performance.get_title()));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
add_performer_button.connect_clicked(clone!(@strong this => move |_| {
let editor = PerformanceEditor::new(this.backend.clone(), &this.parent, None);
editor.set_selected_cb(clone!(@strong this => move |performance| {
let mut performances = this.performances.borrow_mut();
let index = match this.performance_list.get_selected_index() {
Some(index) => index + 1,
None => performances.len(),
};
performances.insert(index, performance);
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
}));
editor.show();
}));
edit_performer_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.performance_list.get_selected_index() {
let performance = &this.performances.borrow()[index];
let editor = PerformanceEditor::new(
this.backend.clone(),
&this.parent,
Some(performance.clone()),
);
editor.set_selected_cb(clone!(@strong this => move |performance| {
let mut performances = this.performances.borrow_mut();
performances[index] = performance;
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
}));
editor.show();
}
}));
remove_performer_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.performance_list.get_selected_index() {
let mut performances = this.performances.borrow_mut();
performances.remove(index);
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
}
}));
// Initialize
if let Some(work) = &*this.work.borrow() {
this.work_selected(work);
}
this.performance_list.show_items(this.performances.borrow().clone());
this
}
/// Set the closure to be called if the editor is canceled.
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 if the recording was created.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to work.
fn work_selected(&self, work: &WorkDescription) {
self.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title));
self.save_button.set_sensitive(true);
}
}

View file

@ -1,63 +0,0 @@
use super::recording_editor::*;
use crate::backend::*;
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a recording.
pub struct RecordingEditorDialog {
pub window: libhandy::Window,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
}
impl RecordingEditorDialog {
/// Create a new recording editor dialog and optionally initialize it.
pub fn new<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
recording: Option<RecordingDescription>,
) -> Rc<Self> {
// Create UI
let window = libhandy::Window::new();
window.set_type_hint(gdk::WindowTypeHint::Dialog);
window.set_modal(true);
window.set_transient_for(Some(parent));
let editor = RecordingEditor::new(backend.clone(), &window, recording);
window.add(&editor.widget);
window.show_all();
let this = Rc::new(Self {
window,
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
editor.set_back_cb(clone!(@strong this => move || {
this.window.close();
}));
editor.set_selected_cb(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording);
this.window.close();
}
}));
this
}
/// Set the closure to be called when the user edited or created a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the recording editor dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,89 +0,0 @@
use super::recording_selector_person_screen::*;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A widget for selecting a recording from a list of existing ones.
pub struct RecordingSelector {
pub widget: libhandy::Leaflet,
backend: Rc<Backend>,
sidebar_box: gtk::Box,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
navigator: Rc<Navigator>,
}
impl RecordingSelector {
/// Create a new recording selector.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_selector.ui");
get_widget!(builder, libhandy::Leaflet, widget);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Box, sidebar_box);
get_widget!(builder, gtk::Box, empty_screen);
let person_list = PersonList::new(backend.clone());
sidebar_box.pack_start(&person_list.widget, true, true, 0);
let navigator = Navigator::new(&empty_screen);
widget.add(&navigator.widget);
let this = Rc::new(Self {
widget,
backend,
sidebar_box,
selected_cb: RefCell::new(None),
add_cb: RefCell::new(None),
navigator,
});
// Connect signals and callbacks
add_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.add_cb.borrow() {
cb();
}
}));
person_list.set_selected(clone!(@strong this => move |person| {
let person_screen = RecordingSelectorPersonScreen::new(
this.backend.clone(),
person.clone(),
);
person_screen.set_selected_cb(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording);
}
}));
this.navigator.clone().push(person_screen);
this.widget.set_visible_child(&this.navigator.widget);
}));
this.navigator.set_back_cb(clone!(@strong this => move || {
this.widget.set_visible_child(&this.sidebar_box);
}));
this
}
/// Set the closure to be called if the user wants to add a new recording.
pub fn set_add_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.add_cb.replace(Some(Box::new(cb)));
}
/// Set the closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -1,126 +0,0 @@
use super::recording_selector_work_screen::*;
use crate::backend::*;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen within the recording selector that presents a list of works and switches to a work
/// screen on selection.
pub struct RecordingSelectorPersonScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingSelectorPersonScreen {
/// Create a new recording selector person screen.
pub fn new(backend: Rc<Backend>, person: Person) -> Rc<Self> {
// Create UI
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_selector_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
header.set_title(Some(&person.name_fl()));
let work_list = List::new(&gettext("No works found."));
stack.add_named(&work_list.widget, "content");
let this = Rc::new(Self {
backend,
widget,
stack,
work_list,
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.work_list.set_make_widget(|work: &WorkDescription| {
let label = gtk::Label::new(Some(&work.title));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
this.work_list
.set_selected(clone!(@strong this => move |work| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let work_screen = RecordingSelectorWorkScreen::new(
this.backend.clone(),
work.clone(),
);
work_screen.set_selected_cb(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording);
}
}));
navigator.push(work_screen);
}
}));
// Initialize
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.await
.unwrap();
clone.work_list.show_items(works);
clone.stack.set_visible_child_name("content");
});
this
}
/// Sets a closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for RecordingSelectorPersonScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,120 +0,0 @@
use crate::backend::*;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen within the recording selector presenting a list of recordings for a work.
pub struct RecordingSelectorWorkScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingSelectorWorkScreen {
/// Create a new recording selector work screen.
pub fn new(backend: Rc<Backend>, work: WorkDescription) -> Rc<Self> {
// Create UI
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_selector_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
header.set_title(Some(&work.title));
header.set_subtitle(Some(&work.composer.name_fl()));
let recording_list = List::new(&gettext("No recordings found."));
stack.add_named(&recording_list.widget, "content");
let this = Rc::new(Self {
backend,
widget,
stack,
recording_list,
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
work_label.set_halign(gtk::Align::Start);
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
performers_label.set_ellipsize(pango::EllipsizeMode::End);
performers_label.set_opacity(0.5);
performers_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&work_label);
vbox.add(&performers_label);
vbox.upcast()
});
this.recording_list
.set_selected(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording.clone());
}
}));
// Initialize
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let recordings = clone
.backend
.get_recordings_for_work(work.id)
.await
.unwrap();
clone.recording_list.show_items(recordings);
clone.stack.set_visible_child_name("content");
});
this
}
/// Sets a closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for RecordingSelectorWorkScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

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: libhandy::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, libhandy::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().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,117 +0,0 @@
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::convert::TryInto;
use std::rc::Rc;
pub struct TrackEditor {
window: libhandy::Window,
}
impl TrackEditor {
pub fn new<W, F>(parent: &W, track: TrackDescription, work: WorkDescription, callback: F) -> Self
where
W: IsA<gtk::Window>,
F: Fn(TrackDescription) -> () + 'static,
{
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::ListBox, list);
window.set_transient_for(Some(parent));
cancel_button.connect_clicked(clone!(@strong window => move |_| {
window.close();
}));
let work = Rc::new(work);
let work_parts = Rc::new(RefCell::new(track.work_parts));
let file_name = track.file_name;
save_button.connect_clicked(clone!(@strong work_parts, @strong window => move |_| {
let mut work_parts = work_parts.borrow_mut();
work_parts.sort();
callback(TrackDescription {
work_parts: work_parts.clone(),
file_name: file_name.clone(),
});
window.close();
}));
for (index, part) in work.parts.iter().enumerate() {
let check = gtk::CheckButton::new();
check.set_active(work_parts.borrow().contains(&index));
check.connect_toggled(clone!(@strong check, @strong work_parts => move |_| {
if check.get_active() {
let mut work_parts = work_parts.borrow_mut();
work_parts.push(index);
} else {
let mut work_parts = work_parts.borrow_mut();
if let Some(pos) = work_parts.iter().position(|part| *part == index) {
work_parts.remove(pos);
}
}
}));
let label = gtk::Label::new(Some(&part.title));
label.set_halign(gtk::Align::Start);
label.set_ellipsize(pango::EllipsizeMode::End);
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
hbox.set_border_width(6);
hbox.add(&check);
hbox.add(&label);
let row = gtk::ListBoxRow::new();
row.add(&hbox);
row.show_all();
list.add(&row);
list.connect_row_activated(
clone!(@strong row, @strong check => move |_, activated_row| {
if *activated_row == row {
check.activate();
}
}),
);
}
let mut section_count = 0;
for section in &work.sections {
let attributes = pango::AttrList::new();
attributes.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap());
let label = gtk::Label::new(Some(&section.title));
label.set_halign(gtk::Align::Start);
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_attributes(Some(&attributes));
let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0);
wrap.set_border_width(6);
wrap.add(&label);
let row = gtk::ListBoxRow::new();
row.set_activatable(false);
row.add(&wrap);
row.show_all();
list.insert(
&row,
(section.before_index + section_count).try_into().unwrap(),
);
section_count += 1;
}
Self { window }
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,284 +0,0 @@
use super::*;
use crate::backend::*;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing a set of tracks.
// TODO: Disable buttons if no track is selected.
pub struct TracksEditor {
backend: Rc<Backend>,
window: libhandy::Window,
save_button: gtk::Button,
recording_stack: gtk::Stack,
work_label: gtk::Label,
performers_label: gtk::Label,
track_list: Rc<List<TrackDescription>>,
recording: RefCell<Option<RecordingDescription>>,
tracks: RefCell<Vec<TrackDescription>>,
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
}
impl TracksEditor {
/// Create a new track editor an optionally initialize it with a recording and a list of
/// tracks.
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
recording: Option<RecordingDescription>,
tracks: Vec<TrackDescription>,
) -> Rc<Self> {
// UI setup
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Button, recording_button);
get_widget!(builder, gtk::Stack, recording_stack);
get_widget!(builder, gtk::Label, work_label);
get_widget!(builder, gtk::Label, performers_label);
get_widget!(builder, gtk::ScrolledWindow, scroll);
get_widget!(builder, gtk::Button, add_track_button);
get_widget!(builder, gtk::Button, edit_track_button);
get_widget!(builder, gtk::Button, remove_track_button);
get_widget!(builder, gtk::Button, move_track_up_button);
get_widget!(builder, gtk::Button, move_track_down_button);
window.set_transient_for(Some(parent));
cancel_button.connect_clicked(clone!(@strong window => move |_| {
window.close();
}));
let track_list = List::new(&gettext("Add some tracks."));
scroll.add(&track_list.widget);
let this = Rc::new(Self {
backend,
window,
save_button,
recording_stack,
work_label,
performers_label,
track_list,
recording: RefCell::new(recording),
tracks: RefCell::new(tracks),
callback: RefCell::new(None),
});
// Signals and callbacks
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let this = this.clone();
context.spawn_local(async move {
this.backend.update_tracks(
this.recording.borrow().as_ref().unwrap().id,
this.tracks.borrow().clone(),
).await.unwrap();
if let Some(callback) = &*this.callback.borrow() {
callback();
}
this.window.close();
});
}));
recording_button.connect_clicked(clone!(@strong this => move |_| {
let dialog = RecordingDialog::new(this.backend.clone(), &this.window);
dialog.set_selected_cb(clone!(@strong this => move |recording| {
this.recording_selected(&recording);
this.recording.replace(Some(recording));
}));
dialog.show();
}
));
this.track_list
.set_make_widget(clone!(@strong this => move |track| {
this.build_track_row(track)
}));
add_track_button.connect_clicked(clone!(@strong this => move |_| {
let music_library_path = this.backend.get_music_library_path().unwrap();
let dialog = gtk::FileChooserNative::new(
Some(&gettext("Select audio files")),
Some(&this.window),
gtk::FileChooserAction::Open,
None,
None,
);
dialog.set_select_multiple(true);
dialog.set_current_folder(&music_library_path);
if let gtk::ResponseType::Accept = dialog.run() {
let mut index = match this.track_list.get_selected_index() {
Some(index) => index + 1,
None => this.tracks.borrow().len(),
};
{
let mut tracks = this.tracks.borrow_mut();
for file_name in dialog.get_filenames() {
let file_name = file_name.strip_prefix(&music_library_path).unwrap();
tracks.insert(index, TrackDescription {
work_parts: Vec::new(),
file_name: String::from(file_name.to_str().unwrap()),
});
index += 1;
}
}
this.track_list.show_items(this.tracks.borrow().clone());
this.autofill_parts();
this.track_list.select_index(index);
}
}));
remove_track_button.connect_clicked(clone!(@strong this => move |_| {
match this.track_list.get_selected_index() {
Some(index) => {
let mut tracks = this.tracks.borrow_mut();
tracks.remove(index);
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index);
}
None => (),
}
}));
move_track_up_button.connect_clicked(clone!(@strong this => move |_| {
match this.track_list.get_selected_index() {
Some(index) => {
if index > 0 {
let mut tracks = this.tracks.borrow_mut();
tracks.swap(index - 1, index);
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index - 1);
}
}
None => (),
}
}));
move_track_down_button.connect_clicked(clone!(@strong this => move |_| {
match this.track_list.get_selected_index() {
Some(index) => {
let mut tracks = this.tracks.borrow_mut();
if index < tracks.len() - 1 {
tracks.swap(index, index + 1);
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index + 1);
}
}
None => (),
}
}));
edit_track_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.track_list.get_selected_index() {
if let Some(recording) = &*this.recording.borrow() {
TrackEditor::new(&this.window, this.tracks.borrow()[index].clone(), recording.work.clone(), clone!(@strong this => move |track| {
let mut tracks = this.tracks.borrow_mut();
tracks[index] = track;
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index);
})).show();
}
}
}));
// Initialization
if let Some(recording) = &*this.recording.borrow() {
this.recording_selected(recording);
}
this.track_list.show_items(this.tracks.borrow().clone());
this
}
/// Set a callback to be called when the tracks are saved.
pub fn set_callback<F: Fn() -> () + 'static>(&self, cb: F) {
self.callback.replace(Some(Box::new(cb)));
}
/// Open the track editor.
pub fn show(&self) {
self.window.show();
}
/// Create a widget representing a track.
fn build_track_row(&self, track: &TrackDescription) -> gtk::Widget {
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
if let Some(recording) = &*self.recording.borrow() {
title_parts.push(recording.work.parts[*part].title.clone());
}
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let title_label = gtk::Label::new(Some(&title));
title_label.set_ellipsize(pango::EllipsizeMode::End);
title_label.set_halign(gtk::Align::Start);
let file_name_label = gtk::Label::new(Some(&track.file_name));
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
file_name_label.set_opacity(0.5);
file_name_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&title_label);
vbox.add(&file_name_label);
vbox.upcast()
}
/// Set everything up after selecting a recording.
fn recording_selected(&self, recording: &RecordingDescription) {
self.work_label.set_text(&recording.work.get_title());
self.performers_label.set_text(&recording.get_performers());
self.recording_stack.set_visible_child_name("selected");
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.track_list.show_items(tracks.clone());
}
}
}

View file

@ -1,11 +0,0 @@
pub mod work_dialog;
pub use work_dialog::*;
pub mod work_editor_dialog;
pub use work_editor_dialog::*;
mod part_editor;
mod section_editor;
mod work_editor;
mod work_selector;
mod work_selector_person_screen;

View file

@ -1,170 +0,0 @@
use crate::backend::*;
use crate::database::*;
use crate::dialogs::*;
use crate::widgets::*;
use gettextrs::gettext;
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 part.
pub struct PartEditor {
backend: Rc<Backend>,
window: libhandy::Window,
title_entry: gtk::Entry,
composer_label: gtk::Label,
reset_composer_button: gtk::Button,
instrument_list: Rc<List<Instrument>>,
composer: RefCell<Option<Person>>,
instruments: RefCell<Vec<Instrument>>,
ready_cb: RefCell<Option<Box<dyn Fn(WorkPartDescription) -> ()>>>,
}
impl PartEditor {
/// Create a new part editor and optionally initialize it.
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
part: Option<WorkPartDescription>,
) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/part_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_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, gtk::Label, composer_label);
get_widget!(builder, gtk::Button, reset_composer_button);
get_widget!(builder, gtk::ScrolledWindow, scroll);
get_widget!(builder, gtk::Button, add_instrument_button);
get_widget!(builder, gtk::Button, remove_instrument_button);
window.set_transient_for(Some(parent));
let instrument_list = List::new(&gettext("No instruments added."));
scroll.add(&instrument_list.widget);
let (composer, instruments) = match part {
Some(part) => {
title_entry.set_text(&part.title);
(part.composer, part.instruments)
}
None => (None, Vec::new()),
};
let this = Rc::new(Self {
backend,
window,
title_entry,
composer_label,
reset_composer_button,
instrument_list,
composer: RefCell::new(composer),
instruments: RefCell::new(instruments),
ready_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.ready_cb.borrow() {
cb(WorkPartDescription {
title: this.title_entry.get_text().to_string(),
composer: this.composer.borrow().clone(),
instruments: this.instruments.borrow().clone(),
});
}
this.window.close();
}));
composer_button.connect_clicked(clone!(@strong this => move |_| {
PersonSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |person| {
this.show_composer(Some(&person));
this.composer.replace(Some(person));
})).show();
}));
this.reset_composer_button
.connect_clicked(clone!(@strong this => move |_| {
this.composer.replace(None);
this.show_composer(None);
}));
this.instrument_list.set_make_widget(|instrument| {
let label = gtk::Label::new(Some(&instrument.name));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
add_instrument_button.connect_clicked(clone!(@strong this => move |_| {
InstrumentSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |instrument| {
let mut instruments = this.instruments.borrow_mut();
let index = match this.instrument_list.get_selected_index() {
Some(index) => index + 1,
None => instruments.len(),
};
instruments.insert(index, instrument);
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
})).show();
}));
remove_instrument_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.instrument_list.get_selected_index() {
let mut instruments = this.instruments.borrow_mut();
instruments.remove(index);
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
}
}));
// Initialize
if let Some(composer) = &*this.composer.borrow() {
this.show_composer(Some(composer));
}
this.instrument_list
.show_items(this.instruments.borrow().clone());
this
}
/// Set the closure to be called when the user wants to save the part.
pub fn set_ready_cb<F: Fn(WorkPartDescription) -> () + 'static>(&self, cb: F) {
self.ready_cb.replace(Some(Box::new(cb)));
}
/// Show the part editor.
pub fn show(&self) {
self.window.show();
}
/// Update the UI according to person.
fn show_composer(&self, person: Option<&Person>) {
if let Some(person) = person {
self.composer_label.set_text(&person.name_fl());
self.reset_composer_button.show();
} else {
self.composer_label.set_text(&gettext("Select …"));
self.reset_composer_button.hide();
}
}
}

View file

@ -1,73 +0,0 @@
use crate::database::*;
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 SectionEditor {
window: libhandy::Window,
title_entry: gtk::Entry,
ready_cb: RefCell<Option<Box<dyn Fn(WorkSectionDescription) -> ()>>>,
}
impl SectionEditor {
/// Create a new section editor and optionally initialize it.
pub fn new<P: IsA<gtk::Window>>(
parent: &P,
section: Option<WorkSectionDescription>,
) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Entry, title_entry);
window.set_transient_for(Some(parent));
if let Some(section) = section {
title_entry.set_text(&section.title);
}
let this = Rc::new(Self {
window,
title_entry,
ready_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.ready_cb.borrow() {
cb(WorkSectionDescription {
before_index: 0,
title: this.title_entry.get_text().to_string(),
});
}
this.window.close();
}));
this
}
/// Set the closure to be called when the user wants to save the section. Note that the
/// resulting object will always have `before_index` set to 0. The caller is expected to
/// change that later before adding the section to the database.
pub fn set_ready_cb<F: Fn(WorkSectionDescription) -> () + 'static>(&self, cb: F) {
self.ready_cb.replace(Some(Box::new(cb)));
}
/// Show the section editor.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,86 +0,0 @@
use super::work_editor::*;
use super::work_selector::*;
use crate::backend::*;
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for selecting and creating a work.
pub struct WorkDialog {
pub window: libhandy::Window,
stack: gtk::Stack,
selector: Rc<WorkSelector>,
editor: Rc<WorkEditor>,
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
}
impl WorkDialog {
/// Create a new work dialog.
pub fn new<W: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &W) -> Rc<Self> {
// Create UI
let window = libhandy::Window::new();
window.set_type_hint(gdk::WindowTypeHint::Dialog);
window.set_modal(true);
window.set_transient_for(Some(parent));
window.set_default_size(600, 424);
let selector = WorkSelector::new(backend.clone());
let editor = WorkEditor::new(backend.clone(), &window, None);
let stack = gtk::Stack::new();
stack.set_transition_type(gtk::StackTransitionType::Crossfade);
stack.add(&selector.widget);
stack.add(&editor.widget);
window.add(&stack);
window.show_all();
let this = Rc::new(Self {
window,
stack,
selector,
editor,
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
this.selector.set_add_cb(clone!(@strong this => move || {
this.stack.set_visible_child(&this.editor.widget);
}));
this.selector
.set_selected_cb(clone!(@strong this => move |work| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(work);
this.window.close();
}
}));
this.editor.set_cancel_cb(clone!(@strong this => move || {
this.stack.set_visible_child(&this.selector.widget);
}));
this.editor
.set_saved_cb(clone!(@strong this => move |work| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(work);
this.window.close();
}
}));
this
}
/// Set the closure to be called when the user has selected or created a work.
pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the work dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,357 +0,0 @@
use super::part_editor::*;
use super::section_editor::*;
use crate::backend::*;
use crate::database::*;
use crate::dialogs::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
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(WorkPartDescription),
Section(WorkSectionDescription),
}
/// A widget for editing and creating works.
pub struct WorkEditor {
pub widget: gtk::Box,
backend: Rc<Backend>,
parent: gtk::Window,
save_button: gtk::Button,
title_entry: gtk::Entry,
composer_label: gtk::Label,
instrument_list: Rc<List<Instrument>>,
part_list: Rc<List<PartOrSection>>,
id: i64,
composer: RefCell<Option<Person>>,
instruments: RefCell<Vec<Instrument>>,
structure: RefCell<Vec<PartOrSection>>,
cancel_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
saved_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
}
impl WorkEditor {
/// Create a new work editor widget and optionally initialize it. The parent window is used
/// as the parent for newly created dialogs.
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
work: Option<WorkDescription>,
) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, cancel_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, gtk::Label, composer_label);
get_widget!(builder, gtk::ScrolledWindow, instruments_scroll);
get_widget!(builder, gtk::Button, add_instrument_button);
get_widget!(builder, gtk::Button, remove_instrument_button);
get_widget!(builder, gtk::ScrolledWindow, structure_scroll);
get_widget!(builder, gtk::Button, add_part_button);
get_widget!(builder, gtk::Button, remove_part_button);
get_widget!(builder, gtk::Button, add_section_button);
get_widget!(builder, gtk::Button, edit_part_button);
get_widget!(builder, gtk::Button, move_part_up_button);
get_widget!(builder, gtk::Button, move_part_down_button);
let instrument_list = List::new(&gettext("No instruments added."));
instruments_scroll.add(&instrument_list.widget);
let part_list = List::new(&gettext("No work parts added."));
structure_scroll.add(&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 => (rand::random::<u32>().into(), None, Vec::new(), Vec::new()),
};
let this = Rc::new(Self {
widget,
backend,
parent: parent.clone().upcast(),
save_button,
id,
title_entry,
composer_label,
instrument_list,
part_list,
composer: RefCell::new(composer),
instruments: RefCell::new(instruments),
structure: RefCell::new(structure),
cancel_cb: RefCell::new(None),
saved_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.cancel_cb.borrow() {
cb();
}
}));
this.save_button.connect_clicked(clone!(@strong this => move |_| {
let mut section_count = 0;
let mut parts = Vec::new();
let mut sections = Vec::new();
for (index, pos) in this.structure.borrow().iter().enumerate() {
match pos {
PartOrSection::Part(part) => parts.push(part.clone()),
PartOrSection::Section(section) => {
let mut section = section.clone();
let index: i64 = index.try_into().unwrap();
section.before_index = index - section_count;
sections.push(section);
section_count += 1;
}
}
}
let work = WorkDescription {
id: this.id,
title: this.title_entry.get_text().to_string(),
composer: this.composer.borrow().clone().expect("Tried to create work without composer!"),
instruments: this.instruments.borrow().clone(),
parts: parts,
sections: sections,
};
let c = glib::MainContext::default();
let clone = this.clone();
c.spawn_local(async move {
clone.backend.update_work(work.clone().into()).await.unwrap();
if let Some(cb) = &*clone.saved_cb.borrow() {
cb(work);
}
});
}));
composer_button.connect_clicked(clone!(@strong this => move |_| {
PersonSelector::new(this.backend.clone(), &this.parent, clone!(@strong this => move |person| {
this.show_composer(&person);
this.composer.replace(Some(person));
})).show();
}));
this.instrument_list.set_make_widget(|instrument| {
let label = gtk::Label::new(Some(&instrument.name));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
add_instrument_button.connect_clicked(clone!(@strong this => move |_| {
InstrumentSelector::new(this.backend.clone(), &this.parent, clone!(@strong this => move |instrument| {
let mut instruments = this.instruments.borrow_mut();
let index = match this.instrument_list.get_selected_index() {
Some(index) => index + 1,
None => instruments.len(),
};
instruments.insert(index, instrument);
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
})).show();
}));
remove_instrument_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.instrument_list.get_selected_index() {
let mut instruments = this.instruments.borrow_mut();
instruments.remove(index);
this.instrument_list.show_items(instruments.clone());
this.instrument_list.select_index(index);
}
}));
this.part_list.set_make_widget(|pos| {
let label = gtk::Label::new(None);
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
match pos {
PartOrSection::Part(part) => {
label.set_text(&part.title);
label.set_margin_start(12);
}
PartOrSection::Section(section) => {
let attrs = pango::AttrList::new();
attrs.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap());
label.set_attributes(Some(&attrs));
label.set_text(&section.title);
label.set_margin_start(6);
}
}
label.upcast()
});
add_part_button.connect_clicked(clone!(@strong this => move |_| {
let editor = PartEditor::new(this.backend.clone(), &this.parent, None);
editor.set_ready_cb(clone!(@strong this => move |part| {
let mut structure = this.structure.borrow_mut();
let index = match this.part_list.get_selected_index() {
Some(index) => index + 1,
None => structure.len(),
};
structure.insert(index, PartOrSection::Part(part));
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
}));
editor.show();
}));
add_section_button.connect_clicked(clone!(@strong this => move |_| {
let editor = SectionEditor::new(&this.parent, None);
editor.set_ready_cb(clone!(@strong this => move |section| {
let mut structure = this.structure.borrow_mut();
let index = match this.part_list.get_selected_index() {
Some(index) => index + 1,
None => structure.len(),
};
structure.insert(index, PartOrSection::Section(section));
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
}));
editor.show();
}));
edit_part_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.part_list.get_selected_index() {
match this.structure.borrow()[index].clone() {
PartOrSection::Part(part) => {
let editor = PartEditor::new(
this.backend.clone(),
&this.parent,
Some(part),
);
editor.set_ready_cb(clone!(@strong this => move |part| {
let mut structure = this.structure.borrow_mut();
structure[index] = PartOrSection::Part(part);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
}));
editor.show();
}
PartOrSection::Section(section) => {
let editor = SectionEditor::new(&this.parent, Some(section));
editor.set_ready_cb(clone!(@strong this => move |section| {
let mut structure = this.structure.borrow_mut();
structure[index] = PartOrSection::Section(section);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
}));
editor.show();
}
}
}
}));
remove_part_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.part_list.get_selected_index() {
let mut structure = this.structure.borrow_mut();
structure.remove(index);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index);
}
}));
move_part_up_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.part_list.get_selected_index() {
if index > 0 {
let mut structure = this.structure.borrow_mut();
structure.swap(index - 1, index);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index - 1);
}
}
}));
move_part_down_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.part_list.get_selected_index() {
let mut structure = this.structure.borrow_mut();
if index < structure.len() - 1 {
structure.swap(index, index + 1);
this.part_list.show_items(structure.clone());
this.part_list.select_index(index + 1);
}
}
}));
// Initialization
if let Some(composer) = &*this.composer.borrow() {
this.show_composer(composer);
}
this.instrument_list.show_items(this.instruments.borrow().clone());
this.part_list.show_items(this.structure.borrow().clone());
this
}
/// The closure to call when the editor is canceled.
pub fn set_cancel_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.cancel_cb.replace(Some(Box::new(cb)));
}
/// The closure to call when a work was created.
pub fn set_saved_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to person.
fn show_composer(&self, person: &Person) {
self.composer_label.set_text(&person.name_fl());
self.save_button.set_sensitive(true);
}
}

View file

@ -1,63 +0,0 @@
use super::work_editor::*;
use crate::backend::*;
use crate::database::*;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for creating or editing a work.
pub struct WorkEditorDialog {
pub window: libhandy::Window,
saved_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
}
impl WorkEditorDialog {
/// Create a new work editor dialog and optionally initialize it.
pub fn new<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
work: Option<WorkDescription>,
) -> Rc<Self> {
// Create UI
let window = libhandy::Window::new();
window.set_type_hint(gdk::WindowTypeHint::Dialog);
window.set_modal(true);
window.set_transient_for(Some(parent));
let editor = WorkEditor::new(backend.clone(), &window, work);
window.add(&editor.widget);
window.show_all();
let this = Rc::new(Self {
window,
saved_cb: RefCell::new(None),
});
// Connect signals and callbacks
editor.set_cancel_cb(clone!(@strong this => move || {
this.window.close();
}));
editor.set_saved_cb(clone!(@strong this => move |work| {
if let Some(cb) = &*this.saved_cb.borrow() {
cb(work);
this.window.close();
}
}));
this
}
/// Set the closure to be called when the user edited or created a work.
pub fn set_saved_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
self.saved_cb.replace(Some(Box::new(cb)));
}
/// Show the work editor dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -1,89 +0,0 @@
use super::work_selector_person_screen::*;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A widget for selecting a work from a list of existing ones.
pub struct WorkSelector {
pub widget: libhandy::Leaflet,
backend: Rc<Backend>,
sidebar_box: gtk::Box,
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
navigator: Rc<Navigator>,
}
impl WorkSelector {
/// Create a new work selector.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_selector.ui");
get_widget!(builder, libhandy::Leaflet, widget);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Box, sidebar_box);
get_widget!(builder, gtk::Box, empty_screen);
let person_list = PersonList::new(backend.clone());
sidebar_box.pack_start(&person_list.widget, true, true, 0);
let navigator = Navigator::new(&empty_screen);
widget.add(&navigator.widget);
let this = Rc::new(Self {
widget,
backend,
sidebar_box,
selected_cb: RefCell::new(None),
add_cb: RefCell::new(None),
navigator,
});
// Connect signals and callbacks
add_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.add_cb.borrow() {
cb();
}
}));
person_list.set_selected(clone!(@strong this => move |person| {
let person_screen = WorkSelectorPersonScreen::new(
this.backend.clone(),
person.clone(),
);
person_screen.set_selected_cb(clone!(@strong this => move |work| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(work);
}
}));
this.navigator.clone().push(person_screen);
this.widget.set_visible_child(&this.navigator.widget);
}));
this.navigator.set_back_cb(clone!(@strong this => move || {
this.widget.set_visible_child(&this.sidebar_box);
}));
this
}
/// Set the closure to be called if the user wants to add a new work.
pub fn set_add_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.add_cb.replace(Some(Box::new(cb)));
}
/// Set the closure to be called when the user has selected a work.
pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -1,114 +0,0 @@
use crate::backend::*;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen within the work selector that presents a list of works by a person.
pub struct WorkSelectorPersonScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(WorkDescription) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl WorkSelectorPersonScreen {
/// Create a new work selector person screen.
pub fn new(backend: Rc<Backend>, person: Person) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_selector_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
header.set_title(Some(&person.name_fl()));
let work_list = List::new(&gettext("No works found."));
stack.add_named(&work_list.widget, "content");
let this = Rc::new(Self {
backend,
widget,
stack,
work_list,
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.work_list.set_make_widget(|work: &WorkDescription| {
let label = gtk::Label::new(Some(&work.title));
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
this.work_list
.set_selected(clone!(@strong this => move |work| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(work.clone());
}
}
}));
// Initialize
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.await
.unwrap();
clone.work_list.show_items(works);
clone.stack.set_visible_child_name("content");
});
this
}
/// Sets a closure to be called when the user has selected a work.
pub fn set_selected_cb<F: Fn(WorkDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for WorkSelectorPersonScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,54 +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;
mod config;
mod backend;
mod database;
mod dialogs;
mod player;
mod screens;
mod widgets;
mod window;
use window::Window;
mod resources;
fn main() {
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
gettextrs::bindtextdomain("musicus", config::LOCALEDIR);
gettextrs::textdomain("musicus");
gtk::init().expect("Failed to initialize GTK!");
libhandy::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,114 +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/backend.rs',
'backend/client.rs',
'backend/mod.rs',
'backend/secure.rs',
'database/database.rs',
'database/mod.rs',
'database/models.rs',
'database/schema.rs',
'database/tables.rs',
'dialogs/about.rs',
'dialogs/ensemble_editor.rs',
'dialogs/ensemble_selector.rs',
'dialogs/instrument_editor.rs',
'dialogs/instrument_selector.rs',
'dialogs/login_dialog.rs',
'dialogs/mod.rs',
'dialogs/person_editor.rs',
'dialogs/person_selector.rs',
'dialogs/preferences.rs',
'dialogs/server_dialog.rs',
'dialogs/recording/mod.rs',
'dialogs/recording/performance_editor.rs',
'dialogs/recording/recording_dialog.rs',
'dialogs/recording/recording_editor_dialog.rs',
'dialogs/recording/recording_editor.rs',
'dialogs/recording/recording_selector_person_screen.rs',
'dialogs/recording/recording_selector.rs',
'dialogs/recording/recording_selector_work_screen.rs',
'dialogs/track_editor.rs',
'dialogs/tracks_editor.rs',
'dialogs/work/mod.rs',
'dialogs/work/part_editor.rs',
'dialogs/work/section_editor.rs',
'dialogs/work/work_dialog.rs',
'dialogs/work/work_editor_dialog.rs',
'dialogs/work/work_editor.rs',
'dialogs/work/work_selector_person_screen.rs',
'dialogs/work/work_selector.rs',
'screens/ensemble_screen.rs',
'screens/mod.rs',
'screens/person_screen.rs',
'screens/player_screen.rs',
'screens/recording_screen.rs',
'screens/work_screen.rs',
'widgets/list.rs',
'widgets/mod.rs',
'widgets/navigator.rs',
'widgets/person_list.rs',
'widgets/player_bar.rs',
'widgets/poe_list.rs',
'widgets/selector_row.rs',
'config.rs',
'config.rs.in',
'main.rs',
'player.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,295 +0,0 @@
use crate::database::*;
use anyhow::anyhow;
use anyhow::Result;
use gstreamer_player::prelude::*;
use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::rc::Rc;
#[derive(Clone)]
pub struct PlaylistItem {
pub recording: RecordingDescription,
pub tracks: Vec<TrackDescription>,
}
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.tracks.is_empty() {
Err(anyhow!(
"Tried to add playlist item without tracks to playlist!"
))
} 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(anyhow!("No current item!"))?;
let mut current_track = self
.current_track
.get()
.ok_or(anyhow!("No 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].tracks.len() - 1;
} else {
return Err(anyhow!("No 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.tracks.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(anyhow!("No current item!"))?;
let mut current_track = self
.current_track
.get()
.ok_or(anyhow!("No current track!"))?;
let playlist = self.playlist.borrow();
let item = &playlist[current_item];
if current_track + 1 < item.tracks.len() {
current_track += 1;
} else if current_item + 1 < playlist.len() {
current_item += 1;
current_track = 0;
} else {
return Err(anyhow!("No next 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()
.get(current_item)
.ok_or(anyhow!("Playlist item doesn't exist!"))?
.tracks
.get(current_track)
.ok_or(anyhow!("Track doesn't exist!"))?
.file_name
.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,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,145 +0,0 @@
use super::*;
use crate::backend::*;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
pub struct EnsembleScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl EnsembleScreen {
pub fn new(backend: Rc<Backend>, ensemble: Ensemble) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::MenuButton, menu_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, recording_frame);
header.set_title(Some(&ensemble.name));
let edit_menu_item = gio::MenuItem::new(Some(&gettext("Edit ensemble")), None);
edit_menu_item.set_action_and_target_value(
Some("win.edit-ensemble"),
Some(&glib::Variant::from(ensemble.id)),
);
let delete_menu_item = gio::MenuItem::new(Some(&gettext("Delete ensemble")), None);
delete_menu_item.set_action_and_target_value(
Some("win.delete-ensemble"),
Some(&glib::Variant::from(ensemble.id)),
);
let menu = gio::Menu::new();
menu.append_item(&edit_menu_item);
menu.append_item(&delete_menu_item);
menu_button.set_menu_model(Some(&menu));
let recording_list = List::new(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
work_label.set_halign(gtk::Align::Start);
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
performers_label.set_ellipsize(pango::EllipsizeMode::End);
performers_label.set_opacity(0.5);
performers_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&work_label);
vbox.add(&performers_label);
vbox.upcast()
});
recording_list.set_filter(
clone!(@strong search_entry => move |recording: &RecordingDescription| {
let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search)
}),
);
recording_frame.add(&recording_list.widget.clone());
let result = Rc::new(Self {
backend,
widget,
stack,
recording_list,
navigator: RefCell::new(None),
});
search_entry.connect_search_changed(clone!(@strong result => move |_| {
result.recording_list.invalidate_filter();
}));
back_button.connect_clicked(clone!(@strong result => move |_| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
result
.recording_list
.set_selected(clone!(@strong result => move |recording| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.push(RecordingScreen::new(result.backend.clone(), recording.clone()));
}
}));
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let recordings = clone
.backend
.get_recordings_for_ensemble(ensemble.id)
.await
.unwrap();
if recordings.is_empty() {
clone.stack.set_visible_child_name("nothing");
} else {
clone.recording_list.show_items(recordings);
clone.stack.set_visible_child_name("content");
}
});
result
}
}
impl NavigatorScreen for EnsembleScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,14 +0,0 @@
pub mod ensemble_screen;
pub use ensemble_screen::*;
pub mod person_screen;
pub use person_screen::*;
pub mod player_screen;
pub use player_screen::*;
pub mod work_screen;
pub use work_screen::*;
pub mod recording_screen;
pub use recording_screen::*;

View file

@ -1,199 +0,0 @@
use super::*;
use crate::backend::*;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
pub struct PersonScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
recording_list: Rc<List<RecordingDescription>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl PersonScreen {
pub fn new(backend: Rc<Backend>, person: Person) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::MenuButton, menu_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Box, work_box);
get_widget!(builder, gtk::Frame, work_frame);
get_widget!(builder, gtk::Box, recording_box);
get_widget!(builder, gtk::Frame, recording_frame);
header.set_title(Some(&person.name_fl()));
let edit_menu_item = gio::MenuItem::new(Some(&gettext("Edit person")), None);
edit_menu_item.set_action_and_target_value(
Some("win.edit-person"),
Some(&glib::Variant::from(person.id)),
);
let delete_menu_item = gio::MenuItem::new(Some(&gettext("Delete person")), None);
delete_menu_item.set_action_and_target_value(
Some("win.delete-person"),
Some(&glib::Variant::from(person.id)),
);
let menu = gio::Menu::new();
menu.append_item(&edit_menu_item);
menu.append_item(&delete_menu_item);
menu_button.set_menu_model(Some(&menu));
let work_list = List::new(&gettext("No works found."));
work_list.set_make_widget(|work: &WorkDescription| {
let label = gtk::Label::new(Some(&work.title));
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
work_list.set_filter(
clone!(@strong search_entry => move |work: &WorkDescription| {
let search = search_entry.get_text().to_string().to_lowercase();
let title = work.title.to_lowercase();
search.is_empty() || title.contains(&search)
}),
);
let recording_list = List::new(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
work_label.set_halign(gtk::Align::Start);
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
performers_label.set_ellipsize(pango::EllipsizeMode::End);
performers_label.set_opacity(0.5);
performers_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&work_label);
vbox.add(&performers_label);
vbox.upcast()
});
recording_list.set_filter(
clone!(@strong search_entry => move |recording: &RecordingDescription| {
let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title() + &recording.get_performers();
search.is_empty() || text.contains(&search)
}),
);
work_frame.add(&work_list.widget);
recording_frame.add(&recording_list.widget);
let result = Rc::new(Self {
backend,
widget,
stack,
work_list,
recording_list,
navigator: RefCell::new(None),
});
search_entry.connect_search_changed(clone!(@strong result => move |_| {
result.work_list.invalidate_filter();
result.recording_list.invalidate_filter();
}));
back_button.connect_clicked(clone!(@strong result => move |_| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}));
result
.work_list
.set_selected(clone!(@strong result => move |work| {
result.recording_list.clear_selection();
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.push(WorkScreen::new(result.backend.clone(), work.clone()));
}
}));
result
.recording_list
.set_selected(clone!(@strong result => move |recording| {
result.work_list.clear_selection();
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.push(RecordingScreen::new(result.backend.clone(), recording.clone()));
}
}));
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.await
.unwrap();
let recordings = clone
.backend
.get_recordings_for_person(person.id)
.await
.unwrap();
if works.is_empty() && recordings.is_empty() {
clone.stack.set_visible_child_name("nothing");
} else {
if works.is_empty() {
work_box.hide();
} else {
clone.work_list.show_items(works);
}
if recordings.is_empty() {
recording_box.hide();
} else {
clone.recording_list.show_items(recordings);
}
clone.stack.set_visible_child_name("content");
}
});
result
}
}
impl NavigatorScreen for PersonScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,334 +0,0 @@
use crate::player::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
struct PlaylistElement {
pub item: usize,
pub track: usize,
pub title: String,
pub subtitle: Option<String>,
pub playable: bool,
}
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<PlaylistElement>>,
player: Rc<RefCell<Option<Rc<Player>>>>,
seeking: Rc<Cell<bool>>,
current_item: Rc<Cell<usize>>,
current_track: Rc<Cell<usize>>,
back_cb: Rc<RefCell<Option<Box<dyn Fn() -> ()>>>>,
}
impl PlayerScreen {
pub fn new() -> 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 back_cb = Rc::new(RefCell::new(None::<Box<dyn Fn() -> ()>>));
back_button.connect_clicked(clone!(@strong back_cb => move |_| {
if let Some(cb) = &*back_cb.borrow() {
cb();
}
}));
let player = Rc::new(RefCell::new(None::<Rc<Player>>));
let seeking = Rc::new(Cell::new(false));
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();
}
}));
stop_button.connect_clicked(clone!(@strong player, @strong back_cb => move |_| {
if let Some(player) = &*player.borrow() {
if let Some(cb) = &*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 seeking, @strong position, @strong position_label => move |_| {
if seeking.get() {
let ms = position.get_value() as u64;
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
position_label.set_text(&format!("{}:{:02}", min, sec));
}
}),
);
let current_item = Rc::new(Cell::<usize>::new(0));
let current_track = Rc::new(Cell::<usize>::new(0));
let list = List::new("");
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.add(&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.add(&subtitle_label);
}
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
hbox.set_border_width(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.add(&image);
} else if element.item > 0 {
hbox.set_margin_top(18);
}
hbox.add(&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();
}
}));
frame.add(&list.widget);
Self {
widget,
title_label,
subtitle_label,
previous_button,
play_button,
next_button,
position_label,
position,
duration_label,
play_image,
pause_image,
list,
player,
seeking,
current_item,
current_track,
back_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.previous_button as previous_button,
@strong self.next_button as next_button,
@strong self.list as list,
@strong playlist
=> move |new_playlist| {
playlist.replace(new_playlist);
previous_button.set_sensitive(player.has_previous());
next_button.set_sensitive(player.has_next());
let mut elements = Vec::new();
for (item_index, item) in playlist.borrow().iter().enumerate() {
elements.push(PlaylistElement {
item: item_index,
track: 0,
title: item.recording.work.get_title(),
subtitle: Some(item.recording.get_performers()),
playable: false,
});
for (track_index, track) in item.tracks.iter().enumerate() {
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.recording.work.parts[*part].title.clone());
}
let title = if parts.is_empty() {
gettext("Unknown")
} else {
parts.join(", ")
};
elements.push(PlaylistElement {
item: item_index,
track: track_index,
title: title,
subtitle: None,
playable: true,
});
}
}
list.show_items(elements);
}
));
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,
@strong self.current_item as self_item,
@strong self.current_track as self_track,
@strong self.list as list
=> 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.tracks[current_track];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.recording.work.parts[*part].title.clone());
}
let mut title = item.recording.work.get_title();
if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", "));
}
title_label.set_text(&title);
subtitle_label.set_text(&item.recording.get_performers());
position_label.set_text("0:00");
self_item.replace(current_item);
self_track.replace(current_track);
list.update();
}
));
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| {
if let Some(child) = play_button.get_child() {
play_button.remove( &child);
}
play_button.add(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)));
}
}

View file

@ -1,156 +0,0 @@
use crate::backend::*;
use crate::database::*;
use crate::player::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
pub struct RecordingScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
tracks: RefCell<Vec<TrackDescription>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingScreen {
pub fn new(backend: Rc<Backend>, recording: RecordingDescription) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::MenuButton, menu_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, frame);
get_widget!(builder, gtk::Button, add_to_playlist_button);
header.set_title(Some(&recording.work.get_title()));
header.set_subtitle(Some(&recording.get_performers()));
let edit_menu_item = gio::MenuItem::new(Some(&gettext("Edit recording")), None);
edit_menu_item.set_action_and_target_value(
Some("win.edit-recording"),
Some(&glib::Variant::from(recording.id)),
);
let delete_menu_item = gio::MenuItem::new(Some(&gettext("Delete recording")), None);
delete_menu_item.set_action_and_target_value(
Some("win.delete-recording"),
Some(&glib::Variant::from(recording.id)),
);
let edit_tracks_menu_item = gio::MenuItem::new(Some(&gettext("Edit tracks")), None);
edit_tracks_menu_item.set_action_and_target_value(
Some("win.edit-tracks"),
Some(&glib::Variant::from(recording.id)),
);
let delete_tracks_menu_item = gio::MenuItem::new(Some(&gettext("Delete tracks")), None);
delete_tracks_menu_item.set_action_and_target_value(
Some("win.delete-tracks"),
Some(&glib::Variant::from(recording.id)),
);
let menu = gio::Menu::new();
menu.append_item(&edit_menu_item);
menu.append_item(&delete_menu_item);
menu.append_item(&edit_tracks_menu_item);
menu.append_item(&delete_tracks_menu_item);
menu_button.set_menu_model(Some(&menu));
let recording = Rc::new(recording);
let list = List::new(&gettext("No tracks found."));
list.set_make_widget(
clone!(@strong recording => move |track: &TrackDescription| {
let mut title_parts = Vec::<String>::new();
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 title_label = gtk::Label::new(Some(&title));
title_label.set_ellipsize(pango::EllipsizeMode::End);
title_label.set_halign(gtk::Align::Start);
let file_name_label = gtk::Label::new(Some(&track.file_name));
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
file_name_label.set_opacity(0.5);
file_name_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&title_label);
vbox.add(&file_name_label);
vbox.upcast()
}),
);
frame.add(&list.widget);
let result = Rc::new(Self {
backend,
widget,
stack,
tracks: RefCell::new(Vec::new()),
navigator: RefCell::new(None),
});
back_button.connect_clicked(clone!(@strong result => move |_| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}));
add_to_playlist_button.connect_clicked(
clone!(@strong result, @strong recording => move |_| {
if let Some(player) = result.backend.get_player() {
player.add_item(PlaylistItem {
recording: (*recording).clone(),
tracks: result.tracks.borrow().clone(),
}).unwrap();
}
}),
);
let context = glib::MainContext::default();
let clone = result.clone();
let id = recording.id;
context.spawn_local(async move {
let tracks = clone.backend.get_tracks(id).await.unwrap();
list.show_items(tracks.clone());
clone.stack.set_visible_child_name("content");
clone.tracks.replace(tracks);
});
result
}
}
impl NavigatorScreen for RecordingScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,144 +0,0 @@
use super::*;
use crate::backend::*;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
pub struct WorkScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl WorkScreen {
pub fn new(backend: Rc<Backend>, work: WorkDescription) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::MenuButton, menu_button);
get_widget!(builder, gtk::SearchEntry, search_entry);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, recording_frame);
header.set_title(Some(&work.title));
header.set_subtitle(Some(&work.composer.name_fl()));
let edit_menu_item = gio::MenuItem::new(Some(&gettext("Edit work")), None);
edit_menu_item.set_action_and_target_value(
Some("win.edit-work"),
Some(&glib::Variant::from(work.id)),
);
let delete_menu_item = gio::MenuItem::new(Some(&gettext("Delete work")), None);
delete_menu_item.set_action_and_target_value(
Some("win.delete-work"),
Some(&glib::Variant::from(work.id)),
);
let menu = gio::Menu::new();
menu.append_item(&edit_menu_item);
menu.append_item(&delete_menu_item);
menu_button.set_menu_model(Some(&menu));
let recording_list = List::new(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &RecordingDescription| {
let work_label = gtk::Label::new(Some(&recording.work.get_title()));
work_label.set_ellipsize(pango::EllipsizeMode::End);
work_label.set_halign(gtk::Align::Start);
let performers_label = gtk::Label::new(Some(&recording.get_performers()));
performers_label.set_ellipsize(pango::EllipsizeMode::End);
performers_label.set_opacity(0.5);
performers_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&work_label);
vbox.add(&performers_label);
vbox.upcast()
});
recording_list.set_filter(clone!(@strong search_entry => move |recording: &RecordingDescription| {
let search = search_entry.get_text().to_string().to_lowercase();
let text = recording.work.get_title().to_lowercase() + &recording.get_performers().to_lowercase();
search.is_empty() || text.contains(&search)
}),);
recording_frame.add(&recording_list.widget);
let result = Rc::new(Self {
backend,
widget,
stack,
recording_list,
navigator: RefCell::new(None),
});
search_entry.connect_search_changed(clone!(@strong result => move |_| {
result.recording_list.invalidate_filter();
}));
back_button.connect_clicked(clone!(@strong result => move |_| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}));
result
.recording_list
.set_selected(clone!(@strong result => move |recording| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.push(RecordingScreen::new(result.backend.clone(), recording.clone()));
}
}));
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let recordings = clone
.backend
.get_recordings_for_work(work.id)
.await
.unwrap();
if recordings.is_empty() {
clone.stack.set_visible_child_name("nothing");
} else {
clone.recording_list.show_items(recordings);
clone.stack.set_visible_child_name("content");
}
});
result
}
}
impl NavigatorScreen for WorkScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,129 +0,0 @@
use super::*;
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::convert::TryInto;
use std::rc::Rc;
pub struct List<T>
where
T: 'static,
{
pub widget: gtk::ListBox,
items: RefCell<Vec<T>>,
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
filter: RefCell<Option<Box<dyn Fn(&T) -> bool>>>,
selected: RefCell<Option<Box<dyn Fn(&T) -> ()>>>,
}
impl<T> List<T>
where
T: 'static,
{
pub fn new(placeholder_text: &str) -> Rc<Self> {
let placeholder_label = gtk::Label::new(Some(placeholder_text));
placeholder_label.set_margin_top(6);
placeholder_label.set_margin_bottom(6);
placeholder_label.set_margin_start(6);
placeholder_label.set_margin_end(6);
placeholder_label.show();
let widget = gtk::ListBox::new();
widget.set_placeholder(Some(&placeholder_label));
widget.show();
let this = Rc::new(Self {
widget,
items: RefCell::new(Vec::new()),
make_widget: RefCell::new(None),
filter: RefCell::new(None),
selected: RefCell::new(None),
});
this.widget
.connect_row_activated(clone!(@strong this => move |_, row| {
if let Some(selected) = &*this.selected.borrow() {
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap();
selected(&this.items.borrow()[index]);
}
}));
this.widget
.set_filter_func(Some(Box::new(clone!(@strong this => move |row| {
if let Some(filter) = &*this.filter.borrow() {
let row = row.get_child().unwrap().downcast::<SelectorRow>().unwrap();
let index: usize = row.get_index().try_into().unwrap();
filter(&this.items.borrow()[index])
} else {
true
}
}))));
this
}
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
self.make_widget.replace(Some(Box::new(make_widget)));
}
pub fn set_filter<F: Fn(&T) -> bool + 'static>(&self, filter: F) {
self.filter.replace(Some(Box::new(filter)));
}
pub fn set_selected<S: Fn(&T) -> () + 'static>(&self, selected: S) {
self.selected.replace(Some(Box::new(selected)));
}
pub fn get_selected_index(&self) -> Option<usize> {
match self.widget.get_selected_rows().first() {
Some(row) => match row.get_child() {
Some(child) => Some(
child
.downcast::<SelectorRow>()
.unwrap()
.get_index()
.try_into()
.unwrap(),
),
None => None,
},
None => None,
}
}
pub fn select_index(&self, index: usize) {
self.widget.select_row(
self.widget
.get_row_at_index(index.try_into().unwrap())
.as_ref(),
);
}
pub fn show_items(&self, items: Vec<T>) {
self.items.replace(items);
self.update();
}
pub fn invalidate_filter(&self) {
self.widget.invalidate_filter();
}
pub fn update(&self) {
for child in self.widget.get_children() {
self.widget.remove(&child);
}
if let Some(make_widget) = &*self.make_widget.borrow() {
for (index, item) in self.items.borrow().iter().enumerate() {
let row = SelectorRow::new(index.try_into().unwrap(), &make_widget(item));
row.show_all();
self.widget.insert(&row, -1);
}
}
}
pub fn clear_selection(&self) {
self.widget.unselect_all();
}
}

View file

@ -1,17 +0,0 @@
pub mod list;
pub use list::*;
pub mod navigator;
pub use navigator::*;
pub mod person_list;
pub use person_list::*;
pub mod player_bar;
pub use player_bar::*;
pub mod poe_list;
pub use poe_list::*;
pub mod selector_row;
pub use selector_row::*;

View file

@ -1,139 +0,0 @@
use glib::clone;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
pub trait NavigatorScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>);
fn get_widget(&self) -> gtk::Widget;
fn detach_navigator(&self);
}
pub struct Navigator {
pub widget: gtk::Stack,
screens: RefCell<Vec<Rc<dyn NavigatorScreen>>>,
old_screens: RefCell<Vec<Rc<dyn NavigatorScreen>>>,
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
}
impl Navigator {
pub fn new<W>(empty_screen: &W) -> Rc<Self>
where
W: IsA<gtk::Widget>,
{
let widget = gtk::Stack::new();
widget.set_transition_type(gtk::StackTransitionType::Crossfade);
widget.set_hexpand(true);
widget.add_named(empty_screen, "empty_screen");
widget.show();
let result = Rc::new(Self {
widget,
screens: RefCell::new(Vec::new()),
old_screens: RefCell::new(Vec::new()),
back_cb: RefCell::new(None),
});
unsafe {
result.widget.connect_notify_unsafe(
Some("transition-running"),
clone!(@strong result => move |_, _| {
if !result.widget.get_transition_running() {
result.clear_old_screens();
}
}),
);
}
result
}
pub fn set_back_cb<F>(&self, cb: F) where F: Fn() -> () + 'static {
self.back_cb.replace(Some(Box::new(cb)));
}
pub fn push<S>(self: Rc<Self>, screen: Rc<S>)
where
S: NavigatorScreen + 'static,
{
if let Some(screen) = self.screens.borrow().last() {
screen.detach_navigator();
}
let widget = screen.get_widget();
self.widget.add(&widget);
self.widget.set_visible_child(&widget);
screen.attach_navigator(self.clone());
self.screens.borrow_mut().push(screen);
}
pub fn pop(self: Rc<Self>) {
let popped = if let Some(screen) = self.screens.borrow_mut().pop() {
screen.detach_navigator();
self.old_screens.borrow_mut().push(screen);
true
} else {
false
};
if popped {
if let Some(screen) = self.screens.borrow().last() {
let widget = screen.get_widget();
self.widget.set_visible_child(&widget);
screen.attach_navigator(self.clone());
} 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_screens();
}
}
}
pub fn replace<S>(self: Rc<Self>, screen: Rc<S>)
where
S: NavigatorScreen + 'static,
{
for screen in self.screens.replace(Vec::new()) {
screen.detach_navigator();
self.old_screens.borrow_mut().push(screen);
}
let widget = screen.get_widget();
self.widget.add(&widget);
self.widget.set_visible_child(&widget);
screen.attach_navigator(self.clone());
self.screens.borrow_mut().push(screen);
if !self.widget.get_transition_running() {
self.clear_old_screens();
}
}
pub fn reset(&self) {
for screen in self.screens.replace(Vec::new()) {
screen.detach_navigator();
self.old_screens.borrow_mut().push(screen);
}
if !self.widget.get_transition_running() {
self.clear_old_screens();
}
}
fn clear_old_screens(&self) {
for screen in self.old_screens.borrow().iter() {
self.widget.remove(&screen.get_widget());
}
self.old_screens.borrow_mut().clear();
}
}

View file

@ -1,82 +0,0 @@
use super::*;
use crate::backend::Backend;
use crate::database::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::rc::Rc;
pub struct PersonList {
pub widget: gtk::Box,
list: Rc<List<Person>>,
backend: Rc<Backend>,
stack: gtk::Stack,
}
impl PersonList {
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_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(&gettext("No persons found."));
list.set_make_widget(|person: &Person| {
let label = gtk::Label::new(Some(&person.name_lf()));
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
list.set_filter(clone!(@strong search_entry => move |person: &Person| {
let search = search_entry.get_text().to_string().to_lowercase();
let name = person.name_fl().to_lowercase();
search.is_empty() || name.contains(&search)
}));
scrolled_window.add(&list.widget);
let result = Rc::new(Self {
widget,
list,
backend,
stack,
});
search_entry.connect_search_changed(clone!(@strong result => move |_| {
result.list.invalidate_filter();
}));
result.clone().reload();
result
}
pub fn set_selected<S>(&self, selected: S)
where
S: Fn(&Person) -> () + 'static,
{
self.list.set_selected(selected);
}
pub fn reload(self: Rc<Self>) {
self.stack.set_visible_child_name("loading");
let context = glib::MainContext::default();
let backend = self.backend.clone();
let list = self.list.clone();
context.spawn_local(async move {
let persons = backend.get_persons().await.unwrap();
list.show_items(persons);
self.stack.set_visible_child_name("content");
});
}
}

View file

@ -1,175 +0,0 @@
use crate::player::*;
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.tracks[current_track];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.recording.work.parts[*part].title.clone());
}
let mut title = item.recording.work.get_title();
if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", "));
}
title_label.set_text(&title);
subtitle_label.set_text(&item.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| {
if let Some(child) = play_button.get_child() {
play_button.remove( &child);
}
play_button.add(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,109 +0,0 @@
use super::*;
use crate::backend::Backend;
use crate::database::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
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,
list: Rc<List<PersonOrEnsemble>>,
backend: Rc<Backend>,
stack: gtk::Stack,
}
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(&gettext("No persons or ensembles found."));
list.set_make_widget(|poe: &PersonOrEnsemble| {
let label = gtk::Label::new(Some(&poe.get_title()));
label.set_halign(gtk::Align::Start);
label.set_margin_start(6);
label.set_margin_end(6);
label.set_margin_top(6);
label.set_margin_bottom(6);
label.upcast()
});
list.set_filter(
clone!(@strong search_entry => move |poe: &PersonOrEnsemble| {
let search = search_entry.get_text().to_string().to_lowercase();
let title = poe.get_title().to_lowercase();
search.is_empty() || title.contains(&search)
}),
);
scrolled_window.add(&list.widget);
let result = Rc::new(Self {
widget,
list,
backend,
stack,
});
search_entry.connect_search_changed(clone!(@strong result => move |_| {
result.list.invalidate_filter();
}));
result
}
pub fn set_selected<S>(&self, selected: S)
where
S: Fn(&PersonOrEnsemble) -> () + 'static,
{
self.list.set_selected(selected);
}
pub fn reload(self: Rc<Self>) {
self.stack.set_visible_child_name("loading");
let context = glib::MainContext::default();
let backend = self.backend.clone();
let list = self.list.clone();
context.spawn_local(async move {
let persons = backend.get_persons().await.unwrap();
let ensembles = backend.get_ensembles().await.unwrap();
let mut poes: Vec<PersonOrEnsemble> = Vec::new();
for person in persons {
poes.push(PersonOrEnsemble::Person(person));
}
for ensemble in ensembles {
poes.push(PersonOrEnsemble::Ensemble(ensemble));
}
list.show_items(poes);
self.stack.set_visible_child_name("content");
});
}
}

View file

@ -1,149 +0,0 @@
use glib::prelude::*;
use glib::subclass;
use glib::subclass::prelude::*;
use glib::translate::*;
use glib::{glib_object_impl, glib_object_subclass, glib_wrapper};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use std::cell::{Cell, RefCell};
glib_wrapper! {
pub struct SelectorRow(
Object<subclass::simple::InstanceStruct<SelectorRowPriv>,
subclass::simple::ClassStruct<SelectorRowPriv>,
SelectorRowClass>
) @extends gtk::Bin, gtk::Container, gtk::Widget;
match fn {
get_type => || SelectorRowPriv::get_type().to_glib(),
}
}
impl SelectorRow {
pub fn new<T: IsA<gtk::Widget>>(index: u64, child: &T) -> Self {
glib::Object::new(
Self::static_type(),
&[("index", &index), ("child", child.upcast_ref())],
)
.expect("Failed to create SelectorRow GObject!")
.downcast()
.expect("SelectorRow GObject is of the wrong type!")
}
pub fn get_index(&self) -> u64 {
self.get_property("index").unwrap().get().unwrap().unwrap()
}
}
pub struct SelectorRowPriv {
index: Cell<u64>,
child: RefCell<Option<gtk::Widget>>,
}
static PROPERTIES: [subclass::Property; 2] = [
subclass::Property("index", |name| {
glib::ParamSpec::uint64(
name,
"Index",
"Index",
0,
u64::MAX,
0,
glib::ParamFlags::READWRITE,
)
}),
subclass::Property("child", |name| {
glib::ParamSpec::object(
name,
"Child",
"Child",
gtk::Widget::static_type(),
glib::ParamFlags::READWRITE,
)
}),
];
impl ObjectSubclass for SelectorRowPriv {
const NAME: &'static str = "SelectorRow";
type ParentType = gtk::Bin;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib_object_subclass!();
fn class_init(klass: &mut Self::Class) {
klass.install_properties(&PROPERTIES);
}
fn new() -> Self {
Self {
index: Cell::new(0),
child: RefCell::new(None),
}
}
}
impl ObjectImpl for SelectorRowPriv {
glib_object_impl!();
fn constructed(&self, object: &glib::Object) {
self.parent_constructed(object);
let row = object.downcast_ref::<SelectorRow>().unwrap();
let child = self.child.borrow();
match child.as_ref() {
Some(child) => row.add(child),
None => (),
}
}
fn set_property(&self, object: &glib::Object, id: usize, value: &glib::Value) {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("index", ..) => {
let index = value
.get_some()
.expect("Wrong type for SelectorRow GObject index property!");
self.index.set(index);
}
subclass::Property("child", ..) => {
let child = value
.get()
.expect("Wrong type for SelectorRow GObject child property!");
let row = object.downcast_ref::<SelectorRow>().unwrap();
{
let old = self.child.borrow();
match old.as_ref() {
Some(old) => row.remove(old),
None => (),
}
}
self.child.replace(child.clone());
match child {
Some(child) => row.add(&child),
None => (),
}
}
_ => unimplemented!(),
}
}
fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
let prop = &PROPERTIES[id];
match *prop {
subclass::Property("index", ..) => Ok(self.index.get().to_value()),
subclass::Property("child", ..) => Ok(self.child.borrow().to_value()),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for SelectorRowPriv {}
impl ContainerImpl for SelectorRowPriv {}
impl BinImpl for SelectorRowPriv {}

View file

@ -1,428 +0,0 @@
use crate::backend::*;
use crate::dialogs::*;
use crate::screens::*;
use crate::widgets::*;
use futures::prelude::*;
use gettextrs::gettext;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::{action, get_widget};
use libhandy::prelude::*;
use std::rc::Rc;
pub struct Window {
backend: Rc<Backend>,
window: libhandy::ApplicationWindow,
stack: gtk::Stack,
leaflet: libhandy::Leaflet,
sidebar_box: gtk::Box,
poe_list: Rc<PoeList>,
navigator: Rc<Navigator>,
player_bar: PlayerBar,
player_screen: 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, libhandy::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, libhandy::Leaflet, leaflet);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Box, sidebar_box);
get_widget!(builder, gtk::Box, empty_screen);
let backend = Rc::new(Backend::new());
backend.clone().init();
let player_screen = PlayerScreen::new();
stack.add_named(&player_screen.widget, "player_screen");
let poe_list = PoeList::new(backend.clone());
let navigator = Navigator::new(&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.add(&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::FileChooserNative::new(
Some(&gettext("Select music library folder")),
Some(&result.window),
gtk::FileChooserAction::SelectFolder,
None,
None);
if let gtk::ResponseType::Accept = dialog.run() {
if let Some(path) = dialog.get_filename() {
let context = glib::MainContext::default();
let backend = result.backend.clone();
context.spawn_local(async move {
backend.set_music_library_path(path).await.unwrap();
});
}
}
}));
add_button.connect_clicked(clone!(@strong result => move |_| {
let editor = TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new());
editor.set_callback(clone!(@strong result => move || {
result.reload();
}));
editor.show();
}));
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,
"preferences",
clone!(@strong result => move |_, _| {
Preferences::new(result.backend.clone(), &result.window).show();
})
);
action!(
result.window,
"about",
clone!(@strong result => move |_, _| {
show_about_dialog(&result.window);
})
);
action!(
result.window,
"add-person",
clone!(@strong result => move |_, _| {
PersonEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |_| {
result.reload();
})).show();
})
);
action!(
result.window,
"add-instrument",
clone!(@strong result => move |_, _| {
InstrumentEditor::new(result.backend.clone(), &result.window, None, |instrument| {
println!("{:?}", instrument);
}).show();
})
);
action!(
result.window,
"add-work",
clone!(@strong result => move |_, _| {
let dialog = WorkDialog::new(result.backend.clone(), &result.window);
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
})
);
action!(
result.window,
"add-ensemble",
clone!(@strong result => move |_, _| {
EnsembleEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |_| {
result.reload();
})).show();
})
);
action!(
result.window,
"add-recording",
clone!(@strong result => move |_, _| {
let dialog = RecordingDialog::new(result.backend.clone(), &result.window);
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
})
);
action!(
result.window,
"add-tracks",
clone!(@strong result => move |_, _| {
let editor = TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new());
editor.set_callback(clone!(@strong result => move || {
result.reload();
}));
editor.show();
})
);
action!(
result.window,
"edit-person",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let person = result.backend.get_person(id).await.unwrap();
PersonEditor::new(result.backend.clone(), &result.window, Some(person), clone!(@strong result => move |_| {
result.reload();
})).show();
});
})
);
action!(
result.window,
"delete-person",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_person(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-ensemble",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let ensemble = result.backend.get_ensemble(id).await.unwrap();
EnsembleEditor::new(result.backend.clone(), &result.window, Some(ensemble), clone!(@strong result => move |_| {
result.reload();
})).show();
});
})
);
action!(
result.window,
"delete-ensemble",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_ensemble(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-work",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let work = result.backend.get_work_description(id).await.unwrap();
let dialog = WorkEditorDialog::new(result.backend.clone(), &result.window, Some(work));
dialog.set_saved_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
});
})
);
action!(
result.window,
"delete-work",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_work(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-recording",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let recording = result.backend.get_recording_description(id).await.unwrap();
let dialog = RecordingEditorDialog::new(result.backend.clone(), &result.window, Some(recording));
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
}));
dialog.show();
});
})
);
action!(
result.window,
"delete-recording",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_recording(id).await.unwrap();
result.reload();
});
})
);
action!(
result.window,
"edit-tracks",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
let recording = result.backend.get_recording_description(id).await.unwrap();
let tracks = result.backend.get_tracks(id).await.unwrap();
let editor = TracksEditor::new(result.backend.clone(), &result.window, Some(recording), tracks);
editor.set_callback(clone!(@strong result => move || {
result.reload();
}));
editor.show();
});
})
);
action!(
result.window,
"delete-tracks",
Some(glib::VariantTy::new("x").unwrap()),
clone!(@strong result => move |_, id| {
let id = id.unwrap().get().unwrap();
let result = result.clone();
let c = glib::MainContext::default();
c.spawn_local(async move {
result.backend.delete_tracks(id).await.unwrap();
result.reload();
});
})
);
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
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.set_player(Some(player));
}
}
}
});
result.leaflet.add(&result.navigator.widget);
result
.poe_list
.set_selected(clone!(@strong result => move |poe| {
result.leaflet.set_visible_child(&result.navigator.widget);
match poe {
PersonOrEnsemble::Person(person) => {
result.navigator.clone().replace(PersonScreen::new(result.backend.clone(), person.clone()));
}
PersonOrEnsemble::Ensemble(ensemble) => {
result.navigator.clone().replace(EnsembleScreen::new(result.backend.clone(), ensemble.clone()));
}
}
}));
result
.sidebar_box
.pack_start(&result.poe_list.widget, true, true, 0);
result
}
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);
}
}