impl: refactor config logic to use generic interface
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