Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f35a216cd | |||
| f0ce15000e | |||
| 7337b3e49b | |||
| d67a19c478 | |||
| 518c50d8a4 | |||
| 847c636f41 |
@@ -0,0 +1 @@
|
|||||||
|
target
|
||||||
@@ -42,6 +42,47 @@ jobs:
|
|||||||
path: target/release/mumble-web2-proxy
|
path: target/release/mumble-web2-proxy
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
|
macos_build:
|
||||||
|
runs-on: macos
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Restore Rust cache
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo
|
||||||
|
./target
|
||||||
|
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
rust-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install cargo binstall
|
||||||
|
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||||
|
|
||||||
|
- name: Install dioxus-cli
|
||||||
|
run: cargo binstall dioxus-cli --version 0.7.3 --no-confirm
|
||||||
|
|
||||||
|
- name: Build dioxus project
|
||||||
|
run: dx bundle --platform macos --release -p mumble-web2-gui
|
||||||
|
|
||||||
|
- name: Save Rust cache
|
||||||
|
if: always()
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo
|
||||||
|
./target
|
||||||
|
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Upload mumble-web2-gui Artifact
|
||||||
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mumble-web2-gui-macos-arm64
|
||||||
|
path: gui/dist
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
windows_build:
|
windows_build:
|
||||||
runs-on: windows
|
runs-on: windows
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
pub struct ClientConfig {
|
pub struct ProxyOverrides {
|
||||||
pub proxy_url: Option<String>,
|
pub proxy_url: Option<String>,
|
||||||
pub cert_hash: Option<Vec<u8>>,
|
pub cert_hash: Option<Vec<u8>>,
|
||||||
pub any_server: bool,
|
pub any_server: bool,
|
||||||
|
|||||||
+7
-9
@@ -1,14 +1,12 @@
|
|||||||
localhost:64444 {
|
localhost:64444 {
|
||||||
tls internal
|
tls internal
|
||||||
|
|
||||||
# Proxy /config path to mumble-web2-proxy
|
# Proxy /config path to mumble-web2-proxy
|
||||||
reverse_proxy /config http://127.0.0.1:4400
|
reverse_proxy /overrides http://127.0.0.1:4400
|
||||||
|
|
||||||
# Proxy /status path to mumble-web2-proxy
|
# Proxy /status path to mumble-web2-proxy
|
||||||
reverse_proxy /status http://127.0.0.1:4400
|
reverse_proxy /status http://127.0.0.1:4400
|
||||||
|
|
||||||
|
|
||||||
# Proxy root path to dx-serve
|
|
||||||
reverse_proxy http://127.0.0.1:8080
|
|
||||||
|
|
||||||
|
# Proxy root path to dx-serve
|
||||||
|
reverse_proxy http://127.0.0.1:8080
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
# volumes:
|
# volumes:
|
||||||
# - ..:/app
|
# - ..:/app
|
||||||
# environment:
|
# environment:
|
||||||
# - MUMBLE_WEB2_GUI_CONFIG_URL=https://localhost:64444/config
|
# - MUMBLE_WEB2_PROXY_OVERRIDES_URL=https://localhost:64444/overrides
|
||||||
# stdin_open: true
|
# stdin_open: true
|
||||||
# tty: true
|
# tty: true
|
||||||
# command: >
|
# command: >
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
|
visibility: visible;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
+105
-58
@@ -2,15 +2,17 @@
|
|||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use ordermap::OrderSet;
|
use ordermap::OrderSet;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::{fmt, sync::Arc};
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ConnectionState {
|
pub enum ConnectionState {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Connecting,
|
Connecting,
|
||||||
@@ -18,12 +20,17 @@ pub enum ConnectionState {
|
|||||||
Failed(String),
|
Failed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AudioSettings {
|
||||||
|
pub denoise: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Connect {
|
Connect {
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
config: ClientConfig,
|
config: ProxyOverrides,
|
||||||
},
|
},
|
||||||
SendChat {
|
SendChat {
|
||||||
markdown: String,
|
markdown: String,
|
||||||
@@ -45,16 +52,14 @@ pub enum Command {
|
|||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
user: UserId,
|
user: UserId,
|
||||||
},
|
},
|
||||||
UpdateMicEffects {
|
UpdateAudioSettings(AudioSettings),
|
||||||
denoise: bool,
|
|
||||||
},
|
|
||||||
Disconnect,
|
Disconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
use Command::*;
|
use Command::*;
|
||||||
use ConnectionState::*;
|
use ConnectionState::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Debug)]
|
||||||
pub struct UserState {
|
pub struct UserState {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub channel: ChannelId,
|
pub channel: ChannelId,
|
||||||
@@ -79,13 +84,14 @@ impl UserState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Chat {
|
pub struct Chat {
|
||||||
pub raw: String,
|
pub raw: String,
|
||||||
pub dangerous_html: String,
|
pub dangerous_html: String,
|
||||||
pub sender: Option<UserId>,
|
pub sender: Option<UserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Debug)]
|
||||||
pub struct ChannelState {
|
pub struct ChannelState {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub children: OrderSet<ChannelId>,
|
pub children: OrderSet<ChannelId>,
|
||||||
@@ -111,7 +117,7 @@ impl ChannelState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Debug)]
|
||||||
pub struct ChannelsState {
|
pub struct ChannelsState {
|
||||||
pub channels: HashMap<ChannelId, ChannelState>,
|
pub channels: HashMap<ChannelId, ChannelState>,
|
||||||
}
|
}
|
||||||
@@ -198,7 +204,7 @@ impl ChannelsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Debug)]
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub channels_state: ChannelsState,
|
pub channels_state: ChannelsState,
|
||||||
pub users: HashMap<UserId, UserState>,
|
pub users: HashMap<UserId, UserState>,
|
||||||
@@ -213,14 +219,21 @@ impl ServerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub status: GlobalSignal<ConnectionState>,
|
pub status: Signal<ConnectionState>,
|
||||||
pub server: GlobalSignal<ServerState>,
|
pub server: Signal<ServerState>,
|
||||||
|
pub audio: Signal<AudioSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static STATE: State = State {
|
impl fmt::Debug for State {
|
||||||
status: Signal::global(|| Disconnected),
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
server: Signal::global(|| Default::default()),
|
f.debug_struct("State")
|
||||||
};
|
.field("status", &self.status.read())
|
||||||
|
.field("server", &self.server.read())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedState = Arc<State>;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum UserIcon {
|
pub enum UserIcon {
|
||||||
@@ -267,7 +280,8 @@ pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn User(id: UserId) -> Element {
|
pub fn User(id: UserId) -> Element {
|
||||||
let server = STATE.server.read();
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
match server.users.get(&id) {
|
match server.users.get(&id) {
|
||||||
Some(state) => rsx!(UserPill {
|
Some(state) => rsx!(UserPill {
|
||||||
name: state.name.clone(),
|
name: state.name.clone(),
|
||||||
@@ -285,7 +299,8 @@ pub fn User(id: UserId) -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn Channel(id: ChannelId) -> Element {
|
pub fn Channel(id: ChannelId) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let server = STATE.server.read();
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
let user = server.session.unwrap();
|
let user = server.session.unwrap();
|
||||||
let Some(state) = server.channels_state.channels.get(&id) else {
|
let Some(state) = server.channels_state.channels.get(&id) else {
|
||||||
return rsx!("missing channel {id}");
|
return rsx!("missing channel {id}");
|
||||||
@@ -354,7 +369,8 @@ pub fn Channel(id: ChannelId) -> Element {
|
|||||||
|
|
||||||
#[cfg(any(feature = "desktop", feature = "web"))]
|
#[cfg(any(feature = "desktop", feature = "web"))]
|
||||||
pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
||||||
let channels = if let Some(user) = STATE.server.read().this_user() {
|
let state = use_context::<SharedState>();
|
||||||
|
let channels = if let Some(user) = state.server.read().this_user() {
|
||||||
vec![user.channel]
|
vec![user.channel]
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@@ -380,11 +396,14 @@ pub fn pick_and_send_file(net: &Coroutine<Command>) {}
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn ChatView() -> Element {
|
pub fn ChatView() -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let server = STATE.server.read();
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
let mut draft = use_signal(|| "".to_string());
|
let mut draft = use_signal(|| "".to_string());
|
||||||
|
|
||||||
let mut do_send = move || {
|
let mut do_send = move || {
|
||||||
if let Some(user) = STATE.server.read().this_user() {
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
|
if let Some(user) = server.this_user() {
|
||||||
net.send(SendChat {
|
net.send(SendChat {
|
||||||
markdown: draft.write().split_off(0),
|
markdown: draft.write().split_off(0),
|
||||||
channels: vec![user.channel],
|
channels: vec![user.channel],
|
||||||
@@ -454,10 +473,12 @@ pub fn ChatView() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let status = &STATE.status;
|
let state = use_context::<SharedState>();
|
||||||
let server = STATE.server.read();
|
let status = &state.status;
|
||||||
|
let server = state.server.read();
|
||||||
|
let audio = state.audio.read();
|
||||||
let Some(&UserState {
|
let Some(&UserState {
|
||||||
deaf,
|
deaf,
|
||||||
self_deaf,
|
self_deaf,
|
||||||
@@ -474,10 +495,10 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
|||||||
|
|
||||||
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
||||||
|
|
||||||
let proxy_url = config
|
let proxy_url = overrides
|
||||||
.read_unchecked()
|
.read_unchecked()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|gui_config| gui_config.proxy_url.clone());
|
.and_then(|overrides| overrides.proxy_url.clone());
|
||||||
|
|
||||||
let connecting_color = "yellow";
|
let connecting_color = "yellow";
|
||||||
let connected_color = "oklch(0.55 0.1184 141.35)";
|
let connected_color = "oklch(0.55 0.1184 141.35)";
|
||||||
@@ -555,7 +576,6 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let denoise = use_signal(|| false);
|
|
||||||
rsx!(
|
rsx!(
|
||||||
// Server control
|
// Server control
|
||||||
div {
|
div {
|
||||||
@@ -596,18 +616,23 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
|||||||
}
|
}
|
||||||
span { class: "spacer" }
|
span { class: "spacer" }
|
||||||
button {
|
button {
|
||||||
class: match denoise() {
|
class: match audio.denoise {
|
||||||
true => "toggle_button is_on",
|
true => "toggle_button is_on",
|
||||||
false => "toggle_button",
|
false => "toggle_button",
|
||||||
},
|
},
|
||||||
role: "switch",
|
role: "switch",
|
||||||
aria_checked: denoise(),
|
aria_checked: audio.denoise,
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let new_denoise = !denoise();
|
let state = use_context::<SharedState>();
|
||||||
*denoise.write_unchecked() = new_denoise;
|
let mut audio = state.audio.read().clone();
|
||||||
net.send(UpdateMicEffects { denoise: new_denoise })
|
audio.denoise = !audio.denoise;
|
||||||
|
let denoise = audio.denoise;
|
||||||
|
*state.audio.write_unchecked() = audio;
|
||||||
|
net.send(UpdateAudioSettings(AudioSettings { denoise: denoise }));
|
||||||
|
let user_config = use_context::<ConfigSystem>();
|
||||||
|
user_config.config_set::<bool>("denoise", &denoise);
|
||||||
},
|
},
|
||||||
match denoise() {
|
match audio.denoise {
|
||||||
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
||||||
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
||||||
}
|
}
|
||||||
@@ -645,9 +670,10 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let server = STATE.server.read();
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
let Some(&UserState {
|
let Some(&UserState {
|
||||||
deaf,
|
deaf,
|
||||||
self_deaf,
|
self_deaf,
|
||||||
@@ -676,14 +702,15 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
|||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "server_control_box",
|
class: "server_control_box",
|
||||||
ControlView { config }
|
ControlView { overrides }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||||
|
let user_config = use_context::<ConfigSystem>();
|
||||||
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,33 +722,36 @@ pub fn LoginView(config: Resource<ClientConfig>) -> 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()
|
||||||
} else {
|
} else {
|
||||||
config()
|
overrides()
|
||||||
.and_then(|c| c.proxy_url.clone())
|
.and_then(|c| c.proxy_url.clone())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let previous_username = Platform::load_username();
|
let mut username = use_signal(|| {
|
||||||
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
|
user_config
|
||||||
|
.config_get::<String>("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 config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
user_config.config_set::<String>("server_url", &address.read());
|
||||||
Platform::set_default_server(&address.read());
|
|
||||||
}
|
}
|
||||||
net.send(Connect {
|
net.send(Connect {
|
||||||
address: address.read().clone(),
|
address: address.read().clone(),
|
||||||
username: username.read().clone(),
|
username: username.read().clone(),
|
||||||
config: config.read().clone().unwrap_or_default(),
|
config: overrides.read().clone().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let status = &STATE.status;
|
let state = use_context::<SharedState>();
|
||||||
|
let status = &state.status;
|
||||||
let bottom = match &*status.read() {
|
let bottom = match &*status.read() {
|
||||||
Disconnected => rsx! {
|
Disconnected => rsx! {
|
||||||
button {
|
button {
|
||||||
@@ -763,7 +793,7 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
|||||||
None => rsx!(),
|
None => rsx!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||||
div {
|
div {
|
||||||
label {
|
label {
|
||||||
for: "address-entry",
|
for: "address-entry",
|
||||||
@@ -855,27 +885,44 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
|||||||
// )
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
pub fn app() -> Element {
|
pub fn app() -> Element {
|
||||||
static STYLE: Asset = asset!("/assets/main.scss");
|
static STYLE: Asset = asset!("/assets/main.scss");
|
||||||
|
|
||||||
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
|
use_effect(|| {
|
||||||
let config = use_resource(|| async move {
|
Platform::request_permissions();
|
||||||
match Platform::load_config().await {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(_) => ClientConfig::default(),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Platform::request_permissions();
|
let user_config = use_root_context(|| ConfigSystem::new().unwrap());
|
||||||
|
let state = use_root_context(|| {
|
||||||
|
SharedState::new(State {
|
||||||
|
status: Signal::new(Disconnected),
|
||||||
|
server: Signal::new(Default::default()),
|
||||||
|
audio: Signal::new(AudioSettings {
|
||||||
|
denoise: user_config.config_get::<bool>("denoise").unwrap_or(true),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let network_state = state.clone();
|
||||||
|
use_coroutine(move |rx: UnboundedReceiver<Command>| {
|
||||||
|
super::network_entrypoint(rx, network_state.clone())
|
||||||
|
});
|
||||||
|
let overrides = use_resource(|| async move {
|
||||||
|
match Platform::load_proxy_overrides().await {
|
||||||
|
Ok(overrides) => overrides,
|
||||||
|
Err(_) => ProxyOverrides::default(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
rsx!(
|
rsx!(
|
||||||
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
|
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
|
||||||
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" }
|
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" }
|
||||||
document::Link{ rel: "stylesheet", href: STYLE }
|
document::Link{ rel: "stylesheet", href: STYLE }
|
||||||
|
|
||||||
match *STATE.status.read() {
|
match *state.status.read() {
|
||||||
Connected => rsx!(ServerView { config }),
|
Connected => rsx!(ServerView { overrides }),
|
||||||
_ => rsx!(LoginView { config }),
|
_ => rsx!(LoginView { overrides }),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
use crate::app::Command;
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
@@ -8,13 +8,13 @@ use tokio::net::TcpStream;
|
|||||||
use tokio_rustls::rustls;
|
use tokio_rustls::rustls;
|
||||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
|
use tokio_rustls::rustls::ClientConfig;
|
||||||
use tokio_rustls::rustls::DigitallySignedStruct;
|
use tokio_rustls::rustls::DigitallySignedStruct;
|
||||||
use tokio_rustls::TlsConnector;
|
use tokio_rustls::TlsConnector;
|
||||||
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NoCertificateVerification;
|
struct NoCertificateVerification;
|
||||||
@@ -73,11 +73,12 @@ pub async fn network_connect(
|
|||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
gui_config: &ClientConfig,
|
overrides: &ProxyOverrides,
|
||||||
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
let config = RlsClientConfig::builder()
|
let config = ClientConfig::builder()
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||||
.with_no_client_auth();
|
.with_no_client_auth();
|
||||||
@@ -102,7 +103,7 @@ pub async fn network_connect(
|
|||||||
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
||||||
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
||||||
|
|
||||||
crate::network_loop(username, event_rx, reader, writer).await
|
crate::network_loop(username, state, event_rx, reader, writer).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
|
|||||||
+8
-58
@@ -1,9 +1,7 @@
|
|||||||
use crate::app::Command;
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Desktop platform implementation using Tokio and native audio.
|
/// Desktop platform implementation using Tokio and native audio.
|
||||||
@@ -11,48 +9,28 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||||
Ok(ClientConfig {
|
Ok(ProxyOverrides {
|
||||||
proxy_url: None,
|
proxy_url: None,
|
||||||
cert_hash: None,
|
cert_hash: None,
|
||||||
any_server: true,
|
any_server: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
gui_config: &ClientConfig,
|
overrides: &ProxyOverrides,
|
||||||
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
super::connect::network_connect(address, username, event_rx, gui_config).await
|
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
@@ -78,31 +56,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(())
|
|
||||||
}
|
|
||||||
|
|||||||
+8
-23
@@ -1,8 +1,7 @@
|
|||||||
use crate::app::Command;
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use std::future::Future;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||||
@@ -10,38 +9,24 @@ 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_config() -> color_eyre::Result<ClientConfig> {
|
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||||
Ok(ClientConfig {
|
Ok(ProxyOverrides {
|
||||||
proxy_url: None,
|
proxy_url: None,
|
||||||
cert_hash: None,
|
cert_hash: None,
|
||||||
any_server: true,
|
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(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
gui_config: &ClientConfig,
|
overrides: &ProxyOverrides,
|
||||||
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
super::connect::network_connect(address, username, event_rx, gui_config).await
|
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
|
|||||||
+42
-21
@@ -4,10 +4,12 @@
|
|||||||
//! The traits make the platform boundary explicit and provide compile-time verification.
|
//! The traits make the platform boundary explicit and provide compile-time verification.
|
||||||
#![allow(async_fn_in_trait)]
|
#![allow(async_fn_in_trait)]
|
||||||
|
|
||||||
use crate::{app::Command, effects::AudioProcessor};
|
use crate::app::{Command, SharedState};
|
||||||
|
use crate::effects::AudioProcessor;
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ClientConfig, 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 +52,24 @@ pub trait AudioPlayerInterface {
|
|||||||
fn play_opus(&mut self, payload: &[u8]);
|
fn play_opus(&mut self, payload: &[u8]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ConfigSystemInterface: Sized + Clone {
|
||||||
|
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();
|
||||||
@@ -67,7 +82,8 @@ pub trait PlatformInterface {
|
|||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
gui_config: &ClientConfig,
|
proxy_overrides: &ProxyOverrides,
|
||||||
|
state: SharedState,
|
||||||
) -> impl Future<Output = Result<(), Error>>;
|
) -> impl Future<Output = Result<(), Error>>;
|
||||||
|
|
||||||
/// Get server status (user count, version, etc.).
|
/// Get server status (user count, version, etc.).
|
||||||
@@ -76,19 +92,7 @@ pub trait PlatformInterface {
|
|||||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
|
|
||||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
|
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 +102,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 +143,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 +176,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,117 @@
|
|||||||
|
use color_eyre::eyre::Error;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
}
|
||||||
+29
-20
@@ -1,15 +1,16 @@
|
|||||||
/// Stub implementation of the platform interface, so that we can
|
/// Stub implementation of the platform interface, so that we can
|
||||||
/// `cargo check` without any --feature flags.
|
/// `cargo check` without any --feature flags.
|
||||||
use crate::effects::AudioProcessor;
|
use crate::{app::SharedState, effects::AudioProcessor};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
pub struct StubPlatform;
|
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")
|
||||||
@@ -23,7 +24,8 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
_address: String,
|
_address: String,
|
||||||
_username: String,
|
_username: String,
|
||||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||||
_gui_config: &ClientConfig,
|
_overrides: &ProxyOverrides,
|
||||||
|
_state: SharedState,
|
||||||
) -> impl Future<Output = Result<(), Error>> {
|
) -> impl Future<Output = Result<(), Error>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
@@ -34,26 +36,10 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>> {
|
||||||
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 +78,29 @@ impl super::AudioPlayerInterface for StubAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||
+78
-38
@@ -1,4 +1,4 @@
|
|||||||
use crate::app::Command;
|
use crate::app::{Command, SharedState};
|
||||||
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
||||||
use color_eyre::eyre::{bail, eyre, Error};
|
use color_eyre::eyre::{bail, eyre, Error};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
@@ -6,8 +6,9 @@ use dioxus::prelude::*;
|
|||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use js_sys::Float32Array;
|
use js_sys::Float32Array;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
use mumble_web2_common::{ClientConfig, 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
|
||||||
@@ -89,53 +91,29 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
// No-op on web
|
// No-op on web
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||||
let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
|
let overrides = match option_env!("MUMBLE_WEB2_PROXY_OVERRIDES_URL") {
|
||||||
Some(url) => Url::parse(url)?,
|
Some(url) => Url::parse(url)?,
|
||||||
None => absolute_url("config")?,
|
None => absolute_url("overrides")?,
|
||||||
};
|
};
|
||||||
info!("loading config from {}", config_url);
|
info!("loading config from {}", overrides);
|
||||||
|
|
||||||
let config = reqwest::get(config_url)
|
let config = reqwest::get(overrides)
|
||||||
.await?
|
.await?
|
||||||
.json::<ClientConfig>()
|
.json::<ProxyOverrides>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
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,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
gui_config: &ClientConfig,
|
overrides: &ProxyOverrides,
|
||||||
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
network_connect(address, username, event_rx, gui_config).await
|
network_connect(address, username, event_rx, overrides, state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
@@ -456,7 +434,8 @@ pub async fn network_connect(
|
|||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
gui_config: &ClientConfig,
|
overrides: &ProxyOverrides,
|
||||||
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
@@ -469,7 +448,7 @@ pub async fn network_connect(
|
|||||||
)
|
)
|
||||||
.ey()?;
|
.ey()?;
|
||||||
|
|
||||||
if let Some(server_hash) = &gui_config.cert_hash {
|
if let Some(server_hash) = &overrides.cert_hash {
|
||||||
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
||||||
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
||||||
}
|
}
|
||||||
@@ -515,7 +494,7 @@ pub async fn network_connect(
|
|||||||
let writer =
|
let writer =
|
||||||
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
||||||
|
|
||||||
crate::network_loop(username, event_rx, reader, writer).await
|
crate::network_loop(username, state, event_rx, reader, writer).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
||||||
@@ -523,3 +502,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()
|
||||||
|
}
|
||||||
|
|||||||
+35
-22
@@ -1,7 +1,6 @@
|
|||||||
use app::Chat;
|
use app::Chat;
|
||||||
use app::Command;
|
use app::Command;
|
||||||
use app::ConnectionState;
|
use app::ConnectionState;
|
||||||
use app::STATE;
|
|
||||||
use asynchronous_codec::FramedRead;
|
use asynchronous_codec::FramedRead;
|
||||||
use asynchronous_codec::FramedWrite;
|
use asynchronous_codec::FramedWrite;
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
@@ -27,6 +26,9 @@ use std::time::Duration;
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::app::AudioSettings;
|
||||||
|
use crate::app::SharedState;
|
||||||
|
use crate::app::State;
|
||||||
use crate::effects::AudioProcessor;
|
use crate::effects::AudioProcessor;
|
||||||
use crate::imp::{
|
use crate::imp::{
|
||||||
AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform,
|
AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform,
|
||||||
@@ -38,7 +40,7 @@ mod effects;
|
|||||||
pub mod imp;
|
pub mod imp;
|
||||||
mod msghtml;
|
mod msghtml;
|
||||||
|
|
||||||
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
|
||||||
loop {
|
loop {
|
||||||
let Some(Command::Connect {
|
let Some(Command::Connect {
|
||||||
address,
|
address,
|
||||||
@@ -49,25 +51,29 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
|||||||
panic!("did not receive connect command")
|
panic!("did not receive connect command")
|
||||||
};
|
};
|
||||||
|
|
||||||
*STATE.server.write() = Default::default();
|
*state.server.write_unchecked() = Default::default();
|
||||||
*STATE.status.write() = ConnectionState::Connecting;
|
*state.status.write_unchecked() = ConnectionState::Connecting;
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
Platform::network_connect(address, username, &mut event_rx, &config).await
|
Platform::network_connect(address, username, &mut event_rx, &config, state.clone())
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
error!("could not connect {:?}", error);
|
error!("could not connect {:?}", error);
|
||||||
*STATE.status.write() = ConnectionState::Failed(error.to_string());
|
*state.status.write_unchecked() = ConnectionState::Failed(error.to_string());
|
||||||
} else {
|
} else {
|
||||||
*STATE.status.write() = ConnectionState::Disconnected;
|
*state.status.write_unchecked() = ConnectionState::Disconnected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
|
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
|
||||||
username: String,
|
username: String,
|
||||||
|
state: SharedState,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
||||||
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
let audio_settings = state.audio.read().clone();
|
||||||
|
|
||||||
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
|
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
while let Some(msg) = writer_recv_chan.next().await {
|
while let Some(msg) = writer_recv_chan.next().await {
|
||||||
@@ -117,10 +123,13 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut audio = AudioSystem::new().await?;
|
let mut audio = AudioSystem::new().await?;
|
||||||
|
if audio_settings.denoise {
|
||||||
|
audio.set_processor(AudioProcessor::new_denoising());
|
||||||
|
}
|
||||||
{
|
{
|
||||||
let send_chan = send_chan.clone();
|
let send_chan = send_chan.clone();
|
||||||
let mut sequence_num = 0;
|
let mut sequence_num = 0;
|
||||||
audio.start_recording(move |opus_frame, is_terminator| {
|
if let Err(err) = audio.start_recording(move |opus_frame, is_terminator| {
|
||||||
let _ =
|
let _ =
|
||||||
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
|
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
|
||||||
_dst: std::marker::PhantomData,
|
_dst: std::marker::PhantomData,
|
||||||
@@ -131,7 +140,9 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
position_info: None,
|
position_info: None,
|
||||||
})));
|
})));
|
||||||
sequence_num = sequence_num.wrapping_add(2);
|
sequence_num = sequence_num.wrapping_add(2);
|
||||||
});
|
}) {
|
||||||
|
error!("could not begin recording: {err:?}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create map of session_id -> AudioDecoder
|
// Create map of session_id -> AudioDecoder
|
||||||
@@ -149,7 +160,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
||||||
info!("receiving packet {:#?}", msg);
|
info!("receiving packet {:#?}", msg);
|
||||||
}
|
}
|
||||||
let res = accept_packet(msg, &mut audio, &mut decoder_map);
|
let res = accept_packet(msg, &mut audio, &mut decoder_map, &state);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("error accepting packet {:?}", err)
|
error!("error accepting packet {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -168,7 +179,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
match command {
|
match command {
|
||||||
Some(Command::Disconnect) => break,
|
Some(Command::Disconnect) => break,
|
||||||
Some(command) => {
|
Some(command) => {
|
||||||
let res = accept_command(command, &mut send_chan, &mut audio);
|
let res = accept_command(command, &mut send_chan, &mut audio, &state);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
info!("error accepting command {:?}", err)
|
info!("error accepting command {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -187,9 +198,10 @@ fn accept_command(
|
|||||||
command: Command,
|
command: Command,
|
||||||
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
||||||
audio: &mut AudioSystem,
|
audio: &mut AudioSystem,
|
||||||
|
state: &State,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
use Command::*;
|
use Command::*;
|
||||||
let Some(session) = STATE.server.read().session else {
|
let Some(session) = state.server.read().session else {
|
||||||
bail!("no session id")
|
bail!("no session id")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,7 +224,7 @@ fn accept_command(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -253,7 +265,7 @@ fn accept_command(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -288,7 +300,7 @@ fn accept_command(
|
|||||||
let _ = send_chan.unbounded_send(u.into());
|
let _ = send_chan.unbounded_send(u.into());
|
||||||
}
|
}
|
||||||
Connect { .. } | Disconnect => (),
|
Connect { .. } | Disconnect => (),
|
||||||
UpdateMicEffects { denoise } => {
|
UpdateAudioSettings(AudioSettings { denoise }) => {
|
||||||
if denoise {
|
if denoise {
|
||||||
audio.set_processor(AudioProcessor::new_denoising());
|
audio.set_processor(AudioProcessor::new_denoising());
|
||||||
} else {
|
} else {
|
||||||
@@ -304,6 +316,7 @@ fn accept_packet(
|
|||||||
msg: ControlPacket<mumble_protocol::Clientbound>,
|
msg: ControlPacket<mumble_protocol::Clientbound>,
|
||||||
audio_context: &mut AudioSystem,
|
audio_context: &mut AudioSystem,
|
||||||
player_map: &mut HashMap<u32, AudioPlayer>,
|
player_map: &mut HashMap<u32, AudioPlayer>,
|
||||||
|
state: &State,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match msg {
|
match msg {
|
||||||
ControlPacket::UDPTunnel(u) => {
|
ControlPacket::UDPTunnel(u) => {
|
||||||
@@ -340,15 +353,15 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelState(u) => {
|
ControlPacket::ChannelState(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
server.channels_state.update_from_channel_state(&u);
|
server.channels_state.update_from_channel_state(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelRemove(u) => {
|
ControlPacket::ChannelRemove(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
server.channels_state.update_from_channel_remove(&u);
|
server.channels_state.update_from_channel_remove(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::UserState(u) => {
|
ControlPacket::UserState(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
let server = &mut *server;
|
let server = &mut *server;
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
|
|
||||||
@@ -392,7 +405,7 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::UserRemove(u) => {
|
ControlPacket::UserRemove(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
if let Some(state) = server.users.remove(&id) {
|
if let Some(state) = server.users.remove(&id) {
|
||||||
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||||
@@ -401,7 +414,7 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::TextMessage(u) => {
|
ControlPacket::TextMessage(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
if u.has_message() {
|
if u.has_message() {
|
||||||
let text = u.get_message().to_string();
|
let text = u.get_message().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
@@ -416,8 +429,8 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ServerSync(u) => {
|
ControlPacket::ServerSync(u) => {
|
||||||
*STATE.status.write() = ConnectionState::Connected;
|
*state.status.write_unchecked() = ConnectionState::Connected;
|
||||||
let mut server = STATE.server.write();
|
let mut server = state.server.write_unchecked();
|
||||||
if u.has_welcome_text() {
|
if u.has_welcome_text() {
|
||||||
let text = u.get_welcome_text().to_string();
|
let text = u.get_welcome_text().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
|
|||||||
+17
-1
@@ -1,6 +1,22 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
|
use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
Platform::init_logging();
|
Platform::init_logging();
|
||||||
dioxus::launch(app::app);
|
dioxus::LaunchBuilder::new()
|
||||||
|
.with_cfg(desktop! {
|
||||||
|
dioxus::desktop::Config::new()
|
||||||
|
// Reduce white flash on startup by setting background color and hiding main element
|
||||||
|
.with_background_color((0, 0, 0, 255))
|
||||||
|
.with_custom_head("<style>html, body { background: black; } #main { visibility: hidden; }</style>".into())
|
||||||
|
.with_disable_context_menu(cfg!(not(debug_assertions)))
|
||||||
|
.with_window(
|
||||||
|
dioxus::desktop::WindowBuilder::new()
|
||||||
|
.with_title("Mumble Web 2")
|
||||||
|
.with_min_inner_size(dioxus::desktop::LogicalSize::new(600.0, 300.0))
|
||||||
|
.with_inner_size(dioxus::desktop::LogicalSize::new(900.0, 700.0))
|
||||||
|
.with_maximized(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.launch(app::app);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-14
@@ -1,5 +1,5 @@
|
|||||||
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
||||||
use salvo::cors::{AllowOrigin, Cors};
|
use salvo::cors::{AllowOrigin, Cors};
|
||||||
@@ -16,7 +16,7 @@ use tokio::net::TcpStream;
|
|||||||
use tokio::pin;
|
use tokio::pin;
|
||||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use tokio_rustls::rustls::{ClientConfig as RlsClientConfig, DigitallySignedStruct};
|
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct};
|
||||||
use tokio_rustls::{rustls, TlsConnector};
|
use tokio_rustls::{rustls, TlsConnector};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing::info_span;
|
use tracing::info_span;
|
||||||
@@ -77,7 +77,7 @@ async fn main() -> Result<()> {
|
|||||||
.install_default()
|
.install_default()
|
||||||
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
||||||
|
|
||||||
let mut client_config = ClientConfig {
|
let mut overrides = ProxyOverrides {
|
||||||
proxy_url: match &server_config.proxy_url {
|
proxy_url: match &server_config.proxy_url {
|
||||||
Some(url) => Some(url.to_string()),
|
Some(url) => Some(url.to_string()),
|
||||||
None => None,
|
None => None,
|
||||||
@@ -102,7 +102,7 @@ async fn main() -> Result<()> {
|
|||||||
let cert = cert_params.self_signed(&key_pair)?;
|
let cert = cert_params.self_signed(&key_pair)?;
|
||||||
|
|
||||||
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
||||||
client_config.cert_hash = Some(hash.into());
|
overrides.cert_hash = Some(hash.into());
|
||||||
|
|
||||||
(cert.pem().into(), key_pair.serialize_pem().into())
|
(cert.pem().into(), key_pair.serialize_pem().into())
|
||||||
}
|
}
|
||||||
@@ -122,14 +122,11 @@ async fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
|
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
|
||||||
|
|
||||||
info!(
|
info!("proxy overrides:\n{}", toml::to_string_pretty(&overrides)?);
|
||||||
"client config:\n{}",
|
|
||||||
toml::to_string_pretty(&client_config)?
|
|
||||||
);
|
|
||||||
|
|
||||||
let config_craft = ConfigCraft {
|
let config_craft = ConfigCraft {
|
||||||
server_config: server_config.clone(),
|
server_config: server_config.clone(),
|
||||||
client_config,
|
overrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_craft = StatusCraft {
|
let status_craft = StatusCraft {
|
||||||
@@ -139,7 +136,7 @@ async fn main() -> Result<()> {
|
|||||||
// Server routing
|
// Server routing
|
||||||
let mut router = Router::new()
|
let mut router = Router::new()
|
||||||
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
|
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
|
||||||
.push(Router::with_path("/config").get(config_craft.get_config()))
|
.push(Router::with_path("/overrides").get(config_craft.get_overrides()))
|
||||||
.push(Router::with_path("/status").get(status_craft.get_status()))
|
.push(Router::with_path("/status").get(status_craft.get_status()))
|
||||||
.hoop(Logger::new());
|
.hoop(Logger::new());
|
||||||
if let Some(gui_path) = server_config.gui_path.clone() {
|
if let Some(gui_path) = server_config.gui_path.clone() {
|
||||||
@@ -252,14 +249,14 @@ impl StatusCraft {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigCraft {
|
pub struct ConfigCraft {
|
||||||
server_config: Arc<Config>,
|
server_config: Arc<Config>,
|
||||||
client_config: ClientConfig,
|
overrides: ProxyOverrides,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[craft]
|
#[craft]
|
||||||
impl ConfigCraft {
|
impl ConfigCraft {
|
||||||
#[craft(handler)]
|
#[craft(handler)]
|
||||||
async fn get_config(&self) -> Json<ClientConfig> {
|
async fn get_overrides(&self) -> Json<ProxyOverrides> {
|
||||||
Json(self.client_config.clone())
|
Json(self.overrides.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[craft(handler)]
|
#[craft(handler)]
|
||||||
@@ -320,7 +317,7 @@ async fn connect_proxy_impl(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("connecting to Mumble server...");
|
info!("connecting to Mumble server...");
|
||||||
|
|
||||||
let config = RlsClientConfig::builder()
|
let config = ClientConfig::builder()
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||||
.with_no_client_auth();
|
.with_no_client_auth();
|
||||||
|
|||||||
Reference in New Issue
Block a user