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:
Elias Projahn 2022-01-23 13:18:37 +01:00
parent 384ca255f3
commit f165c6cae8
48 changed files with 96 additions and 2633 deletions

868
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,2 +1,2 @@
[workspace] [workspace]
members = ["backend", "client", "database", "import", "musicus"] members = ["backend", "database", "import", "musicus"]

View file

@ -10,7 +10,6 @@ glib = "0.14.0"
gstreamer = "0.17.0" gstreamer = "0.17.0"
gstreamer-player = "0.17.0" gstreamer-player = "0.17.0"
log = { version = "0.4.14", features = ["std"] } log = { version = "0.4.14", features = ["std"] }
musicus_client = { version = "0.1.0", path = "../client" }
musicus_database = { version = "0.1.0", path = "../database" } musicus_database = { version = "0.1.0", path = "../database" }
musicus_import = { version = "0.1.0", path = "../import" } musicus_import = { version = "0.1.0", path = "../import" }
thiserror = "1.0.23" thiserror = "1.0.23"
@ -18,4 +17,3 @@ tokio = { version = "1.4.0", features = ["sync"] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
mpris-player = "0.6.0" mpris-player = "0.6.0"
secret-service = "2.0.1"

View file

@ -1,16 +1,9 @@
/// An error that can happened within the backend. /// An error that happened within the backend.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error(transparent)]
ClientError(#[from] musicus_client::Error),
#[error(transparent)] #[error(transparent)]
DatabaseError(#[from] musicus_database::Error), 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.")] #[error("An error happened while decoding to UTF-8.")]
Utf8Error(#[from] std::str::Utf8Error), Utf8Error(#[from] std::str::Utf8Error),

View file

@ -1,13 +1,9 @@
use gio::prelude::*;
use log::warn;
use musicus_client::{Client, LoginData};
use musicus_database::DbThread; use musicus_database::DbThread;
use std::cell::{Cell, RefCell}; use std::cell::RefCell;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use tokio::sync::{broadcast, broadcast::Sender}; use tokio::sync::{broadcast, broadcast::Sender};
pub use musicus_client as client;
pub use musicus_database as db; pub use musicus_database as db;
pub use musicus_import as import; pub use musicus_import as import;
@ -22,9 +18,6 @@ mod logger;
pub mod player; pub mod player;
pub use player::*; pub use player::*;
#[cfg(all(feature = "dbus"))]
mod secure;
/// General states the application can be in. /// General states the application can be in.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum BackendState { pub enum BackendState {
@ -49,9 +42,6 @@ pub struct Backend {
/// Access to GSettings. /// Access to GSettings.
settings: gio::Settings, 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 /// 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. /// is guaranteed to be Some, when the state is set to BackendState::Ready.
music_library_path: RefCell<Option<PathBuf>>, 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 /// The player handling playlist and playback. This can be assumed to exist, when the state is
/// set to BackendState::Ready. /// set to BackendState::Ready.
player: RefCell<Option<Rc<Player>>>, player: RefCell<Option<Rc<Player>>>,
/// A client for the Wolfgang server.
client: Client,
} }
impl Backend { impl Backend {
@ -83,12 +70,10 @@ impl Backend {
Backend { Backend {
state_sender, state_sender,
settings: gio::Settings::new("de.johrpan.musicus"), settings: gio::Settings::new("de.johrpan.musicus"),
use_server: Cell::new(true),
music_library_path: RefCell::new(None), music_library_path: RefCell::new(None),
library_updated_sender, library_updated_sender,
database: RefCell::new(None), database: RefCell::new(None),
player: RefCell::new(None), player: RefCell::new(None)
client: Client::new(),
} }
} }
@ -102,24 +87,6 @@ impl Backend {
pub async fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
self.init_library().await?; 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() { if self.get_music_library_path().is_none() {
self.set_state(BackendState::NoMusicLibrary); self.set_state(BackendState::NoMusicLibrary);
} else { } else {
@ -129,80 +96,6 @@ impl Backend {
Ok(()) 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. /// Set the current state and notify the user interface.
fn set_state(&self, state: BackendState) { fn set_state(&self, state: BackendState) {
self.state_sender.send(state).unwrap(); self.state_sender.send(state).unwrap();

View file

@ -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)
}
}

View file

@ -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"

View file

@ -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(())
}
}

View file

@ -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>;

View file

@ -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(())
}
}

View file

@ -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!"))
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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)
}
}

View file

@ -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(())
}
}

View file

@ -1,37 +1,36 @@
{ {
"app-id" : "de.johrpan.musicus", "app-id": "de.johrpan.musicus",
"runtime" : "org.gnome.Platform", "runtime": "org.gnome.Platform",
"runtime-version" : "master", "runtime-version": "master",
"sdk" : "org.gnome.Sdk", "sdk": "org.gnome.Sdk",
"sdk-extensions" : [ "sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable" "org.freedesktop.Sdk.Extension.rust-stable"
], ],
"command" : "musicus", "command": "musicus",
"finish-args" : [ "finish-args": [
"--share=network", "--share=network",
"--share=ipc", "--share=ipc",
"--socket=x11", "--socket=x11",
"--socket=wayland", "--socket=wayland",
"--socket=pulseaudio", "--socket=pulseaudio",
"--filesystem=host", "--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", "--own-name=org.mpris.MediaPlayer2.de.johrpan.musicus",
"--device=all" "--device=all"
], ],
"build-options" : { "build-options": {
"append-path" : "/usr/lib/sdk/rust-stable/bin", "append-path": "/usr/lib/sdk/rust-stable/bin",
"build-args" : [ "build-args": [
"--share=network" "--share=network"
], ],
"env" : { "env": {
"RUSTFLAGS" : "-L=/app/lib", "RUSTFLAGS": "-L=/app/lib",
"CARGO_HOME" : "/run/build/musicus/cargo", "CARGO_HOME": "/run/build/musicus/cargo",
"RUST_BACKTRACE" : "1", "RUST_BACKTRACE": "1",
"RUST_LOG" : "musicus=debug" "RUST_LOG": "musicus=debug"
} }
}, },
"cleanup" : [ "cleanup": [
"/include", "/include",
"/lib/pkgconfig", "/lib/pkgconfig",
"/man", "/man",
@ -42,8 +41,7 @@
"*.la", "*.la",
"*.a" "*.a"
], ],
"modules" : [ "modules": [{
{
"name": "cdparanoia", "name": "cdparanoia",
"buildsystem": "simple", "buildsystem": "simple",
"build-commands": [ "build-commands": [
@ -52,13 +50,11 @@
"make all slib", "make all slib",
"make install" "make install"
], ],
"sources": [ "sources": [{
{ "type": "archive",
"type": "archive", "url": "http://downloads.xiph.org/releases/cdparanoia/cdparanoia-III-10.2.src.tgz",
"url": "http://downloads.xiph.org/releases/cdparanoia/cdparanoia-III-10.2.src.tgz", "sha256": "005db45ef4ee017f5c32ec124f913a0546e77014266c6a1c50df902a55fe64df"
"sha256": "005db45ef4ee017f5c32ec124f913a0546e77014266c6a1c50df902a55fe64df" }]
}
]
}, },
{ {
"name": "gst-plugins-base", "name": "gst-plugins-base",
@ -68,26 +64,22 @@
"-Dauto_features=disabled", "-Dauto_features=disabled",
"-Dcdparanoia=enabled" "-Dcdparanoia=enabled"
], ],
"cleanup": [ "*.la", "/share/gtk-doc" ], "cleanup": ["*.la", "/share/gtk-doc"],
"sources": [ "sources": [{
{ "type": "git",
"type": "git", "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git", "branch": "1.16.2",
"branch" : "1.16.2", "commit": "9d3581b2e6f12f0b7e790d1ebb63b90cf5b1ef4e"
"commit" : "9d3581b2e6f12f0b7e790d1ebb63b90cf5b1ef4e" }]
}
]
}, },
{ {
"name" : "musicus", "name": "musicus",
"builddir" : true, "builddir": true,
"buildsystem" : "meson", "buildsystem": "meson",
"sources" : [ "sources": [{
{ "type": "git",
"type" : "git", "url": "."
"url" : "." }]
}
]
} }
] ]
} }

View file

@ -5,17 +5,5 @@
<default>""</default> <default>""</default>
<summary>Path to the music library folder</summary> <summary>Path to the music library folder</summary>
</key> </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> </schema>
</schemalist> </schemalist>

View file

@ -3,7 +3,6 @@
<gresource prefix="/de/johrpan/musicus"> <gresource prefix="/de/johrpan/musicus">
<file preprocess="xml-stripblanks">ui/editor.ui</file> <file preprocess="xml-stripblanks">ui/editor.ui</file>
<file preprocess="xml-stripblanks">ui/import_screen.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/main_screen.ui</file>
<file preprocess="xml-stripblanks">ui/medium_editor.ui</file> <file preprocess="xml-stripblanks">ui/medium_editor.ui</file>
<file preprocess="xml-stripblanks">ui/medium_preview.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/screen.ui</file>
<file preprocess="xml-stripblanks">ui/section.ui</file> <file preprocess="xml-stripblanks">ui/section.ui</file>
<file preprocess="xml-stripblanks">ui/selector.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/source_selector.ui</file>
<file preprocess="xml-stripblanks">ui/track_editor.ui</file> <file preprocess="xml-stripblanks">ui/track_editor.ui</file>
<file preprocess="xml-stripblanks">ui/track_selector.ui</file> <file preprocess="xml-stripblanks">ui/track_selector.ui</file>

View file

@ -45,12 +45,6 @@
</attributes> </attributes>
</object> </object>
</child> </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> </object>
</child> </child>
<child> <child>

View file

@ -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>

View file

@ -82,19 +82,6 @@
</child> </child>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>

View file

@ -29,49 +29,7 @@
</child> </child>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>
<object class="GtkSizeGroup">
<widgets>
<widget name="select_music_library_path_button"/>
<widget name="url_button"/>
<widget name="login_button"/>
</widgets>
</object>
</interface> </interface>

View file

@ -99,19 +99,6 @@
</child> </child>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>

View file

@ -60,13 +60,6 @@
<property name="placeholder-text" translatable="yes">Search …</property> <property name="placeholder-text" translatable="yes">Search …</property>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>
@ -118,57 +111,6 @@
</property> </property>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>

View file

@ -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>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<interface> <interface>
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0" />
<requires lib="libadwaita" version="1.0"/> <requires lib="libadwaita" version="1.0" />
<object class="GtkStack" id="widget"> <object class="GtkStack" id="widget">
<child> <child>
<object class="GtkStackPage"> <object class="GtkStackPage">
@ -17,13 +17,13 @@
<object class="GtkLabel"> <object class="GtkLabel">
<property name="label" translatable="yes">Work</property> <property name="label" translatable="yes">Work</property>
<style> <style>
<class name="title"/> <class name="title" />
</style> </style>
</object> </object>
</property> </property>
<child> <child>
<object class="GtkButton" id="back_button"> <object class="GtkButton" id="back_button">
<property name="icon-name">go-previous-symbolic</property> <property name="icon-name">go-previous-symbolic</property>
</object> </object>
</child> </child>
<child type="end"> <child type="end">
@ -31,7 +31,7 @@
<property name="sensitive">False</property> <property name="sensitive">False</property>
<property name="icon-name">object-select-symbolic</property> <property name="icon-name">object-select-symbolic</property>
<style> <style>
<class name="suggested-action"/> <class name="suggested-action" />
</style> </style>
</object> </object>
</child> </child>
@ -64,7 +64,7 @@
<property name="margin-bottom">6</property> <property name="margin-bottom">6</property>
<property name="label" translatable="yes">Overview</property> <property name="label" translatable="yes">Overview</property>
<attributes> <attributes>
<attribute name="weight" value="bold"/> <attribute name="weight" value="bold" />
</attributes> </attributes>
</object> </object>
</child> </child>
@ -99,19 +99,6 @@
</child> </child>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>
@ -128,7 +115,7 @@
<property name="hexpand">True</property> <property name="hexpand">True</property>
<property name="label" translatable="yes">Instruments</property> <property name="label" translatable="yes">Instruments</property>
<attributes> <attributes>
<attribute name="weight" value="bold"/> <attribute name="weight" value="bold" />
</attributes> </attributes>
</object> </object>
</child> </child>
@ -141,7 +128,7 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkFrame" id="instrument_frame"/> <object class="GtkFrame" id="instrument_frame" />
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
@ -155,7 +142,7 @@
<property name="hexpand">True</property> <property name="hexpand">True</property>
<property name="label" translatable="yes">Structure</property> <property name="label" translatable="yes">Structure</property>
<attributes> <attributes>
<attribute name="weight" value="bold"/> <attribute name="weight" value="bold" />
</attributes> </attributes>
</object> </object>
</child> </child>
@ -174,7 +161,7 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkFrame" id="structure_frame"/> <object class="GtkFrame" id="structure_frame" />
</child> </child>
</object> </object>
</child> </child>
@ -200,7 +187,7 @@
<object class="GtkLabel"> <object class="GtkLabel">
<property name="label" translatable="yes">Work</property> <property name="label" translatable="yes">Work</property>
<style> <style>
<class name="title"/> <class name="title" />
</style> </style>
</object> </object>
</property> </property>

View file

@ -1,5 +1,5 @@
use crate::navigator::{NavigationHandle, Screen}; use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget}; use crate::widgets::{Editor, EntryRow, Section, Widget};
use anyhow::Result; use anyhow::Result;
use gettextrs::gettext; use gettextrs::gettext;
use glib::clone; use glib::clone;
@ -16,7 +16,6 @@ pub struct EnsembleEditor {
editor: Editor, editor: Editor,
name: EntryRow, name: EntryRow,
upload: Rc<UploadSection>,
} }
impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor { impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
@ -33,10 +32,7 @@ impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
list.append(&name.widget); list.append(&name.widget);
let section = Section::new(&gettext("General"), &list); let section = Section::new(&gettext("General"), &list);
let upload = UploadSection::new(Rc::clone(&handle.backend));
editor.add_content(&section.widget); editor.add_content(&section.widget);
editor.add_content(&upload.widget);
let id = match ensemble { let id = match ensemble {
Some(ensemble) => { Some(ensemble) => {
@ -51,7 +47,6 @@ impl Screen<Option<Ensemble>, Ensemble> for EnsembleEditor {
id, id,
editor, editor,
name, name,
upload,
}); });
// Connect signals and callbacks // Connect signals and callbacks
@ -91,7 +86,7 @@ impl EnsembleEditor {
self.editor.set_may_save(!self.name.get_text().is_empty()); 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> { async fn save(&self) -> Result<Ensemble> {
let name = self.name.get_text(); let name = self.name.get_text();
@ -100,15 +95,12 @@ impl EnsembleEditor {
name, name,
}; };
if self.upload.get_active() {
self.handle.backend.cl().post_ensemble(&ensemble).await?;
}
self.handle self.handle
.backend .backend
.db() .db()
.update_ensemble(ensemble.clone()) .update_ensemble(ensemble.clone())
.await?; .await?;
self.handle.backend.library_changed(); self.handle.backend.library_changed();
Ok(ensemble) Ok(ensemble)

View file

@ -1,5 +1,5 @@
use crate::navigator::{NavigationHandle, Screen}; use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget}; use crate::widgets::{Editor, EntryRow, Section, Widget};
use anyhow::Result; use anyhow::Result;
use gettextrs::gettext; use gettextrs::gettext;
use glib::clone; use glib::clone;
@ -16,7 +16,6 @@ pub struct InstrumentEditor {
editor: Editor, editor: Editor,
name: EntryRow, name: EntryRow,
upload: Rc<UploadSection>,
} }
impl Screen<Option<Instrument>, Instrument> for InstrumentEditor { impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
@ -33,10 +32,7 @@ impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
list.append(&name.widget); list.append(&name.widget);
let section = Section::new(&gettext("General"), &list); let section = Section::new(&gettext("General"), &list);
let upload = UploadSection::new(Rc::clone(&handle.backend));
editor.add_content(&section.widget); editor.add_content(&section.widget);
editor.add_content(&upload.widget);
let id = match instrument { let id = match instrument {
Some(instrument) => { Some(instrument) => {
@ -51,7 +47,6 @@ impl Screen<Option<Instrument>, Instrument> for InstrumentEditor {
id, id,
editor, editor,
name, name,
upload,
}); });
// Connect signals and callbacks // Connect signals and callbacks
@ -91,7 +86,7 @@ impl InstrumentEditor {
self.editor.set_may_save(!self.name.get_text().is_empty()); 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> { async fn save(&self) -> Result<Instrument> {
let name = self.name.get_text(); let name = self.name.get_text();
@ -100,19 +95,12 @@ impl InstrumentEditor {
name, name,
}; };
if self.upload.get_active() {
self.handle
.backend
.cl()
.post_instrument(&instrument)
.await?;
}
self.handle self.handle
.backend .backend
.db() .db()
.update_instrument(instrument.clone()) .update_instrument(instrument.clone())
.await?; .await?;
self.handle.backend.library_changed(); self.handle.backend.library_changed();
Ok(instrument) Ok(instrument)

View file

@ -1,5 +1,5 @@
use crate::navigator::{NavigationHandle, Screen}; use crate::navigator::{NavigationHandle, Screen};
use crate::widgets::{Editor, EntryRow, Section, UploadSection, Widget}; use crate::widgets::{Editor, EntryRow, Section, Widget};
use anyhow::Result; use anyhow::Result;
use gettextrs::gettext; use gettextrs::gettext;
use glib::clone; use glib::clone;
@ -17,7 +17,6 @@ pub struct PersonEditor {
editor: Editor, editor: Editor,
first_name: EntryRow, first_name: EntryRow,
last_name: EntryRow, last_name: EntryRow,
upload: Rc<UploadSection>,
} }
impl Screen<Option<Person>, Person> for PersonEditor { impl Screen<Option<Person>, Person> for PersonEditor {
@ -37,10 +36,7 @@ impl Screen<Option<Person>, Person> for PersonEditor {
list.append(&last_name.widget); list.append(&last_name.widget);
let section = Section::new(&gettext("General"), &list); let section = Section::new(&gettext("General"), &list);
let upload = UploadSection::new(Rc::clone(&handle.backend));
editor.add_content(&section.widget); editor.add_content(&section.widget);
editor.add_content(&upload.widget);
let id = match person { let id = match person {
Some(person) => { Some(person) => {
@ -58,7 +54,6 @@ impl Screen<Option<Person>, Person> for PersonEditor {
editor, editor,
first_name, first_name,
last_name, last_name,
upload,
}); });
// Connect signals and callbacks // 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> { async fn save(self: &Rc<Self>) -> Result<Person> {
let first_name = self.first_name.get_text(); let first_name = self.first_name.get_text();
let last_name = self.last_name.get_text(); let last_name = self.last_name.get_text();
@ -115,10 +110,6 @@ impl PersonEditor {
last_name, last_name,
}; };
if self.upload.get_active() {
self.handle.backend.cl().post_person(&person).await?;
}
self.handle self.handle
.backend .backend
.db() .db()

View file

@ -19,7 +19,6 @@ pub struct RecordingEditor {
info_bar: gtk::InfoBar, info_bar: gtk::InfoBar,
work_row: adw::ActionRow, work_row: adw::ActionRow,
comment_entry: gtk::Entry, comment_entry: gtk::Entry,
upload_switch: gtk::Switch,
performance_list: Rc<List>, performance_list: Rc<List>,
id: String, id: String,
work: RefCell<Option<Work>>, 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, adw::ActionRow, work_row);
get_widget!(builder, gtk::Button, work_button); get_widget!(builder, gtk::Button, work_button);
get_widget!(builder, gtk::Entry, comment_entry); 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::Frame, performance_frame);
get_widget!(builder, gtk::Button, add_performer_button); get_widget!(builder, gtk::Button, add_performer_button);
upload_switch.set_active(handle.backend.use_server());
let performance_list = List::new(); let performance_list = List::new();
performance_frame.set_child(Some(&performance_list.widget)); performance_frame.set_child(Some(&performance_list.widget));
@ -64,7 +60,6 @@ impl Screen<Option<Recording>, Recording> for RecordingEditor {
info_bar, info_bar,
work_row, work_row,
comment_entry, comment_entry,
upload_switch,
performance_list, performance_list,
id, id,
work: RefCell::new(work), work: RefCell::new(work),
@ -183,7 +178,7 @@ impl RecordingEditor {
self.save_button.set_sensitive(true); 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> { async fn save(self: &Rc<Self>) -> Result<Recording> {
let recording = Recording { let recording = Recording {
id: self.id.clone(), id: self.id.clone(),
@ -196,11 +191,6 @@ impl RecordingEditor {
performances: self.performances.borrow().clone(), performances: self.performances.borrow().clone(),
}; };
let upload = self.upload_switch.state();
if upload {
self.handle.backend.cl().post_recording(&recording).await?;
}
self.handle self.handle
.backend .backend
.db() .db()

View file

@ -36,7 +36,6 @@ pub struct WorkEditor {
title_entry: gtk::Entry, title_entry: gtk::Entry,
info_bar: gtk::InfoBar, info_bar: gtk::InfoBar,
composer_row: adw::ActionRow, composer_row: adw::ActionRow,
upload_switch: gtk::Switch,
instrument_list: Rc<List>, instrument_list: Rc<List>,
part_list: Rc<List>, part_list: Rc<List>,
id: String, id: String,
@ -59,7 +58,6 @@ impl Screen<Option<Work>, Work> for WorkEditor {
get_widget!(builder, gtk::Entry, title_entry); get_widget!(builder, gtk::Entry, title_entry);
get_widget!(builder, gtk::Button, composer_button); get_widget!(builder, gtk::Button, composer_button);
get_widget!(builder, adw::ActionRow, composer_row); 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::Frame, instrument_frame);
get_widget!(builder, gtk::Button, add_instrument_button); get_widget!(builder, gtk::Button, add_instrument_button);
get_widget!(builder, gtk::Frame, structure_frame); 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()), None => (generate_id(), None, Vec::new(), Vec::new()),
}; };
upload_switch.set_active(handle.backend.use_server());
let this = Rc::new(Self { let this = Rc::new(Self {
handle, handle,
widget, widget,
@ -102,7 +98,6 @@ impl Screen<Option<Work>, Work> for WorkEditor {
info_bar, info_bar,
title_entry, title_entry,
composer_row, composer_row,
upload_switch,
instrument_list, instrument_list,
part_list, part_list,
composer: RefCell::new(composer), composer: RefCell::new(composer),
@ -317,7 +312,7 @@ impl WorkEditor {
.set_sensitive(!self.title_entry.text().is_empty() && self.composer.borrow().is_some()); .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> { async fn save(self: &Rc<Self>) -> Result<Work> {
let mut section_count: usize = 0; let mut section_count: usize = 0;
let mut parts = Vec::new(); let mut parts = Vec::new();
@ -348,11 +343,6 @@ impl WorkEditor {
sections, sections,
}; };
let upload = self.upload_switch.state();
if upload {
self.handle.backend.cl().post_work(&work).await?;
}
self.handle self.handle
.backend .backend
.db() .db()

View file

@ -8,7 +8,6 @@ use glib::clone;
use gtk_macros::get_widget; use gtk_macros::get_widget;
use musicus_backend::db::Medium; use musicus_backend::db::Medium;
use musicus_backend::import::ImportSession; use musicus_backend::import::ImportSession;
use musicus_backend::Error;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
@ -17,24 +16,19 @@ pub struct ImportScreen {
handle: NavigationHandle<()>, handle: NavigationHandle<()>,
session: Arc<ImportSession>, session: Arc<ImportSession>,
widget: gtk::Box, widget: gtk::Box,
server_check_button: gtk::CheckButton,
matching_stack: gtk::Stack, matching_stack: gtk::Stack,
error_row: adw::ActionRow, error_row: adw::ActionRow,
matching_list: gtk::ListBox, matching_list: gtk::ListBox,
} }
impl ImportScreen { impl ImportScreen {
/// Find matching mediums on the server. /// Find matching mediums in the library.
fn load_matches(self: &Rc<Self>) { fn load_matches(self: &Rc<Self>) {
self.matching_stack.set_visible_child_name("loading"); self.matching_stack.set_visible_child_name("loading");
let this = self; let this = self;
spawn!(@clone this, async move { spawn!(@clone this, async move {
let mediums: Result<Vec<Medium>, Error> = if this.server_check_button.is_active() { let mediums = this.handle.backend.db().get_mediums_by_source_id(this.session.source_id()).await;
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())
};
match mediums { match mediums {
Ok(mediums) => { Ok(mediums) => {
@ -113,18 +107,14 @@ impl Screen<Arc<ImportSession>, ()> for ImportScreen {
get_widget!(builder, gtk::Stack, matching_stack); get_widget!(builder, gtk::Stack, matching_stack);
get_widget!(builder, gtk::Button, try_again_button); get_widget!(builder, gtk::Button, try_again_button);
get_widget!(builder, adw::ActionRow, error_row); 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::ListBox, matching_list);
get_widget!(builder, gtk::Button, select_button); get_widget!(builder, gtk::Button, select_button);
get_widget!(builder, gtk::Button, add_button); get_widget!(builder, gtk::Button, add_button);
server_check_button.set_active(handle.backend.use_server());
let this = Rc::new(Self { let this = Rc::new(Self {
handle, handle,
session, session,
widget, widget,
server_check_button,
matching_stack, matching_stack,
error_row, error_row,
matching_list, matching_list,
@ -136,12 +126,6 @@ impl Screen<Arc<ImportSession>, ()> for ImportScreen {
this.handle.pop(None); 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 |_| { try_again_button.connect_clicked(clone!(@weak this => move |_| {
this.load_matches(); this.load_matches();
})); }));

View file

@ -18,7 +18,6 @@ pub struct MediumEditor {
widget: gtk::Stack, widget: gtk::Stack,
done_button: gtk::Button, done_button: gtk::Button,
name_entry: gtk::Entry, name_entry: gtk::Entry,
publish_switch: gtk::Switch,
status_page: adw::StatusPage, status_page: adw::StatusPage,
track_set_list: Rc<List>, track_set_list: Rc<List>,
track_sets: RefCell<Vec<TrackSetData>>, 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, back_button);
get_widget!(builder, gtk::Button, done_button); get_widget!(builder, gtk::Button, done_button);
get_widget!(builder, gtk::Entry, name_entry); 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::Button, add_button);
get_widget!(builder, gtk::Frame, frame); get_widget!(builder, gtk::Frame, frame);
get_widget!(builder, adw::StatusPage, status_page); get_widget!(builder, adw::StatusPage, status_page);
get_widget!(builder, gtk::Button, try_again_button); get_widget!(builder, gtk::Button, try_again_button);
get_widget!(builder, gtk::Button, cancel_button); get_widget!(builder, gtk::Button, cancel_button);
publish_switch.set_active(handle.backend.use_server());
let list = List::new(); let list = List::new();
frame.set_child(Some(&list.widget)); frame.set_child(Some(&list.widget));
@ -56,7 +52,6 @@ impl Screen<(Arc<ImportSession>, Option<Medium>), Medium> for MediumEditor {
widget, widget,
done_button, done_button,
name_entry, name_entry,
publish_switch,
status_page, status_page,
track_set_list: list, track_set_list: list,
track_sets: RefCell::new(Vec::new()), 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( this.track_set_list.set_make_widget_cb(
clone!(@weak this => @default-panic, move |index| { clone!(@weak this => @default-panic, move |index| {
let track_set = &this.track_sets.borrow()[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> { async fn save(&self) -> Result<Medium> {
// Convert the track set data to real track sets. // Convert the track set data to real track sets.
@ -214,11 +204,6 @@ impl MediumEditor {
tracks, 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 // 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 is actually imported into the music library. This step will be handled by the
// medium preview dialog. // medium preview dialog.

View file

@ -29,10 +29,4 @@ impl NavigatorWindow {
this 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));
}
} }

View file

@ -1,4 +1,3 @@
use crate::navigator::NavigatorWindow;
use adw::prelude::*; use adw::prelude::*;
use gettextrs::gettext; use gettextrs::gettext;
use glib::clone; use glib::clone;
@ -6,21 +5,11 @@ use gtk_macros::get_widget;
use musicus_backend::Backend; use musicus_backend::Backend;
use std::rc::Rc; use std::rc::Rc;
mod login;
use login::LoginDialog;
mod server;
use server::ServerDialog;
mod register;
/// A dialog for configuring the app. /// A dialog for configuring the app.
pub struct Preferences { pub struct Preferences {
backend: Rc<Backend>, backend: Rc<Backend>,
window: adw::Window, window: adw::Window,
music_library_path_row: adw::ActionRow, music_library_path_row: adw::ActionRow,
url_row: adw::ActionRow,
login_row: adw::ActionRow,
} }
impl Preferences { impl Preferences {
@ -32,10 +21,6 @@ impl Preferences {
get_widget!(builder, adw::Window, window); get_widget!(builder, adw::Window, window);
get_widget!(builder, adw::ActionRow, music_library_path_row); get_widget!(builder, adw::ActionRow, music_library_path_row);
get_widget!(builder, gtk::Button, select_music_library_path_button); 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)); window.set_transient_for(Some(parent));
@ -43,8 +28,6 @@ impl Preferences {
backend, backend,
window, window,
music_library_path_row, music_library_path_row,
url_row,
login_row,
}); });
// Connect signals and callbacks // Connect signals and callbacks
@ -80,31 +63,6 @@ impl Preferences {
dialog.show(); 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 // Initialize
if let Some(path) = this.backend.get_music_library_path() { if let Some(path) = this.backend.get_music_library_path() {
@ -112,14 +70,6 @@ impl Preferences {
.set_subtitle(path.to_str().unwrap()); .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 this
} }

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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();
}
}

View file

@ -19,7 +19,7 @@ impl Screen<(), Ensemble> for EnsembleSelector {
fn new(_: (), handle: NavigationHandle<Ensemble>) -> Rc<Self> { fn new(_: (), handle: NavigationHandle<Ensemble>) -> Rc<Self> {
// Create UI // Create UI
let selector = Selector::<Ensemble>::new(Rc::clone(&handle.backend)); let selector = Selector::<Ensemble>::new();
selector.set_title(&gettext("Select ensemble")); selector.set_title(&gettext("Select ensemble"));
let this = Rc::new(Self { handle, selector }); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
let clone = this; let clone = this;

View file

@ -19,7 +19,7 @@ impl Screen<(), Instrument> for InstrumentSelector {
fn new(_: (), handle: NavigationHandle<Instrument>) -> Rc<Self> { fn new(_: (), handle: NavigationHandle<Instrument>) -> Rc<Self> {
// Create UI // Create UI
let selector = Selector::<Instrument>::new(Rc::clone(&handle.backend)); let selector = Selector::<Instrument>::new();
selector.set_title(&gettext("Select instrument")); selector.set_title(&gettext("Select instrument"));
let this = Rc::new(Self { handle, selector }); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
let clone = this; let clone = this;

View file

@ -17,7 +17,7 @@ impl Screen<(), Medium> for MediumSelector {
fn new(_: (), handle: NavigationHandle<Medium>) -> Rc<Self> { fn new(_: (), handle: NavigationHandle<Medium>) -> Rc<Self> {
// Create UI // Create UI
let selector = Selector::<PersonOrEnsemble>::new(Rc::clone(&handle.backend)); let selector = Selector::<PersonOrEnsemble>::new();
selector.set_title(&gettext("Select performer")); selector.set_title(&gettext("Select performer"));
let this = Rc::new(Self { handle, selector }); let this = Rc::new(Self { handle, selector });
@ -28,26 +28,6 @@ impl Screen<(), Medium> for MediumSelector {
this.handle.pop(None); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
async move { async move {
@ -109,7 +89,7 @@ struct MediumSelectorMediumScreen {
impl Screen<PersonOrEnsemble, Medium> for MediumSelectorMediumScreen { impl Screen<PersonOrEnsemble, Medium> for MediumSelectorMediumScreen {
fn new(poe: PersonOrEnsemble, handle: NavigationHandle<Medium>) -> Rc<Self> { 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_title(&gettext("Select medium"));
selector.set_subtitle(&poe.get_title()); selector.set_subtitle(&poe.get_title());

View file

@ -19,7 +19,7 @@ impl Screen<(), Person> for PersonSelector {
fn new(_: (), handle: NavigationHandle<Person>) -> Rc<Self> { fn new(_: (), handle: NavigationHandle<Person>) -> Rc<Self> {
// Create UI // Create UI
let selector = Selector::<Person>::new(Rc::clone(&handle.backend)); let selector = Selector::<Person>::new();
selector.set_title(&gettext("Select person")); selector.set_title(&gettext("Select person"));
let this = Rc::new(Self { handle, selector }); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
let clone = this; let clone = this;

View file

@ -18,7 +18,7 @@ impl Screen<(), Recording> for RecordingSelector {
fn new(_: (), handle: NavigationHandle<Recording>) -> Rc<Self> { fn new(_: (), handle: NavigationHandle<Recording>) -> Rc<Self> {
// Create UI // Create UI
let selector = Selector::<Person>::new(Rc::clone(&handle.backend)); let selector = Selector::<Person>::new();
selector.set_title(&gettext("Select composer")); selector.set_title(&gettext("Select composer"));
let this = Rc::new(Self { handle, selector }); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_persons().await.unwrap() } async move { this.handle.backend.db().get_persons().await.unwrap() }
@ -108,7 +103,7 @@ struct RecordingSelectorWorkScreen {
impl Screen<Person, Work> for RecordingSelectorWorkScreen { impl Screen<Person, Work> for RecordingSelectorWorkScreen {
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> { 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_title(&gettext("Select work"));
selector.set_subtitle(&person.name_fl()); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() } 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 { impl Screen<Work, Recording> for RecordingSelectorRecordingScreen {
fn new(work: Work, handle: NavigationHandle<Recording>) -> Rc<Self> { 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_title(&gettext("Select recording"));
selector.set_subtitle(&work.get_title()); 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 || { 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() } async move { this.handle.backend.db().get_recordings_for_work(&this.work.id).await.unwrap() }
})); }));

View file

@ -2,36 +2,30 @@ use crate::widgets::List;
use glib::clone; use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use gtk_macros::get_widget; use gtk_macros::get_widget;
use musicus_backend::{Backend, Result};
use std::cell::RefCell; use std::cell::RefCell;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
/// A screen that presents a list of items. It allows to switch between the server and the local /// A screen that presents a list of items from the library.
/// database and to search within the list.
pub struct Selector<T: 'static> { pub struct Selector<T: 'static> {
pub widget: gtk::Box, pub widget: gtk::Box,
backend: Rc<Backend>,
title_label: gtk::Label, title_label: gtk::Label,
subtitle_label: gtk::Label, subtitle_label: gtk::Label,
search_entry: gtk::SearchEntry, search_entry: gtk::SearchEntry,
server_check_button: gtk::CheckButton,
stack: gtk::Stack, stack: gtk::Stack,
list: Rc<List>, list: Rc<List>,
items: RefCell<Vec<T>>, items: RefCell<Vec<T>>,
back_cb: RefCell<Option<Box<dyn Fn()>>>, back_cb: RefCell<Option<Box<dyn Fn()>>>,
add_cb: RefCell<Option<Box<dyn Fn()>>>, add_cb: RefCell<Option<Box<dyn Fn()>>>,
make_widget: RefCell<Option<Box<dyn Fn(&T) -> gtk::Widget>>>, 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>>>>>>, load_local: RefCell<Option<Box<dyn Fn() -> Box<dyn Future<Output = Vec<T>>>>>>,
filter: RefCell<Option<Box<dyn Fn(&str, &T) -> bool>>>, filter: RefCell<Option<Box<dyn Fn(&str, &T) -> bool>>>,
} }
impl<T> Selector<T> { impl<T> Selector<T> {
/// Create a new selector. `use_server` is used to decide whether to search /// Create a new selector.
/// online initially. pub fn new() -> Rc<Self> {
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI // Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/selector.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, back_button);
get_widget!(builder, gtk::Button, add_button); get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::SearchEntry, search_entry); 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::Stack, stack);
get_widget!(builder, gtk::Frame, frame); get_widget!(builder, gtk::Frame, frame);
get_widget!(builder, gtk::Button, try_again_button);
let list = List::new(); let list = List::new();
frame.set_child(Some(&list.widget)); frame.set_child(Some(&list.widget));
let this = Rc::new(Self { let this = Rc::new(Self {
widget, widget,
backend,
title_label, title_label,
subtitle_label, subtitle_label,
search_entry, search_entry,
server_check_button,
stack, stack,
list, list,
items: RefCell::new(Vec::new()), items: RefCell::new(Vec::new()),
back_cb: RefCell::new(None), back_cb: RefCell::new(None),
add_cb: RefCell::new(None), add_cb: RefCell::new(None),
make_widget: RefCell::new(None), make_widget: RefCell::new(None),
load_online: RefCell::new(None),
load_local: RefCell::new(None), load_local: RefCell::new(None),
filter: RefCell::new(None), filter: RefCell::new(None),
}); });
@ -87,18 +76,6 @@ impl<T> Selector<T> {
this.list.invalidate_filter(); 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 this.list
.set_make_widget_cb(clone!(@strong this => move |index| { .set_make_widget_cb(clone!(@strong this => move |index| {
if let Some(cb) = &*this.make_widget.borrow() { 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 // Initialize
if this.backend.use_server() { this.clone().load_local();
this.clone().load_online();
} else {
this.server_check_button.set_active(false);
}
this this
} }
@ -156,17 +125,6 @@ impl<T> Selector<T> {
self.add_cb.replace(Some(Box::new(cb))); 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. /// Set the async closure to be called to get local items.
pub fn set_load_local<F, R>(&self, cb: F) pub fn set_load_local<F, R>(&self, cb: F)
where where
@ -188,26 +146,6 @@ impl<T> Selector<T> {
self.filter.replace(Some(Box::new(filter))); 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>) { fn load_local(self: Rc<Self>) {
let context = glib::MainContext::default(); let context = glib::MainContext::default();
let clone = self.clone(); let clone = self.clone();

View file

@ -18,7 +18,7 @@ impl Screen<(), Work> for WorkSelector {
fn new(_: (), handle: NavigationHandle<Work>) -> Rc<Self> { fn new(_: (), handle: NavigationHandle<Work>) -> Rc<Self> {
// Create UI // Create UI
let selector = Selector::<Person>::new(Rc::clone(&handle.backend)); let selector = Selector::<Person>::new();
selector.set_title(&gettext("Select composer")); selector.set_title(&gettext("Select composer"));
let this = Rc::new(Self { handle, selector }); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_persons().await.unwrap() } async move { this.handle.backend.db().get_persons().await.unwrap() }
@ -98,7 +93,7 @@ struct WorkSelectorWorkScreen {
impl Screen<Person, Work> for WorkSelectorWorkScreen { impl Screen<Person, Work> for WorkSelectorWorkScreen {
fn new(person: Person, handle: NavigationHandle<Work>) -> Rc<Self> { 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_title(&gettext("Select work"));
selector.set_subtitle(&person.name_fl()); 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 this.selector
.set_load_local(clone!(@weak this => @default-panic, move || { .set_load_local(clone!(@weak this => @default-panic, move || {
async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() } async move { this.handle.backend.db().get_works(&this.person.id).await.unwrap() }

View file

@ -21,9 +21,6 @@ pub use screen::*;
pub mod section; pub mod section;
pub use section::*; pub use section::*;
pub mod upload_section;
pub use upload_section::*;
mod indexed_list_model; mod indexed_list_model;
/// Something that can be represented as a GTK widget. /// Something that can be represented as a GTK widget.

View file

@ -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()
}
}