wip improved trait shit

This commit is contained in:
2026-01-24 20:16:02 -07:00
committed by Liam Warfield
parent ff14f577fe
commit 411d923c2a
9 changed files with 118 additions and 181 deletions
+9 -9
View File
@@ -6,7 +6,7 @@ use mumble_web2_common::{ClientConfig, ServerStatus};
use ordermap::OrderSet; use ordermap::OrderSet;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::imp; use crate::imp::{Platform, PlatformInterface as _};
pub type ChannelId = u32; pub type ChannelId = u32;
pub type UserId = u32; pub type UserId = u32;
@@ -690,12 +690,12 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
use_resource(move || async move { use_resource(move || async move {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
loop { loop {
*last_status.write_unchecked() = Some(imp::get_status(&client).await); *last_status.write_unchecked() = Some(Platform::get_status(&client).await);
imp::sleep(std::time::Duration::from_secs_f32(1.0)).await; Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
} }
}); });
let mut address_input = use_signal(|| imp::load_server_url()); let mut address_input = use_signal(|| Platform::load_server_url());
let address = use_memo(move || { let address = use_memo(move || {
if let Some(addr) = address_input() { if let Some(addr) = address_input() {
addr.clone() addr.clone()
@@ -706,14 +706,14 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
} }
}); });
let previous_username = imp::load_username(); let previous_username = Platform::load_username();
let mut username = use_signal(|| previous_username.unwrap_or(String::new())); let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
let do_connect = move |_| { let do_connect = move |_| {
//let _ = set_default_username(&username.read()); //let _ = set_default_username(&username.read());
let _ = imp::set_default_username(&username.read()); let _ = Platform::set_default_username(&username.read());
if config.read().as_ref().is_some_and(|cfg| cfg.any_server) { if config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
imp::set_default_server(&address.read()); Platform::set_default_server(&address.read());
} }
net.send(Connect { net.send(Connect {
address: address.read().clone(), address: address.read().clone(),
@@ -860,13 +860,13 @@ pub fn app() -> Element {
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx)); use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
let config = use_resource(|| async move { let config = use_resource(|| async move {
match imp::load_config().await { match Platform::load_config().await {
Ok(config) => config, Ok(config) => config,
Err(_) => ClientConfig::default(), Err(_) => ClientConfig::default(),
} }
}); });
imp::request_permissions(); Platform::request_permissions();
rsx!( rsx!(
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" } document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
+8 -9
View File
@@ -7,10 +7,7 @@ use std::cell::RefCell;
use std::sync::Arc; use std::sync::Arc;
use tracing::{error, info}; use tracing::{error, info};
use crate::imp::{CurrentPlatform, PlatformRuntime, SpawnHandleTrait}; use crate::imp::{SpawnHandle, SpawnHandleInterface as _};
/// The spawn handle type for the current platform.
type SpawnHandle = <CurrentPlatform as PlatformRuntime>::SpawnHandle;
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz"); static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
// TODO: make this user configurable. // TODO: make this user configurable.
@@ -35,10 +32,7 @@ enum DenoisingModelState {
Availible(Box<DfTract>), Availible(Box<DfTract>),
} }
fn with_denoising_model<O>( fn with_denoising_model<O>(spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract) -> O) -> Option<O> {
spawn: &SpawnHandle,
func: impl FnOnce(&mut DfTract) -> O,
) -> Option<O> {
// Using a thread local is super gross, but DfTract is not Send (so it can never leave the current // Using a thread local is super gross, but DfTract is not Send (so it can never leave the current
// thread) while AudioProcessing itself might change threads whenever. // thread) while AudioProcessing itself might change threads whenever.
thread_local! { thread_local! {
@@ -126,7 +120,12 @@ impl AudioProcessor {
} }
impl AudioProcessor { impl AudioProcessor {
pub fn process(&mut self, audio: &[f32], channels: usize, output: &mut Vec<f32>) -> TransmitState { pub fn process(
&mut self,
audio: &[f32],
channels: usize,
output: &mut Vec<f32>,
) -> TransmitState {
let mut include_raw = true; let mut include_raw = true;
if self.denoise { if self.denoise {
with_denoising_model(&self.spawn, |df| { with_denoising_model(&self.spawn, |df| {
+5 -2
View File
@@ -7,7 +7,10 @@ use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use std::time::Duration; use std::time::Duration;
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; use super::{
PlatformConfig, PlatformInit, PlatformInterface, PlatformNetwork, PlatformRuntime,
SpawnHandleTrait,
};
pub use super::connect::*; pub use super::connect::*;
pub use super::native_audio::*; pub use super::native_audio::*;
@@ -107,7 +110,7 @@ impl PlatformInit for DesktopPlatform {
} }
} }
impl Platform for DesktopPlatform {} impl PlatformInterface for DesktopPlatform {}
fn get_config_path() -> std::path::PathBuf { fn get_config_path() -> std::path::PathBuf {
let strategy = choose_app_strategy(AppStrategyArgs { let strategy = choose_app_strategy(AppStrategyArgs {
+2 -2
View File
@@ -7,7 +7,7 @@ use mumble_web2_common::{ClientConfig, ServerStatus};
use std::future::Future; use std::future::Future;
use std::time::Duration; use std::time::Duration;
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait}; use super::{PlatformInterface, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use super::connect::*; pub use super::connect::*;
pub use super::native_audio::*; pub use super::native_audio::*;
@@ -107,7 +107,7 @@ impl PlatformInit for MobilePlatform {
} }
} }
impl Platform for MobilePlatform {} impl PlatformInterface for MobilePlatform {}
pub fn set_default_username(_username: &str) -> Option<()> { pub fn set_default_username(_username: &str) -> Option<()> {
None None
+45 -96
View File
@@ -15,8 +15,7 @@ use std::time::Duration;
// ============================================================================ // ============================================================================
/// Trait for spawn handles that can be stored and used to spawn tasks later. /// Trait for spawn handles that can be stored and used to spawn tasks later.
#[cfg(feature = "web")] pub trait SpawnHandleInterface: Clone + 'static {
pub trait SpawnHandleTrait: Clone + 'static {
/// Spawn an async task using this handle. /// Spawn an async task using this handle.
fn spawn<F>(&self, future: F) fn spawn<F>(&self, future: F)
where where
@@ -26,51 +25,39 @@ pub trait SpawnHandleTrait: Clone + 'static {
fn current() -> Self; fn current() -> Self;
} }
/// Trait for spawn handles that can be stored and used to spawn tasks later. pub trait AudioSystemInterface {
#[cfg(any(feature = "desktop", feature = "mobile"))] type AudioPlayer: AudioPlayerInterface;
pub trait SpawnHandleTrait: Clone + 'static {
/// Spawn an async task using this handle.
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static;
/// Get a spawn handle for the current context.
fn current() -> Self;
} }
/// Runtime primitives: task spawning and async sleep. pub trait AudioPlayerInterface {}
#[cfg(feature = "web")]
pub trait PlatformRuntime {
/// The spawn handle type for this platform.
type SpawnHandle: SpawnHandleTrait;
/// Spawn an async task. /// This is the main trait that each platform must implement. It combines all
fn spawn<F>(future: F) /// platform-specific functionality into a single interface, providing compile-time
where /// verification that all platforms implement the required functionality.
F: Future<Output = ()> + 'static; pub trait PlatformInterface {
type AudioSystem: AudioSystemInterface;
type SpawnHandle: SpawnHandleInterface;
/// Async sleep for the given duration. /// Initialize logging for the platform.
fn sleep(duration: Duration) -> impl Future<Output = ()>; fn init_logging();
}
/// Runtime primitives: task spawning and async sleep. /// Request runtime permissions (Android audio recording, etc.).
#[cfg(any(feature = "desktop", feature = "mobile"))] fn request_permissions();
pub trait PlatformRuntime {
/// The spawn handle type for this platform.
type SpawnHandle: SpawnHandleTrait;
/// Spawn an async task. /// Establish a connection to the Mumble server and run the network loop.
fn spawn<F>(future: F) fn network_connect(
where address: String,
F: Future<Output = ()> + Send + 'static; username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> impl Future<Output = Result<(), Error>>;
/// Async sleep for the given duration. /// Get server status (user count, version, etc.).
fn sleep(duration: Duration) -> impl Future<Output = ()>; fn get_status(
} client: &reqwest::Client,
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
/// Configuration persistence: loading and saving user preferences. /// Load the proxy overrides (proxy URL, cert hash, etc.).
pub trait PlatformConfig {
/// Load the client configuration (proxy URL, cert hash, etc.).
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>; fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
/// Load saved username. /// Load saved username.
@@ -84,39 +71,16 @@ pub trait PlatformConfig {
/// Save the default server URL. /// Save the default server URL.
fn set_default_server(server: &str) -> Option<()>; fn set_default_server(server: &str) -> Option<()>;
/// Spawn an async task.
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static;
/// Async sleep for the given duration.
fn sleep(duration: Duration) -> impl Future<Output = ()>;
} }
/// Network operations: connecting to servers.
pub trait PlatformNetwork {
/// Establish a connection to the Mumble server and run the network loop.
fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> impl Future<Output = Result<(), Error>>;
/// Get server status (user count, version, etc.).
fn get_status(client: &reqwest::Client)
-> impl Future<Output = color_eyre::Result<ServerStatus>>;
}
/// Platform initialization.
pub trait PlatformInit {
/// Initialize logging for the platform.
fn init_logging();
/// Request runtime permissions (Android audio recording, etc.).
fn request_permissions();
}
/// Combined platform trait.
///
/// This is the main trait that each platform must implement. It combines all
/// platform-specific functionality into a single interface, providing compile-time
/// verification that all platforms implement the required functionality.
pub trait Platform: PlatformRuntime + PlatformConfig + PlatformNetwork + PlatformInit {}
// ============================================================================ // ============================================================================
// Platform Modules // Platform Modules
// ============================================================================ // ============================================================================
@@ -138,36 +102,21 @@ mod mobile;
// Platform Type Alias // Platform Type Alias
// ============================================================================ // ============================================================================
/// The current platform type, selected at compile time based on features.
#[cfg(feature = "web")] #[cfg(feature = "web")]
pub type CurrentPlatform = web::WebPlatform; pub type Platform = web::WebPlatform;
#[cfg(feature = "desktop")] #[cfg(all(feature = "desktop", not(feature = "web")))]
pub type CurrentPlatform = desktop::DesktopPlatform; pub type Platform = desktop::DesktopPlatform;
#[cfg(feature = "mobile")] #[cfg(all(feature = "mobile", not(feature = "web"), not(feature = "desktop")))]
pub type CurrentPlatform = mobile::MobilePlatform; pub type Platform = mobile::MobilePlatform;
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
pub type SpawnHandle = <Platform as PlatformInterface>::SpawnHandle;
/// Compile-time assertion that CurrentPlatform implements Platform. /// Compile-time assertion that CurrentPlatform implements Platform.
const _: () = { const _: () = {
fn assert_platform<T: Platform>() {} fn assert_platform<T: PlatformInterface>() {}
let _ = assert_platform::<CurrentPlatform>; let _ = assert_platform::<Platform>;
}; };
// ============================================================================
// Platform Re-exports
// ============================================================================
#[cfg(feature = "desktop")]
pub use desktop::*;
#[cfg(feature = "mobile")]
pub use mobile::*;
#[cfg(feature = "mobile")]
pub use mobile::request_permissions;
#[cfg(any(feature = "desktop", feature = "web"))]
pub fn request_permissions() {}
#[cfg(all(feature = "web", not(any(feature = "desktop", feature = "mobile"))))]
pub use web::*;
+2 -1
View File
@@ -124,7 +124,8 @@ impl AudioSystem {
if let Some(new_processor) = processors.take() { if let Some(new_processor) = processors.take() {
current_processor = new_processor; current_processor = new_processor;
} }
let state = current_processor.process(frame, config.channels as usize, &mut output_buffer); let state =
current_processor.process(frame, config.channels as usize, &mut output_buffer);
encode_and_send(state, &mut output_buffer, &mut encoder, &mut each); encode_and_send(state, &mut output_buffer, &mut encoder, &mut each);
}; };
+33 -53
View File
@@ -38,8 +38,6 @@ use web_sys::WorkletOptions;
use web_sys::{console, window}; use web_sys::{console, window};
use web_sys::{AudioContext, AudioDataCopyToOptions}; use web_sys::{AudioContext, AudioDataCopyToOptions};
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use wasm_bindgen_futures::spawn_local as spawn; pub use wasm_bindgen_futures::spawn_local as spawn;
pub trait ImpRead: AsyncRead + Unpin + 'static {} pub trait ImpRead: AsyncRead + Unpin + 'static {}
@@ -55,15 +53,14 @@ impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
/// Web platform implementation using WebTransport and Web Audio API. /// Web platform implementation using WebTransport and Web Audio API.
pub struct WebPlatform; pub struct WebPlatform;
pub async fn sleep(d: Duration) {
TimeoutFuture::new(d.as_millis() as u32).await
}
// ============================================================================ // ============================================================================
// Trait Implementations // Trait Implementations
// ============================================================================ // ============================================================================
impl SpawnHandleTrait for SpawnHandle { #[derive(Clone)]
pub struct WebSpawnHandle;
impl super::SpawnHandleInterface for WebSpawnHandle {
fn spawn<F>(&self, future: F) fn spawn<F>(&self, future: F)
where where
F: Future<Output = ()> + 'static, F: Future<Output = ()> + 'static,
@@ -72,26 +69,22 @@ impl SpawnHandleTrait for SpawnHandle {
} }
fn current() -> Self { fn current() -> Self {
SpawnHandle WebSpawnHandle
} }
} }
impl PlatformRuntime for WebPlatform { impl super::PlatformInterface for WebPlatform {
type SpawnHandle = SpawnHandle; type AudioSystem = WebAudioSystem;
type SpawnHandle = WebSpawnHandle;
fn spawn<F>(future: F) fn init_logging() {
where init_logging();
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
} }
async fn sleep(duration: Duration) { fn request_permissions() {
TimeoutFuture::new(duration.as_millis() as u32).await; // No-op on web
}
} }
impl PlatformConfig for WebPlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> { async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await load_config().await
} }
@@ -111,9 +104,7 @@ impl PlatformConfig for WebPlatform {
fn set_default_server(server: &str) -> Option<()> { fn set_default_server(server: &str) -> Option<()> {
set_default_server(server) set_default_server(server)
} }
}
impl PlatformNetwork for WebPlatform {
async fn network_connect( async fn network_connect(
address: String, address: String,
username: String, username: String,
@@ -126,20 +117,19 @@ impl PlatformNetwork for WebPlatform {
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> { async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await get_status(client).await
} }
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
} }
impl PlatformInit for WebPlatform { async fn sleep(duration: Duration) {
fn init_logging() { TimeoutFuture::new(duration.as_millis() as u32).await;
init_logging();
}
fn request_permissions() {
// No-op on web
} }
} }
impl Platform for WebPlatform {}
trait ResultExt<T> { trait ResultExt<T> {
fn ey(self) -> Result<T, Error>; fn ey(self) -> Result<T, Error>;
} }
@@ -162,7 +152,7 @@ impl<T> ResultExt<T> for Result<T, JsError> {
} }
} }
pub struct AudioSystem { pub struct WebAudioSystem {
webctx: AudioContext, webctx: AudioContext,
processors: AudioProcessorSender, processors: AudioProcessorSender,
} }
@@ -193,7 +183,11 @@ async fn attach_worklet(audio_context: &AudioContext) -> Result<(), Error> {
Ok(()) Ok(())
} }
impl AudioSystem { impl super::AudioSystemInterface for WebAudioSystem {
type AudioPlayer = WebAudioPlayer;
}
impl WebAudioSystem {
pub async fn new() -> Result<Self, Error> { pub async fn new() -> Result<Self, Error> {
// 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.
@@ -202,7 +196,7 @@ impl AudioSystem {
let processors = AudioProcessorSender::default(); let processors = AudioProcessorSender::default();
Ok(AudioSystem { webctx, processors }) Ok(WebAudioSystem { webctx, processors })
} }
pub fn set_processor(&self, processor: AudioProcessor) { pub fn set_processor(&self, processor: AudioProcessor) {
@@ -224,7 +218,7 @@ impl AudioSystem {
Ok(()) Ok(())
} }
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> { pub fn create_player(&mut self) -> Result<WebAudioPlayer, Error> {
let sink_node = AudioWorkletNode::new(&self.webctx, "rust_speaker_worklet").ey()?; let sink_node = AudioWorkletNode::new(&self.webctx, "rust_speaker_worklet").ey()?;
// Connect worklet to destination // Connect worklet to destination
@@ -277,13 +271,15 @@ impl AudioSystem {
decoder_error.forget(); decoder_error.forget();
output.forget(); output.forget();
Ok(AudioPlayer(audio_decoder)) Ok(WebAudioPlayer(audio_decoder))
} }
} }
pub struct AudioPlayer(AudioDecoder); pub struct WebAudioPlayer(AudioDecoder);
impl AudioPlayer { impl super::AudioPlayerInterface for WebAudioPlayer {}
impl WebAudioPlayer {
pub fn play_opus(&mut self, payload: &[u8]) { pub fn play_opus(&mut self, payload: &[u8]) {
let js_audio_payload = Uint8Array::from(payload); let js_audio_payload = Uint8Array::from(payload);
let _ = self.0.decode( let _ = self.0.decode(
@@ -583,19 +579,3 @@ pub fn init_logging() {
info!("logging initiated"); info!("logging initiated");
} }
#[derive(Clone)]
pub struct SpawnHandle;
impl SpawnHandle {
pub fn current() -> Self {
SpawnHandle
}
pub fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
spawn(future);
}
}
+13 -8
View File
@@ -7,11 +7,12 @@ use asynchronous_codec::FramedWrite;
use color_eyre::eyre::{bail, Error}; use color_eyre::eyre::{bail, Error};
use dioxus::prelude::*; use dioxus::prelude::*;
use futures::select; use futures::select;
use futures::AsyncRead;
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::UnboundedSender;
pub use imp::spawn;
use msghtml::process_message_html; 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;
@@ -27,7 +28,9 @@ use tracing::error;
use tracing::info; use tracing::info;
use crate::effects::AudioProcessor; use crate::effects::AudioProcessor;
use crate::imp::{AudioSystem, CurrentPlatform, Platform, PlatformNetwork, PlatformRuntime}; use crate::imp::{
AudioPlayer, AudioSystem, AudioSystemInterface as _, Platform, PlatformInterface as _,
};
pub mod app; pub mod app;
mod effects; mod effects;
@@ -47,7 +50,9 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
*STATE.server.write() = Default::default(); *STATE.server.write() = Default::default();
*STATE.status.write() = ConnectionState::Connecting; *STATE.status.write() = ConnectionState::Connecting;
if let Err(error) = CurrentPlatform::network_connect(address, username, &mut event_rx, &config).await { if let Err(error) =
Platform::network_connect(address, username, &mut event_rx, &config).await
{
error!("could not connect {:?}", error); error!("could not connect {:?}", error);
*STATE.status.write() = ConnectionState::Failed(error.to_string()); *STATE.status.write() = ConnectionState::Failed(error.to_string());
} else { } else {
@@ -56,7 +61,7 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
} }
} }
pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>( pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>, mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
@@ -105,12 +110,12 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
break; break;
} }
CurrentPlatform::sleep(Duration::from_millis(3000)).await; Platform::sleep(Duration::from_millis(3000)).await;
} }
}); });
} }
let mut audio = imp::AudioSystem::new().await?; let mut audio = AudioSystem::new().await?;
{ {
let send_chan = send_chan.clone(); let send_chan = send_chan.clone();
let mut sequence_num = 0; let mut sequence_num = 0;
@@ -296,8 +301,8 @@ fn accept_command(
fn accept_packet( fn accept_packet(
msg: ControlPacket<mumble_protocol::Clientbound>, msg: ControlPacket<mumble_protocol::Clientbound>,
audio_context: &mut imp::AudioSystem, audio_context: &mut AudioSystem,
player_map: &mut HashMap<u32, imp::AudioPlayer>, player_map: &mut HashMap<u32, AudioPlayer>,
) -> Result<(), Error> { ) -> Result<(), Error> {
match msg { match msg {
ControlPacket::UDPTunnel(u) => { ControlPacket::UDPTunnel(u) => {
+2 -2
View File
@@ -1,6 +1,6 @@
use mumble_web2_gui::{app, imp}; use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
pub fn main() { pub fn main() {
imp::init_logging(); Platform::init_logging();
dioxus::launch(app::app); dioxus::launch(app::app);
} }