Create new generic config abstraction (#25)
Build Mumble Web 2 / macos_build (push) Successful in 55s
Build Mumble Web 2 / linux_build (push) Successful in 1m18s
Build Mumble Web 2 / android_build (push) Successful in 5m36s
Build Mumble Web 2 / windows_build (push) Successful in 8m4s
Build android container / android-release-builder-container-build (push) Successful in 7s
Build Mumble Web 2 release builder containers / windows-release-builder-container-build (push) Successful in 14s
Build Mumble Web 2 / macos_build (push) Successful in 55s
Build Mumble Web 2 / linux_build (push) Successful in 1m18s
Build Mumble Web 2 / android_build (push) Successful in 5m36s
Build Mumble Web 2 / windows_build (push) Successful in 8m4s
Build android container / android-release-builder-container-build (push) Successful in 7s
Build Mumble Web 2 release builder containers / windows-release-builder-container-build (push) Successful in 14s
This change migrates the config logic to a new generic key+value abstraction. This allows config parameters to be get and set with arbitrary string keys. Config value types can be anything that serde knows how to serialize / deserialize.
Implementations:
Desktop:
Uses a json file in a platform specific directory (pulled from etcetera). This is mostly the same as the existing code. Implemented in `native_config.rs`
Android:
Uses the same mechanism as desktop, with a different path selection that calls out to the android apis (via jni) to get the correct directory.
Web:
Uses browser local storage. Values are stored as strings instead of actual json objects to keep things simple for now. We might want to update this at some point.
Desktop support:

```
% cat ~/.config/mumble-web2/config.json
{
"username": "restitux-test",
"server_url": "voip.ohea.xyz"
}%
```
Web support:

Android support:

```
root@c053bdd1b4da:/# adb shell
tokay:/ $ run-as xyz.ohea.mumble_web_2
tokay:/data/user/0/xyz.ohea.mumble_web_2 $ ls
app_textures app_webview cache code_cache files no_backup shared_prefs
tokay:/data/user/0/xyz.ohea.mumble_web_2 $ ls files
config.json oat permission_manager.dex
tokay:/data/user/0/xyz.ohea.mumble_web_2 $ cat files/config.json
{
"server_url": "voip.ohea.xyz",
"username": "test"
}tokay:/data/user/0/xyz.ohea.mumble_web_2 $
```
Reviewed-on: #25
Reviewed-by: Sam Sartor <cap@samsartor.com>
This commit was merged in pull request #25.
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
target
|
||||||
+11
-10
@@ -6,7 +6,7 @@ use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
|||||||
use ordermap::OrderSet;
|
use ordermap::OrderSet;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::imp::{Platform, PlatformInterface as _};
|
use crate::imp::{ConfigSystem, ConfigSystemInterface as _, Platform, PlatformInterface as _};
|
||||||
|
|
||||||
pub type ChannelId = u32;
|
pub type ChannelId = u32;
|
||||||
pub type UserId = u32;
|
pub type UserId = u32;
|
||||||
@@ -645,7 +645,7 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
|
pub fn ServerView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let server = STATE.server.read();
|
let server = STATE.server.read();
|
||||||
let Some(&UserState {
|
let Some(&UserState {
|
||||||
@@ -683,7 +683,7 @@ pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
pub fn LoginView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
|
|
||||||
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
||||||
@@ -695,7 +695,7 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut address_input = use_signal(|| Platform::load_server_url());
|
let mut address_input = use_signal(|| user_config.config_get::<String>("server_url"));
|
||||||
let address = use_memo(move || {
|
let address = use_memo(move || {
|
||||||
if let Some(addr) = address_input() {
|
if let Some(addr) = address_input() {
|
||||||
addr.clone()
|
addr.clone()
|
||||||
@@ -706,14 +706,13 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let previous_username = Platform::load_username();
|
let previous_username = user_config.config_get::<String>("username");
|
||||||
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
|
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
|
||||||
|
|
||||||
let do_connect = move |_| {
|
let do_connect = move |_| {
|
||||||
//let _ = set_default_username(&username.read());
|
let _ = user_config.config_set::<String>("username", &username.read());
|
||||||
let _ = Platform::set_default_username(&username.read());
|
|
||||||
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||||
Platform::set_default_server(&address.read());
|
user_config.config_set::<String>("server_url", &address.read());
|
||||||
}
|
}
|
||||||
net.send(Connect {
|
net.send(Connect {
|
||||||
address: address.read().clone(),
|
address: address.read().clone(),
|
||||||
@@ -866,6 +865,8 @@ pub fn app() -> Element {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let user_config = ConfigSystem::new().unwrap();
|
||||||
|
|
||||||
Platform::request_permissions();
|
Platform::request_permissions();
|
||||||
|
|
||||||
rsx!(
|
rsx!(
|
||||||
@@ -874,8 +875,8 @@ pub fn app() -> Element {
|
|||||||
document::Link{ rel: "stylesheet", href: STYLE }
|
document::Link{ rel: "stylesheet", href: STYLE }
|
||||||
|
|
||||||
match *STATE.status.read() {
|
match *STATE.status.read() {
|
||||||
Connected => rsx!(ServerView { overrides }),
|
Connected => rsx!(ServerView { overrides, user_config }),
|
||||||
_ => rsx!(LoginView { overrides }),
|
_ => rsx!(LoginView { overrides, user_config }),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use crate::app::Command;
|
||||||
|
use color_eyre::eyre::Error;
|
||||||
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
|
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||||
|
pub struct MobilePlatform;
|
||||||
|
|
||||||
|
impl super::PlatformInterface for MobilePlatform {
|
||||||
|
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||||
|
|
||||||
|
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||||
|
Ok(ClientConfig {
|
||||||
|
proxy_url: None,
|
||||||
|
cert_hash: None,
|
||||||
|
any_server: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_username() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_server_url() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_username(_username: &str) -> Option<()> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_server(server: &str) -> Option<()> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn network_connect(
|
||||||
|
address: String,
|
||||||
|
username: String,
|
||||||
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
|
gui_config: &ClientConfig,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
super::connect::network_connect(address, username, event_rx, gui_config).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
|
super::connect::get_status(client).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logging() {
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
|
|
||||||
|
let env_filter = EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy();
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_target(true)
|
||||||
|
.with_level(true)
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_permissions() {
|
||||||
|
request_recording_permission();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sleep(duration: Duration) {
|
||||||
|
tokio::time::sleep(duration).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
pub fn request_recording_permission() {}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn request_recording_permission() {
|
||||||
|
use android_permissions::{PermissionManager, RECORD_AUDIO};
|
||||||
|
use jni::{objects::JObject, JavaVM};
|
||||||
|
|
||||||
|
let ctx = ndk_context::android_context();
|
||||||
|
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()).unwrap() };
|
||||||
|
let activity = unsafe { JObject::from_raw(ctx.context().cast()) };
|
||||||
|
|
||||||
|
let manager = PermissionManager::create(vm, activity).unwrap();
|
||||||
|
if !manager.check(&RECORD_AUDIO).unwrap() {
|
||||||
|
manager.request(&[&RECORD_AUDIO]).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-50
@@ -11,6 +11,7 @@ pub struct DesktopPlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for DesktopPlatform {
|
impl super::PlatformInterface for DesktopPlatform {
|
||||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||||
|
type ConfigSystem = super::native_config::NativeConfigSystem;
|
||||||
|
|
||||||
async fn sleep(duration: Duration) {
|
async fn sleep(duration: Duration) {
|
||||||
tokio::time::sleep(duration).await;
|
tokio::time::sleep(duration).await;
|
||||||
@@ -24,28 +25,6 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_username() -> Option<String> {
|
|
||||||
let config = load_config_map();
|
|
||||||
config.get("username").cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_server_url() -> Option<String> {
|
|
||||||
let config = load_config_map();
|
|
||||||
config.get("server").cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_username(username: &str) -> Option<()> {
|
|
||||||
let mut config = load_config_map();
|
|
||||||
config.insert("username".to_string(), username.to_string());
|
|
||||||
save_config_map(&config).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_server(server: &str) -> Option<()> {
|
|
||||||
let mut config = load_config_map();
|
|
||||||
config.insert("server".to_string(), server.to_string());
|
|
||||||
save_config_map(&config).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn network_connect(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -78,31 +57,3 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
// No-op on desktop
|
// No-op on desktop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_config_path() -> std::path::PathBuf {
|
|
||||||
let strategy = choose_app_strategy(AppStrategyArgs {
|
|
||||||
top_level_domain: "com".to_string(),
|
|
||||||
author: "Ohea Corp".to_string(),
|
|
||||||
app_name: "Mumble Web2".to_string(),
|
|
||||||
})
|
|
||||||
.expect("failed to choose app strategy");
|
|
||||||
strategy.config_dir().join("config.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_config_map() -> HashMap<String, String> {
|
|
||||||
let config_path = get_config_path();
|
|
||||||
match std::fs::read_to_string(&config_path) {
|
|
||||||
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
|
||||||
Err(_) => HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_config_map(config: &HashMap<String, String>) -> color_eyre::Result<()> {
|
|
||||||
let config_path = get_config_path();
|
|
||||||
if let Some(parent) = config_path.parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let contents = serde_json::to_string_pretty(config)?;
|
|
||||||
std::fs::write(&config_path, contents)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-16
@@ -9,6 +9,7 @@ pub struct MobilePlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for MobilePlatform {
|
impl super::PlatformInterface for MobilePlatform {
|
||||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||||
|
type ConfigSystem = super::native_config::NativeConfigSystem;
|
||||||
|
|
||||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||||
Ok(ProxyOverrides {
|
Ok(ProxyOverrides {
|
||||||
@@ -18,22 +19,6 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_username() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_server_url() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_username(_username: &str) -> Option<()> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_server(server: &str) -> Option<()> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn network_connect(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
|
|||||||
+36
-17
@@ -8,6 +8,7 @@ use crate::{app::Command, effects::AudioProcessor};
|
|||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -50,11 +51,24 @@ pub trait AudioPlayerInterface {
|
|||||||
fn play_opus(&mut self, payload: &[u8]);
|
fn play_opus(&mut self, payload: &[u8]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ConfigSystemInterface: Sized {
|
||||||
|
fn new() -> Result<Self, Error>;
|
||||||
|
|
||||||
|
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
fn config_set<T>(&self, key: &str, value: &T)
|
||||||
|
where
|
||||||
|
T: serde::Serialize;
|
||||||
|
}
|
||||||
|
|
||||||
/// This is the main trait that each platform must implement. It combines all
|
/// This is the main trait that each platform must implement. It combines all
|
||||||
/// platform-specific functionality into a single interface, providing compile-time
|
/// platform-specific functionality into a single interface, providing compile-time
|
||||||
/// verification that all platforms implement the required functionality.
|
/// verification that all platforms implement the required functionality.
|
||||||
pub trait PlatformInterface {
|
pub trait PlatformInterface {
|
||||||
type AudioSystem: AudioSystemInterface;
|
type AudioSystem: AudioSystemInterface;
|
||||||
|
type ConfigSystem: ConfigSystemInterface;
|
||||||
|
|
||||||
/// Initialize logging for the platform.
|
/// Initialize logging for the platform.
|
||||||
fn init_logging();
|
fn init_logging();
|
||||||
@@ -78,18 +92,6 @@ pub trait PlatformInterface {
|
|||||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>>;
|
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>>;
|
||||||
|
|
||||||
/// Load saved username.
|
|
||||||
fn load_username() -> Option<String>;
|
|
||||||
|
|
||||||
/// Load saved server URL.
|
|
||||||
fn load_server_url() -> Option<String>;
|
|
||||||
|
|
||||||
/// Save the default username.
|
|
||||||
fn set_default_username(username: &str) -> Option<()>;
|
|
||||||
|
|
||||||
/// Save the default server URL.
|
|
||||||
fn set_default_server(server: &str) -> Option<()>;
|
|
||||||
|
|
||||||
/// Async sleep for the given duration.
|
/// Async sleep for the given duration.
|
||||||
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
||||||
}
|
}
|
||||||
@@ -98,15 +100,21 @@ pub trait PlatformInterface {
|
|||||||
// Platform Modules
|
// Platform Modules
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
mod stub;
|
||||||
|
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||||
mod connect;
|
mod connect;
|
||||||
#[cfg(feature = "desktop")]
|
|
||||||
mod desktop;
|
|
||||||
#[cfg(feature = "mobile")]
|
|
||||||
mod mobile;
|
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||||
mod native_audio;
|
mod native_audio;
|
||||||
mod stub;
|
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||||
|
mod native_config;
|
||||||
|
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
mod desktop;
|
||||||
|
|
||||||
|
#[cfg(feature = "mobile")]
|
||||||
|
mod mobile;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
@@ -133,6 +141,8 @@ pub type Platform = stub::StubPlatform;
|
|||||||
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
|
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
|
||||||
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
|
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
|
||||||
|
|
||||||
|
pub type ConfigSystem = <Platform as PlatformInterface>::ConfigSystem;
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Platform Async Runtime
|
// Platform Async Runtime
|
||||||
// ========================
|
// ========================
|
||||||
@@ -164,3 +174,12 @@ const _: () = {
|
|||||||
let _ = assert_platform::<mobile::MobilePlatform>;
|
let _ = assert_platform::<mobile::MobilePlatform>;
|
||||||
let _ = assert_platform::<stub::StubPlatform>;
|
let _ = assert_platform::<stub::StubPlatform>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn global_default_config() -> HashMap<String, serde_json::Value> {
|
||||||
|
serde_json::json!({})
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use crate::app::Command;
|
||||||
|
use color_eyre::eyre::Error;
|
||||||
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
|
use mumble_web2_common::ServerStatus;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct NativeConfigSystem {
|
||||||
|
config_path: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::ConfigSystemInterface for NativeConfigSystem {
|
||||||
|
fn new() -> color_eyre::Result<Self, Error> {
|
||||||
|
return Ok(NativeConfigSystem {
|
||||||
|
config_path: get_config_path()?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
let config = load_config_map(&self.config_path);
|
||||||
|
|
||||||
|
let Some(value_untyped) = config.get(key).cloned().or_else(|| config_get_default(key))
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
match serde_json::from_value::<T>(value_untyped) {
|
||||||
|
Ok(v) => Some(v),
|
||||||
|
Err(_) => {
|
||||||
|
let default_value = config_get_default(key)
|
||||||
|
.expect("Default value required after config parse failure");
|
||||||
|
Some(
|
||||||
|
serde_json::from_value::<T>(default_value)
|
||||||
|
.expect("Default value could not be parsed"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_set<T>(&self, key: &str, value: &T)
|
||||||
|
where
|
||||||
|
T: serde::Serialize,
|
||||||
|
{
|
||||||
|
let mut config = load_config_map(&self.config_path);
|
||||||
|
let json_value = serde_json::to_value(value).expect("failed to serialize config value");
|
||||||
|
config.insert(key.to_string(), json_value);
|
||||||
|
save_config_map(&config).expect("failed to set config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "desktop"))]
|
||||||
|
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
||||||
|
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||||
|
|
||||||
|
let strategy = choose_app_strategy(AppStrategyArgs {
|
||||||
|
top_level_domain: "xyz".to_string(),
|
||||||
|
author: "ohea".to_string(),
|
||||||
|
app_name: "Mumble Web2".to_string(),
|
||||||
|
})
|
||||||
|
.expect("failed to choose app strategy");
|
||||||
|
Ok(strategy.config_dir().join("config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
||||||
|
let ctx = ndk_context::android_context();
|
||||||
|
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?;
|
||||||
|
let mut env = vm.attach_current_thread()?;
|
||||||
|
let ctx = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
|
||||||
|
let cache_dir = env
|
||||||
|
.call_method(ctx, "getFilesDir", "()Ljava/io/File;", &[])?
|
||||||
|
.l()?;
|
||||||
|
let cache_dir: jni::objects::JString = env
|
||||||
|
.call_method(&cache_dir, "toString", "()Ljava/lang/String;", &[])?
|
||||||
|
.l()?
|
||||||
|
.try_into()?;
|
||||||
|
let cache_dir = env.get_string(&cache_dir)?;
|
||||||
|
let cache_dir = cache_dir.to_str()?;
|
||||||
|
Ok(std::path::PathBuf::from(cache_dir).join("config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config_map(config_path: &std::path::PathBuf) -> HashMap<String, serde_json::Value> {
|
||||||
|
match std::fs::read_to_string(config_path) {
|
||||||
|
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
||||||
|
Err(_) => HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_config_map(config: &HashMap<String, serde_json::Value>) -> color_eyre::Result<()> {
|
||||||
|
let config_path = get_config_path().expect("Could not get config file path.");
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
info!("Creating config directory: {}", parent.display());
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let contents = serde_json::to_string_pretty(config)?;
|
||||||
|
info!("Writing config to {}", config_path.display());
|
||||||
|
std::fs::write(&config_path, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
||||||
|
let default_config = platform_default_config();
|
||||||
|
default_config
|
||||||
|
.get(key)
|
||||||
|
.cloned()
|
||||||
|
.or(super::global_default_config().get(key).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
||||||
|
serde_json::json!({})
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
+23
-16
@@ -10,6 +10,7 @@ pub struct StubPlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for StubPlatform {
|
impl super::PlatformInterface for StubPlatform {
|
||||||
type AudioSystem = StubAudioSystem;
|
type AudioSystem = StubAudioSystem;
|
||||||
|
type ConfigSystem = StubConfigSystem;
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
panic!("stubbed platform")
|
panic!("stubbed platform")
|
||||||
@@ -38,22 +39,6 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_username() -> Option<String> {
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_server_url() -> Option<String> {
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_username(_username: &str) -> Option<()> {
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_server(_server: &str) -> Option<()> {
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
@@ -92,6 +77,28 @@ impl super::AudioPlayerInterface for StubAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StubConfigSystem;
|
||||||
|
|
||||||
|
impl super::ConfigSystemInterface for StubConfigSystem {
|
||||||
|
fn new() -> Result<Self, Error> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_set<T>(&self, key: &str, value: &T)
|
||||||
|
where
|
||||||
|
T: serde::Serialize,
|
||||||
|
{
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct SpawnHandle;
|
pub struct SpawnHandle;
|
||||||
|
|
||||||
|
|||||||
+63
-25
@@ -8,6 +8,7 @@ use js_sys::Float32Array;
|
|||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -62,6 +63,7 @@ pub struct WebPlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for WebPlatform {
|
impl super::PlatformInterface for WebPlatform {
|
||||||
type AudioSystem = WebAudioSystem;
|
type AudioSystem = WebAudioSystem;
|
||||||
|
type ConfigSystem = WebConfigSystem;
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
// copied from tracing_web example usage
|
// copied from tracing_web example usage
|
||||||
@@ -104,31 +106,6 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_username() -> Option<String> {
|
|
||||||
web_sys::window()
|
|
||||||
.unwrap()
|
|
||||||
.local_storage()
|
|
||||||
.ok()??
|
|
||||||
.get_item("username")
|
|
||||||
.ok()?
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_server_url() -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_username(username: &str) -> Option<()> {
|
|
||||||
web_sys::window()?
|
|
||||||
.local_storage()
|
|
||||||
.ok()??
|
|
||||||
.set_item("username", username)
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_server(_server: &str) -> Option<()> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn network_connect(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -523,3 +500,64 @@ pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
|||||||
let location = window.location();
|
let location = window.location();
|
||||||
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct WebConfigSystem {}
|
||||||
|
|
||||||
|
impl super::ConfigSystemInterface for WebConfigSystem {
|
||||||
|
fn new() -> Result<Self, Error> {
|
||||||
|
return Ok(WebConfigSystem {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
// Get Storage
|
||||||
|
let storage = web_sys::window()?.local_storage().ok()??;
|
||||||
|
|
||||||
|
// Try localStorage first
|
||||||
|
if let Ok(Some(raw)) = storage.get_item(key) {
|
||||||
|
if let Ok(parsed) = serde_json::from_str::<T>(&raw) {
|
||||||
|
return Some(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default if deserialization fails or key missing
|
||||||
|
let default_value = config_get_default(key)?;
|
||||||
|
serde_json::from_value::<T>(default_value).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_set<T>(&self, key: &str, value: &T)
|
||||||
|
where
|
||||||
|
T: serde::Serialize,
|
||||||
|
{
|
||||||
|
let storage = window()
|
||||||
|
.and_then(|w| w.local_storage().ok().flatten())
|
||||||
|
.expect("localStorage not available");
|
||||||
|
|
||||||
|
let json_value =
|
||||||
|
serde_json::to_string(value).expect("failed to serialize config value to JSON string");
|
||||||
|
|
||||||
|
storage
|
||||||
|
.set_item(key, &json_value)
|
||||||
|
.expect("failed to write to localStorage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
||||||
|
let default_config = platform_default_config();
|
||||||
|
default_config
|
||||||
|
.get(key)
|
||||||
|
.cloned()
|
||||||
|
.or(super::global_default_config().get(key).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
||||||
|
serde_json::json!({})
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user