Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aac401e841 | |||
| d67a19c478 | |||
| 518c50d8a4 | |||
| 847c636f41 |
@@ -0,0 +1 @@
|
||||
target
|
||||
@@ -42,6 +42,47 @@ jobs:
|
||||
path: target/release/mumble-web2-proxy
|
||||
retention-days: 5
|
||||
|
||||
macos_build:
|
||||
runs-on: macos
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
./target
|
||||
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ runner.os }}-
|
||||
|
||||
- name: Install cargo binstall
|
||||
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||
|
||||
- name: Install dioxus-cli
|
||||
run: cargo binstall dioxus-cli --version 0.7.3 --no-confirm
|
||||
|
||||
- name: Build dioxus project
|
||||
run: dx bundle --platform macos --release -p mumble-web2-gui
|
||||
|
||||
- name: Save Rust cache
|
||||
if: always()
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
./target
|
||||
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Upload mumble-web2-gui Artifact
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: mumble-web2-gui-macos-arm64
|
||||
path: gui/dist
|
||||
retention-days: 5
|
||||
|
||||
windows_build:
|
||||
runs-on: windows
|
||||
steps:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct ClientConfig {
|
||||
pub struct ProxyOverrides {
|
||||
pub proxy_url: Option<String>,
|
||||
pub cert_hash: Option<Vec<u8>>,
|
||||
pub any_server: bool,
|
||||
|
||||
@@ -2,13 +2,11 @@ localhost:64444 {
|
||||
tls internal
|
||||
|
||||
# Proxy /config path to mumble-web2-proxy
|
||||
reverse_proxy /config http://127.0.0.1:4400
|
||||
reverse_proxy /overrides http://127.0.0.1:4400
|
||||
|
||||
# Proxy /status path to mumble-web2-proxy
|
||||
reverse_proxy /status http://127.0.0.1:4400
|
||||
|
||||
|
||||
# Proxy root path to dx-serve
|
||||
reverse_proxy http://127.0.0.1:8080
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
FROM archlinux:latest
|
||||
|
||||
# Base system + toolchain deps
|
||||
RUN pacman -Sy --noconfirm archlinux-keyring && \
|
||||
pacman-key --init && \
|
||||
pacman-key --populate archlinux && \
|
||||
pacman -Syu --noconfirm && \
|
||||
pacman -S --noconfirm --needed \
|
||||
base-devel git sudo xdotool
|
||||
|
||||
# Create non-root build user for AUR
|
||||
RUN useradd -m -G wheel -s /bin/bash builder && \
|
||||
echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder
|
||||
|
||||
USER builder
|
||||
WORKDIR /home/builder
|
||||
|
||||
# Install yay from AUR
|
||||
RUN git clone https://aur.archlinux.org/yay.git /home/builder/yay && \
|
||||
cd /home/builder/yay && \
|
||||
makepkg -si --noconfirm
|
||||
|
||||
# Use yay to install claude-code (or claude-code-stable)
|
||||
RUN yay -S --noconfirm claude-code
|
||||
|
||||
# Optional: switch back to root for cleanup
|
||||
USER root
|
||||
RUN rm -rf /home/builder/yay && \
|
||||
pacman -Scc --noconfirm
|
||||
|
||||
# Default working user/environment
|
||||
USER builder
|
||||
WORKDIR /home/builder
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
# volumes:
|
||||
# - ..:/app
|
||||
# environment:
|
||||
# - MUMBLE_WEB2_GUI_CONFIG_URL=https://localhost:64444/config
|
||||
# - MUMBLE_WEB2_PROXY_OVERRIDES_URL=https://localhost:64444/overrides
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# command: >
|
||||
|
||||
@@ -68,6 +68,7 @@ etcetera = { version = "0.10.0", optional = true }
|
||||
# Base Dependencies
|
||||
# ================
|
||||
dioxus = { version = "0.7.2" }
|
||||
dioxus-native = { git = "https://github.com/DioxusLabs/blitz", rev = "e64a3d8", features = ["prelude"], optional = true }
|
||||
once_cell = "1.19.0"
|
||||
asynchronous-codec = { workspace = true }
|
||||
futures = "^0.3.30"
|
||||
@@ -106,7 +107,7 @@ crossbeam = "0.8.4"
|
||||
# ====================
|
||||
# rfd only supports windows, macos, linux, and wasm32. No support for Android or iOS
|
||||
[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.17.0", default-features = false, optional = true }
|
||||
|
||||
# Android dependencies for requesting permissions
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
@@ -114,12 +115,6 @@ 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 = [
|
||||
"dioxus/web",
|
||||
@@ -155,3 +150,4 @@ mobile = [
|
||||
"cpal",
|
||||
"dasp_ring_buffer",
|
||||
]
|
||||
blitz = ["dioxus-native"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M18 15.75q0 2.6-1.825 4.425T11.75 22t-4.425-1.825T5.5 15.75V6.5q0-1.875 1.313-3.187T10 2t3.188 1.313T14.5 6.5v8.75q0 1.15-.8 1.95t-1.95.8t-1.95-.8t-.8-1.95V6h2v9.25q0 .325.213.538t.537.212t.538-.213t.212-.537V6.5q-.025-1.05-.737-1.775T10 4t-1.775.725T7.5 6.5v9.25q-.025 1.775 1.225 3.013T11.75 20q1.75 0 2.975-1.237T16 15.75V6h2z"/></svg>
|
||||
|
After Width: | Height: | Size: 453 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M11 21V7h2v14zm-4-3v-8h2v8zm8 0v-8h2v8zM3 15v-2h2v2zm16 0v-2h2v2zM2 10V8h1.175q1.05 0 1.963-.525T6.6 6.05q.85-1.425 2.288-2.238T12 3t3.113.813T17.4 6.05q.55.9 1.463 1.425T20.825 8H22v2h-1.15q-1.575 0-2.963-.775T15.7 7.1q-.575-.975-1.562-1.537T12 5q-1.125 0-2.113.563T8.326 7.1q-.8 1.35-2.187 2.125T3.175 10z"/></svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12.713 16.713Q13 16.425 13 16t-.288-.712T12 15t-.712.288T11 16t.288.713T12 17t.713-.288M11 13h2V7h-2zm1 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>
|
||||
|
After Width: | Height: | Size: 392 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M7 18V6h2v12zm4 4V2h2v20zm-8-8v-4h2v4zm12 4V6h2v12zm4-4v-4h2v4z"/></svg>
|
||||
|
After Width: | Height: | Size: 187 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M9.875 13.125Q9 12.25 9 11V5q0-1.25.875-2.125T12 2t2.125.875T15 5v6q0 1.25-.875 2.125T12 14t-2.125-.875M11 21v-3.075q-2.6-.35-4.3-2.325T5 11h2q0 2.075 1.463 3.538T12 16t3.538-1.463T17 11h2q0 2.625-1.7 4.6T13 17.925V21z"/></svg>
|
||||
|
After Width: | Height: | Size: 342 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M17.75 14.95L16.3 13.5q.35-.575.525-1.2T17 11h2q0 1.1-.325 2.088t-.925 1.862m-2.95-3L9 6.15V5q0-1.25.875-2.125T12 2t2.125.875T15 5v6q0 .275-.062.5t-.138.45M11 21v-3.1q-2.6-.35-4.3-2.312T5 11h2q0 2.075 1.463 3.538T12 16q.85 0 1.613-.262T15 15l1.425 1.425q-.725.575-1.588.963T13 17.9V21zm8.8 1.6L1.4 4.2l1.4-1.4l18.4 18.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M14 21v-3.075l5.525-5.5q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-5.5 5.5zM4 20v-2.8q0-.85.438-1.562T5.6 14.55q1.55-.775 3.15-1.162T12 13q.925 0 1.825.113t1.8.362L12 17.1V20zm16.575-4.6l.925-.975l-.925-.925l-.95.95zm-11.4-4.575Q8 9.65 8 8t1.175-2.825T12 4t2.825 1.175T16 8t-1.175 2.825T12 12t-2.825-1.175"/></svg>
|
||||
|
After Width: | Height: | Size: 480 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M3 20v-6l8-2l-8-2V4l19 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 149 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M5 20v-6h3v6zm6 0V9h3v11zm6 0V4h3v16z"/></svg>
|
||||
|
After Width: | Height: | Size: 161 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M5 20v-6h3v6zm6 0V9h3v11z"/></svg>
|
||||
|
After Width: | Height: | Size: 149 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="m17.1 14.275l-1.225-1.225q.55-.65.838-1.425T17 10q0-1-.4-1.9t-1.1-1.6l1.2-1.2q.95.95 1.475 2.15T18.7 10q0 1.2-.425 2.288T17.1 14.275M14.125 11.3L10.7 7.875q.3-.175.625-.275T12 7.5q1.05 0 1.775.725T14.5 10q0 .35-.1.675t-.275.625m5.375 5.35l-1.2-1.2q1-1.125 1.5-2.537T20.3 10q0-1.65-.612-3.187T17.9 4.1l1.2-1.2q1.375 1.45 2.138 3.275T22 10q0 1.85-.638 3.563T19.5 16.65m.275 5.95L13 15.825V21h-2v-7.175L7 9.85V10q0 1 .4 1.9t1.1 1.6l-1.2 1.2q-.95-.95-1.475-2.15T5.3 10q0-.425.05-.825t.175-.825L4.25 7.075q-.275.725-.413 1.45T3.7 10q0 1.65.612 3.188T6.1 15.9l-1.2 1.2q-1.375-1.45-2.137-3.275T2 10q0-1.1.238-2.162t.712-2.063L1.4 4.225L2.8 2.8l18.4 18.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 771 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="m19.8 22.6l-3.025-3.025q-.625.4-1.325.688t-1.45.462v-2.05q.35-.125.688-.25t.637-.3L12 14.8V20l-5-5H3V9h3.2L1.4 4.2l1.4-1.4l18.4 18.4zm-.2-5.8l-1.45-1.45q.425-.775.638-1.625t.212-1.75q0-2.35-1.375-4.2T14 5.275v-2.05q3.1.7 5.05 3.138T21 11.975q0 1.325-.363 2.55T19.6 16.8m-3.35-3.35L14 11.2V7.95q1.175.55 1.838 1.65T16.5 12q0 .375-.062.738t-.188.712M12 9.2L9.4 6.6L12 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 492 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M14 20.725v-2.05q2.25-.65 3.625-2.5t1.375-4.2t-1.375-4.2T14 5.275v-2.05q3.1.7 5.05 3.138T21 11.975t-1.95 5.613T14 20.725M3 15V9h4l5-5v16l-5-5zm11 1V7.95q1.175.55 1.838 1.65T16.5 12q0 1.275-.663 2.363T14 16"/></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
@@ -173,6 +173,7 @@ a:visited {
|
||||
&_box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
background-color: var(--light-bg-color);
|
||||
@@ -185,6 +186,12 @@ a:visited {
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
.material-symbols-outlined {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
color: white;
|
||||
background-color: var(--light-bg-color);
|
||||
@@ -207,10 +214,10 @@ a:visited {
|
||||
border-radius: 50%;
|
||||
aspect-ratio: 1 / 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: clamp(4px, 0.5vw, 8px);
|
||||
}
|
||||
|
||||
.button_row {
|
||||
@@ -232,9 +239,9 @@ a:visited {
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
vertical-align: middle;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,19 +275,16 @@ a:visited {
|
||||
|
||||
border: solid rgb(255 255 255 / 0.1) clamp(1px, 0.3vw, 3px);
|
||||
border-radius: clamp(4px, 0.8vw, 10px);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
&.is_on {
|
||||
background-color: oklch(0.5 0.1381 21.71 / 20.12%);
|
||||
color: oklch(0.53 0.1505 21.71 / 89.38%);
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
vertical-align: middle;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.server {
|
||||
@@ -344,7 +348,10 @@ a:visited {
|
||||
|
||||
.connection_status {
|
||||
.material-symbols-outlined {
|
||||
font-size: var(--control-icon-size);
|
||||
width: 24px;
|
||||
width: var(--control-icon-size);
|
||||
height: 24px;
|
||||
height: var(--control-icon-size);
|
||||
}
|
||||
.status_text {
|
||||
font-size: var(--control-text-size);
|
||||
@@ -356,7 +363,10 @@ a:visited {
|
||||
|
||||
.user_edit_button {
|
||||
.material-symbols-outlined {
|
||||
font-size: var(--user-icon-size);
|
||||
width: 36px;
|
||||
width: var(--user-icon-size);
|
||||
height: 36px;
|
||||
height: var(--user-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +381,10 @@ a:visited {
|
||||
|
||||
.toggle_button {
|
||||
.material-symbols-outlined {
|
||||
font-size: var(--toggle-icon-size);
|
||||
width: 28px;
|
||||
width: var(--toggle-icon-size);
|
||||
height: 28px;
|
||||
height: var(--toggle-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,72 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
#[cfg(feature = "blitz")]
|
||||
use dioxus_native::prelude::*;
|
||||
#[cfg(not(feature = "blitz"))]
|
||||
use dioxus::prelude::*;
|
||||
use mime_guess::Mime;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use ordermap::OrderSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::imp::{Platform, PlatformInterface as _};
|
||||
use crate::imp::{ConfigSystem, ConfigSystemInterface as _, Platform, PlatformInterface as _};
|
||||
|
||||
// Material Symbols icon component.
|
||||
// On blitz builds, renders <img> with a data URI SVG containing the explicit fill color,
|
||||
// since web fonts aren't available and <img> can't inherit CSS color.
|
||||
// On non-blitz builds, renders the icon font span as usual.
|
||||
|
||||
fn icon_svg_path(name: &str) -> &'static str {
|
||||
// Paths from Google Material Symbols Outlined, weight 700, FILL 1, 24px.
|
||||
// Coordinate space: viewBox="0 -960 960 960"
|
||||
match name {
|
||||
"attach_file" => "M772-320q0 117-87 195.5T479-46q-119 0-205-78.5T188-320v-392q0-86 63.5-144T403-914q88 0 150.5 58T616-712v371q0 55-40.5 92.5T479-211q-56 0-95.5-37.5T344-341v-370h116v370q0 7 5.5 11t13.5 4q8 0 14.5-3.5T500-341v-370q0-38-29-63t-68-25q-39 0-69 24.5T304-712v392q0 69 52.5 114T480-161q71 0 123.5-45T656-320v-429h116v429Z",
|
||||
"cadence" => "M417-86v-555h125v555H417ZM252-210v-308h125v308H252Zm330 0v-308h125v308H582ZM86-301v-126h126v126H86Zm661 0v-126h126v126H747ZM46-542v-125h69q37.98 0 70.99-18.5T239-737q37.96-64.01 102.17-100.5 64.2-36.5 139.02-36.5 74.81 0 138.87 36.5Q683.12-801.01 721-737q19.75 32.31 52.52 51.15Q806.29-667 844-667h70v125h-69q-72 0-133-34.5T614-672q-20.82-35.75-56.59-56.38Q521.65-749 480-749q-42 0-77.63 20.62Q366.75-707.75 346-672q-37 61-98 95.5T115-542H46Z",
|
||||
"error" => "M479.77-246Q509-246 529-265.77q20-19.77 20-49t-19.77-49.73q-19.77-20.5-49-20.5T431-364.5q-20 20.5-20 49.73 0 29.23 19.77 49t49 19.77ZM417-438h126v-263H417v263Zm63 392q-91 0-169.99-34.08-78.98-34.09-137.41-92.52-58.43-58.43-92.52-137.41Q46-389 46-480q0-91 34.08-169.99 34.09-78.98 92.52-137.41 58.43-58.43 137.41-92.52Q389-914 480-914q91 0 169.99 34.08 78.98 34.09 137.41 92.52 58.43 58.43 92.52 137.41Q914-571 914-480q0 91-34.08 169.99-34.09 78.98-92.52 137.41-58.43 58.43-137.41 92.52Q571-46 480-46Z",
|
||||
"graphic_eq" => "M255-215v-530h111v530H255ZM425-46v-868h110v868H425ZM86-385v-190h111v190H86Zm507 170v-530h111v530H593Zm170-170v-190h111v190H763Z",
|
||||
"mic" => "M479.88-354Q414-354 368-400.08 322-446.17 322-512v-233q0-65.83 46.12-111.92 46.12-46.08 112-46.08T592-856.92q46 46.09 46 111.92v233q0 65.83-46.12 111.92-46.12 46.08-112 46.08ZM425-59v-127q-121-16-199-109.12T148-512h111q0 92 64.7 156.5T480.2-291q91.8 0 156.3-64.64Q701-420.29 701-512h111q0 124-78 217T535-186v127H425Z",
|
||||
"mic_off" => "m772-347-82-82q9-18 13-37.5t4-45.5h110q0 44-11 87t-34 78ZM639-480 336-783v-11q13-44 54-76.5t95-32.5q66 0 112.5 46T644-745v233q0 9-1.5 18t-3.5 14ZM430-59v-127q-121-16-199-109t-78-217h111q0 92 64.5 156.5T485-291q43 0 81.5-15.5T634-349l80 80q-35 33-79 54.5T540-186v127H430Zm357-5L51-800l66-66 736 736-66 66Z",
|
||||
"person_edit" => "M554-86v-151l227-226q12-12.18 26.67-17.59Q822.33-486 837-486q16 0 30.55 6T894-462l37 37q10.82 12 16.91 26.67Q954-383.67 954-369q0 16-5.5 30.5T931-312L705-86H554Zm-428-23v-148q0-43.3 22.7-79.6 22.69-36.3 60.3-55.4 65-32 132.96-48.5Q409.92-457 480-457q42 0 81.33 4.97Q600.67-447.05 640-436L474-270v161H126Zm721-231 27-29-37-37-28 28 38 38ZM480-497q-81 0-137.5-56.5T286-691q0-81 56.5-137T480-884q81 0 137.5 56T674-691q0 81-56.5 137.5T480-497Z",
|
||||
"send" => "M89-128v-244l366-108L89-588v-244l831 352L89-128Z",
|
||||
"signal_cellular_alt" => "M176-126v-208h166v208H176Zm246 0v-408h166v408H422Zm246 0v-708h166v708H668Z",
|
||||
"signal_cellular_alt_2_bar" => "M176-126v-208h166v208H176Zm246 0v-408h166v408H422Z",
|
||||
"signal_disconnected" => "m703-385-66-66q17-24 26-52t9-57q0-38-15-73t-42-62l65-65q40 40 62.5 91.5T765-560q0 48-16.5 92.5T703-385ZM588-500 420-668q14-8 29-11.5t31-3.5q51 0 87 36t36 87q0 16-3.5 31T588-500Zm225 224-66-66q38-46 58.5-102T826-560q0-69-26.5-132.5T724-804l65-66q62 62 95.5 142T918-560q0 78-27 151t-78 133Zm16 263L543-299v202H417v-328L288-554v2q2 36 16.5 68.5T345-425l-65 65q-41-40-63-91.5T195-560q0-20 2.5-38.5T206-636l-48-48q-11 30-17.5 61t-6.5 63q0 69 26.5 132T236-316l-66 66q-60-63-94-142.5T42-560q0-51 11-100t34-94l-74-75 67-67L896-80l-67 67Z",
|
||||
"volume_off" => "M802-24 L679-149q-21 12-44.5 21T585-114v-99l12-4q6-2 12-5L505-328v249L258-326H78v-308h130L22-826l66-66L869-91l-67 67Zm18-253-69-71q17-30 25.5-63.5T785-481q0-93-56-166.5T585-749v-99q130 29 213 131.5T881-481q0 57-16 108t-45 96ZM687-413 585-517v-137q51 24 83.5 70.5T701-480q0 18-3.5 35T687-413ZM505-600 367-743l138-138v281Z",
|
||||
"volume_up" => "M586-114v-99q89-28 144.5-101.5T786-481q0-93-55.5-166.5T586-749v-99q130 29 212.5 131.5T881-481q0 133-82.5 235.5T586-114ZM79-326v-308h180l247-247v802L259-326H79Zm507 18v-346q51 25 83 71t32 103q0 56-32 102t-83 70Z",
|
||||
_ => panic!("unknown icon: {name}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_data_uri(name: &str, color: &str, opacity: f64) -> String {
|
||||
use base64::Engine;
|
||||
let path_d = icon_svg_path(name);
|
||||
let opacity_attr = if opacity < 1.0 {
|
||||
format!(r#" fill-opacity="{opacity}""#)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let svg = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="{color}"{opacity_attr} d="{path_d}"/></svg>"#
|
||||
);
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(svg.as_bytes());
|
||||
format!("data:image/svg+xml;base64,{b64}")
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Icon(
|
||||
name: String,
|
||||
#[props(default)] style: String,
|
||||
#[props(default)] color: String,
|
||||
#[props(default = 1.0)] opacity: f64,
|
||||
) -> Element {
|
||||
let fill = if color.is_empty() { "white" } else { &color };
|
||||
let src = icon_data_uri(&name, fill, opacity);
|
||||
rsx!(img {
|
||||
class: "material-symbols-outlined",
|
||||
style: "{style}",
|
||||
src: "{src}",
|
||||
})
|
||||
}
|
||||
|
||||
pub type ChannelId = u32;
|
||||
pub type UserId = u32;
|
||||
@@ -23,7 +83,7 @@ pub enum Command {
|
||||
Connect {
|
||||
address: String,
|
||||
username: String,
|
||||
config: ClientConfig,
|
||||
config: ProxyOverrides,
|
||||
},
|
||||
SendChat {
|
||||
markdown: String,
|
||||
@@ -430,17 +490,13 @@ pub fn ChatView() -> Element {
|
||||
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",
|
||||
Icon { name: "attach_file", color: "#ffffff", opacity: 0.5 }
|
||||
}
|
||||
}
|
||||
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",
|
||||
Icon { name: "send", color: "#ffffff", opacity: 0.5 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,7 +510,7 @@ pub fn ChatView() -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
let status = &STATE.status;
|
||||
let server = STATE.server.read();
|
||||
@@ -474,10 +530,10 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
|
||||
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
||||
|
||||
let proxy_url = config
|
||||
let proxy_url = overrides
|
||||
.read_unchecked()
|
||||
.as_ref()
|
||||
.and_then(|gui_config| gui_config.proxy_url.clone());
|
||||
.and_then(|overrides| overrides.proxy_url.clone());
|
||||
|
||||
let connecting_color = "yellow";
|
||||
let connected_color = "oklch(0.55 0.1184 141.35)";
|
||||
@@ -490,10 +546,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
class: "connection_status",
|
||||
style: "color: {connecting_color};",
|
||||
div {
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_cellular_alt_2_bar"
|
||||
}
|
||||
Icon { name: "signal_cellular_alt_2_bar", color: "yellow" }
|
||||
span {
|
||||
class: "status_text",
|
||||
" Connecting"
|
||||
@@ -506,10 +559,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
class: "connection_status",
|
||||
div {
|
||||
style: "color: {connected_color};",
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_cellular_alt"
|
||||
}
|
||||
Icon { name: "signal_cellular_alt", color: "#46823e" }
|
||||
span {
|
||||
class: "status_text",
|
||||
" Connected"
|
||||
@@ -526,10 +576,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
class: "connection_status",
|
||||
style: "color: {disconnected_color};",
|
||||
div {
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_disconnected"
|
||||
}
|
||||
Icon { name: "signal_disconnected", color: "gray" }
|
||||
span {
|
||||
class: "status_text",
|
||||
" Disconnected"
|
||||
@@ -542,10 +589,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
class: "connection_status",
|
||||
style: "color: {failed_color};",
|
||||
div {
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"error"
|
||||
}
|
||||
Icon { name: "error", color: "red" }
|
||||
span {
|
||||
class: "status_text",
|
||||
" Failed"
|
||||
@@ -567,10 +611,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
button {
|
||||
class: "toggle_button",
|
||||
onclick: move |_| net.send(Disconnect),
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_disconnected"
|
||||
}
|
||||
Icon { name: "signal_disconnected", color: "#ffffff", opacity: 0.5 }
|
||||
}
|
||||
}
|
||||
hr { style: "width: 100%;" }
|
||||
@@ -579,11 +620,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
class: "button_row",
|
||||
button {
|
||||
class: "user_edit_button",
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
style: "color: oklch(0.65 0.2245 28.06);",
|
||||
"person_edit"
|
||||
}
|
||||
Icon { name: "person_edit", color: "#fa3f36" }
|
||||
}
|
||||
div {
|
||||
class: "user_info",
|
||||
@@ -608,8 +645,8 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
net.send(UpdateMicEffects { denoise: new_denoise })
|
||||
},
|
||||
match denoise() {
|
||||
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
||||
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
||||
true => rsx!(Icon { name: "cadence", color: "#b23f43", opacity: 0.8938 }),
|
||||
false => rsx!(Icon { name: "graphic_eq", color: "#ffffff", opacity: 0.5 }),
|
||||
}
|
||||
}
|
||||
button {
|
||||
@@ -622,8 +659,8 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
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"}),
|
||||
true => rsx!(Icon { name: "mic_off", color: "#b23f43", opacity: 0.8938 }),
|
||||
false => rsx!(Icon { name: "mic", color: "#ffffff", opacity: 0.5 }),
|
||||
}
|
||||
}
|
||||
button {
|
||||
@@ -636,8 +673,8 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
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"}),
|
||||
true => rsx!(Icon { name: "volume_off", color: "#b23f43", opacity: 0.8938 }),
|
||||
false => rsx!(Icon { name: "volume_up", color: "#ffffff", opacity: 0.5 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,7 +682,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
||||
pub fn ServerView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
let server = STATE.server.read();
|
||||
let Some(&UserState {
|
||||
@@ -676,14 +713,14 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
||||
}
|
||||
div {
|
||||
class: "server_control_box",
|
||||
ControlView { config }
|
||||
ControlView { overrides }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
||||
pub fn LoginView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
|
||||
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
||||
@@ -695,30 +732,29 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
||||
}
|
||||
});
|
||||
|
||||
let mut address_input = use_signal(|| Platform::load_server_url());
|
||||
let mut address_input = use_signal(|| user_config.config_get::<String>("server_url"));
|
||||
let address = use_memo(move || {
|
||||
if let Some(addr) = address_input() {
|
||||
addr.clone()
|
||||
} else {
|
||||
config()
|
||||
overrides()
|
||||
.and_then(|c| c.proxy_url.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
});
|
||||
|
||||
let previous_username = Platform::load_username();
|
||||
let previous_username = user_config.config_get::<String>("username");
|
||||
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
|
||||
|
||||
let do_connect = move |_| {
|
||||
//let _ = set_default_username(&username.read());
|
||||
let _ = Platform::set_default_username(&username.read());
|
||||
if config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||
Platform::set_default_server(&address.read());
|
||||
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: config.read().clone().unwrap_or_default(),
|
||||
config: overrides.read().clone().unwrap_or_default(),
|
||||
})
|
||||
};
|
||||
let status = &STATE.status;
|
||||
@@ -763,7 +799,7 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
||||
None => rsx!(),
|
||||
}
|
||||
}
|
||||
if config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
||||
div {
|
||||
label {
|
||||
for: "address-entry",
|
||||
@@ -859,23 +895,24 @@ pub fn app() -> Element {
|
||||
static STYLE: Asset = asset!("/assets/main.scss");
|
||||
|
||||
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
|
||||
let config = use_resource(|| async move {
|
||||
match Platform::load_config().await {
|
||||
Ok(config) => config,
|
||||
Err(_) => ClientConfig::default(),
|
||||
let overrides = use_resource(|| async move {
|
||||
match Platform::load_proxy_overrides().await {
|
||||
Ok(overrides) => overrides,
|
||||
Err(_) => ProxyOverrides::default(),
|
||||
}
|
||||
});
|
||||
|
||||
let user_config = ConfigSystem::new().unwrap();
|
||||
|
||||
Platform::request_permissions();
|
||||
|
||||
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 { config }),
|
||||
_ => rsx!(LoginView { config }),
|
||||
Connected => rsx!(ServerView { overrides, user_config }),
|
||||
_ => rsx!(LoginView { overrides, user_config }),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
||||
use df::tract::{DfParams, DfTract, RuntimeParams};
|
||||
#[cfg(feature = "blitz")]
|
||||
use dioxus_native::prelude::{asset, manganis, Asset};
|
||||
#[cfg(not(feature = "blitz"))]
|
||||
use dioxus::prelude::{asset, manganis, Asset};
|
||||
use dioxus_asset_resolver::read_asset_bytes;
|
||||
use std::cell::RefCell;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
use crate::app::Command;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||
pub struct MobilePlatform;
|
||||
|
||||
impl super::PlatformInterface for MobilePlatform {
|
||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
Ok(ClientConfig {
|
||||
proxy_url: None,
|
||||
cert_hash: None,
|
||||
any_server: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_username(_username: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_server(server: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, gui_config).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
super::connect::get_status(client).await
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
}
|
||||
|
||||
fn request_permissions() {
|
||||
request_recording_permission();
|
||||
}
|
||||
|
||||
async fn sleep(duration: Duration) {
|
||||
tokio::time::sleep(duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn request_recording_permission() {}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn request_recording_permission() {
|
||||
use android_permissions::{PermissionManager, RECORD_AUDIO};
|
||||
use jni::{objects::JObject, JavaVM};
|
||||
|
||||
let ctx = ndk_context::android_context();
|
||||
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()).unwrap() };
|
||||
let activity = unsafe { JObject::from_raw(ctx.context().cast()) };
|
||||
|
||||
let manager = PermissionManager::create(vm, activity).unwrap();
|
||||
if !manager.check(&RECORD_AUDIO).unwrap() {
|
||||
manager.request(&[&RECORD_AUDIO]).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ use tokio::net::TcpStream;
|
||||
use tokio_rustls::rustls;
|
||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
|
||||
use tokio_rustls::rustls::ClientConfig;
|
||||
use tokio_rustls::rustls::DigitallySignedStruct;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoCertificateVerification;
|
||||
@@ -73,11 +73,11 @@ pub async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
overrides: &ProxyOverrides,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
let config = RlsClientConfig::builder()
|
||||
let config = ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::app::Command;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -11,48 +11,27 @@ pub struct DesktopPlatform;
|
||||
|
||||
impl super::PlatformInterface for DesktopPlatform {
|
||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||
type ConfigSystem = super::native_config::NativeConfigSystem;
|
||||
|
||||
async fn sleep(duration: Duration) {
|
||||
tokio::time::sleep(duration).await;
|
||||
}
|
||||
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
Ok(ClientConfig {
|
||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||
Ok(ProxyOverrides {
|
||||
proxy_url: None,
|
||||
cert_hash: None,
|
||||
any_server: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
let config = load_config_map();
|
||||
config.get("username").cloned()
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
let config = load_config_map();
|
||||
config.get("server").cloned()
|
||||
}
|
||||
|
||||
fn set_default_username(username: &str) -> Option<()> {
|
||||
let mut config = load_config_map();
|
||||
config.insert("username".to_string(), username.to_string());
|
||||
save_config_map(&config).ok()
|
||||
}
|
||||
|
||||
fn set_default_server(server: &str) -> Option<()> {
|
||||
let mut config = load_config_map();
|
||||
config.insert("server".to_string(), server.to_string());
|
||||
save_config_map(&config).ok()
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
overrides: &ProxyOverrides,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, gui_config).await
|
||||
super::connect::network_connect(address, username, event_rx, overrides).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
@@ -78,31 +57,3 @@ impl super::PlatformInterface for DesktopPlatform {
|
||||
// No-op on desktop
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config_path() -> std::path::PathBuf {
|
||||
let strategy = choose_app_strategy(AppStrategyArgs {
|
||||
top_level_domain: "com".to_string(),
|
||||
author: "Ohea Corp".to_string(),
|
||||
app_name: "Mumble Web2".to_string(),
|
||||
})
|
||||
.expect("failed to choose app strategy");
|
||||
strategy.config_dir().join("config.json")
|
||||
}
|
||||
|
||||
fn load_config_map() -> HashMap<String, String> {
|
||||
let config_path = get_config_path();
|
||||
match std::fs::read_to_string(&config_path) {
|
||||
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
||||
Err(_) => HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_config_map(config: &HashMap<String, String>) -> color_eyre::Result<()> {
|
||||
let config_path = get_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let contents = serde_json::to_string_pretty(config)?;
|
||||
std::fs::write(&config_path, contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::app::Command;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use std::future::Future;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||
@@ -10,38 +9,23 @@ pub struct MobilePlatform;
|
||||
|
||||
impl super::PlatformInterface for MobilePlatform {
|
||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||
type ConfigSystem = super::native_config::NativeConfigSystem;
|
||||
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
Ok(ClientConfig {
|
||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||
Ok(ProxyOverrides {
|
||||
proxy_url: None,
|
||||
cert_hash: None,
|
||||
any_server: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_username(_username: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_server(server: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
overrides: &ProxyOverrides,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, gui_config).await
|
||||
super::connect::network_connect(address, username, event_rx, overrides).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
use crate::{app::Command, effects::AudioProcessor};
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -50,11 +51,24 @@ pub trait AudioPlayerInterface {
|
||||
fn play_opus(&mut self, payload: &[u8]);
|
||||
}
|
||||
|
||||
pub trait ConfigSystemInterface: Sized {
|
||||
fn new() -> Result<Self, Error>;
|
||||
|
||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned;
|
||||
|
||||
fn config_set<T>(&self, key: &str, value: &T)
|
||||
where
|
||||
T: serde::Serialize;
|
||||
}
|
||||
|
||||
/// This is the main trait that each platform must implement. It combines all
|
||||
/// platform-specific functionality into a single interface, providing compile-time
|
||||
/// verification that all platforms implement the required functionality.
|
||||
pub trait PlatformInterface {
|
||||
type AudioSystem: AudioSystemInterface;
|
||||
type ConfigSystem: ConfigSystemInterface;
|
||||
|
||||
/// Initialize logging for the platform.
|
||||
fn init_logging();
|
||||
@@ -67,7 +81,7 @@ pub trait PlatformInterface {
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
proxy_overrides: &ProxyOverrides,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
|
||||
/// Get server status (user count, version, etc.).
|
||||
@@ -76,19 +90,7 @@ pub trait PlatformInterface {
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||
|
||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
|
||||
|
||||
/// Load saved username.
|
||||
fn load_username() -> Option<String>;
|
||||
|
||||
/// Load saved server URL.
|
||||
fn load_server_url() -> Option<String>;
|
||||
|
||||
/// Save the default username.
|
||||
fn set_default_username(username: &str) -> Option<()>;
|
||||
|
||||
/// Save the default server URL.
|
||||
fn set_default_server(server: &str) -> Option<()>;
|
||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>>;
|
||||
|
||||
/// Async sleep for the given duration.
|
||||
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
||||
@@ -98,15 +100,21 @@ pub trait PlatformInterface {
|
||||
// Platform Modules
|
||||
// ============================================================================
|
||||
|
||||
mod stub;
|
||||
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod connect;
|
||||
#[cfg(feature = "desktop")]
|
||||
mod desktop;
|
||||
#[cfg(feature = "mobile")]
|
||||
mod mobile;
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod native_audio;
|
||||
mod stub;
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod native_config;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
mod desktop;
|
||||
|
||||
#[cfg(feature = "mobile")]
|
||||
mod mobile;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
mod web;
|
||||
|
||||
@@ -133,6 +141,8 @@ pub type Platform = stub::StubPlatform;
|
||||
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
|
||||
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
|
||||
|
||||
pub type ConfigSystem = <Platform as PlatformInterface>::ConfigSystem;
|
||||
|
||||
// ========================
|
||||
// Platform Async Runtime
|
||||
// ========================
|
||||
@@ -164,3 +174,12 @@ const _: () = {
|
||||
let _ = assert_platform::<mobile::MobilePlatform>;
|
||||
let _ = assert_platform::<stub::StubPlatform>;
|
||||
};
|
||||
|
||||
fn global_default_config() -> HashMap<String, serde_json::Value> {
|
||||
serde_json::json!({})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::app::Command;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::ServerStatus;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct NativeConfigSystem {
|
||||
config_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl super::ConfigSystemInterface for NativeConfigSystem {
|
||||
fn new() -> color_eyre::Result<Self, Error> {
|
||||
return Ok(NativeConfigSystem {
|
||||
config_path: get_config_path()?,
|
||||
});
|
||||
}
|
||||
|
||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let config = load_config_map(&self.config_path);
|
||||
|
||||
let Some(value_untyped) = config.get(key).cloned().or_else(|| config_get_default(key))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
match serde_json::from_value::<T>(value_untyped) {
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => {
|
||||
let default_value = config_get_default(key)
|
||||
.expect("Default value required after config parse failure");
|
||||
Some(
|
||||
serde_json::from_value::<T>(default_value)
|
||||
.expect("Default value could not be parsed"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn config_set<T>(&self, key: &str, value: &T)
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
let mut config = load_config_map(&self.config_path);
|
||||
let json_value = serde_json::to_value(value).expect("failed to serialize config value");
|
||||
config.insert(key.to_string(), json_value);
|
||||
save_config_map(&config).expect("failed to set config")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "desktop"))]
|
||||
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||
|
||||
let strategy = choose_app_strategy(AppStrategyArgs {
|
||||
top_level_domain: "xyz".to_string(),
|
||||
author: "ohea".to_string(),
|
||||
app_name: "Mumble Web2".to_string(),
|
||||
})
|
||||
.expect("failed to choose app strategy");
|
||||
Ok(strategy.config_dir().join("config.json"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
||||
let ctx = ndk_context::android_context();
|
||||
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?;
|
||||
let mut env = vm.attach_current_thread()?;
|
||||
let ctx = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
|
||||
let cache_dir = env
|
||||
.call_method(ctx, "getFilesDir", "()Ljava/io/File;", &[])?
|
||||
.l()?;
|
||||
let cache_dir: jni::objects::JString = env
|
||||
.call_method(&cache_dir, "toString", "()Ljava/lang/String;", &[])?
|
||||
.l()?
|
||||
.try_into()?;
|
||||
let cache_dir = env.get_string(&cache_dir)?;
|
||||
let cache_dir = cache_dir.to_str()?;
|
||||
Ok(std::path::PathBuf::from(cache_dir).join("config.json"))
|
||||
}
|
||||
|
||||
fn load_config_map(config_path: &std::path::PathBuf) -> HashMap<String, serde_json::Value> {
|
||||
match std::fs::read_to_string(config_path) {
|
||||
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
||||
Err(_) => HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_config_map(config: &HashMap<String, serde_json::Value>) -> color_eyre::Result<()> {
|
||||
let config_path = get_config_path().expect("Could not get config file path.");
|
||||
if let Some(parent) = config_path.parent() {
|
||||
info!("Creating config directory: {}", parent.display());
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let contents = serde_json::to_string_pretty(config)?;
|
||||
info!("Writing config to {}", config_path.display());
|
||||
std::fs::write(&config_path, contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
||||
let default_config = platform_default_config();
|
||||
default_config
|
||||
.get(key)
|
||||
.cloned()
|
||||
.or(super::global_default_config().get(key).cloned())
|
||||
}
|
||||
|
||||
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
||||
serde_json::json!({})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
@@ -3,13 +3,14 @@
|
||||
use crate::effects::AudioProcessor;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use std::future::Future;
|
||||
|
||||
pub struct StubPlatform;
|
||||
|
||||
impl super::PlatformInterface for StubPlatform {
|
||||
type AudioSystem = StubAudioSystem;
|
||||
type ConfigSystem = StubConfigSystem;
|
||||
|
||||
fn init_logging() {
|
||||
panic!("stubbed platform")
|
||||
@@ -23,7 +24,7 @@ impl super::PlatformInterface for StubPlatform {
|
||||
_address: String,
|
||||
_username: String,
|
||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||
_gui_config: &ClientConfig,
|
||||
_overrides: &ProxyOverrides,
|
||||
) -> impl Future<Output = Result<(), Error>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
@@ -34,26 +35,10 @@ impl super::PlatformInterface for StubPlatform {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn set_default_username(_username: &str) -> Option<()> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn set_default_server(_server: &str) -> Option<()> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
@@ -92,6 +77,28 @@ impl super::AudioPlayerInterface for StubAudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StubConfigSystem;
|
||||
|
||||
impl super::ConfigSystemInterface for StubConfigSystem {
|
||||
fn new() -> Result<Self, Error> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn config_set<T>(&self, key: &str, value: &T)
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct SpawnHandle;
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ use dioxus::prelude::*;
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use js_sys::Float32Array;
|
||||
use mumble_protocol::control::ClientControlCodec;
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -62,6 +63,7 @@ pub struct WebPlatform;
|
||||
|
||||
impl super::PlatformInterface for WebPlatform {
|
||||
type AudioSystem = WebAudioSystem;
|
||||
type ConfigSystem = WebConfigSystem;
|
||||
|
||||
fn init_logging() {
|
||||
// copied from tracing_web example usage
|
||||
@@ -89,53 +91,28 @@ impl super::PlatformInterface for WebPlatform {
|
||||
// No-op on web
|
||||
}
|
||||
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
|
||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||
let overrides = match option_env!("MUMBLE_WEB2_PROXY_OVERRIDES_URL") {
|
||||
Some(url) => Url::parse(url)?,
|
||||
None => absolute_url("config")?,
|
||||
None => absolute_url("overrides")?,
|
||||
};
|
||||
info!("loading config from {}", config_url);
|
||||
info!("loading config from {}", overrides);
|
||||
|
||||
let config = reqwest::get(config_url)
|
||||
let config = reqwest::get(overrides)
|
||||
.await?
|
||||
.json::<ClientConfig>()
|
||||
.json::<ProxyOverrides>()
|
||||
.await?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn load_username() -> Option<String> {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.get_item("username")
|
||||
.ok()?
|
||||
}
|
||||
|
||||
fn load_server_url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_username(username: &str) -> Option<()> {
|
||||
web_sys::window()?
|
||||
.local_storage()
|
||||
.ok()??
|
||||
.set_item("username", username)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn set_default_server(_server: &str) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
overrides: &ProxyOverrides,
|
||||
) -> Result<(), Error> {
|
||||
network_connect(address, username, event_rx, gui_config).await
|
||||
network_connect(address, username, event_rx, overrides).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
@@ -456,7 +433,7 @@ pub async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
gui_config: &ClientConfig,
|
||||
overrides: &ProxyOverrides,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
@@ -469,7 +446,7 @@ pub async fn network_connect(
|
||||
)
|
||||
.ey()?;
|
||||
|
||||
if let Some(server_hash) = &gui_config.cert_hash {
|
||||
if let Some(server_hash) = &overrides.cert_hash {
|
||||
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
||||
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
||||
}
|
||||
@@ -523,3 +500,64 @@ pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
||||
let location = window.location();
|
||||
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct WebConfigSystem {}
|
||||
|
||||
impl super::ConfigSystemInterface for WebConfigSystem {
|
||||
fn new() -> Result<Self, Error> {
|
||||
return Ok(WebConfigSystem {});
|
||||
}
|
||||
|
||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
// Get Storage
|
||||
let storage = web_sys::window()?.local_storage().ok()??;
|
||||
|
||||
// Try localStorage first
|
||||
if let Ok(Some(raw)) = storage.get_item(key) {
|
||||
if let Ok(parsed) = serde_json::from_str::<T>(&raw) {
|
||||
return Some(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default if deserialization fails or key missing
|
||||
let default_value = config_get_default(key)?;
|
||||
serde_json::from_value::<T>(default_value).ok()
|
||||
}
|
||||
|
||||
fn config_set<T>(&self, key: &str, value: &T)
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
let storage = window()
|
||||
.and_then(|w| w.local_storage().ok().flatten())
|
||||
.expect("localStorage not available");
|
||||
|
||||
let json_value =
|
||||
serde_json::to_string(value).expect("failed to serialize config value to JSON string");
|
||||
|
||||
storage
|
||||
.set_item(key, &json_value)
|
||||
.expect("failed to write to localStorage");
|
||||
}
|
||||
}
|
||||
|
||||
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
||||
let default_config = platform_default_config();
|
||||
default_config
|
||||
.get(key)
|
||||
.cloned()
|
||||
.or(super::global_default_config().get(key).cloned())
|
||||
}
|
||||
|
||||
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
||||
serde_json::json!({})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ use app::STATE;
|
||||
use asynchronous_codec::FramedRead;
|
||||
use asynchronous_codec::FramedWrite;
|
||||
use color_eyre::eyre::{bail, Error};
|
||||
#[cfg(feature = "blitz")]
|
||||
use dioxus_native::prelude::*;
|
||||
#[cfg(not(feature = "blitz"))]
|
||||
use dioxus::prelude::*;
|
||||
use futures::select;
|
||||
use futures::AsyncRead;
|
||||
|
||||
@@ -2,5 +2,8 @@ use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
|
||||
|
||||
pub fn main() {
|
||||
Platform::init_logging();
|
||||
#[cfg(feature = "blitz")]
|
||||
dioxus_native::launch(app::app);
|
||||
#[cfg(not(feature = "blitz"))]
|
||||
dioxus::launch(app::app);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use rand::Rng;
|
||||
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
||||
use salvo::cors::{AllowOrigin, Cors};
|
||||
@@ -16,7 +16,7 @@ use tokio::net::TcpStream;
|
||||
use tokio::pin;
|
||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls::{ClientConfig as RlsClientConfig, DigitallySignedStruct};
|
||||
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct};
|
||||
use tokio_rustls::{rustls, TlsConnector};
|
||||
use tracing::info;
|
||||
use tracing::info_span;
|
||||
@@ -77,7 +77,7 @@ async fn main() -> Result<()> {
|
||||
.install_default()
|
||||
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
||||
|
||||
let mut client_config = ClientConfig {
|
||||
let mut overrides = ProxyOverrides {
|
||||
proxy_url: match &server_config.proxy_url {
|
||||
Some(url) => Some(url.to_string()),
|
||||
None => None,
|
||||
@@ -102,7 +102,7 @@ async fn main() -> Result<()> {
|
||||
let cert = cert_params.self_signed(&key_pair)?;
|
||||
|
||||
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
||||
client_config.cert_hash = Some(hash.into());
|
||||
overrides.cert_hash = Some(hash.into());
|
||||
|
||||
(cert.pem().into(), key_pair.serialize_pem().into())
|
||||
}
|
||||
@@ -122,14 +122,11 @@ async fn main() -> Result<()> {
|
||||
};
|
||||
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
|
||||
|
||||
info!(
|
||||
"client config:\n{}",
|
||||
toml::to_string_pretty(&client_config)?
|
||||
);
|
||||
info!("proxy overrides:\n{}", toml::to_string_pretty(&overrides)?);
|
||||
|
||||
let config_craft = ConfigCraft {
|
||||
server_config: server_config.clone(),
|
||||
client_config,
|
||||
overrides,
|
||||
};
|
||||
|
||||
let status_craft = StatusCraft {
|
||||
@@ -139,7 +136,7 @@ async fn main() -> Result<()> {
|
||||
// Server routing
|
||||
let mut router = Router::new()
|
||||
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
|
||||
.push(Router::with_path("/config").get(config_craft.get_config()))
|
||||
.push(Router::with_path("/overrides").get(config_craft.get_overrides()))
|
||||
.push(Router::with_path("/status").get(status_craft.get_status()))
|
||||
.hoop(Logger::new());
|
||||
if let Some(gui_path) = server_config.gui_path.clone() {
|
||||
@@ -252,14 +249,14 @@ impl StatusCraft {
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigCraft {
|
||||
server_config: Arc<Config>,
|
||||
client_config: ClientConfig,
|
||||
overrides: ProxyOverrides,
|
||||
}
|
||||
|
||||
#[craft]
|
||||
impl ConfigCraft {
|
||||
#[craft(handler)]
|
||||
async fn get_config(&self) -> Json<ClientConfig> {
|
||||
Json(self.client_config.clone())
|
||||
async fn get_overrides(&self) -> Json<ProxyOverrides> {
|
||||
Json(self.overrides.clone())
|
||||
}
|
||||
|
||||
#[craft(handler)]
|
||||
@@ -320,7 +317,7 @@ async fn connect_proxy_impl(
|
||||
) -> Result<()> {
|
||||
info!("connecting to Mumble server...");
|
||||
|
||||
let config = RlsClientConfig::builder()
|
||||
let config = ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||