mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 03:47:23 +01:00
Remove server synchronization code
This commit (tries to) remove all code for synchronyzing to a music metadata server. Because the intended use cases of the application have shifted over time, this isn't a central feature anymore. However, it may well be decided to reintroduce the functionality at some point in the future.
This commit is contained in:
parent
384ca255f3
commit
f165c6cae8
48 changed files with 96 additions and 2633 deletions
868
Cargo.lock
generated
868
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +1,2 @@
|
|||
[workspace]
|
||||
members = ["backend", "client", "database", "import", "musicus"]
|
||||
members = ["backend", "database", "import", "musicus"]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ glib = "0.14.0"
|
|||
gstreamer = "0.17.0"
|
||||
gstreamer-player = "0.17.0"
|
||||
log = { version = "0.4.14", features = ["std"] }
|
||||
musicus_client = { version = "0.1.0", path = "../client" }
|
||||
musicus_database = { version = "0.1.0", path = "../database" }
|
||||
musicus_import = { version = "0.1.0", path = "../import" }
|
||||
thiserror = "1.0.23"
|
||||
|
|
@ -18,4 +17,3 @@ tokio = { version = "1.4.0", features = ["sync"] }
|
|||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
mpris-player = "0.6.0"
|
||||
secret-service = "2.0.1"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,9 @@
|
|||
/// An error that can happened within the backend.
|
||||
/// An error that happened within the backend.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ClientError(#[from] musicus_client::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] musicus_database::Error),
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error("An error happened using the SecretService.")]
|
||||
SecretServiceError(#[from] secret_service::Error),
|
||||
|
||||
#[error("An error happened while decoding to UTF-8.")]
|
||||
Utf8Error(#[from] std::str::Utf8Error),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
use gio::prelude::*;
|
||||
use log::warn;
|
||||
use musicus_client::{Client, LoginData};
|
||||
use musicus_database::DbThread;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use tokio::sync::{broadcast, broadcast::Sender};
|
||||
|
||||
pub use musicus_client as client;
|
||||
pub use musicus_database as db;
|
||||
pub use musicus_import as import;
|
||||
|
||||
|
|
@ -22,9 +18,6 @@ mod logger;
|
|||
pub mod player;
|
||||
pub use player::*;
|
||||
|
||||
#[cfg(all(feature = "dbus"))]
|
||||
mod secure;
|
||||
|
||||
/// General states the application can be in.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BackendState {
|
||||
|
|
@ -49,9 +42,6 @@ pub struct Backend {
|
|||
/// Access to GSettings.
|
||||
settings: gio::Settings,
|
||||
|
||||
/// Whether the server should be used by default when searching for or changing items.
|
||||
use_server: Cell<bool>,
|
||||
|
||||
/// The current path to the music library, which is used by the player and the database. This
|
||||
/// is guaranteed to be Some, when the state is set to BackendState::Ready.
|
||||
music_library_path: RefCell<Option<PathBuf>>,
|
||||
|
|
@ -65,9 +55,6 @@ pub struct Backend {
|
|||
/// The player handling playlist and playback. This can be assumed to exist, when the state is
|
||||
/// set to BackendState::Ready.
|
||||
player: RefCell<Option<Rc<Player>>>,
|
||||
|
||||
/// A client for the Wolfgang server.
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
|
|
@ -83,12 +70,10 @@ impl Backend {
|
|||
Backend {
|
||||
state_sender,
|
||||
settings: gio::Settings::new("de.johrpan.musicus"),
|
||||
use_server: Cell::new(true),
|
||||
music_library_path: RefCell::new(None),
|
||||
library_updated_sender,
|
||||
database: RefCell::new(None),
|
||||
player: RefCell::new(None),
|
||||
client: Client::new(),
|
||||
player: RefCell::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,24 +87,6 @@ impl Backend {
|
|||
pub async fn init(&self) -> Result<()> {
|
||||
self.init_library().await?;
|
||||
|
||||
let url = self.settings.string("server-url");
|
||||
if !url.is_empty() {
|
||||
self.client.set_server_url(&url);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "dbus"))]
|
||||
match Self::load_login_data().await {
|
||||
Ok(Some(data)) => self.client.set_login_data(Some(data)),
|
||||
Err(err) => warn!(
|
||||
"The login data could not be loaded from SecretService. It will not \
|
||||
be available. Error message: {}",
|
||||
err
|
||||
),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.use_server.set(self.settings.boolean("use-server"));
|
||||
|
||||
if self.get_music_library_path().is_none() {
|
||||
self.set_state(BackendState::NoMusicLibrary);
|
||||
} else {
|
||||
|
|
@ -129,80 +96,6 @@ impl Backend {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the server should be used by default.
|
||||
///
|
||||
/// This will return `false` if no server URL is set up. Otherwise, the
|
||||
/// value is based on the users "use-server" preference.
|
||||
pub fn use_server(&self) -> bool {
|
||||
self.client.get_server_url().is_some() && self.use_server.get()
|
||||
}
|
||||
|
||||
/// Set whether the server should be used by default.
|
||||
pub fn set_use_server(&self, enabled: bool) {
|
||||
self.use_server.set(enabled);
|
||||
|
||||
if let Err(err) = self.settings.set_boolean("use-server", enabled) {
|
||||
warn!(
|
||||
"An error happened whilte trying to save the \"use-server\" setting to GSettings. \
|
||||
Error message: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the URL of the Musicus server to connect to.
|
||||
pub fn set_server_url(&self, url: &str) {
|
||||
if let Err(err) = self.settings.set_string("server-url", url) {
|
||||
warn!(
|
||||
"An error happened while trying to save the server URL to GSettings. Most \
|
||||
likely it will not be available at the next startup. Error message: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
self.client.set_server_url(url);
|
||||
}
|
||||
|
||||
/// Get the currently set server URL.
|
||||
pub fn get_server_url(&self) -> Option<String> {
|
||||
self.client.get_server_url()
|
||||
}
|
||||
|
||||
/// Set the user credentials to use.
|
||||
pub async fn set_login_data(&self, data: Option<LoginData>) {
|
||||
#[cfg(all(feature = "dbus"))]
|
||||
if let Some(data) = &data {
|
||||
if let Err(err) = Self::store_login_data(data.clone()).await {
|
||||
warn!(
|
||||
"An error happened while trying to store the login data using SecretService. \
|
||||
This means, that they will not be available at the next startup most likely. \
|
||||
Error message: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if let Err(err) = Self::delete_secrets().await {
|
||||
warn!(
|
||||
"An error happened while trying to delete the login data from SecretService. \
|
||||
This may result in the login data being reloaded at the next startup. Error \
|
||||
message: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.client.set_login_data(data);
|
||||
}
|
||||
|
||||
pub fn cl(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
/// Get the currently stored login credentials.
|
||||
pub fn get_login_data(&self) -> Option<LoginData> {
|
||||
self.client.get_login_data()
|
||||
}
|
||||
|
||||
/// Set the current state and notify the user interface.
|
||||
fn set_state(&self, state: BackendState) {
|
||||
self.state_sender.send(state).unwrap();
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
use crate::{Backend, Error, Result};
|
||||
use futures_channel::oneshot;
|
||||
use musicus_client::LoginData;
|
||||
use secret_service::{Collection, EncryptionType, SecretService};
|
||||
use std::collections::HashMap;
|
||||
use std::thread;
|
||||
|
||||
impl Backend {
|
||||
/// Get the login credentials from secret storage.
|
||||
pub(super) async fn load_login_data() -> Result<Option<LoginData>> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::load_login_data_priv()).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
pub(super) async fn store_login_data(data: LoginData) -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::store_login_data_priv(data)).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Delete all stored secrets.
|
||||
pub(super) async fn delete_secrets() -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
thread::spawn(move || sender.send(Self::delete_secrets_priv()).unwrap());
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Get the login credentials from secret storage.
|
||||
fn load_login_data_priv() -> Result<Option<LoginData>> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let items = collection.get_all_items()?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
let item = items
|
||||
.iter()
|
||||
.find(|item| item.get_label().unwrap_or_default() == key);
|
||||
|
||||
Ok(match item {
|
||||
Some(item) => {
|
||||
let username = item
|
||||
.get_attributes()?
|
||||
.get("username")
|
||||
.ok_or(Error::Other(
|
||||
"Missing username in SecretService attributes.",
|
||||
))?
|
||||
.to_owned();
|
||||
|
||||
let password = std::str::from_utf8(&item.get_secret()?)?.to_owned();
|
||||
|
||||
Some(LoginData { username, password })
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
fn store_login_data_priv(data: LoginData) -> Result<()> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
Self::delete_secrets_for_key(&collection, key)?;
|
||||
|
||||
let mut attributes = HashMap::new();
|
||||
attributes.insert("username", data.username.as_str());
|
||||
collection.create_item(
|
||||
key,
|
||||
attributes,
|
||||
data.password.as_bytes(),
|
||||
true,
|
||||
"text/plain",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all stored secrets.
|
||||
fn delete_secrets_priv() -> Result<()> {
|
||||
let ss = SecretService::new(EncryptionType::Dh)?;
|
||||
let collection = Self::get_collection(&ss)?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
Self::delete_secrets_for_key(&collection, key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all stored secrets for the provided key.
|
||||
fn delete_secrets_for_key(collection: &Collection, key: &str) -> Result<()> {
|
||||
let items = collection.get_all_items()?;
|
||||
|
||||
for item in items {
|
||||
if item.get_label().unwrap_or_default() == key {
|
||||
item.delete()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the default SecretService collection and unlock it.
|
||||
fn get_collection<'a>(ss: &'a SecretService) -> Result<Collection<'a>> {
|
||||
let collection = ss.get_default_collection()?;
|
||||
collection.unlock()?;
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "musicus_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
isahc = "1.1.0"
|
||||
log = "0.4.14"
|
||||
musicus_database = { version = "0.1.0", path = "../database" }
|
||||
serde = { version = "1.0.117", features = ["derive"] }
|
||||
serde_json = "1.0.59"
|
||||
thiserror = "1.0.23"
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use log::info;
|
||||
use musicus_database::Ensemble;
|
||||
|
||||
impl Client {
|
||||
/// Get all available ensembles from the server.
|
||||
pub async fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
|
||||
info!("Get ensembles");
|
||||
let body = self.get("ensembles").await?;
|
||||
let ensembles: Vec<Ensemble> = serde_json::from_str(&body)?;
|
||||
Ok(ensembles)
|
||||
}
|
||||
|
||||
/// Post a new ensemble to the server.
|
||||
pub async fn post_ensemble(&self, data: &Ensemble) -> Result<()> {
|
||||
info!("Post ensemble {:?}", data);
|
||||
self.post("ensembles", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
use isahc::http::StatusCode;
|
||||
|
||||
/// An error within the client.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("The users login credentials were wrong.")]
|
||||
LoginFailed,
|
||||
|
||||
#[error("The user has to be logged in to perform this action.")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("The user is not allowed to perform this action.")]
|
||||
Forbidden,
|
||||
|
||||
#[error("The server returned an unexpected status code: {0}.")]
|
||||
UnexpectedResponse(StatusCode),
|
||||
|
||||
#[error("A networking error happened.")]
|
||||
NetworkError(#[from] isahc::Error),
|
||||
|
||||
#[error("A networking error happened.")]
|
||||
HttpError(#[from] isahc::http::Error),
|
||||
|
||||
#[error("An error happened when serializing/deserializing.")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
|
||||
#[error("An IO error happened.")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("An error happened: {0}")]
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use log::info;
|
||||
use musicus_database::Instrument;
|
||||
|
||||
impl Client {
|
||||
/// Get all available instruments from the server.
|
||||
pub async fn get_instruments(&self) -> Result<Vec<Instrument>> {
|
||||
info!("Get instruments");
|
||||
let body = self.get("instruments").await?;
|
||||
let instruments: Vec<Instrument> = serde_json::from_str(&body)?;
|
||||
Ok(instruments)
|
||||
}
|
||||
|
||||
/// Post a new instrument to the server.
|
||||
pub async fn post_instrument(&self, data: &Instrument) -> Result<()> {
|
||||
info!("Post instrument {:?}", data);
|
||||
self.post("instruments", serde_json::to_string(data)?)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
use isahc::http::StatusCode;
|
||||
use isahc::prelude::*;
|
||||
use isahc::{AsyncBody, Request, Response};
|
||||
use log::info;
|
||||
use serde::Serialize;
|
||||
use std::cell::RefCell;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod ensembles;
|
||||
pub use ensembles::*;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
pub mod instruments;
|
||||
pub use instruments::*;
|
||||
|
||||
pub mod mediums;
|
||||
pub use mediums::*;
|
||||
|
||||
pub mod persons;
|
||||
pub use persons::*;
|
||||
|
||||
pub mod recordings;
|
||||
pub use recordings::*;
|
||||
|
||||
pub mod register;
|
||||
pub use register::*;
|
||||
|
||||
pub mod works;
|
||||
pub use works::*;
|
||||
|
||||
/// Credentials used for login.
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginData {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// A client for accessing the Wolfgang API.
|
||||
pub struct Client {
|
||||
server_url: RefCell<Option<String>>,
|
||||
login_data: RefCell<Option<LoginData>>,
|
||||
token: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
server_url: RefCell::new(None),
|
||||
login_data: RefCell::new(None),
|
||||
token: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the URL of the Musicus server to connect to.
|
||||
pub fn set_server_url(&self, url: &str) {
|
||||
self.server_url.replace(Some(url.to_owned()));
|
||||
}
|
||||
|
||||
/// 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 fn set_login_data(&self, data: Option<LoginData>) {
|
||||
self.login_data.replace(data);
|
||||
self.token.replace(None);
|
||||
}
|
||||
|
||||
/// Get the currently stored login credentials.
|
||||
pub fn get_login_data(&self) -> Option<LoginData> {
|
||||
self.login_data.borrow().clone()
|
||||
}
|
||||
|
||||
/// Try to login a user with the provided credentials and return, wether the login suceeded.
|
||||
pub async fn login(&self) -> Result<bool> {
|
||||
info!("Login");
|
||||
|
||||
let server_url = self.server_url()?;
|
||||
let data = self.login_data()?;
|
||||
|
||||
let request = Request::post(format!("{}/login", server_url))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_string(&data)?)?;
|
||||
|
||||
let mut response = isahc::send_async(request).await?;
|
||||
|
||||
let success = match response.status() {
|
||||
StatusCode::OK => {
|
||||
let token = response.text().await?;
|
||||
self.token.replace(Some(token));
|
||||
true
|
||||
}
|
||||
StatusCode::UNAUTHORIZED => false,
|
||||
status_code => return Err(Error::UnexpectedResponse(status_code)),
|
||||
};
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
/// Make an unauthenticated get request to the server.
|
||||
async fn get(&self, url: &str) -> Result<String> {
|
||||
let server_url = self.server_url()?;
|
||||
|
||||
let mut response = Request::get(format!("{}/{}", server_url, url))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.body(())?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => Ok(response.text().await?),
|
||||
status_code => Err(Error::UnexpectedResponse(status_code)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make an authenticated post request to the server.
|
||||
async fn post(&self, url: &str, body: String) -> Result<String> {
|
||||
// Try to do the request using a cached login token.
|
||||
if self.token.borrow().is_some() {
|
||||
let mut response = self.post_priv(url, body.clone()).await?;
|
||||
|
||||
// If authorization failed, try again below. Else, return early.
|
||||
match response.status() {
|
||||
StatusCode::UNAUTHORIZED => info!("Token may be expired"),
|
||||
StatusCode::OK => return Ok(response.text().await?),
|
||||
status_code => return Err(Error::UnexpectedResponse(status_code)),
|
||||
}
|
||||
}
|
||||
|
||||
if self.login().await? {
|
||||
let mut response = self.post_priv(url, body).await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => Ok(response.text().await?),
|
||||
StatusCode::UNAUTHORIZED => Err(Error::Unauthorized),
|
||||
status_code => Err(Error::UnexpectedResponse(status_code)),
|
||||
}
|
||||
} else {
|
||||
Err(Error::LoginFailed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Post something to the server assuming there is a valid login token.
|
||||
async fn post_priv(&self, url: &str, body: String) -> Result<Response<AsyncBody>> {
|
||||
let server_url = self.server_url()?;
|
||||
let token = self.token()?;
|
||||
|
||||
let response = Request::post(format!("{}/{}", server_url, url))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Require the server URL to be set.
|
||||
fn server_url(&self) -> Result<String> {
|
||||
self.get_server_url()
|
||||
.ok_or(Error::Other("The server URL is not available!"))
|
||||
}
|
||||
|
||||
/// Require the login data to be set.
|
||||
fn login_data(&self) -> Result<LoginData> {
|
||||
self.get_login_data()
|
||||
.ok_or(Error::Other("The login data is unset!"))
|
||||
}
|
||||
|
||||
/// Require a login token to be set.
|
||||
fn token(&self) -> Result<String> {
|
||||
self.token
|
||||
.borrow()
|
||||
.clone()
|
||||
.ok_or(Error::Other("No login token found!"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use log::info;
|
||||
use musicus_database::Medium;
|
||||
|
||||
impl Client {
|
||||
/// Get all available mediums from the server, that contain the specified
|
||||
/// recording.
|
||||
pub async fn get_mediums_for_recording(&self, recording_id: &str) -> Result<Vec<Medium>> {
|
||||
info!("Get mediums for recording {}", recording_id);
|
||||
let body = self
|
||||
.get(&format!("recordings/{}/mediums", recording_id))
|
||||
.await?;
|
||||
let mediums: Vec<Medium> = serde_json::from_str(&body)?;
|
||||
Ok(mediums)
|
||||
}
|
||||
|
||||
/// Get all available mediums from the server, that match the specified
|
||||
/// DiscID.
|
||||
pub async fn get_mediums_by_discid(&self, discid: &str) -> Result<Vec<Medium>> {
|
||||
info!("Get mediums by discid {}", discid);
|
||||
let body = self.get(&format!("discids/{}/mediums", discid)).await?;
|
||||
let mediums: Vec<Medium> = serde_json::from_str(&body)?;
|
||||
Ok(mediums)
|
||||
}
|
||||
|
||||
/// Post a new medium to the server.
|
||||
pub async fn post_medium(&self, data: &Medium) -> Result<()> {
|
||||
info!("Post medium {:?}", data);
|
||||
self.post("mediums", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use log::info;
|
||||
use musicus_database::Person;
|
||||
|
||||
impl Client {
|
||||
/// Get all available persons from the server.
|
||||
pub async fn get_persons(&self) -> Result<Vec<Person>> {
|
||||
info!("Get persons");
|
||||
let body = self.get("persons").await?;
|
||||
let persons: Vec<Person> = serde_json::from_str(&body)?;
|
||||
Ok(persons)
|
||||
}
|
||||
|
||||
/// Post a new person to the server.
|
||||
pub async fn post_person(&self, data: &Person) -> Result<()> {
|
||||
info!("Post person {:?}", data);
|
||||
self.post("persons", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use log::info;
|
||||
use musicus_database::Recording;
|
||||
|
||||
impl Client {
|
||||
/// Get all available recordings from the server.
|
||||
pub async fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> {
|
||||
info!("Get recordings for work {}", work_id);
|
||||
let body = self.get(&format!("works/{}/recordings", work_id)).await?;
|
||||
let recordings: Vec<Recording> = serde_json::from_str(&body)?;
|
||||
Ok(recordings)
|
||||
}
|
||||
|
||||
/// Post a new recording to the server.
|
||||
pub async fn post_recording(&self, data: &Recording) -> Result<()> {
|
||||
info!("Post recording {:?}", data);
|
||||
self.post("recordings", serde_json::to_string(data)?)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use isahc::http::StatusCode;
|
||||
use isahc::prelude::*;
|
||||
use isahc::Request;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Response body data for captcha requests.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Captcha {
|
||||
pub id: String,
|
||||
pub question: String,
|
||||
}
|
||||
|
||||
/// Request body data for user registration.
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserRegistration {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub email: Option<String>,
|
||||
pub captcha_id: String,
|
||||
pub answer: String,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Request a new captcha for registration.
|
||||
pub async fn get_captcha(&self) -> Result<Captcha> {
|
||||
info!("Get captcha");
|
||||
let body = self.get("captcha").await?;
|
||||
let captcha = serde_json::from_str(&body)?;
|
||||
Ok(captcha)
|
||||
}
|
||||
|
||||
/// Register a new user and return whether the process suceeded. This will
|
||||
/// not store the new login credentials.
|
||||
pub async fn register(&self, data: UserRegistration) -> Result<bool> {
|
||||
// Make sure to not log the password accidentally!
|
||||
info!("Register user '{}'", data.username);
|
||||
info!("Captcha ID: {}", data.captcha_id);
|
||||
info!("Captcha answer: {}", data.answer);
|
||||
|
||||
let server_url = self.server_url()?;
|
||||
|
||||
let response = Request::post(format!("{}/users", server_url))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_string(&data)?)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
Ok(response.status() == StatusCode::OK)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
use crate::{Client, Result};
|
||||
use log::info;
|
||||
use musicus_database::Work;
|
||||
|
||||
impl Client {
|
||||
/// Get all available works from the server.
|
||||
pub async fn get_works(&self, composer_id: &str) -> Result<Vec<Work>> {
|
||||
info!("Get works by composer {}", composer_id);
|
||||
let body = self.get(&format!("persons/{}/works", composer_id)).await?;
|
||||
let works: Vec<Work> = serde_json::from_str(&body)?;
|
||||
Ok(works)
|
||||
}
|
||||
|
||||
/// Post a new work to the server.
|
||||
pub async fn post_work(&self, data: &Work) -> Result<()> {
|
||||
info!("Post work {:?}", data);
|
||||
self.post("works", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,36 @@
|
|||
{
|
||||
"app-id" : "de.johrpan.musicus",
|
||||
"runtime" : "org.gnome.Platform",
|
||||
"runtime-version" : "master",
|
||||
"sdk" : "org.gnome.Sdk",
|
||||
"sdk-extensions" : [
|
||||
"app-id": "de.johrpan.musicus",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "master",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"sdk-extensions": [
|
||||
"org.freedesktop.Sdk.Extension.rust-stable"
|
||||
],
|
||||
"command" : "musicus",
|
||||
"finish-args" : [
|
||||
"command": "musicus",
|
||||
"finish-args": [
|
||||
"--share=network",
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--socket=pulseaudio",
|
||||
"--filesystem=host",
|
||||
"--talk-name=org.freedesktop.secrets",
|
||||
"--talk-name=org.mpris.MediaPlayer2.Player",
|
||||
"--talk-name=org.mpris.MediaPlayer2.Player",
|
||||
"--own-name=org.mpris.MediaPlayer2.de.johrpan.musicus",
|
||||
"--device=all"
|
||||
],
|
||||
"build-options" : {
|
||||
"append-path" : "/usr/lib/sdk/rust-stable/bin",
|
||||
"build-args" : [
|
||||
"build-options": {
|
||||
"append-path": "/usr/lib/sdk/rust-stable/bin",
|
||||
"build-args": [
|
||||
"--share=network"
|
||||
],
|
||||
"env" : {
|
||||
"RUSTFLAGS" : "-L=/app/lib",
|
||||
"CARGO_HOME" : "/run/build/musicus/cargo",
|
||||
"RUST_BACKTRACE" : "1",
|
||||
"RUST_LOG" : "musicus=debug"
|
||||
"env": {
|
||||
"RUSTFLAGS": "-L=/app/lib",
|
||||
"CARGO_HOME": "/run/build/musicus/cargo",
|
||||
"RUST_BACKTRACE": "1",
|
||||
"RUST_LOG": "musicus=debug"
|
||||
}
|
||||
},
|
||||
"cleanup" : [
|
||||
"cleanup": [
|
||||
"/include",
|
||||
"/lib/pkgconfig",
|
||||
"/man",
|
||||
|
|
@ -42,8 +41,7 @@
|
|||
"*.la",
|
||||
"*.a"
|
||||
],
|
||||
"modules" : [
|
||||
{
|
||||
"modules": [{
|
||||
"name": "cdparanoia",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
|
|
@ -52,13 +50,11 @@
|
|||
"make all slib",
|
||||
"make install"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "http://downloads.xiph.org/releases/cdparanoia/cdparanoia-III-10.2.src.tgz",
|
||||
"sha256": "005db45ef4ee017f5c32ec124f913a0546e77014266c6a1c50df902a55fe64df"
|
||||
}
|
||||
]
|
||||
"sources": [{
|
||||
"type": "archive",
|
||||
"url": "http://downloads.xiph.org/releases/cdparanoia/cdparanoia-III-10.2.src.tgz",
|
||||
"sha256": "005db45ef4ee017f5c32ec124f913a0546e77014266c6a1c50df902a55fe64df"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "gst-plugins-base",
|
||||
|
|
@ -68,26 +64,22 @@
|
|||
"-Dauto_features=disabled",
|
||||
"-Dcdparanoia=enabled"
|
||||
],
|
||||
"cleanup": [ "*.la", "/share/gtk-doc" ],
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git",
|
||||
"branch" : "1.16.2",
|
||||
"commit" : "9d3581b2e6f12f0b7e790d1ebb63b90cf5b1ef4e"
|
||||
}
|
||||
]
|
||||
"cleanup": ["*.la", "/share/gtk-doc"],
|
||||
"sources": [{
|
||||
"type": "git",
|
||||
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git",
|
||||
"branch": "1.16.2",
|
||||
"commit": "9d3581b2e6f12f0b7e790d1ebb63b90cf5b1ef4e"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name" : "musicus",
|
||||
"builddir" : true,
|
||||
"buildsystem" : "meson",
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"url" : "."
|
||||
}
|
||||
]
|
||||
"name": "musicus",
|
||||
"builddir": true,
|
||||
"buildsystem": "meson",
|
||||
"sources": [{
|
||||
"type": "git",
|
||||
"url": "."
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -5,17 +5,5 @@
|
|||
<default>""</default>
|
||||
<summary>Path to the music library folder</summary>
|
||||
</key>
|
||||
<key name="server-url" type="s">
|
||||
<default>"https://wolfgang.johrpan.de"</default>
|
||||
<summary>URL of the Wolfgang server to use</summary>
|
||||
</key>
|
||||
<key name="use-server" type="b">
|
||||
<default>true</default>
|
||||
<summary>Whether to use the Wolfgang server</summary>
|
||||
<description>
|
||||
This setting determines whether the Wolfgang server will be used for
|
||||
finding new items as well as to upload new additions and edits.
|
||||
</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
<gresource prefix="/de/johrpan/musicus">
|
||||
<file preprocess="xml-stripblanks">ui/editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/import_screen.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/main_screen.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/medium_editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/medium_preview.ui</file>
|
||||
|
|
@ -16,7 +15,6 @@
|
|||
<file preprocess="xml-stripblanks">ui/screen.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/section.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/selector.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/server_dialog.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/source_selector.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/track_editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/track_selector.ui</file>
|
||||
|
|
|
|||
|
|
@ -45,12 +45,6 @@
|
|||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="server_check_button">
|
||||
<property name="label" translatable="yes">Use the Musicus server</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
|
|||
|
|
@ -1,223 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="GtkStack" id="widget">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="login_button">
|
||||
<property name="label" translatable="yes">Login</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="revealed">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="maximum-size">800</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Login to existing account</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Username</property>
|
||||
<property name="activatable-widget">username_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="username_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Password</property>
|
||||
<property name="activatable-widget">password_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="password_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="input-purpose">password</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="register_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Create a new account</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Register a new account</property>
|
||||
<property name="activatable-widget">register_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="register_button">
|
||||
<property name="label" translatable="yes">Start</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="logout_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<property name="visible">false</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Logout</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Don't use an account</property>
|
||||
<property name="activatable-widget">logout_button</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="logout_button">
|
||||
<property name="label" translatable="yes">Logout</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Login</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="spinning">true</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -82,19 +82,6 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Publish to the server</property>
|
||||
<property name="activatable-widget">publish_switch</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="publish_switch">
|
||||
<property name="valign">center</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
|||
|
|
@ -29,49 +29,7 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Server connection</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="url_row">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Server URL</property>
|
||||
<property name="activatable-widget">url_button</property>
|
||||
<property name="subtitle" translatable="yes">Not set</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="url_button">
|
||||
<property name="label" translatable="yes">Change</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="login_row">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Login credentials</property>
|
||||
<property name="activatable-widget">login_button</property>
|
||||
<property name="subtitle" translatable="yes">Not logged in</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="login_button">
|
||||
<property name="label" translatable="yes">Change</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkSizeGroup">
|
||||
<widgets>
|
||||
<widget name="select_music_library_path_button"/>
|
||||
<widget name="url_button"/>
|
||||
<widget name="login_button"/>
|
||||
</widgets>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -99,19 +99,6 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Publish to the server</property>
|
||||
<property name="activatable-widget">upload_switch</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="upload_switch">
|
||||
<property name="valign">center</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
|||
|
|
@ -60,13 +60,6 @@
|
|||
<property name="placeholder-text" translatable="yes">Search …</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="server_check_button">
|
||||
<property name="label" translatable="yes">Use the Musicus server</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -118,57 +111,6 @@
|
|||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">error</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin-start">18</property>
|
||||
<property name="margin-end">18</property>
|
||||
<property name="margin-top">18</property>
|
||||
<property name="margin-bottom">18</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">18</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="opacity">0.5</property>
|
||||
<property name="pixel-size">80</property>
|
||||
<property name="icon-name">network-error-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="opacity">0.5</property>
|
||||
<property name="label" translatable="yes">An error occured!</property>
|
||||
<attributes>
|
||||
<attribute name="size" value="16384"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="opacity">0.5</property>
|
||||
<property name="label" translatable="yes">The server was not reachable or responded with an error. Please check your internet connection.</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max-width-chars">40</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="try_again_button">
|
||||
<property name="label" translatable="yes">Try again</property>
|
||||
<property name="halign">center</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<object class="AdwWindow" id="window">
|
||||
<property name="modal">True</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-start-title-buttons">false</property>
|
||||
<property name="show-end-title-buttons">false</property>
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Server</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="set_button">
|
||||
<property name="label" translatable="yes">Set</property>
|
||||
<property name="has-default">True</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="selection-mode">none</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">URL</property>
|
||||
<property name="activatable-widget">url_entry</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<property name="valign">center</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<requires lib="gtk" version="4.0" />
|
||||
<requires lib="libadwaita" version="1.0" />
|
||||
<object class="GtkStack" id="widget">
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Work</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
<class name="title" />
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<property name="sensitive">False</property>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="suggested-action" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
<property name="margin-bottom">6</property>
|
||||
<property name="label" translatable="yes">Overview</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="weight" value="bold" />
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
|
|
@ -99,19 +99,6 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="focusable">False</property>
|
||||
<property name="title" translatable="yes">Publish to the server</property>
|
||||
<property name="activatable-widget">upload_switch</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="upload_switch">
|
||||
<property name="valign">center</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -128,7 +115,7 @@
|
|||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Instruments</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="weight" value="bold" />
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
|
|
@ -141,7 +128,7 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="instrument_frame"/>
|
||||
<object class="GtkFrame" id="instrument_frame" />
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
|
|
@ -155,7 +142,7 @@
|
|||
<property name="hexpand">True</property>
|
||||
<property name="label" translatable="yes">Structure</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="weight" value="bold" />
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
|
|
@ -174,7 +161,7 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="structure_frame"/>
|
||||
<object class="GtkFrame" id="structure_frame" />
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
|
@ -200,7 +187,7 @@
|
|||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Work</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
<class name="title" />
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
|
||||
use crate::widgets::{Editor, EntryRow, Section, Widget};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
|
|
@ -16,7 +16,6 @@ pub struct EnsembleEditor {
|
|||
|
||||
editor: Editor,
|
||||
name: EntryRow,
|
||||
upload: Rc<UploadSection>,
|
||||
}
|
||||
|
||||
impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
|
||||
|
|
@ -33,10 +32,7 @@ impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
|
|||
list.append(&name.widget);
|
||||
|
||||
let section = Section::new(&gettext("General"), &list);
|
||||
let upload = UploadSection::new(Rc::clone(&handle.backend));
|
||||
|
||||
editor.add_content(§ion.widget);
|
||||
editor.add_content(&upload.widget);
|
||||
|
||||
let id = match ensemble {
|
||||
Some(ensemble) => {
|
||||
|
|
@ -51,7 +47,6 @@ impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
|
|||
id,
|
||||
editor,
|
||||
name,
|
||||
upload,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
|
@ -91,7 +86,7 @@ impl EnsembleEditor {
|
|||
self.editor.set_may_save(!self.name.get_text().is_empty());
|
||||
}
|
||||
|
||||
/// Save the ensemble and possibly upload it to the server.
|
||||
/// Save the ensemble.
|
||||
async fn save(&self) -> Result<Ensemble> {
|
||||
let name = self.name.get_text();
|
||||
|
||||
|
|
@ -100,15 +95,12 @@ impl EnsembleEditor {
|
|||
name,
|
||||
};
|
||||
|
||||
if self.upload.get_active() {
|
||||
self.handle.backend.cl().post_ensemble(&ensemble).await?;
|
||||
}
|
||||
|
||||
self.handle
|
||||
.backend
|
||||
.db()
|
||||
.update_ensemble(ensemble.clone())
|
||||
.await?;
|
||||
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(ensemble)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
|
||||
use crate::widgets::{Editor, EntryRow, Section, Widget};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
|
|
@ -16,7 +16,6 @@ pub struct InstrumentEditor {
|
|||
|
||||
editor: Editor,
|
||||
name: EntryRow,
|
||||
upload: Rc<UploadSection>,
|
||||
}
|
||||
|
||||
impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
|
||||
|
|
@ -33,10 +32,7 @@ impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
|
|||
list.append(&name.widget);
|
||||
|
||||
let section = Section::new(&gettext("General"), &list);
|
||||
let upload = UploadSection::new(Rc::clone(&handle.backend));
|
||||
|
||||
editor.add_content(§ion.widget);
|
||||
editor.add_content(&upload.widget);
|
||||
|
||||
let id = match instrument {
|
||||
Some(instrument) => {
|
||||
|
|
@ -51,7 +47,6 @@ impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
|
|||
id,
|
||||
editor,
|
||||
name,
|
||||
upload,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
|
@ -91,7 +86,7 @@ impl InstrumentEditor {
|
|||
self.editor.set_may_save(!self.name.get_text().is_empty());
|
||||
}
|
||||
|
||||
/// Save the instrument and possibly upload it to the server.
|
||||
/// Save the instrument.
|
||||
async fn save(&self) -> Result<Instrument> {
|
||||
let name = self.name.get_text();
|
||||
|
||||
|
|
@ -100,19 +95,12 @@ impl InstrumentEditor {
|
|||
name,
|
||||
};
|
||||
|
||||
if self.upload.get_active() {
|
||||
self.handle
|
||||
.backend
|
||||
.cl()
|
||||
.post_instrument(&instrument)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.handle
|
||||
.backend
|
||||
.db()
|
||||
.update_instrument(instrument.clone())
|
||||
.await?;
|
||||
|
||||
self.handle.backend.library_changed();
|
||||
|
||||
Ok(instrument)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget};
|
||||
use crate::widgets::{Editor, EntryRow, Section, Widget};
|
||||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
|
|
@ -17,7 +17,6 @@ pub struct PersonEditor {
|
|||
editor: Editor,
|
||||
first_name: EntryRow,
|
||||
last_name: EntryRow,
|
||||
upload: Rc<UploadSection>,
|
||||
}
|
||||
|
||||
impl Screen<Option<Person>, Person> for PersonEditor {
|
||||
|
|
@ -37,10 +36,7 @@ impl Screen<Option<Person>, Person> for PersonEditor {
|
|||
list.append(&last_name.widget);
|
||||
|
||||
let section = Section::new(&gettext("General"), &list);
|
||||
let upload = UploadSection::new(Rc::clone(&handle.backend));
|
||||
|
||||
editor.add_content(§ion.widget);
|
||||
editor.add_content(&upload.widget);
|
||||
|
||||
let id = match person {
|
||||
Some(person) => {
|
||||
|
|
@ -58,7 +54,6 @@ impl Screen<Option<Person>, Person> for PersonEditor {
|
|||
editor,
|
||||
first_name,
|
||||
last_name,
|
||||
upload,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
|
@ -104,7 +99,7 @@ impl PersonEditor {
|
|||
);
|
||||
}
|
||||
|
||||
/// Save the person and possibly upload it to the server.
|
||||
/// Save the person.
|
||||
async fn save(self: &Rc<Self>) -> Result<Person> {
|
||||
let first_name = self.first_name.get_text();
|
||||
let last_name = self.last_name.get_text();
|
||||
|
|
@ -115,10 +110,6 @@ impl PersonEditor {
|
|||
last_name,
|
||||
};
|
||||
|
||||
if self.upload.get_active() {
|
||||
self.handle.backend.cl().post_person(&person).await?;
|
||||
}
|
||||
|
||||
self.handle
|
||||
.backend
|
||||
.db()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ pub struct RecordingEditor {
|
|||
info_bar: gtk::InfoBar,
|
||||
work_row: adw::ActionRow,
|
||||
comment_entry: gtk::Entry,
|
||||
upload_switch: gtk::Switch,
|
||||
performance_list: Rc<List>,
|
||||
id: String,
|
||||
work: RefCell<Option<Work>>,
|
||||
|
|
@ -40,12 +39,9 @@ impl Screen<Option<Recording>, Recording> for RecordingEditor {
|
|||
get_widget!(builder, adw::ActionRow, work_row);
|
||||
get_widget!(builder, gtk::Button, work_button);
|
||||
get_widget!(builder, gtk::Entry, comment_entry);
|
||||
get_widget!(builder, gtk::Switch, upload_switch);
|
||||
get_widget!(builder, gtk::Frame, performance_frame);
|
||||
get_widget!(builder, gtk::Button, add_performer_button);
|
||||
|
||||
upload_switch.set_active(handle.backend.use_server());
|
||||
|
||||
let performance_list = List::new();
|
||||
performance_frame.set_child(Some(&performance_list.widget));
|
||||
|
||||
|
|
@ -64,7 +60,6 @@ impl Screen<Option<Recording>, Recording> for RecordingEditor {
|
|||
info_bar,
|
||||
work_row,
|
||||
comment_entry,
|
||||
upload_switch,
|
||||
performance_list,
|
||||
id,
|
||||
work: RefCell::new(work),
|
||||
|
|
@ -183,7 +178,7 @@ impl RecordingEditor {
|
|||
self.save_button.set_sensitive(true);
|
||||
}
|
||||
|
||||
/// Save the recording and possibly upload it to the server.
|
||||
/// Save the recording.
|
||||
async fn save(self: &Rc<Self>) -> Result<Recording> {
|
||||
let recording = Recording {
|
||||
id: self.id.clone(),
|
||||
|
|
@ -196,11 +191,6 @@ impl RecordingEditor {
|
|||
performances: self.performances.borrow().clone(),
|
||||
};
|
||||
|
||||
let upload = self.upload_switch.state();
|
||||
if upload {
|
||||
self.handle.backend.cl().post_recording(&recording).await?;
|
||||
}
|
||||
|
||||
self.handle
|
||||
.backend
|
||||
.db()
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ pub struct WorkEditor {
|
|||
title_entry: gtk::Entry,
|
||||
info_bar: gtk::InfoBar,
|
||||
composer_row: adw::ActionRow,
|
||||
upload_switch: gtk::Switch,
|
||||
instrument_list: Rc<List>,
|
||||
part_list: Rc<List>,
|
||||
id: String,
|
||||
|
|
@ -59,7 +58,6 @@ impl Screen<Option<Work>, Work> for WorkEditor {
|
|||
get_widget!(builder, gtk::Entry, title_entry);
|
||||
get_widget!(builder, gtk::Button, composer_button);
|
||||
get_widget!(builder, adw::ActionRow, composer_row);
|
||||
get_widget!(builder, gtk::Switch, upload_switch);
|
||||
get_widget!(builder, gtk::Frame, instrument_frame);
|
||||
get_widget!(builder, gtk::Button, add_instrument_button);
|
||||
get_widget!(builder, gtk::Frame, structure_frame);
|
||||
|
|
@ -92,8 +90,6 @@ impl Screen<Option<Work>, Work> for WorkEditor {
|
|||
None => (generate_id(), None, Vec::new(), Vec::new()),
|
||||
};
|
||||
|
||||
upload_switch.set_active(handle.backend.use_server());
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
|
|
@ -102,7 +98,6 @@ impl Screen<Option<Work>, Work> for WorkEditor {
|
|||
info_bar,
|
||||
title_entry,
|
||||
composer_row,
|
||||
upload_switch,
|
||||
instrument_list,
|
||||
part_list,
|
||||
composer: RefCell::new(composer),
|
||||
|
|
@ -317,7 +312,7 @@ impl WorkEditor {
|
|||
.set_sensitive(!self.title_entry.text().is_empty() && self.composer.borrow().is_some());
|
||||
}
|
||||
|
||||
/// Save the work and possibly upload it to the server.
|
||||
/// Save the work.
|
||||
async fn save(self: &Rc<Self>) -> Result<Work> {
|
||||
let mut section_count: usize = 0;
|
||||
let mut parts = Vec::new();
|
||||
|
|
@ -348,11 +343,6 @@ impl WorkEditor {
|
|||
sections,
|
||||
};
|
||||
|
||||
let upload = self.upload_switch.state();
|
||||
if upload {
|
||||
self.handle.backend.cl().post_work(&work).await?;
|
||||
}
|
||||
|
||||
self.handle
|
||||
.backend
|
||||
.db()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use glib::clone;
|
|||
use gtk_macros::get_widget;
|
||||
use musicus_backend::db::Medium;
|
||||
use musicus_backend::import::ImportSession;
|
||||
use musicus_backend::Error;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -17,24 +16,19 @@ pub struct ImportScreen {
|
|||
handle: NavigationHandle<()>,
|
||||
session: Arc<ImportSession>,
|
||||
widget: gtk::Box,
|
||||
server_check_button: gtk::CheckButton,
|
||||
matching_stack: gtk::Stack,
|
||||
error_row: adw::ActionRow,
|
||||
matching_list: gtk::ListBox,
|
||||
}
|
||||
|
||||
impl ImportScreen {
|
||||
/// Find matching mediums on the server.
|
||||
/// Find matching mediums in the library.
|
||||
fn load_matches(self: &Rc<Self>) {
|
||||
self.matching_stack.set_visible_child_name("loading");
|
||||
|
||||
let this = self;
|
||||
spawn!(@clone this, async move {
|
||||
let mediums: Result<Vec<Medium>, Error> = if this.server_check_button.is_active() {
|
||||
this.handle.backend.cl().get_mediums_by_discid(this.session.source_id()).await.map_err(|err| err.into())
|
||||
} else {
|
||||
this.handle.backend.db().get_mediums_by_source_id(this.session.source_id()).await.map_err(|err| err.into())
|
||||
};
|
||||
let mediums = this.handle.backend.db().get_mediums_by_source_id(this.session.source_id()).await;
|
||||
|
||||
match mediums {
|
||||
Ok(mediums) => {
|
||||
|
|
@ -113,18 +107,14 @@ impl Screen<Arc<ImportSession>, ()> for ImportScreen {
|
|||
get_widget!(builder, gtk::Stack, matching_stack);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
get_widget!(builder, adw::ActionRow, error_row);
|
||||
get_widget!(builder, gtk::CheckButton, server_check_button);
|
||||
get_widget!(builder, gtk::ListBox, matching_list);
|
||||
get_widget!(builder, gtk::Button, select_button);
|
||||
get_widget!(builder, gtk::Button, add_button);
|
||||
|
||||
server_check_button.set_active(handle.backend.use_server());
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
session,
|
||||
widget,
|
||||
server_check_button,
|
||||
matching_stack,
|
||||
error_row,
|
||||
matching_list,
|
||||
|
|
@ -136,12 +126,6 @@ impl Screen<Arc<ImportSession>, ()> for ImportScreen {
|
|||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.server_check_button
|
||||
.connect_toggled(clone!(@weak this => move |_| {
|
||||
this.handle.backend.set_use_server(this.server_check_button.is_active());
|
||||
this.load_matches();
|
||||
}));
|
||||
|
||||
try_again_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.load_matches();
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ pub struct MediumEditor {
|
|||
widget: gtk::Stack,
|
||||
done_button: gtk::Button,
|
||||
name_entry: gtk::Entry,
|
||||
publish_switch: gtk::Switch,
|
||||
status_page: adw::StatusPage,
|
||||
track_set_list: Rc<List>,
|
||||
track_sets: RefCell<Vec<TrackSetData>>,
|
||||
|
|
@ -38,15 +37,12 @@ impl Screen<(Arc<ImportSession>, Option<Medium>), Medium> for MediumEditor {
|
|||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Button, done_button);
|
||||
get_widget!(builder, gtk::Entry, name_entry);
|
||||
get_widget!(builder, gtk::Switch, publish_switch);
|
||||
get_widget!(builder, gtk::Button, add_button);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
get_widget!(builder, adw::StatusPage, status_page);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
get_widget!(builder, gtk::Button, cancel_button);
|
||||
|
||||
publish_switch.set_active(handle.backend.use_server());
|
||||
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
|
|
@ -56,7 +52,6 @@ impl Screen<(Arc<ImportSession>, Option<Medium>), Medium> for MediumEditor {
|
|||
widget,
|
||||
done_button,
|
||||
name_entry,
|
||||
publish_switch,
|
||||
status_page,
|
||||
track_set_list: list,
|
||||
track_sets: RefCell::new(Vec::new()),
|
||||
|
|
@ -100,11 +95,6 @@ impl Screen<(Arc<ImportSession>, Option<Medium>), Medium> for MediumEditor {
|
|||
});
|
||||
}));
|
||||
|
||||
this.publish_switch
|
||||
.connect_state_notify(clone!(@weak this => move |_| {
|
||||
this.handle.backend.set_use_server(this.publish_switch.state());
|
||||
}));
|
||||
|
||||
this.track_set_list.set_make_widget_cb(
|
||||
clone!(@weak this => @default-panic, move |index| {
|
||||
let track_set = &this.track_sets.borrow()[index];
|
||||
|
|
@ -188,7 +178,7 @@ impl MediumEditor {
|
|||
);
|
||||
}
|
||||
|
||||
/// Create the medium and, if necessary, upload it to the server.
|
||||
/// Create the medium.
|
||||
async fn save(&self) -> Result<Medium> {
|
||||
// Convert the track set data to real track sets.
|
||||
|
||||
|
|
@ -214,11 +204,6 @@ impl MediumEditor {
|
|||
tracks,
|
||||
};
|
||||
|
||||
let upload = self.publish_switch.state();
|
||||
if upload {
|
||||
self.handle.backend.cl().post_medium(&medium).await?;
|
||||
}
|
||||
|
||||
// The medium is not added to the database, because the track paths are not known until the
|
||||
// medium is actually imported into the music library. This step will be handled by the
|
||||
// medium preview dialog.
|
||||
|
|
|
|||
|
|
@ -29,10 +29,4 @@ impl NavigatorWindow {
|
|||
|
||||
this
|
||||
}
|
||||
|
||||
/// Make the wrapped window transient. This will make the window modal.
|
||||
pub fn set_transient_for<W: IsA<gtk::Window>>(&self, window: &W) {
|
||||
self.window.set_modal(true);
|
||||
self.window.set_transient_for(Some(window));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::navigator::NavigatorWindow;
|
||||
use adw::prelude::*;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
|
|
@ -6,21 +5,11 @@ use gtk_macros::get_widget;
|
|||
use musicus_backend::Backend;
|
||||
use std::rc::Rc;
|
||||
|
||||
mod login;
|
||||
use login::LoginDialog;
|
||||
|
||||
mod server;
|
||||
use server::ServerDialog;
|
||||
|
||||
mod register;
|
||||
|
||||
/// A dialog for configuring the app.
|
||||
pub struct Preferences {
|
||||
backend: Rc<Backend>,
|
||||
window: adw::Window,
|
||||
music_library_path_row: adw::ActionRow,
|
||||
url_row: adw::ActionRow,
|
||||
login_row: adw::ActionRow,
|
||||
}
|
||||
|
||||
impl Preferences {
|
||||
|
|
@ -32,10 +21,6 @@ impl Preferences {
|
|||
get_widget!(builder, adw::Window, window);
|
||||
get_widget!(builder, adw::ActionRow, music_library_path_row);
|
||||
get_widget!(builder, gtk::Button, select_music_library_path_button);
|
||||
get_widget!(builder, adw::ActionRow, url_row);
|
||||
get_widget!(builder, gtk::Button, url_button);
|
||||
get_widget!(builder, adw::ActionRow, login_row);
|
||||
get_widget!(builder, gtk::Button, login_button);
|
||||
|
||||
window.set_transient_for(Some(parent));
|
||||
|
||||
|
|
@ -43,8 +28,6 @@ impl Preferences {
|
|||
backend,
|
||||
window,
|
||||
music_library_path_row,
|
||||
url_row,
|
||||
login_row,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
|
@ -80,31 +63,6 @@ impl Preferences {
|
|||
dialog.show();
|
||||
}));
|
||||
|
||||
url_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let dialog = ServerDialog::new(this.backend.clone(), &this.window);
|
||||
|
||||
dialog.set_selected_cb(clone!(@strong this => move |url| {
|
||||
this.url_row.set_subtitle(&url);
|
||||
}));
|
||||
|
||||
dialog.show();
|
||||
}));
|
||||
|
||||
login_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
let window = NavigatorWindow::new(this.backend.clone());
|
||||
window.set_transient_for(&this.window);
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(data) = replace!(window.navigator, LoginDialog, this.backend.get_login_data()).await {
|
||||
if let Some(data) = data {
|
||||
this.login_row.set_subtitle(&data.username);
|
||||
} else {
|
||||
this.login_row.set_subtitle(&gettext("Not logged in"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// Initialize
|
||||
|
||||
if let Some(path) = this.backend.get_music_library_path() {
|
||||
|
|
@ -112,14 +70,6 @@ impl Preferences {
|
|||
.set_subtitle(path.to_str().unwrap());
|
||||
}
|
||||
|
||||
if let Some(url) = this.backend.get_server_url() {
|
||||
this.url_row.set_subtitle(&url);
|
||||
}
|
||||
|
||||
if let Some(data) = this.backend.get_login_data() {
|
||||
this.login_row.set_subtitle(&data.username);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
use super::register::RegisterDialog;
|
||||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::push;
|
||||
use crate::widgets::Widget;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::client::LoginData;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for entering login credentials.
|
||||
pub struct LoginDialog {
|
||||
handle: NavigationHandle<Option<LoginData>>,
|
||||
widget: gtk::Stack,
|
||||
info_bar: gtk::InfoBar,
|
||||
username_entry: gtk::Entry,
|
||||
password_entry: gtk::Entry,
|
||||
}
|
||||
|
||||
impl Screen<Option<LoginData>, Option<LoginData>> for LoginDialog {
|
||||
fn new(data: Option<LoginData>, handle: NavigationHandle<Option<LoginData>>) -> Rc<Self> {
|
||||
// Create UI
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/login_dialog.ui");
|
||||
|
||||
get_widget!(builder, gtk::Stack, widget);
|
||||
get_widget!(builder, gtk::InfoBar, info_bar);
|
||||
get_widget!(builder, gtk::Button, cancel_button);
|
||||
get_widget!(builder, gtk::Button, login_button);
|
||||
get_widget!(builder, gtk::Entry, username_entry);
|
||||
get_widget!(builder, gtk::Entry, password_entry);
|
||||
get_widget!(builder, gtk::Box, register_box);
|
||||
get_widget!(builder, gtk::Button, register_button);
|
||||
get_widget!(builder, gtk::Box, logout_box);
|
||||
get_widget!(builder, gtk::Button, logout_button);
|
||||
|
||||
if let Some(data) = data {
|
||||
username_entry.set_text(&data.username);
|
||||
register_box.hide();
|
||||
logout_box.show();
|
||||
}
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
info_bar,
|
||||
username_entry,
|
||||
password_entry,
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
cancel_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
login_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
|
||||
let data = LoginData {
|
||||
username: this.username_entry.text().to_string(),
|
||||
password: this.password_entry.text().to_string(),
|
||||
};
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.set_login_data(Some(data.clone())).await;
|
||||
if this.handle.backend.cl().login().await.unwrap() {
|
||||
this.handle.pop(Some(Some(data)));
|
||||
} else {
|
||||
this.widget.set_visible_child_name("content");
|
||||
this.info_bar.set_revealed(true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
register_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
if let Some(data) = push!(this.handle, RegisterDialog).await {
|
||||
this.handle.pop(Some(Some(data)));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
logout_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
spawn!(@clone this, async move {
|
||||
this.handle.backend.set_login_data(None).await;
|
||||
this.handle.pop(Some(None));
|
||||
});
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for LoginDialog {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
use crate::navigator::{NavigationHandle, Screen};
|
||||
use crate::widgets::Widget;
|
||||
use adw::prelude::*;
|
||||
use glib::clone;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::client::{LoginData, UserRegistration};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for creating a new user account.
|
||||
pub struct RegisterDialog {
|
||||
handle: NavigationHandle<LoginData>,
|
||||
widget: gtk::Stack,
|
||||
username_entry: gtk::Entry,
|
||||
email_entry: gtk::Entry,
|
||||
password_entry: gtk::Entry,
|
||||
repeat_password_entry: gtk::Entry,
|
||||
captcha_row: adw::ActionRow,
|
||||
captcha_entry: gtk::Entry,
|
||||
captcha_id: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl Screen<(), LoginData> for RegisterDialog {
|
||||
/// Create a new register dialog.
|
||||
fn new(_: (), handle: NavigationHandle<LoginData>) -> Rc<Self> {
|
||||
// Create UI
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/register_dialog.ui");
|
||||
|
||||
get_widget!(builder, gtk::Stack, widget);
|
||||
get_widget!(builder, gtk::Button, cancel_button);
|
||||
get_widget!(builder, gtk::Button, register_button);
|
||||
get_widget!(builder, gtk::Entry, username_entry);
|
||||
get_widget!(builder, gtk::Entry, email_entry);
|
||||
get_widget!(builder, gtk::Entry, password_entry);
|
||||
get_widget!(builder, gtk::Entry, repeat_password_entry);
|
||||
get_widget!(builder, adw::ActionRow, captcha_row);
|
||||
get_widget!(builder, gtk::Entry, captcha_entry);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
handle,
|
||||
widget,
|
||||
username_entry,
|
||||
email_entry,
|
||||
password_entry,
|
||||
repeat_password_entry,
|
||||
captcha_row,
|
||||
captcha_entry,
|
||||
captcha_id: RefCell::new(None),
|
||||
});
|
||||
|
||||
// Connect signals and callbacks
|
||||
|
||||
cancel_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
register_button.connect_clicked(clone!(@weak this => move |_| {
|
||||
let password = this.password_entry.text().to_string();
|
||||
let repeat = this.repeat_password_entry.text().to_string();
|
||||
|
||||
if password != repeat {
|
||||
// TODO: Show error and validate other input.
|
||||
} else {
|
||||
this.widget.set_visible_child_name("loading");
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let username = this.username_entry.text().to_string();
|
||||
let email = this.email_entry.text().to_string();
|
||||
let captcha_id = this.captcha_id.borrow().clone().unwrap();
|
||||
let answer = this.captcha_entry.text().to_string();
|
||||
|
||||
let email = if email.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(email)
|
||||
};
|
||||
|
||||
let registration = UserRegistration {
|
||||
username: username.clone(),
|
||||
password: password.clone(),
|
||||
email,
|
||||
captcha_id,
|
||||
answer,
|
||||
};
|
||||
|
||||
// TODO: Handle errors.
|
||||
if this.handle.backend.cl().register(registration).await.unwrap() {
|
||||
let data = LoginData {
|
||||
username,
|
||||
password,
|
||||
};
|
||||
|
||||
this.handle.pop(Some(data));
|
||||
} else {
|
||||
this.widget.set_visible_child_name("content");
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Initialize
|
||||
|
||||
spawn!(@clone this, async move {
|
||||
let captcha = this.handle.backend.cl().get_captcha().await.unwrap();
|
||||
this.captcha_row.set_title(&captcha.question);
|
||||
this.captcha_id.replace(Some(captcha.id));
|
||||
this.widget.set_visible_child_name("content");
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RegisterDialog {
|
||||
fn get_widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::Backend;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A dialog for setting up the server.
|
||||
pub struct ServerDialog {
|
||||
backend: Rc<Backend>,
|
||||
window: adw::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, adw::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.text().to_string();
|
||||
this.backend.set_server_url(&url);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ impl Screen<(), Ensemble> for EnsembleSelector {
|
|||
fn new(_: (), handle: NavigationHandle<Ensemble>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Ensemble>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Ensemble>::new();
|
||||
selector.set_title(&gettext("Select ensemble"));
|
||||
|
||||
let this = Rc::new(Self { handle, selector });
|
||||
|
|
@ -38,12 +38,6 @@ impl Screen<(), Ensemble> for EnsembleSelector {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
let clone = this;
|
||||
async move { Ok(clone.handle.backend.cl().get_ensembles().await?) }
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
let clone = this;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ impl Screen<(), Instrument> for InstrumentSelector {
|
|||
fn new(_: (), handle: NavigationHandle<Instrument>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Instrument>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Instrument>::new();
|
||||
selector.set_title(&gettext("Select instrument"));
|
||||
|
||||
let this = Rc::new(Self { handle, selector });
|
||||
|
|
@ -38,12 +38,6 @@ impl Screen<(), Instrument> for InstrumentSelector {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
let clone = this;
|
||||
async move { Ok(clone.handle.backend.cl().get_instruments().await?) }
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
let clone = this;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ impl Screen<(), Medium> for MediumSelector {
|
|||
fn new(_: (), handle: NavigationHandle<Medium>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<PersonOrEnsemble>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<PersonOrEnsemble>::new();
|
||||
selector.set_title(&gettext("Select performer"));
|
||||
|
||||
let this = Rc::new(Self { handle, selector });
|
||||
|
|
@ -28,26 +28,6 @@ impl Screen<(), Medium> for MediumSelector {
|
|||
this.handle.pop(None);
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
async move {
|
||||
let mut poes = Vec::new();
|
||||
|
||||
let persons = this.handle.backend.cl().get_persons().await?;
|
||||
let ensembles = this.handle.backend.cl().get_ensembles().await?;
|
||||
|
||||
for person in persons {
|
||||
poes.push(PersonOrEnsemble::Person(person));
|
||||
}
|
||||
|
||||
for ensemble in ensembles {
|
||||
poes.push(PersonOrEnsemble::Ensemble(ensemble));
|
||||
}
|
||||
|
||||
Ok(poes)
|
||||
}
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
async move {
|
||||
|
|
@ -109,7 +89,7 @@ struct MediumSelectorMediumScreen {
|
|||
|
||||
impl Screen<PersonOrEnsemble, Medium> for MediumSelectorMediumScreen {
|
||||
fn new(poe: PersonOrEnsemble, handle: NavigationHandle<Medium>) -> Rc<Self> {
|
||||
let selector = Selector::<Medium>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Medium>::new();
|
||||
selector.set_title(&gettext("Select medium"));
|
||||
selector.set_subtitle(&poe.get_title());
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ impl Screen<(), Person> for PersonSelector {
|
|||
fn new(_: (), handle: NavigationHandle<Person>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Person>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Person>::new();
|
||||
selector.set_title(&gettext("Select person"));
|
||||
|
||||
let this = Rc::new(Self { handle, selector });
|
||||
|
|
@ -38,12 +38,6 @@ impl Screen<(), Person> for PersonSelector {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
let clone = this;
|
||||
async move { Ok(clone.handle.backend.cl().get_persons().await?) }
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
let clone = this;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl Screen<(), Recording> for RecordingSelector {
|
|||
fn new(_: (), handle: NavigationHandle<Recording>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Person>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Person>::new();
|
||||
selector.set_title(&gettext("Select composer"));
|
||||
|
||||
let this = Rc::new(Self { handle, selector });
|
||||
|
|
@ -50,11 +50,6 @@ impl Screen<(), Recording> for RecordingSelector {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
async move { Ok(this.handle.backend.cl().get_persons().await?) }
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
async move { this.handle.backend.db().get_persons().await.unwrap() }
|
||||
|
|
@ -108,7 +103,7 @@ struct RecordingSelectorWorkScreen {
|
|||
|
||||
impl Screen<Person, Work> for RecordingSelectorWorkScreen {
|
||||
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> {
|
||||
let selector = Selector::<Work>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Work>::new();
|
||||
selector.set_title(&gettext("Select work"));
|
||||
selector.set_subtitle(&person.name_fl());
|
||||
|
||||
|
|
@ -131,11 +126,6 @@ impl Screen<Person, Work> for RecordingSelectorWorkScreen {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
async move { Ok(this.handle.backend.cl().get_works(&this.person.id).await?) }
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
|
||||
|
|
@ -178,7 +168,7 @@ struct RecordingSelectorRecordingScreen {
|
|||
|
||||
impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
|
||||
fn new(work: Work, handle: NavigationHandle<Recording>) -> Rc<Self> {
|
||||
let selector = Selector::<Recording>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Recording>::new();
|
||||
selector.set_title(&gettext("Select recording"));
|
||||
selector.set_subtitle(&work.get_title());
|
||||
|
||||
|
|
@ -201,10 +191,6 @@ impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
async move { Ok(this.handle.backend.cl().get_recordings_for_work(&this.work.id).await?) }
|
||||
}));
|
||||
|
||||
this.selector.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
async move { this.handle.backend.db().get_recordings_for_work(&this.work.id).await.unwrap() }
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -2,36 +2,30 @@ use crate::widgets::List;
|
|||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk_macros::get_widget;
|
||||
use musicus_backend::{Backend, Result};
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A screen that presents a list of items. It allows to switch between the server and the local
|
||||
/// database and to search within the list.
|
||||
/// A screen that presents a list of items from the library.
|
||||
pub struct Selector<T: 'static> {
|
||||
pub widget: gtk::Box,
|
||||
backend: Rc<Backend>,
|
||||
title_label: gtk::Label,
|
||||
subtitle_label: gtk::Label,
|
||||
search_entry: gtk::SearchEntry,
|
||||
server_check_button: gtk::CheckButton,
|
||||
stack: gtk::Stack,
|
||||
list: Rc<List>,
|
||||
items: RefCell<Vec<T>>,
|
||||
back_cb: RefCell<Option<Box<dyn Fn()>>>,
|
||||
add_cb: RefCell<Option<Box<dyn Fn()>>>,
|
||||
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>,
|
||||
load_online: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Result<Vec<T>>>>>>>,
|
||||
load_local: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Vec<T>>>>>>,
|
||||
filter: RefCell<Option<Box<dyn Fn(&str, &T) -> bool>>>,
|
||||
}
|
||||
|
||||
impl<T> Selector<T> {
|
||||
/// Create a new selector. `use_server` is used to decide whether to search
|
||||
/// online initially.
|
||||
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
|
||||
/// Create a new selector.
|
||||
pub fn new() -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/selector.ui");
|
||||
|
|
@ -42,28 +36,23 @@ impl<T> Selector<T> {
|
|||
get_widget!(builder, gtk::Button, back_button);
|
||||
get_widget!(builder, gtk::Button, add_button);
|
||||
get_widget!(builder, gtk::SearchEntry, search_entry);
|
||||
get_widget!(builder, gtk::CheckButton, server_check_button);
|
||||
get_widget!(builder, gtk::Stack, stack);
|
||||
get_widget!(builder, gtk::Frame, frame);
|
||||
get_widget!(builder, gtk::Button, try_again_button);
|
||||
|
||||
let list = List::new();
|
||||
frame.set_child(Some(&list.widget));
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget,
|
||||
backend,
|
||||
title_label,
|
||||
subtitle_label,
|
||||
search_entry,
|
||||
server_check_button,
|
||||
stack,
|
||||
list,
|
||||
items: RefCell::new(Vec::new()),
|
||||
back_cb: RefCell::new(None),
|
||||
add_cb: RefCell::new(None),
|
||||
make_widget: RefCell::new(None),
|
||||
load_online: RefCell::new(None),
|
||||
load_local: RefCell::new(None),
|
||||
filter: RefCell::new(None),
|
||||
});
|
||||
|
|
@ -87,18 +76,6 @@ impl<T> Selector<T> {
|
|||
this.list.invalidate_filter();
|
||||
}));
|
||||
|
||||
this.server_check_button
|
||||
.connect_toggled(clone!(@strong this => move |_| {
|
||||
let active = this.server_check_button.is_active();
|
||||
this.backend.set_use_server(active);
|
||||
|
||||
if active {
|
||||
this.clone().load_online();
|
||||
} else {
|
||||
this.clone().load_local();
|
||||
}
|
||||
}));
|
||||
|
||||
this.list
|
||||
.set_make_widget_cb(clone!(@strong this => move |index| {
|
||||
if let Some(cb) = &*this.make_widget.borrow() {
|
||||
|
|
@ -121,16 +98,8 @@ impl<T> Selector<T> {
|
|||
}
|
||||
}));
|
||||
|
||||
try_again_button.connect_clicked(clone!(@strong this => move |_| {
|
||||
this.clone().load_online();
|
||||
}));
|
||||
|
||||
// Initialize
|
||||
if this.backend.use_server() {
|
||||
this.clone().load_online();
|
||||
} else {
|
||||
this.server_check_button.set_active(false);
|
||||
}
|
||||
this.clone().load_local();
|
||||
|
||||
this
|
||||
}
|
||||
|
|
@ -156,17 +125,6 @@ impl<T> Selector<T> {
|
|||
self.add_cb.replace(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
/// Set the async closure to be called to fetch items from the server. If that results in an
|
||||
/// error, an error screen is shown allowing to try again.
|
||||
pub fn set_load_online<F, R>(&self, cb: F)
|
||||
where
|
||||
F: (Fn() -> R) + 'static,
|
||||
R: Future<Output = Result<Vec<T>>> + 'static,
|
||||
{
|
||||
self.load_online
|
||||
.replace(Some(Box::new(move || Box::new(cb()))));
|
||||
}
|
||||
|
||||
/// Set the async closure to be called to get local items.
|
||||
pub fn set_load_local<F, R>(&self, cb: F)
|
||||
where
|
||||
|
|
@ -188,26 +146,6 @@ impl<T> Selector<T> {
|
|||
self.filter.replace(Some(Box::new(filter)));
|
||||
}
|
||||
|
||||
fn load_online(self: Rc<Self>) {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = self.clone();
|
||||
context.spawn_local(async move {
|
||||
if let Some(cb) = &*self.load_online.borrow() {
|
||||
self.stack.set_visible_child_name("loading");
|
||||
|
||||
match Pin::from(cb()).await {
|
||||
Ok(items) => {
|
||||
clone.show_items(items);
|
||||
}
|
||||
Err(_) => {
|
||||
clone.show_items(Vec::new());
|
||||
clone.stack.set_visible_child_name("error");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn load_local(self: Rc<Self>) {
|
||||
let context = glib::MainContext::default();
|
||||
let clone = self.clone();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ impl Screen<(), Work> for WorkSelector {
|
|||
fn new(_: (), handle: NavigationHandle<Work>) -> Rc<Self> {
|
||||
// Create UI
|
||||
|
||||
let selector = Selector::<Person>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Person>::new();
|
||||
selector.set_title(&gettext("Select composer"));
|
||||
|
||||
let this = Rc::new(Self { handle, selector });
|
||||
|
|
@ -44,11 +44,6 @@ impl Screen<(), Work> for WorkSelector {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
async move { Ok(this.handle.backend.cl().get_persons().await?) }
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
async move { this.handle.backend.db().get_persons().await.unwrap() }
|
||||
|
|
@ -98,7 +93,7 @@ struct WorkSelectorWorkScreen {
|
|||
|
||||
impl Screen<Person, Work> for WorkSelectorWorkScreen {
|
||||
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> {
|
||||
let selector = Selector::<Work>::new(Rc::clone(&handle.backend));
|
||||
let selector = Selector::<Work>::new();
|
||||
selector.set_title(&gettext("Select work"));
|
||||
selector.set_subtitle(&person.name_fl());
|
||||
|
||||
|
|
@ -121,11 +116,6 @@ impl Screen<Person, Work> for WorkSelectorWorkScreen {
|
|||
});
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_online(clone!(@weak this => @default-panic, move || {
|
||||
async move { Ok(this.handle.backend.cl().get_works(&this.person.id).await?) }
|
||||
}));
|
||||
|
||||
this.selector
|
||||
.set_load_local(clone!(@weak this => @default-panic, move || {
|
||||
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ pub use screen::*;
|
|||
pub mod section;
|
||||
pub use section::*;
|
||||
|
||||
pub mod upload_section;
|
||||
pub use upload_section::*;
|
||||
|
||||
mod indexed_list_model;
|
||||
|
||||
/// Something that can be represented as a GTK widget.
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
use super::Section;
|
||||
use adw::prelude::*;
|
||||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use musicus_backend::Backend;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A section showing a switch to enable uploading an item.
|
||||
pub struct UploadSection {
|
||||
/// The GTK widget of the wrapped section.
|
||||
pub widget: gtk::Box,
|
||||
|
||||
backend: Rc<Backend>,
|
||||
|
||||
/// The upload switch.
|
||||
switch: gtk::Switch,
|
||||
}
|
||||
|
||||
impl UploadSection {
|
||||
/// Create a new upload section which will be initially switched on.
|
||||
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
|
||||
let list = gtk::ListBoxBuilder::new()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.build();
|
||||
|
||||
let switch = gtk::SwitchBuilder::new()
|
||||
.active(backend.use_server())
|
||||
.valign(gtk::Align::Center)
|
||||
.build();
|
||||
|
||||
let row = adw::ActionRowBuilder::new()
|
||||
.focusable(false)
|
||||
.title("Upload changes to the server")
|
||||
.activatable_widget(&switch)
|
||||
.build();
|
||||
|
||||
row.add_suffix(&switch);
|
||||
list.append(&row);
|
||||
|
||||
let section = Section::new(&gettext("Upload"), &list);
|
||||
|
||||
let this = Rc::new(Self {
|
||||
widget: section.widget,
|
||||
backend,
|
||||
switch,
|
||||
});
|
||||
|
||||
this.switch
|
||||
.connect_state_notify(clone!(@weak this => move |_| {
|
||||
this.backend.set_use_server(this.switch.state());
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// Return whether the user has enabled the upload switch.
|
||||
pub fn get_active(&self) -> bool {
|
||||
self.switch.state()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue