split into gui and client crates
This commit is contained in:
+1
-1
@@ -6,4 +6,4 @@ server_hash.txt
|
|||||||
proxy/bundle
|
proxy/bundle
|
||||||
/config.toml
|
/config.toml
|
||||||
proxy/config.toml
|
proxy/config.toml
|
||||||
gui/assets/*_onnx.tar.gz
|
*_onnx.tar.gz
|
||||||
|
|||||||
Generated
+24
-13
@@ -1587,9 +1587,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-core-types"
|
name = "dioxus-core-types"
|
||||||
version = "0.7.3"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfc4b8cdc440a55c17355542fc2089d97949bba674255d84cac77805e1db8c9f"
|
checksum = "b99d7d199aad72431b549759550002e7d72c8a257eba500dca9fbdb2122de103"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-desktop"
|
name = "dioxus-desktop"
|
||||||
@@ -4220,14 +4220,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mumble-web2-common"
|
name = "mumble-web2-client"
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mumble-web2-gui"
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-permissions",
|
"android-permissions",
|
||||||
@@ -4241,9 +4234,8 @@ dependencies = [
|
|||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"dasp_ring_buffer",
|
"dasp_ring_buffer",
|
||||||
"deep_filter",
|
"deep_filter",
|
||||||
"dioxus",
|
|
||||||
"dioxus-asset-resolver",
|
"dioxus-asset-resolver",
|
||||||
"dioxus-web",
|
"dioxus-signals",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -4252,6 +4244,7 @@ dependencies = [
|
|||||||
"jni",
|
"jni",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"lol_html 2.7.0",
|
"lol_html 2.7.0",
|
||||||
|
"manganis",
|
||||||
"markdown",
|
"markdown",
|
||||||
"merge-io",
|
"merge-io",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
@@ -4263,7 +4256,6 @@ dependencies = [
|
|||||||
"opus",
|
"opus",
|
||||||
"ordermap",
|
"ordermap",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rfd 0.16.0",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4279,6 +4271,25 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mumble-web2-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mumble-web2-gui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"color-eyre",
|
||||||
|
"dioxus",
|
||||||
|
"dioxus-web",
|
||||||
|
"mumble-web2-client",
|
||||||
|
"mumble-web2-common",
|
||||||
|
"rfd 0.16.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mumble-web2-proxy"
|
name = "mumble-web2-proxy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["common", "gui", "proxy"]
|
members = ["client", "common", "gui", "proxy"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
[package]
|
||||||
|
name = "mumble-web2-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Web Dependencies
|
||||||
|
# ================
|
||||||
|
wasm-bindgen = { version = "^0.2.92", optional = true }
|
||||||
|
wasm-bindgen-futures = { version = "^0.4.42", optional = true }
|
||||||
|
wasm-streams = { version = "^0.4.0", optional = true }
|
||||||
|
serde-wasm-bindgen = { version = "^0.6.5", optional = true }
|
||||||
|
js-sys = { version = "=0.3.82", optional = true }
|
||||||
|
web-sys = { version = "=0.3.82", features = [
|
||||||
|
"WebTransport",
|
||||||
|
"console",
|
||||||
|
"WebTransportOptions",
|
||||||
|
"WebTransportBidirectionalStream",
|
||||||
|
"WebTransportSendStream",
|
||||||
|
"WebTransportReceiveStream",
|
||||||
|
"Navigator",
|
||||||
|
"MediaDevices",
|
||||||
|
"AudioDecoder",
|
||||||
|
"AudioDecoderInit",
|
||||||
|
"AudioData",
|
||||||
|
"AudioEncoderConfig",
|
||||||
|
"AudioDecoderConfig",
|
||||||
|
"EncodedAudioChunk",
|
||||||
|
"EncodedAudioChunkInit",
|
||||||
|
"EncodedAudioChunkType",
|
||||||
|
"CodecState",
|
||||||
|
"AudioContext",
|
||||||
|
"AudioContextOptions",
|
||||||
|
"MediaStream",
|
||||||
|
"GainNode",
|
||||||
|
"MediaStreamAudioSourceNode",
|
||||||
|
"BaseAudioContext",
|
||||||
|
"AudioDestinationNode",
|
||||||
|
"AudioWorkletNode",
|
||||||
|
"AudioWorklet",
|
||||||
|
"AudioWorkletProcessor",
|
||||||
|
"MessagePort",
|
||||||
|
"MediaStreamConstraints",
|
||||||
|
"WorkletOptions",
|
||||||
|
"AudioEncoder",
|
||||||
|
"AudioEncoderInit",
|
||||||
|
"AudioDataInit",
|
||||||
|
"HtmlAnchorElement",
|
||||||
|
"Url",
|
||||||
|
"Blob",
|
||||||
|
"AudioDataCopyToOptions",
|
||||||
|
"AudioSampleFormat",
|
||||||
|
"Storage",
|
||||||
|
], optional = true }
|
||||||
|
gloo-timers = { version = "^0.3.0", features = ["futures"], optional = true }
|
||||||
|
tracing-web = { version = "^0.1.3", optional = true }
|
||||||
|
|
||||||
|
# Desktop Dependecies
|
||||||
|
# ===================
|
||||||
|
tokio = { version = "^1.41.1", features = ["net", "rt"], optional = true }
|
||||||
|
tokio-rustls = { version = "^0.26.0", optional = true }
|
||||||
|
opus = { version = "0.3.0", optional = true }
|
||||||
|
cpal = { version = "0.15.3", optional = true }
|
||||||
|
dasp_ring_buffer = { version = "0.11.0", optional = true }
|
||||||
|
etcetera = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
|
# Base Dependencies
|
||||||
|
# ================
|
||||||
|
dioxus-signals = "0.7.2"
|
||||||
|
manganis = "0.7.2"
|
||||||
|
once_cell = "1.19.0"
|
||||||
|
asynchronous-codec = { workspace = true }
|
||||||
|
futures = "^0.3.30"
|
||||||
|
merge-io = "^0.3.0"
|
||||||
|
mumble-protocol = { workspace = true }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio-util = { version = "^0.7.11", features = ["codec", "compat"] }
|
||||||
|
byteorder = "1.5"
|
||||||
|
ogg = "^0.9.1"
|
||||||
|
ordermap = "^0.5.3"
|
||||||
|
html-purifier = "^0.3.0"
|
||||||
|
markdown = "^0.3.0"
|
||||||
|
futures-channel = "^0.3.30"
|
||||||
|
mumble-web2-common = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
tracing-subscriber = { version = "^0.3.18", features = ["ansi"] }
|
||||||
|
tracing = "^0.1.40"
|
||||||
|
color-eyre = "^0.6.3"
|
||||||
|
crossbeam-queue = "^0.3.11"
|
||||||
|
lol_html = "^2.2.0"
|
||||||
|
base64 = "^0.22"
|
||||||
|
mime_guess = "^2.0.5"
|
||||||
|
async_cell = "^0.2.3"
|
||||||
|
reqwest = { version = "^0.12.22", features = ["json"] }
|
||||||
|
dioxus-asset-resolver = "0.7.2"
|
||||||
|
|
||||||
|
# Denoising
|
||||||
|
# =========
|
||||||
|
deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d375b2d8309e0935d165700c91da9de862a99c31", features = [
|
||||||
|
"tract",
|
||||||
|
] }
|
||||||
|
crossbeam = "0.8.4"
|
||||||
|
|
||||||
|
# Android dependencies for requesting permissions
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
android-permissions = "0.1.2"
|
||||||
|
jni = "0.21.1"
|
||||||
|
ndk-context = "0.1.1"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
tract-hir = "=0.12.4"
|
||||||
|
tract-core = "=0.12.4"
|
||||||
|
tract-onnx = "=0.12.4"
|
||||||
|
tract-pulse = "=0.12.4"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
web = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"js-sys",
|
||||||
|
"web-sys",
|
||||||
|
"gloo-timers",
|
||||||
|
"tracing-web",
|
||||||
|
"deep_filter/wasm",
|
||||||
|
]
|
||||||
|
desktop = [
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tracing-subscriber/env-filter",
|
||||||
|
"opus",
|
||||||
|
"cpal",
|
||||||
|
"dasp_ring_buffer",
|
||||||
|
"etcetera",
|
||||||
|
]
|
||||||
|
mobile = [
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tracing-subscriber/env-filter",
|
||||||
|
"opus",
|
||||||
|
"cpal",
|
||||||
|
"dasp_ring_buffer",
|
||||||
|
]
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
use dioxus_signals::{ReadableExt as _, Signal};
|
||||||
|
use mime_guess::Mime;
|
||||||
|
use mumble_web2_common::ProxyOverrides;
|
||||||
|
use ordermap::OrderSet;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::{fmt, sync::Arc};
|
||||||
|
|
||||||
|
pub type ChannelId = u32;
|
||||||
|
pub type UserId = u32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConnectionState {
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Failed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AudioSettings {
|
||||||
|
pub denoise: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Command {
|
||||||
|
Connect {
|
||||||
|
address: String,
|
||||||
|
username: String,
|
||||||
|
config: ProxyOverrides,
|
||||||
|
},
|
||||||
|
SendChat {
|
||||||
|
markdown: String,
|
||||||
|
channels: Vec<ChannelId>,
|
||||||
|
},
|
||||||
|
SendFile {
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
name: String,
|
||||||
|
mime: Option<Mime>,
|
||||||
|
channels: Vec<ChannelId>,
|
||||||
|
},
|
||||||
|
SetMute {
|
||||||
|
mute: bool,
|
||||||
|
},
|
||||||
|
SetDeaf {
|
||||||
|
deaf: bool,
|
||||||
|
},
|
||||||
|
EnterChannel {
|
||||||
|
channel: ChannelId,
|
||||||
|
user: UserId,
|
||||||
|
},
|
||||||
|
UpdateAudioSettings(AudioSettings),
|
||||||
|
Disconnect,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct UserState {
|
||||||
|
pub name: String,
|
||||||
|
pub channel: ChannelId,
|
||||||
|
pub deaf: bool,
|
||||||
|
pub mute: bool,
|
||||||
|
pub suppress: bool,
|
||||||
|
pub self_deaf: bool,
|
||||||
|
pub self_mute: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Chat {
|
||||||
|
pub raw: String,
|
||||||
|
pub dangerous_html: String,
|
||||||
|
pub sender: Option<UserId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ChannelState {
|
||||||
|
pub name: String,
|
||||||
|
pub children: OrderSet<ChannelId>,
|
||||||
|
pub users: OrderSet<UserId>,
|
||||||
|
pub parent: Option<ChannelId>,
|
||||||
|
pub position: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelState {
|
||||||
|
pub fn update_from_channel_state(
|
||||||
|
&mut self,
|
||||||
|
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
||||||
|
) {
|
||||||
|
if channel_state.has_position() {
|
||||||
|
self.position = channel_state.get_position();
|
||||||
|
}
|
||||||
|
if channel_state.has_parent() {
|
||||||
|
self.parent = Some(channel_state.get_parent());
|
||||||
|
}
|
||||||
|
if channel_state.has_name() {
|
||||||
|
self.name = channel_state.get_name().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ChannelsState {
|
||||||
|
pub channels: HashMap<ChannelId, ChannelState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelsState {
|
||||||
|
pub fn update_from_channel_state(
|
||||||
|
&mut self,
|
||||||
|
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
||||||
|
) {
|
||||||
|
self.channels
|
||||||
|
.entry(channel_state.get_channel_id())
|
||||||
|
.or_default()
|
||||||
|
.update_from_channel_state(channel_state);
|
||||||
|
|
||||||
|
self.update_channel_parents();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_from_channel_remove(
|
||||||
|
&mut self,
|
||||||
|
channel_remove: &mumble_protocol::control::msgs::ChannelRemove,
|
||||||
|
) {
|
||||||
|
self.channels.remove(&channel_remove.get_channel_id());
|
||||||
|
self.update_channel_parents();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_channel_parents(&mut self) {
|
||||||
|
// Zero out existing children
|
||||||
|
for state in self.channels.values_mut() {
|
||||||
|
state.children.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut to_sort: Vec<(ChannelId, Option<ChannelId>, i32, String)> = Vec::new();
|
||||||
|
for (id, state) in self.channels.iter() {
|
||||||
|
// Handle channels with no parent (the root channel)
|
||||||
|
let Some(parent_id) = state.parent else {
|
||||||
|
to_sort.push((*id, None, 0, state.name.clone()));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If a channel has a parent that we haven't gotten a channel
|
||||||
|
// state packet for, ignore it
|
||||||
|
if !self.channels.contains_key(&parent_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
to_sort.push((*id, Some(parent_id), state.position, state.name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos_name: HashMap<ChannelId, (i32, String)> = self
|
||||||
|
.channels
|
||||||
|
.iter()
|
||||||
|
.map(|(&id, state)| (id, (state.position, state.name.clone())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut updated: HashSet<ChannelId> = HashSet::new();
|
||||||
|
|
||||||
|
while updated.len() < to_sort.len() {
|
||||||
|
for &(id, ref parent_id, position, ref name) in &to_sort {
|
||||||
|
let Some(parent_id) = parent_id else {
|
||||||
|
updated.insert(id);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if updated.contains(&id) || !updated.contains(&parent_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap should never fail here since we pre filter
|
||||||
|
let parent = self.channels.get_mut(&parent_id).unwrap();
|
||||||
|
|
||||||
|
let mut insert_index = parent.children.len();
|
||||||
|
for (i, &child) in parent.children.iter().enumerate() {
|
||||||
|
let (p, ref n) = pos_name[&child];
|
||||||
|
if (position == p && name < n) || p > position {
|
||||||
|
insert_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.insert_before(insert_index, id);
|
||||||
|
updated.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ServerState {
|
||||||
|
pub channels_state: ChannelsState,
|
||||||
|
pub users: HashMap<UserId, UserState>,
|
||||||
|
pub chat: Vec<Chat>,
|
||||||
|
pub session: Option<UserId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
pub fn this_user(&self) -> Option<&UserState> {
|
||||||
|
self.users.get(&self.session?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub status: Signal<ConnectionState>,
|
||||||
|
pub server: Signal<ServerState>,
|
||||||
|
pub audio: Signal<AudioSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for State {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("State")
|
||||||
|
.field("status", &self.status.read())
|
||||||
|
.field("server", &self.server.read())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedState = Arc<State>;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
||||||
use df::tract::{DfParams, DfTract, RuntimeParams};
|
use df::tract::{DfParams, DfTract, RuntimeParams};
|
||||||
use dioxus::prelude::{asset, manganis, Asset};
|
|
||||||
use dioxus_asset_resolver::read_asset_bytes;
|
use dioxus_asset_resolver::read_asset_bytes;
|
||||||
|
use manganis::{asset, Asset};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
@@ -44,11 +44,12 @@ fn with_denoising_model<O>(spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract)
|
|||||||
let cell = Arc::new(AtomicCell::new(None));
|
let cell = Arc::new(AtomicCell::new(None));
|
||||||
let cell_task = cell.clone();
|
let cell_task = cell.clone();
|
||||||
*state = DenoisingModelState::Downloading(cell);
|
*state = DenoisingModelState::Downloading(cell);
|
||||||
|
let model = DF_MODEL.to_string();
|
||||||
spawn.spawn(async move {
|
spawn.spawn(async move {
|
||||||
let model_bytes = match read_asset_bytes(&DF_MODEL).await {
|
let model_bytes = match read_asset_bytes(&model).await {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("could not read denoising model from \"{DF_MODEL}\": {e:?}");
|
error!("could not read denoising model from \"{model}\": {e:?}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -96,20 +97,9 @@ pub struct AudioProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AudioProcessor {
|
impl AudioProcessor {
|
||||||
pub fn new_plain() -> Self {
|
pub fn new(denoise: bool) -> Self {
|
||||||
AudioProcessor {
|
AudioProcessor {
|
||||||
denoise: false,
|
denoise,
|
||||||
spawn: SpawnHandle::current(),
|
|
||||||
buffer: Vec::new(),
|
|
||||||
noise_floor: DEFAULT_NOISE_FLOOR,
|
|
||||||
was_transmitting: false,
|
|
||||||
hold_samples: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_denoising() -> Self {
|
|
||||||
AudioProcessor {
|
|
||||||
denoise: true,
|
|
||||||
spawn: SpawnHandle::current(),
|
spawn: SpawnHandle::current(),
|
||||||
buffer: Vec::new(),
|
buffer: Vec::new(),
|
||||||
noise_floor: DEFAULT_NOISE_FLOOR,
|
noise_floor: DEFAULT_NOISE_FLOOR,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -103,7 +103,9 @@ 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, state, event_rx, reader, writer).await
|
let (outgoing_send, outgoing_recv) = futures_channel::mpsc::unbounded();
|
||||||
|
spawn(crate::sender_loop(outgoing_recv, writer));
|
||||||
|
crate::network_loop(username, state, event_rx, outgoing_send, reader).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> {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use crate::effects::AudioProcessor;
|
use crate::effects::AudioProcessor;
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
@@ -112,7 +112,7 @@ impl super::AudioSystemInterface for NativeAudioSystem {
|
|||||||
);
|
);
|
||||||
let mut encoder =
|
let mut encoder =
|
||||||
opus::Encoder::new(SAMPLE_RATE, opus::Channels::Mono, opus::Application::Voip)?;
|
opus::Encoder::new(SAMPLE_RATE, opus::Channels::Mono, opus::Application::Voip)?;
|
||||||
let mut current_processor = AudioProcessor::new_plain();
|
let mut current_processor = AudioProcessor::new(false);
|
||||||
let mut output_buffer = Vec::new();
|
let mut output_buffer = Vec::new();
|
||||||
let processors = self.processors.clone();
|
let processors = self.processors.clone();
|
||||||
let error_callback = move |e: cpal::StreamError| error!("error recording: {e:?}");
|
let error_callback = move |e: cpal::StreamError| error!("error recording: {e:?}");
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
/// `cargo check` without any --feature flags.
|
/// `cargo check` without any --feature flags.
|
||||||
use crate::{app::SharedState, effects::AudioProcessor};
|
use crate::{app::SharedState, effects::AudioProcessor};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
@@ -2,9 +2,10 @@ 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;
|
||||||
use dioxus::prelude::*;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use js_sys::Float32Array;
|
use js_sys::Float32Array;
|
||||||
|
use manganis::asset;
|
||||||
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;
|
||||||
@@ -157,7 +158,7 @@ pub struct WebAudioSystem {
|
|||||||
processors: AudioProcessorSender,
|
processors: AudioProcessorSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> {
|
async fn attach_worklet(audio_context: &AudioContext, worklet_url: &str) -> Result<(), Error> {
|
||||||
// Create worklets to process mic and speaker audio
|
// Create worklets to process mic and speaker audio
|
||||||
// Speaker audio processing worklet only required on
|
// Speaker audio processing worklet only required on
|
||||||
// browsers that don't support MediaStreamTrackGenerator
|
// browsers that don't support MediaStreamTrackGenerator
|
||||||
@@ -170,12 +171,11 @@ async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> {
|
|||||||
)
|
)
|
||||||
.ey()?;
|
.ey()?;
|
||||||
|
|
||||||
let module = asset!("assets/rust_audio_worklet.js").to_string();
|
info!("loading mic worklet from {worklet_url:?}");
|
||||||
info!("loading mic worklet from {module:?}");
|
|
||||||
audio_context
|
audio_context
|
||||||
.audio_worklet()
|
.audio_worklet()
|
||||||
.ey()?
|
.ey()?
|
||||||
.add_module_with_options(&module, &options)
|
.add_module_with_options(worklet_url, &options)
|
||||||
.ey()?
|
.ey()?
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
@@ -190,7 +190,11 @@ impl super::AudioSystemInterface for WebAudioSystem {
|
|||||||
// Create MediaStreams to playback decoded audio
|
// Create MediaStreams to playback decoded audio
|
||||||
// The audio context is used to reproduce audio.
|
// The audio context is used to reproduce audio.
|
||||||
let webctx = configure_audio_context();
|
let webctx = configure_audio_context();
|
||||||
attach_worklet(&webctx).await?;
|
attach_worklet(
|
||||||
|
&webctx,
|
||||||
|
&asset!("/assets/rust_audio_worklet.js").to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let processors = AudioProcessorSender::default();
|
let processors = AudioProcessorSender::default();
|
||||||
|
|
||||||
@@ -381,7 +385,7 @@ async fn run_encoder_worklet(
|
|||||||
audio_encoder.configure(&encoder_config);
|
audio_encoder.configure(&encoder_config);
|
||||||
info!("created audio encoder");
|
info!("created audio encoder");
|
||||||
|
|
||||||
let mut current_processor = AudioProcessor::new_plain();
|
let mut current_processor = AudioProcessor::new(false);
|
||||||
let onmessage: Closure<dyn FnMut(MessageEvent)> = Closure::new(move |event: MessageEvent| {
|
let onmessage: Closure<dyn FnMut(MessageEvent)> = Closure::new(move |event: MessageEvent| {
|
||||||
if let Some(new_processor) = processors.take() {
|
if let Some(new_processor) = processors.take() {
|
||||||
current_processor = new_processor;
|
current_processor = new_processor;
|
||||||
@@ -494,7 +498,9 @@ 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, state, event_rx, reader, writer).await
|
let (outgoing_send, outgoing_recv) = futures_channel::mpsc::unbounded();
|
||||||
|
spawn(crate::sender_loop(outgoing_recv, writer));
|
||||||
|
crate::network_loop(username, state, event_rx, outgoing_send, reader).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mod app;
|
||||||
|
mod effects;
|
||||||
|
mod imp;
|
||||||
|
mod mainloop;
|
||||||
|
mod msghtml;
|
||||||
|
|
||||||
|
pub use app::*;
|
||||||
|
pub use imp::*;
|
||||||
|
pub use mainloop::*;
|
||||||
|
pub use mime_guess;
|
||||||
|
pub use reqwest;
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
use app::Chat;
|
use crate::msghtml::process_message_html;
|
||||||
use app::Command;
|
use crate::AudioSettings;
|
||||||
use app::ConnectionState;
|
use crate::Chat;
|
||||||
|
use crate::Command;
|
||||||
|
use crate::ConnectionState;
|
||||||
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};
|
||||||
use dioxus::prelude::*;
|
use dioxus_signals::ReadableExt as _;
|
||||||
|
use dioxus_signals::WritableExt as _;
|
||||||
use futures::select;
|
use futures::select;
|
||||||
use futures::AsyncRead;
|
use futures::AsyncRead;
|
||||||
use futures::AsyncWrite;
|
use futures::AsyncWrite;
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
use futures::SinkExt as _;
|
use futures::SinkExt as _;
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use futures_channel::mpsc::UnboundedSender;
|
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
use msghtml::process_message_html;
|
|
||||||
use mumble_protocol::control::msgs;
|
use mumble_protocol::control::msgs;
|
||||||
use mumble_protocol::control::ControlCodec;
|
use mumble_protocol::control::ControlCodec;
|
||||||
use mumble_protocol::control::ControlPacket;
|
use mumble_protocol::control::ControlPacket;
|
||||||
@@ -26,20 +28,14 @@ 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::SharedState;
|
||||||
use crate::app::State;
|
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,
|
spawn, AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _,
|
||||||
PlatformInterface as _,
|
Platform, PlatformInterface as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod app;
|
|
||||||
mod effects;
|
|
||||||
pub mod imp;
|
|
||||||
mod msghtml;
|
|
||||||
|
|
||||||
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
|
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
|
||||||
loop {
|
loop {
|
||||||
let Some(Command::Connect {
|
let Some(Command::Connect {
|
||||||
@@ -65,28 +61,30 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
|
pub(crate) async fn sender_loop<W: AsyncWrite + Unpin + 'static>(
|
||||||
|
mut outgoing: UnboundedReceiver<ControlPacket<Serverbound>>,
|
||||||
|
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
||||||
|
) {
|
||||||
|
while let Some(msg) = outgoing.next().await {
|
||||||
|
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
|
||||||
|
info!("sending packet {:#?}", msg);
|
||||||
|
}
|
||||||
|
if let Err(e) = writer.send(msg).await {
|
||||||
|
error!("error sending packet {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
||||||
username: String,
|
username: String,
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
|
mut outgoing: UnboundedSender<ControlPacket<Serverbound>>,
|
||||||
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
||||||
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let audio_settings = state.audio.read().clone();
|
let audio_settings = state.audio.read().clone();
|
||||||
|
|
||||||
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
|
|
||||||
spawn(async move {
|
|
||||||
while let Some(msg) = writer_recv_chan.next().await {
|
|
||||||
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
|
|
||||||
info!("sending packet {:#?}", msg);
|
|
||||||
}
|
|
||||||
if let Err(e) = writer.send(msg).await {
|
|
||||||
error!("error sending packet {:?}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get version packet
|
// Get version packet
|
||||||
let version = match reader.next().await {
|
let version = match reader.next().await {
|
||||||
Some(Ok(v)) => v,
|
Some(Ok(v)) => v,
|
||||||
@@ -100,17 +98,17 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
msg.set_version(0x000010204);
|
msg.set_version(0x000010204);
|
||||||
msg.set_release(format!("{} {}", "mumbleweb2", "6.9.0"));
|
msg.set_release(format!("{} {}", "mumbleweb2", "6.9.0"));
|
||||||
//msg.set_os("Chrome".to_string());
|
//msg.set_os("Chrome".to_string());
|
||||||
send_chan.send(msg.into()).await.unwrap();
|
outgoing.send(msg.into()).await.unwrap();
|
||||||
|
|
||||||
// Send authenticate packet
|
// Send authenticate packet
|
||||||
let mut msg = msgs::Authenticate::new();
|
let mut msg = msgs::Authenticate::new();
|
||||||
msg.set_username(username);
|
msg.set_username(username);
|
||||||
msg.set_opus(true);
|
msg.set_opus(true);
|
||||||
send_chan.send(msg.into()).await.unwrap();
|
outgoing.send(msg.into()).await.unwrap();
|
||||||
|
|
||||||
// Spawn worker to send pings
|
// Spawn worker to send pings
|
||||||
{
|
{
|
||||||
let mut send_chan = send_chan.clone();
|
let mut send_chan = outgoing.clone();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
if let Err(_) = send_chan.send(msgs::Ping::new().into()).await {
|
if let Err(_) = send_chan.send(msgs::Ping::new().into()).await {
|
||||||
@@ -123,11 +121,9 @@ 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(audio_settings.denoise));
|
||||||
audio.set_processor(AudioProcessor::new_denoising());
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
let send_chan = send_chan.clone();
|
let send_chan = outgoing.clone();
|
||||||
let mut sequence_num = 0;
|
let mut sequence_num = 0;
|
||||||
if let Err(err) = audio.start_recording(move |opus_frame, is_terminator| {
|
if let Err(err) = audio.start_recording(move |opus_frame, is_terminator| {
|
||||||
let _ =
|
let _ =
|
||||||
@@ -179,7 +175,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, &state);
|
let res = accept_command(command, &mut outgoing, &mut audio, &state);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
info!("error accepting command {:?}", err)
|
info!("error accepting command {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -189,7 +185,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = send_chan.close();
|
let _ = outgoing.close();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -301,11 +297,7 @@ fn accept_command(
|
|||||||
}
|
}
|
||||||
Connect { .. } | Disconnect => (),
|
Connect { .. } | Disconnect => (),
|
||||||
UpdateAudioSettings(AudioSettings { denoise }) => {
|
UpdateAudioSettings(AudioSettings { denoise }) => {
|
||||||
if denoise {
|
audio.set_processor(AudioProcessor::new(denoise));
|
||||||
audio.set_processor(AudioProcessor::new_denoising());
|
|
||||||
} else {
|
|
||||||
audio.set_processor(AudioProcessor::new_plain());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+6
-129
@@ -4,103 +4,11 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Web Dependencies
|
|
||||||
# ================
|
|
||||||
dioxus-web = { version = "0.7.1", optional = true }
|
|
||||||
wasm-bindgen = { version = "^0.2.92", optional = true }
|
|
||||||
wasm-bindgen-futures = { version = "^0.4.42", optional = true }
|
|
||||||
wasm-streams = { version = "^0.4.0", optional = true }
|
|
||||||
serde-wasm-bindgen = { version = "^0.6.5", optional = true }
|
|
||||||
js-sys = { version = "^0.3.70", optional = true }
|
|
||||||
web-sys = { version = "^0.3.72", features = [
|
|
||||||
"WebTransport",
|
|
||||||
"console",
|
|
||||||
"WebTransportOptions",
|
|
||||||
"WebTransportBidirectionalStream",
|
|
||||||
"WebTransportSendStream",
|
|
||||||
"WebTransportReceiveStream",
|
|
||||||
"Navigator",
|
|
||||||
"MediaDevices",
|
|
||||||
"AudioDecoder",
|
|
||||||
"AudioDecoderInit",
|
|
||||||
"AudioData",
|
|
||||||
"AudioEncoderConfig",
|
|
||||||
"AudioDecoderConfig",
|
|
||||||
"EncodedAudioChunk",
|
|
||||||
"EncodedAudioChunkInit",
|
|
||||||
"EncodedAudioChunkType",
|
|
||||||
"CodecState",
|
|
||||||
"AudioContext",
|
|
||||||
"AudioContextOptions",
|
|
||||||
"MediaStream",
|
|
||||||
"GainNode",
|
|
||||||
"MediaStreamAudioSourceNode",
|
|
||||||
"BaseAudioContext",
|
|
||||||
"AudioDestinationNode",
|
|
||||||
"AudioWorkletNode",
|
|
||||||
"AudioWorklet",
|
|
||||||
"AudioWorkletProcessor",
|
|
||||||
"MessagePort",
|
|
||||||
"MediaStreamConstraints",
|
|
||||||
"WorkletOptions",
|
|
||||||
"AudioEncoder",
|
|
||||||
"AudioEncoderInit",
|
|
||||||
"AudioDataInit",
|
|
||||||
"HtmlAnchorElement",
|
|
||||||
"Url",
|
|
||||||
"Blob",
|
|
||||||
"AudioDataCopyToOptions",
|
|
||||||
"AudioSampleFormat",
|
|
||||||
"Storage",
|
|
||||||
], optional = true }
|
|
||||||
gloo-timers = { version = "^0.3.0", features = ["futures"], optional = true }
|
|
||||||
tracing-web = { version = "^0.1.3", optional = true }
|
|
||||||
|
|
||||||
# Desktop Dependecies
|
|
||||||
# ===================
|
|
||||||
tokio = { version = "^1.41.1", features = ["net", "rt"], optional = true }
|
|
||||||
tokio-rustls = { version = "^0.26.0", optional = true }
|
|
||||||
opus = { version = "0.3.0", optional = true }
|
|
||||||
cpal = { version = "0.15.3", optional = true }
|
|
||||||
dasp_ring_buffer = { version = "0.11.0", optional = true }
|
|
||||||
etcetera = { version = "0.10.0", optional = true }
|
|
||||||
|
|
||||||
# Base Dependencies
|
|
||||||
# ================
|
|
||||||
dioxus = { version = "0.7.2" }
|
dioxus = { version = "0.7.2" }
|
||||||
once_cell = "1.19.0"
|
dioxus-web = { version = "0.7.2", optional = true }
|
||||||
asynchronous-codec = { workspace = true }
|
mumble-web2-client = { version = "0.1.0", path = "../client" }
|
||||||
futures = "^0.3.30"
|
mumble-web2-common = { version = "0.1.0", path = "../common" }
|
||||||
merge-io = "^0.3.0"
|
|
||||||
mumble-protocol = { workspace = true }
|
|
||||||
serde_json = "1"
|
|
||||||
tokio-util = { version = "^0.7.11", features = ["codec", "compat"] }
|
|
||||||
byteorder = "1.5"
|
|
||||||
ogg = "^0.9.1"
|
|
||||||
ordermap = "^0.5.3"
|
|
||||||
html-purifier = "^0.3.0"
|
|
||||||
markdown = "^0.3.0"
|
|
||||||
futures-channel = "^0.3.30"
|
|
||||||
mumble-web2-common = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
tracing-subscriber = { version = "^0.3.18", features = ["ansi"] }
|
|
||||||
tracing = "^0.1.40"
|
|
||||||
color-eyre = "^0.6.3"
|
color-eyre = "^0.6.3"
|
||||||
crossbeam-queue = "^0.3.11"
|
|
||||||
lol_html = "^2.2.0"
|
|
||||||
base64 = "^0.22"
|
|
||||||
mime_guess = "^2.0.5"
|
|
||||||
async_cell = "^0.2.3"
|
|
||||||
reqwest = { version = "^0.12.22", features = ["json"] }
|
|
||||||
dioxus-asset-resolver = "0.7.2"
|
|
||||||
|
|
||||||
|
|
||||||
# Denoising
|
|
||||||
# =========
|
|
||||||
deep_filter = { git = "https://github.com/Rikorose/DeepFilterNet.git", rev = "d375b2d8309e0935d165700c91da9de862a99c31", features = [
|
|
||||||
"tract",
|
|
||||||
] }
|
|
||||||
crossbeam = "0.8.4"
|
|
||||||
|
|
||||||
# Platform Integration
|
# Platform Integration
|
||||||
# ====================
|
# ====================
|
||||||
@@ -108,50 +16,19 @@ crossbeam = "0.8.4"
|
|||||||
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos", target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos", target_arch = "wasm32"))'.dependencies]
|
||||||
rfd = { git = "https://github.com/PolyMeilex/rfd.git", version = "^0.16.0", default-features = false, optional = true }
|
rfd = { git = "https://github.com/PolyMeilex/rfd.git", version = "^0.16.0", default-features = false, optional = true }
|
||||||
|
|
||||||
# Android dependencies for requesting permissions
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
|
||||||
android-permissions = "0.1.2"
|
|
||||||
jni = "0.21.1"
|
|
||||||
ndk-context = "0.1.1"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
tract-hir = "=0.12.4"
|
|
||||||
tract-core = "=0.12.4"
|
|
||||||
tract-onnx = "=0.12.4"
|
|
||||||
tract-pulse = "=0.12.4"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
web = [
|
web = [
|
||||||
"dioxus/web",
|
"dioxus/web",
|
||||||
"dioxus-web",
|
"dioxus-web",
|
||||||
"wasm-bindgen",
|
"mumble-web2-client/web",
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"wasm-streams",
|
|
||||||
"serde-wasm-bindgen",
|
|
||||||
"js-sys",
|
|
||||||
"web-sys",
|
|
||||||
"gloo-timers",
|
|
||||||
"tracing-web",
|
|
||||||
"deep_filter/wasm",
|
|
||||||
"rfd",
|
"rfd",
|
||||||
]
|
]
|
||||||
desktop = [
|
desktop = [
|
||||||
"dioxus/desktop",
|
"dioxus/desktop",
|
||||||
"tokio",
|
"mumble-web2-client/desktop",
|
||||||
"tokio-rustls",
|
|
||||||
"tracing-subscriber/env-filter",
|
|
||||||
"opus",
|
|
||||||
"cpal",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
"rfd/xdg-portal",
|
"rfd/xdg-portal",
|
||||||
"etcetera",
|
|
||||||
]
|
]
|
||||||
mobile = [
|
mobile = [
|
||||||
"dioxus/mobile",
|
"dioxus/mobile",
|
||||||
"tokio",
|
"mumble-web2-client/mobile"
|
||||||
"tokio-rustls",
|
|
||||||
"tracing-subscriber/env-filter",
|
|
||||||
"opus",
|
|
||||||
"cpal",
|
|
||||||
"dasp_ring_buffer",
|
|
||||||
]
|
]
|
||||||
|
|||||||
-928
@@ -1,928 +0,0 @@
|
|||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use mime_guess::Mime;
|
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
|
||||||
use ordermap::OrderSet;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::{fmt, sync::Arc};
|
|
||||||
|
|
||||||
use crate::imp::{ConfigSystem, ConfigSystemInterface as _, Platform, PlatformInterface as _};
|
|
||||||
|
|
||||||
pub type ChannelId = u32;
|
|
||||||
pub type UserId = u32;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ConnectionState {
|
|
||||||
Disconnected,
|
|
||||||
Connecting,
|
|
||||||
Connected,
|
|
||||||
Failed(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AudioSettings {
|
|
||||||
pub denoise: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Command {
|
|
||||||
Connect {
|
|
||||||
address: String,
|
|
||||||
username: String,
|
|
||||||
config: ProxyOverrides,
|
|
||||||
},
|
|
||||||
SendChat {
|
|
||||||
markdown: String,
|
|
||||||
channels: Vec<ChannelId>,
|
|
||||||
},
|
|
||||||
SendFile {
|
|
||||||
bytes: Vec<u8>,
|
|
||||||
name: String,
|
|
||||||
mime: Option<Mime>,
|
|
||||||
channels: Vec<ChannelId>,
|
|
||||||
},
|
|
||||||
SetMute {
|
|
||||||
mute: bool,
|
|
||||||
},
|
|
||||||
SetDeaf {
|
|
||||||
deaf: bool,
|
|
||||||
},
|
|
||||||
EnterChannel {
|
|
||||||
channel: ChannelId,
|
|
||||||
user: UserId,
|
|
||||||
},
|
|
||||||
UpdateAudioSettings(AudioSettings),
|
|
||||||
Disconnect,
|
|
||||||
}
|
|
||||||
|
|
||||||
use Command::*;
|
|
||||||
use ConnectionState::*;
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct UserState {
|
|
||||||
pub name: String,
|
|
||||||
pub channel: ChannelId,
|
|
||||||
pub deaf: bool,
|
|
||||||
pub mute: bool,
|
|
||||||
pub suppress: bool,
|
|
||||||
pub self_deaf: bool,
|
|
||||||
pub self_mute: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserState {
|
|
||||||
pub fn icon(&self) -> UserIcon {
|
|
||||||
if self.deaf || self.self_deaf {
|
|
||||||
UserIcon::Deafened
|
|
||||||
} else if self.mute || self.self_mute {
|
|
||||||
UserIcon::Muted
|
|
||||||
} else if self.suppress {
|
|
||||||
UserIcon::Suppressed
|
|
||||||
} else {
|
|
||||||
UserIcon::Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Chat {
|
|
||||||
pub raw: String,
|
|
||||||
pub dangerous_html: String,
|
|
||||||
pub sender: Option<UserId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct ChannelState {
|
|
||||||
pub name: String,
|
|
||||||
pub children: OrderSet<ChannelId>,
|
|
||||||
pub users: OrderSet<UserId>,
|
|
||||||
pub parent: Option<ChannelId>,
|
|
||||||
pub position: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChannelState {
|
|
||||||
pub fn update_from_channel_state(
|
|
||||||
&mut self,
|
|
||||||
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
|
||||||
) {
|
|
||||||
if channel_state.has_position() {
|
|
||||||
self.position = channel_state.get_position();
|
|
||||||
}
|
|
||||||
if channel_state.has_parent() {
|
|
||||||
self.parent = Some(channel_state.get_parent());
|
|
||||||
}
|
|
||||||
if channel_state.has_name() {
|
|
||||||
self.name = channel_state.get_name().to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct ChannelsState {
|
|
||||||
pub channels: HashMap<ChannelId, ChannelState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChannelsState {
|
|
||||||
pub fn update_from_channel_state(
|
|
||||||
&mut self,
|
|
||||||
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
|
||||||
) {
|
|
||||||
self.channels
|
|
||||||
.entry(channel_state.get_channel_id())
|
|
||||||
.or_default()
|
|
||||||
.update_from_channel_state(channel_state);
|
|
||||||
|
|
||||||
self.update_channel_parents();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_from_channel_remove(
|
|
||||||
&mut self,
|
|
||||||
channel_remove: &mumble_protocol::control::msgs::ChannelRemove,
|
|
||||||
) {
|
|
||||||
self.channels.remove(&channel_remove.get_channel_id());
|
|
||||||
self.update_channel_parents();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_channel_parents(&mut self) {
|
|
||||||
// Zero out existing children
|
|
||||||
for state in self.channels.values_mut() {
|
|
||||||
state.children.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut to_sort: Vec<(ChannelId, Option<ChannelId>, i32, String)> = Vec::new();
|
|
||||||
for (id, state) in self.channels.iter() {
|
|
||||||
// Handle channels with no parent (the root channel)
|
|
||||||
let Some(parent_id) = state.parent else {
|
|
||||||
to_sort.push((*id, None, 0, state.name.clone()));
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If a channel has a parent that we haven't gotten a channel
|
|
||||||
// state packet for, ignore it
|
|
||||||
if !self.channels.contains_key(&parent_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
to_sort.push((*id, Some(parent_id), state.position, state.name.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let pos_name: HashMap<ChannelId, (i32, String)> = self
|
|
||||||
.channels
|
|
||||||
.iter()
|
|
||||||
.map(|(&id, state)| (id, (state.position, state.name.clone())))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut updated: HashSet<ChannelId> = HashSet::new();
|
|
||||||
|
|
||||||
while updated.len() < to_sort.len() {
|
|
||||||
for &(id, ref parent_id, position, ref name) in &to_sort {
|
|
||||||
let Some(parent_id) = parent_id else {
|
|
||||||
updated.insert(id);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if updated.contains(&id) || !updated.contains(&parent_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap should never fail here since we pre filter
|
|
||||||
let parent = self.channels.get_mut(&parent_id).unwrap();
|
|
||||||
|
|
||||||
let mut insert_index = parent.children.len();
|
|
||||||
for (i, &child) in parent.children.iter().enumerate() {
|
|
||||||
let (p, ref n) = pos_name[&child];
|
|
||||||
if (position == p && name < n) || p > position {
|
|
||||||
insert_index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.children.insert_before(insert_index, id);
|
|
||||||
updated.insert(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct ServerState {
|
|
||||||
pub channels_state: ChannelsState,
|
|
||||||
pub users: HashMap<UserId, UserState>,
|
|
||||||
pub chat: Vec<Chat>,
|
|
||||||
pub session: Option<UserId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerState {
|
|
||||||
pub fn this_user(&self) -> Option<&UserState> {
|
|
||||||
self.users.get(&self.session?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct State {
|
|
||||||
pub status: Signal<ConnectionState>,
|
|
||||||
pub server: Signal<ServerState>,
|
|
||||||
pub audio: Signal<AudioSettings>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for State {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
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)]
|
|
||||||
pub enum UserIcon {
|
|
||||||
Normal,
|
|
||||||
Muted,
|
|
||||||
Deafened,
|
|
||||||
Suppressed,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserIcon {
|
|
||||||
pub fn url(self) -> Option<Asset> {
|
|
||||||
// speaker from https://www.svgrepo.com/collection/ikono-bold-line-icons/
|
|
||||||
// mic from https://www.svgrepo.com/collection/hashicorp-line-interface-icons/
|
|
||||||
|
|
||||||
use UserIcon::*;
|
|
||||||
Some(match self {
|
|
||||||
Normal => asset!("assets/mic-svgrepo-com.svg"),
|
|
||||||
Muted | Suppressed => asset!("assets/mic-off-svgrepo-com.svg"),
|
|
||||||
Deafened => asset!("assets/speaker-muted-svgrepo-com.svg"),
|
|
||||||
None => return Option::None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
|
|
||||||
let color = match icon {
|
|
||||||
UserIcon::Normal => "var(--accent-normal)",
|
|
||||||
UserIcon::Muted => "var(--accent-muted)",
|
|
||||||
UserIcon::Suppressed | UserIcon::Deafened => "var(--accent-deafened)",
|
|
||||||
UserIcon::None => "var(--accent-normal)",
|
|
||||||
};
|
|
||||||
|
|
||||||
rsx!(
|
|
||||||
div {
|
|
||||||
class: match isself { true => "userpil is_self", false => "userpil" },
|
|
||||||
style: "background-color: {color}",
|
|
||||||
{ icon.url().map(|url| rsx!(img { src: url })) }
|
|
||||||
"\u{00A0}{name}\u{00A0}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn User(id: UserId) -> Element {
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let server = state.server.read();
|
|
||||||
match server.users.get(&id) {
|
|
||||||
Some(state) => rsx!(UserPill {
|
|
||||||
name: state.name.clone(),
|
|
||||||
icon: state.icon(),
|
|
||||||
isself: server.session.unwrap() == id,
|
|
||||||
}),
|
|
||||||
None => rsx!(UserPill {
|
|
||||||
name: format!("unknown user ({id})"),
|
|
||||||
icon: UserIcon::None,
|
|
||||||
isself: false,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Channel(id: ChannelId) -> Element {
|
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let server = state.server.read();
|
|
||||||
let user = server.session.unwrap();
|
|
||||||
let Some(state) = server.channels_state.channels.get(&id) else {
|
|
||||||
return rsx!("missing channel {id}");
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut open = use_signal(|| true);
|
|
||||||
|
|
||||||
let has_children = !state.users.is_empty() || !state.children.is_empty();
|
|
||||||
|
|
||||||
rsx!(
|
|
||||||
div {
|
|
||||||
class: "channel_details",
|
|
||||||
|
|
||||||
div {
|
|
||||||
class: "channel_header",
|
|
||||||
// Arrow: only toggles open
|
|
||||||
if has_children {
|
|
||||||
span {
|
|
||||||
class: "channel_arrow",
|
|
||||||
onclick: move |evt| {
|
|
||||||
evt.stop_propagation();
|
|
||||||
evt.prevent_default();
|
|
||||||
let mut w = open.write();
|
|
||||||
*w = !*w;
|
|
||||||
},
|
|
||||||
if *open.read() { "▾" } else { "▸" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
span {
|
|
||||||
class: "channel_arrow channel_arrow--placeholder",
|
|
||||||
" "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clickable row area (everything except the arrow)
|
|
||||||
div {
|
|
||||||
class: "channel_row_click",
|
|
||||||
ondblclick: move |evt| {
|
|
||||||
evt.stop_propagation();
|
|
||||||
evt.prevent_default();
|
|
||||||
net.send(EnterChannel { channel: id, user })
|
|
||||||
},
|
|
||||||
// remove dblclick from the inner span
|
|
||||||
span {
|
|
||||||
class: "channel_title",
|
|
||||||
"{state.name}"
|
|
||||||
}
|
|
||||||
// if you add icons/badges later, put them here too
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if *open.read() && has_children {
|
|
||||||
div {
|
|
||||||
class: "channel_children",
|
|
||||||
for id in state.users.iter() {
|
|
||||||
User { id: *id }
|
|
||||||
}
|
|
||||||
for child in state.children.iter() {
|
|
||||||
Channel { id: *child }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "desktop", feature = "web"))]
|
|
||||||
pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let channels = if let Some(user) = state.server.read().this_user() {
|
|
||||||
vec![user.channel]
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let dialog = rfd::AsyncFileDialog::new().pick_file();
|
|
||||||
let sender = net.tx();
|
|
||||||
spawn(async move {
|
|
||||||
let Some(handle) = dialog.await else { return };
|
|
||||||
let name = handle.file_name();
|
|
||||||
let bytes = handle.read().await;
|
|
||||||
let mime = mime_guess::from_path(&name).first();
|
|
||||||
let _ = sender.unbounded_send(SendFile {
|
|
||||||
bytes,
|
|
||||||
name,
|
|
||||||
mime,
|
|
||||||
channels,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#[cfg(not(any(feature = "desktop", feature = "web")))]
|
|
||||||
pub fn pick_and_send_file(net: &Coroutine<Command>) {}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn ChatView() -> Element {
|
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let server = state.server.read();
|
|
||||||
let mut draft = use_signal(|| "".to_string());
|
|
||||||
|
|
||||||
let mut do_send = move || {
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let server = state.server.read();
|
|
||||||
if let Some(user) = server.this_user() {
|
|
||||||
net.send(SendChat {
|
|
||||||
markdown: draft.write().split_off(0),
|
|
||||||
channels: vec![user.channel],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rsx!(
|
|
||||||
div {
|
|
||||||
class: "chat_panel",
|
|
||||||
div {
|
|
||||||
class: "chat_history",
|
|
||||||
for chat in server.chat.iter() {
|
|
||||||
div {
|
|
||||||
class: "chat_message",
|
|
||||||
if let Some(sender) = chat.sender.and_then(|u| server.users.get(&u)) {
|
|
||||||
UserPill {
|
|
||||||
name: sender.name.clone(),
|
|
||||||
icon: UserIcon::None,
|
|
||||||
isself: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
dangerous_inner_html: "{chat.dangerous_html}",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "chat_box_wrapper",
|
|
||||||
div {
|
|
||||||
class: "chat_box",
|
|
||||||
input {
|
|
||||||
placeholder: "say something",
|
|
||||||
value: "{draft.read()}",
|
|
||||||
oninput: move |evt| draft.set(evt.value().clone()),
|
|
||||||
onkeypress: move |evt: Event<KeyboardData>| {
|
|
||||||
if evt.code() == Code::Enter && evt.modifiers().is_empty() {
|
|
||||||
do_send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
span {
|
|
||||||
onclick: move |_| pick_and_send_file(&net),
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
style: "color: rgba(255, 255, 255, 0.5); font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; vertical-align: middle; font-size: 35px; user-select: none;",
|
|
||||||
"attach_file",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
span {
|
|
||||||
onclick: move |_| do_send(),
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
style: "color: rgba(255, 255, 255, 0.5); font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; vertical-align: middle; font-size: 35px; user-select: none;",
|
|
||||||
"send",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//button {
|
|
||||||
// onclick: move |_| do_send(),
|
|
||||||
// "Send"
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let status = &state.status;
|
|
||||||
let server = state.server.read();
|
|
||||||
let audio = state.audio.read();
|
|
||||||
let Some(&UserState {
|
|
||||||
deaf,
|
|
||||||
self_deaf,
|
|
||||||
mute,
|
|
||||||
suppress,
|
|
||||||
self_mute,
|
|
||||||
ref name,
|
|
||||||
channel,
|
|
||||||
..
|
|
||||||
}) = server.this_user()
|
|
||||||
else {
|
|
||||||
return rsx!();
|
|
||||||
};
|
|
||||||
|
|
||||||
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
|
||||||
|
|
||||||
let proxy_url = overrides
|
|
||||||
.read_unchecked()
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|overrides| overrides.proxy_url.clone());
|
|
||||||
|
|
||||||
let connecting_color = "yellow";
|
|
||||||
let connected_color = "oklch(0.55 0.1184 141.35)";
|
|
||||||
let disconnected_color = "gray";
|
|
||||||
let failed_color = "red";
|
|
||||||
|
|
||||||
let connection_status = match &*status.read() {
|
|
||||||
Connecting => rsx! {
|
|
||||||
div {
|
|
||||||
class: "connection_status",
|
|
||||||
style: "color: {connecting_color};",
|
|
||||||
div {
|
|
||||||
span {
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
"signal_cellular_alt_2_bar"
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
class: "status_text",
|
|
||||||
" Connecting"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Connected => rsx! {
|
|
||||||
div {
|
|
||||||
class: "connection_status",
|
|
||||||
div {
|
|
||||||
style: "color: {connected_color};",
|
|
||||||
span {
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
"signal_cellular_alt"
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
class: "status_text",
|
|
||||||
" Connected"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "channel_text",
|
|
||||||
span { "{current_channel_name}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Disconnected => rsx! {
|
|
||||||
div {
|
|
||||||
class: "connection_status",
|
|
||||||
style: "color: {disconnected_color};",
|
|
||||||
div {
|
|
||||||
span {
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
"signal_disconnected"
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
class: "status_text",
|
|
||||||
" Disconnected"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Failed(_) => rsx! {
|
|
||||||
div {
|
|
||||||
class: "connection_status",
|
|
||||||
style: "color: {failed_color};",
|
|
||||||
div {
|
|
||||||
span {
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
"error"
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
class: "status_text",
|
|
||||||
" Failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
rsx!(
|
|
||||||
// Server control
|
|
||||||
div {
|
|
||||||
class: "button_row",
|
|
||||||
div {
|
|
||||||
{connection_status}
|
|
||||||
}
|
|
||||||
span { class: "spacer" }
|
|
||||||
button {
|
|
||||||
class: "toggle_button",
|
|
||||||
onclick: move |_| net.send(Disconnect),
|
|
||||||
span {
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
"signal_disconnected"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hr { style: "width: 100%;" }
|
|
||||||
// User control
|
|
||||||
div {
|
|
||||||
class: "button_row",
|
|
||||||
button {
|
|
||||||
class: "user_edit_button",
|
|
||||||
span {
|
|
||||||
class: "material-symbols-outlined",
|
|
||||||
style: "color: oklch(0.65 0.2245 28.06);",
|
|
||||||
"person_edit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "user_info",
|
|
||||||
div {
|
|
||||||
span { class: "user_name", "{name}" }
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
span { class: "user_data", "some data" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span { class: "spacer" }
|
|
||||||
button {
|
|
||||||
class: match audio.denoise {
|
|
||||||
true => "toggle_button is_on",
|
|
||||||
false => "toggle_button",
|
|
||||||
},
|
|
||||||
role: "switch",
|
|
||||||
aria_checked: audio.denoise,
|
|
||||||
onclick: move |_| {
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let mut audio = state.audio.read().clone();
|
|
||||||
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 audio.denoise {
|
|
||||||
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
|
||||||
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
class: match mute || suppress || self_mute {
|
|
||||||
true => "toggle_button is_on",
|
|
||||||
false => "toggle_button",
|
|
||||||
},
|
|
||||||
role: "switch",
|
|
||||||
aria_checked: mute || suppress || self_mute,
|
|
||||||
disabled: mute || suppress,
|
|
||||||
onclick: move |_| net.send(SetMute { mute: !self_mute }),
|
|
||||||
match mute || suppress || self_mute {
|
|
||||||
true => rsx!(span { class: "material-symbols-outlined", "mic_off"}),
|
|
||||||
false => rsx!(span { class: "material-symbols-outlined", "mic"}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
class: match deaf || self_deaf {
|
|
||||||
true => "toggle_button in_on",
|
|
||||||
false => "toggle_button",
|
|
||||||
},
|
|
||||||
role: "switch",
|
|
||||||
aria_checked: deaf || self_deaf,
|
|
||||||
disabled: deaf,
|
|
||||||
onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }),
|
|
||||||
match deaf || self_deaf {
|
|
||||||
true => rsx!(span { class: "material-symbols-outlined", "volume_off"}),
|
|
||||||
false => rsx!(span { class: "material-symbols-outlined", "volume_up"}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
|
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let server = state.server.read();
|
|
||||||
let Some(&UserState {
|
|
||||||
deaf,
|
|
||||||
self_deaf,
|
|
||||||
mute,
|
|
||||||
self_mute,
|
|
||||||
..
|
|
||||||
}) = server.this_user()
|
|
||||||
else {
|
|
||||||
return rsx!();
|
|
||||||
};
|
|
||||||
|
|
||||||
rsx!(
|
|
||||||
div {
|
|
||||||
class: "server_grid",
|
|
||||||
div {
|
|
||||||
class: "server_channel_box",
|
|
||||||
for (id, state) in server.channels_state.channels.iter() {
|
|
||||||
if state.parent.is_none() {
|
|
||||||
Channel { id: *id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "server_chat_box",
|
|
||||||
ChatView {}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "server_control_box",
|
|
||||||
ControlView { overrides }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
|
||||||
let user_config = use_context::<ConfigSystem>();
|
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
|
||||||
|
|
||||||
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
|
||||||
use_resource(move || async move {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
loop {
|
|
||||||
*last_status.write_unchecked() = Some(Platform::get_status(&client).await);
|
|
||||||
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut address_input = use_signal(|| user_config.config_get::<String>("server_url"));
|
|
||||||
let address = use_memo(move || {
|
|
||||||
if let Some(addr) = address_input() {
|
|
||||||
addr.clone()
|
|
||||||
} else {
|
|
||||||
overrides()
|
|
||||||
.and_then(|c| c.proxy_url.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut username = use_signal(|| {
|
|
||||||
user_config
|
|
||||||
.config_get::<String>("username")
|
|
||||||
.unwrap_or(String::new())
|
|
||||||
});
|
|
||||||
|
|
||||||
let do_connect = move |_| {
|
|
||||||
let _ = user_config.config_set::<String>("username", &username.read());
|
|
||||||
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
|
||||||
user_config.config_set::<String>("server_url", &address.read());
|
|
||||||
}
|
|
||||||
net.send(Connect {
|
|
||||||
address: address.read().clone(),
|
|
||||||
username: username.read().clone(),
|
|
||||||
config: overrides.read().clone().unwrap_or_default(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let state = use_context::<SharedState>();
|
|
||||||
let status = &state.status;
|
|
||||||
let bottom = match &*status.read() {
|
|
||||||
Disconnected => rsx! {
|
|
||||||
button {
|
|
||||||
class: "login_bttn",
|
|
||||||
onclick: do_connect.clone(),
|
|
||||||
"Connect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Connecting => rsx! {
|
|
||||||
div {
|
|
||||||
class: "login_bttn",
|
|
||||||
"Connecting..."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Failed(msg) => rsx!(
|
|
||||||
button {
|
|
||||||
class: "login_bttn",
|
|
||||||
onclick: do_connect.clone(),
|
|
||||||
"Reconnect"
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "login_error",
|
|
||||||
"Failed to connect:"
|
|
||||||
pre {
|
|
||||||
"{msg}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Connected => unreachable!(),
|
|
||||||
};
|
|
||||||
let version = option_env!("MUMBLE_WEB2_VERSION");
|
|
||||||
rsx!(
|
|
||||||
div {
|
|
||||||
class: "login",
|
|
||||||
h1 {
|
|
||||||
"Mumble Web"
|
|
||||||
match version {
|
|
||||||
Some(v) => rsx!(" " span { class: "login_version", "({v})" }),
|
|
||||||
None => rsx!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
|
||||||
div {
|
|
||||||
label {
|
|
||||||
for: "address-entry",
|
|
||||||
"Server Address:"
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
id: "address-entry",
|
|
||||||
placeholder: "address",
|
|
||||||
value: "{address.read()}",
|
|
||||||
autofocus: "true",
|
|
||||||
oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
label {
|
|
||||||
for: "username-entry",
|
|
||||||
"Username:"
|
|
||||||
//style: "color: rgba(255, 255, 255, 0.5); font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; vertical-align: middle; font-size: 35px; user-select: none;",
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
id: "username-entry",
|
|
||||||
placeholder: "username",
|
|
||||||
value: "{username.read()}",
|
|
||||||
autofocus: "true",
|
|
||||||
oninput: move |evt| username.set(evt.value().clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
match &*last_status.read() {
|
|
||||||
None => rsx!(div {
|
|
||||||
class: "login_status",
|
|
||||||
span {"···"}
|
|
||||||
}),
|
|
||||||
Some(Ok(ServerStatus { success: false, .. })) => rsx!(div {
|
|
||||||
class: "login_status is_error",
|
|
||||||
span {
|
|
||||||
"Could not reach server"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Some(Ok(status)) => rsx!(div {
|
|
||||||
class: "login_status",
|
|
||||||
if let (Some(users), Some(max_users)) = (status.users, status.max_users) {
|
|
||||||
span {"{users}/{max_users} Online"}
|
|
||||||
} else {
|
|
||||||
span {"Unknown Online"}
|
|
||||||
}
|
|
||||||
span {"-"}
|
|
||||||
if let Some((maj, min, pat)) = status.version {
|
|
||||||
span {"Version: {maj}.{min}.{pat}"}
|
|
||||||
} else {
|
|
||||||
span {"Unknown Version"}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Some(Err(_)) => rsx!(div {
|
|
||||||
class: "login_status is_error",
|
|
||||||
span {
|
|
||||||
"Could not reach proxy server"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
{bottom}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// rsx!(
|
|
||||||
// div {
|
|
||||||
// class: "{login_box}",
|
|
||||||
// h1 {
|
|
||||||
// "Mumble Web"
|
|
||||||
// }
|
|
||||||
// input {
|
|
||||||
// placeholder: "username",
|
|
||||||
// value: "{username.read()}",
|
|
||||||
// autofocus: "true",
|
|
||||||
// oninput: move |evt| username.set(evt.value().clone()),
|
|
||||||
// }
|
|
||||||
// input {
|
|
||||||
// placeholder: "server address",
|
|
||||||
// value: "{address.read()}",
|
|
||||||
// autofocus: "true",
|
|
||||||
// oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
|
||||||
// }
|
|
||||||
// {bottom}
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn app() -> Element {
|
|
||||||
static STYLE: Asset = asset!("/assets/main.scss");
|
|
||||||
|
|
||||||
use_effect(|| {
|
|
||||||
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!(
|
|
||||||
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: STYLE }
|
|
||||||
|
|
||||||
match *state.status.read() {
|
|
||||||
Connected => rsx!(ServerView { overrides }),
|
|
||||||
_ => rsx!(LoginView { overrides }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+717
-2
@@ -1,5 +1,720 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
|
use mumble_web2_client::{
|
||||||
|
network_entrypoint, reqwest, AudioSettings, ChannelId, Command, ConfigSystem,
|
||||||
|
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, ServerState,
|
||||||
|
SharedState, State, UserId, UserState,
|
||||||
|
};
|
||||||
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::{fmt, sync::Arc};
|
||||||
|
use Command::*;
|
||||||
|
use ConnectionState::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum UserIcon {
|
||||||
|
Normal,
|
||||||
|
Muted,
|
||||||
|
Deafened,
|
||||||
|
Suppressed,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserIcon {
|
||||||
|
pub fn icon(user: &UserState) -> UserIcon {
|
||||||
|
if user.deaf || user.self_deaf {
|
||||||
|
UserIcon::Deafened
|
||||||
|
} else if user.mute || user.self_mute {
|
||||||
|
UserIcon::Muted
|
||||||
|
} else if user.suppress {
|
||||||
|
UserIcon::Suppressed
|
||||||
|
} else {
|
||||||
|
UserIcon::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(self) -> Option<Asset> {
|
||||||
|
// speaker from https://www.svgrepo.com/collection/ikono-bold-line-icons/
|
||||||
|
// mic from https://www.svgrepo.com/collection/hashicorp-line-interface-icons/
|
||||||
|
|
||||||
|
use UserIcon::*;
|
||||||
|
Some(match self {
|
||||||
|
Normal => asset!("assets/mic-svgrepo-com.svg"),
|
||||||
|
Muted | Suppressed => asset!("assets/mic-off-svgrepo-com.svg"),
|
||||||
|
Deafened => asset!("assets/speaker-muted-svgrepo-com.svg"),
|
||||||
|
None => return Option::None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
|
||||||
|
let color = match icon {
|
||||||
|
UserIcon::Normal => "var(--accent-normal)",
|
||||||
|
UserIcon::Muted => "var(--accent-muted)",
|
||||||
|
UserIcon::Suppressed | UserIcon::Deafened => "var(--accent-deafened)",
|
||||||
|
UserIcon::None => "var(--accent-normal)",
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
class: match isself { true => "userpil is_self", false => "userpil" },
|
||||||
|
style: "background-color: {color}",
|
||||||
|
{ icon.url().map(|url| rsx!(img { src: url })) }
|
||||||
|
"\u{00A0}{name}\u{00A0}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn User(id: UserId) -> Element {
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
|
match server.users.get(&id) {
|
||||||
|
Some(state) => rsx!(UserPill {
|
||||||
|
name: state.name.clone(),
|
||||||
|
icon: UserIcon::icon(state),
|
||||||
|
isself: server.session.unwrap() == id,
|
||||||
|
}),
|
||||||
|
None => rsx!(UserPill {
|
||||||
|
name: format!("unknown user ({id})"),
|
||||||
|
icon: UserIcon::None,
|
||||||
|
isself: false,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Channel(id: ChannelId) -> Element {
|
||||||
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
|
let user = server.session.unwrap();
|
||||||
|
let Some(state) = server.channels_state.channels.get(&id) else {
|
||||||
|
return rsx!("missing channel {id}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut open = use_signal(|| true);
|
||||||
|
|
||||||
|
let has_children = !state.users.is_empty() || !state.children.is_empty();
|
||||||
|
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
class: "channel_details",
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: "channel_header",
|
||||||
|
// Arrow: only toggles open
|
||||||
|
if has_children {
|
||||||
|
span {
|
||||||
|
class: "channel_arrow",
|
||||||
|
onclick: move |evt| {
|
||||||
|
evt.stop_propagation();
|
||||||
|
evt.prevent_default();
|
||||||
|
let mut w = open.write();
|
||||||
|
*w = !*w;
|
||||||
|
},
|
||||||
|
if *open.read() { "▾" } else { "▸" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
span {
|
||||||
|
class: "channel_arrow channel_arrow--placeholder",
|
||||||
|
" "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clickable row area (everything except the arrow)
|
||||||
|
div {
|
||||||
|
class: "channel_row_click",
|
||||||
|
ondblclick: move |evt| {
|
||||||
|
evt.stop_propagation();
|
||||||
|
evt.prevent_default();
|
||||||
|
net.send(EnterChannel { channel: id, user })
|
||||||
|
},
|
||||||
|
// remove dblclick from the inner span
|
||||||
|
span {
|
||||||
|
class: "channel_title",
|
||||||
|
"{state.name}"
|
||||||
|
}
|
||||||
|
// if you add icons/badges later, put them here too
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *open.read() && has_children {
|
||||||
|
div {
|
||||||
|
class: "channel_children",
|
||||||
|
for id in state.users.iter() {
|
||||||
|
User { id: *id }
|
||||||
|
}
|
||||||
|
for child in state.children.iter() {
|
||||||
|
Channel { id: *child }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "desktop", feature = "web"))]
|
||||||
|
pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let channels = if let Some(user) = state.server.read().this_user() {
|
||||||
|
vec![user.channel]
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let dialog = rfd::AsyncFileDialog::new().pick_file();
|
||||||
|
let sender = net.tx();
|
||||||
|
spawn(async move {
|
||||||
|
let Some(handle) = dialog.await else { return };
|
||||||
|
let name = handle.file_name();
|
||||||
|
let bytes = handle.read().await;
|
||||||
|
let mime = mumble_web2_client::mime_guess::from_path(&name).first();
|
||||||
|
let _ = sender.unbounded_send(SendFile {
|
||||||
|
bytes,
|
||||||
|
name,
|
||||||
|
mime,
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(not(any(feature = "desktop", feature = "web")))]
|
||||||
|
pub fn pick_and_send_file(net: &Coroutine<Command>) {}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ChatView() -> Element {
|
||||||
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
|
let mut draft = use_signal(|| "".to_string());
|
||||||
|
|
||||||
|
let mut do_send = move || {
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
|
if let Some(user) = server.this_user() {
|
||||||
|
net.send(SendChat {
|
||||||
|
markdown: draft.write().split_off(0),
|
||||||
|
channels: vec![user.channel],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
class: "chat_panel",
|
||||||
|
div {
|
||||||
|
class: "chat_history",
|
||||||
|
for chat in server.chat.iter() {
|
||||||
|
div {
|
||||||
|
class: "chat_message",
|
||||||
|
if let Some(sender) = chat.sender.and_then(|u| server.users.get(&u)) {
|
||||||
|
UserPill {
|
||||||
|
name: sender.name.clone(),
|
||||||
|
icon: UserIcon::None,
|
||||||
|
isself: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
dangerous_inner_html: "{chat.dangerous_html}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "chat_box_wrapper",
|
||||||
|
div {
|
||||||
|
class: "chat_box",
|
||||||
|
input {
|
||||||
|
placeholder: "say something",
|
||||||
|
value: "{draft.read()}",
|
||||||
|
oninput: move |evt| draft.set(evt.value().clone()),
|
||||||
|
onkeypress: move |evt: Event<KeyboardData>| {
|
||||||
|
if evt.code() == Code::Enter && evt.modifiers().is_empty() {
|
||||||
|
do_send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
onclick: move |_| pick_and_send_file(&net),
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
style: "color: rgba(255, 255, 255, 0.5); font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; vertical-align: middle; font-size: 35px; user-select: none;",
|
||||||
|
"attach_file",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
onclick: move |_| do_send(),
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
style: "color: rgba(255, 255, 255, 0.5); font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; vertical-align: middle; font-size: 35px; user-select: none;",
|
||||||
|
"send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//button {
|
||||||
|
// onclick: move |_| do_send(),
|
||||||
|
// "Send"
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||||
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let status = &state.status;
|
||||||
|
let server = state.server.read();
|
||||||
|
let audio = state.audio.read();
|
||||||
|
let Some(&UserState {
|
||||||
|
deaf,
|
||||||
|
self_deaf,
|
||||||
|
mute,
|
||||||
|
suppress,
|
||||||
|
self_mute,
|
||||||
|
ref name,
|
||||||
|
channel,
|
||||||
|
..
|
||||||
|
}) = server.this_user()
|
||||||
|
else {
|
||||||
|
return rsx!();
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
||||||
|
|
||||||
|
let proxy_url = overrides
|
||||||
|
.read_unchecked()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|overrides| overrides.proxy_url.clone());
|
||||||
|
|
||||||
|
let connecting_color = "yellow";
|
||||||
|
let connected_color = "oklch(0.55 0.1184 141.35)";
|
||||||
|
let disconnected_color = "gray";
|
||||||
|
let failed_color = "red";
|
||||||
|
|
||||||
|
let connection_status = match &*status.read() {
|
||||||
|
Connecting => rsx! {
|
||||||
|
div {
|
||||||
|
class: "connection_status",
|
||||||
|
style: "color: {connecting_color};",
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
"signal_cellular_alt_2_bar"
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
class: "status_text",
|
||||||
|
" Connecting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Connected => rsx! {
|
||||||
|
div {
|
||||||
|
class: "connection_status",
|
||||||
|
div {
|
||||||
|
style: "color: {connected_color};",
|
||||||
|
span {
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
"signal_cellular_alt"
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
class: "status_text",
|
||||||
|
" Connected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "channel_text",
|
||||||
|
span { "{current_channel_name}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Disconnected => rsx! {
|
||||||
|
div {
|
||||||
|
class: "connection_status",
|
||||||
|
style: "color: {disconnected_color};",
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
"signal_disconnected"
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
class: "status_text",
|
||||||
|
" Disconnected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Failed(_) => rsx! {
|
||||||
|
div {
|
||||||
|
class: "connection_status",
|
||||||
|
style: "color: {failed_color};",
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
class: "status_text",
|
||||||
|
" Failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx!(
|
||||||
|
// Server control
|
||||||
|
div {
|
||||||
|
class: "button_row",
|
||||||
|
div {
|
||||||
|
{connection_status}
|
||||||
|
}
|
||||||
|
span { class: "spacer" }
|
||||||
|
button {
|
||||||
|
class: "toggle_button",
|
||||||
|
onclick: move |_| net.send(Disconnect),
|
||||||
|
span {
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
"signal_disconnected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hr { style: "width: 100%;" }
|
||||||
|
// User control
|
||||||
|
div {
|
||||||
|
class: "button_row",
|
||||||
|
button {
|
||||||
|
class: "user_edit_button",
|
||||||
|
span {
|
||||||
|
class: "material-symbols-outlined",
|
||||||
|
style: "color: oklch(0.65 0.2245 28.06);",
|
||||||
|
"person_edit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "user_info",
|
||||||
|
div {
|
||||||
|
span { class: "user_name", "{name}" }
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
span { class: "user_data", "some data" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span { class: "spacer" }
|
||||||
|
button {
|
||||||
|
class: match audio.denoise {
|
||||||
|
true => "toggle_button is_on",
|
||||||
|
false => "toggle_button",
|
||||||
|
},
|
||||||
|
role: "switch",
|
||||||
|
aria_checked: audio.denoise,
|
||||||
|
onclick: move |_| {
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let mut audio = state.audio.read().clone();
|
||||||
|
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 audio.denoise {
|
||||||
|
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
||||||
|
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: match mute || suppress || self_mute {
|
||||||
|
true => "toggle_button is_on",
|
||||||
|
false => "toggle_button",
|
||||||
|
},
|
||||||
|
role: "switch",
|
||||||
|
aria_checked: mute || suppress || self_mute,
|
||||||
|
disabled: mute || suppress,
|
||||||
|
onclick: move |_| net.send(SetMute { mute: !self_mute }),
|
||||||
|
match mute || suppress || self_mute {
|
||||||
|
true => rsx!(span { class: "material-symbols-outlined", "mic_off"}),
|
||||||
|
false => rsx!(span { class: "material-symbols-outlined", "mic"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: match deaf || self_deaf {
|
||||||
|
true => "toggle_button in_on",
|
||||||
|
false => "toggle_button",
|
||||||
|
},
|
||||||
|
role: "switch",
|
||||||
|
aria_checked: deaf || self_deaf,
|
||||||
|
disabled: deaf,
|
||||||
|
onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }),
|
||||||
|
match deaf || self_deaf {
|
||||||
|
true => rsx!(span { class: "material-symbols-outlined", "volume_off"}),
|
||||||
|
false => rsx!(span { class: "material-symbols-outlined", "volume_up"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||||
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let server = state.server.read();
|
||||||
|
let Some(&UserState {
|
||||||
|
deaf,
|
||||||
|
self_deaf,
|
||||||
|
mute,
|
||||||
|
self_mute,
|
||||||
|
..
|
||||||
|
}) = server.this_user()
|
||||||
|
else {
|
||||||
|
return rsx!();
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
class: "server_grid",
|
||||||
|
div {
|
||||||
|
class: "server_channel_box",
|
||||||
|
for (id, state) in server.channels_state.channels.iter() {
|
||||||
|
if state.parent.is_none() {
|
||||||
|
Channel { id: *id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "server_chat_box",
|
||||||
|
ChatView {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "server_control_box",
|
||||||
|
ControlView { overrides }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||||
|
let user_config = use_context::<ConfigSystem>();
|
||||||
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
|
|
||||||
|
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
||||||
|
use_resource(move || async move {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
loop {
|
||||||
|
*last_status.write_unchecked() = Some(Platform::get_status(&client).await);
|
||||||
|
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut address_input = use_signal(|| user_config.config_get::<String>("server_url"));
|
||||||
|
let address = use_memo(move || {
|
||||||
|
if let Some(addr) = address_input() {
|
||||||
|
addr.clone()
|
||||||
|
} else {
|
||||||
|
overrides()
|
||||||
|
.and_then(|c| c.proxy_url.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut username = use_signal(|| {
|
||||||
|
user_config
|
||||||
|
.config_get::<String>("username")
|
||||||
|
.unwrap_or(String::new())
|
||||||
|
});
|
||||||
|
|
||||||
|
let do_connect = move |_| {
|
||||||
|
let _ = user_config.config_set::<String>("username", &username.read());
|
||||||
|
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||||
|
user_config.config_set::<String>("server_url", &address.read());
|
||||||
|
}
|
||||||
|
net.send(Connect {
|
||||||
|
address: address.read().clone(),
|
||||||
|
username: username.read().clone(),
|
||||||
|
config: overrides.read().clone().unwrap_or_default(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let state = use_context::<SharedState>();
|
||||||
|
let status = &state.status;
|
||||||
|
let bottom = match &*status.read() {
|
||||||
|
Disconnected => rsx! {
|
||||||
|
button {
|
||||||
|
class: "login_bttn",
|
||||||
|
onclick: do_connect.clone(),
|
||||||
|
"Connect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Connecting => rsx! {
|
||||||
|
div {
|
||||||
|
class: "login_bttn",
|
||||||
|
"Connecting..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Failed(msg) => rsx!(
|
||||||
|
button {
|
||||||
|
class: "login_bttn",
|
||||||
|
onclick: do_connect.clone(),
|
||||||
|
"Reconnect"
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "login_error",
|
||||||
|
"Failed to connect:"
|
||||||
|
pre {
|
||||||
|
"{msg}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Connected => unreachable!(),
|
||||||
|
};
|
||||||
|
let version = option_env!("MUMBLE_WEB2_VERSION");
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
class: "login",
|
||||||
|
h1 {
|
||||||
|
"Mumble Web"
|
||||||
|
match version {
|
||||||
|
Some(v) => rsx!(" " span { class: "login_version", "({v})" }),
|
||||||
|
None => rsx!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||||
|
div {
|
||||||
|
label {
|
||||||
|
for: "address-entry",
|
||||||
|
"Server Address:"
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
id: "address-entry",
|
||||||
|
placeholder: "address",
|
||||||
|
value: "{address.read()}",
|
||||||
|
autofocus: "true",
|
||||||
|
oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label {
|
||||||
|
for: "username-entry",
|
||||||
|
"Username:"
|
||||||
|
//style: "color: rgba(255, 255, 255, 0.5); font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; vertical-align: middle; font-size: 35px; user-select: none;",
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
id: "username-entry",
|
||||||
|
placeholder: "username",
|
||||||
|
value: "{username.read()}",
|
||||||
|
autofocus: "true",
|
||||||
|
oninput: move |evt| username.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
match &*last_status.read() {
|
||||||
|
None => rsx!(div {
|
||||||
|
class: "login_status",
|
||||||
|
span {"···"}
|
||||||
|
}),
|
||||||
|
Some(Ok(ServerStatus { success: false, .. })) => rsx!(div {
|
||||||
|
class: "login_status is_error",
|
||||||
|
span {
|
||||||
|
"Could not reach server"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Some(Ok(status)) => rsx!(div {
|
||||||
|
class: "login_status",
|
||||||
|
if let (Some(users), Some(max_users)) = (status.users, status.max_users) {
|
||||||
|
span {"{users}/{max_users} Online"}
|
||||||
|
} else {
|
||||||
|
span {"Unknown Online"}
|
||||||
|
}
|
||||||
|
span {"-"}
|
||||||
|
if let Some((maj, min, pat)) = status.version {
|
||||||
|
span {"Version: {maj}.{min}.{pat}"}
|
||||||
|
} else {
|
||||||
|
span {"Unknown Version"}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Some(Err(_)) => rsx!(div {
|
||||||
|
class: "login_status is_error",
|
||||||
|
span {
|
||||||
|
"Could not reach proxy server"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
{bottom}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// rsx!(
|
||||||
|
// div {
|
||||||
|
// class: "{login_box}",
|
||||||
|
// h1 {
|
||||||
|
// "Mumble Web"
|
||||||
|
// }
|
||||||
|
// input {
|
||||||
|
// placeholder: "username",
|
||||||
|
// value: "{username.read()}",
|
||||||
|
// autofocus: "true",
|
||||||
|
// oninput: move |evt| username.set(evt.value().clone()),
|
||||||
|
// }
|
||||||
|
// input {
|
||||||
|
// placeholder: "server address",
|
||||||
|
// value: "{address.read()}",
|
||||||
|
// autofocus: "true",
|
||||||
|
// oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
||||||
|
// }
|
||||||
|
// {bottom}
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn app() -> Element {
|
||||||
|
static STYLE: Asset = asset!("/assets/main.scss");
|
||||||
|
|
||||||
|
use_effect(|| {
|
||||||
|
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>| {
|
||||||
|
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!(
|
||||||
|
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: STYLE }
|
||||||
|
|
||||||
|
match *state.status.read() {
|
||||||
|
Connected => rsx!(ServerView { overrides }),
|
||||||
|
_ => rsx!(LoginView { overrides }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
Platform::init_logging();
|
Platform::init_logging();
|
||||||
@@ -18,5 +733,5 @@ pub fn main() {
|
|||||||
.with_maximized(false),
|
.with_maximized(false),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.launch(app::app);
|
.launch(app);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user