Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26a08acc36 | |||
| b20ed1ff56 | |||
| 765446392d | |||
| 2c22942fb3 | |||
| 75990ca9ce | |||
| 9f6557bb92 |
@@ -1 +0,0 @@
|
||||
target
|
||||
@@ -42,47 +42,6 @@ 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 ProxyOverrides {
|
||||
pub struct ClientConfig {
|
||||
pub proxy_url: Option<String>,
|
||||
pub cert_hash: Option<Vec<u8>>,
|
||||
pub any_server: bool,
|
||||
@@ -16,3 +16,13 @@ pub struct ServerStatus {
|
||||
pub max_users: Option<u32>,
|
||||
pub bandwidth: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
||||
pub struct ServerEntry {
|
||||
pub name: String,
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
localhost:64444 {
|
||||
tls internal
|
||||
tls internal
|
||||
|
||||
# Proxy /config path to mumble-web2-proxy
|
||||
reverse_proxy /overrides http://127.0.0.1:4400
|
||||
# Proxy /config path to mumble-web2-proxy
|
||||
reverse_proxy /config http://127.0.0.1:4400
|
||||
|
||||
# Proxy /status path to mumble-web2-proxy
|
||||
reverse_proxy /status 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
|
||||
|
||||
# Proxy root path to dx-serve
|
||||
reverse_proxy http://127.0.0.1:8080
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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_PROXY_OVERRIDES_URL=https://localhost:64444/overrides
|
||||
# - MUMBLE_WEB2_GUI_CONFIG_URL=https://localhost:64444/config
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
# command: >
|
||||
|
||||
@@ -68,7 +68,6 @@ 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"
|
||||
@@ -107,7 +106,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.17.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]
|
||||
@@ -115,6 +114,12 @@ 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",
|
||||
@@ -150,4 +155,3 @@ mobile = [
|
||||
"cpal",
|
||||
"dasp_ring_buffer",
|
||||
]
|
||||
blitz = ["dioxus-native"]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#000000" fill-rule="evenodd" d="M11.7071,4.29289 L15.4142,8 L11.7071,11.7071 C11.3166,12.0976 10.6834,12.0976 10.2929,11.7071 C9.90237,11.3166 9.90237,10.6834 10.2929,10.2929 L11.5858,9 L2,9 C1.44771,9 1,8.55228 1,8 C1,7.44772 1.44771,7 2,7 L11.5858,7 L10.2929,5.70711 C9.90237,5.31658 9.90237,4.68342 10.2929,4.29289 C10.6834,3.90237 11.3166,3.90237 11.7071,4.29289 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 601 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12V17" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 12V17" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 7H20" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 10V18C6 19.6569 7.34315 21 9 21H15C16.6569 21 18 19.6569 18 18V10" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 862 B |
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#000000;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M491.878,156.348C472.437,110.39,439.989,71.33,399.14,43.731C358.307,16.131,308.964-0.008,256,0
|
||||
c-35.304,0-69.011,7.167-99.652,20.122C110.39,39.564,71.33,72.011,43.731,112.86C16.131,153.693-0.008,203.036,0,256
|
||||
c0,35.304,7.167,69.02,20.122,99.653c19.442,45.957,51.889,85.016,92.738,112.616c40.832,27.6,90.176,43.74,143.14,43.731
|
||||
c35.305,0,69.02-7.166,99.653-20.122c45.957-19.442,85.017-51.889,112.617-92.738c27.6-40.832,43.74-90.176,43.731-143.14
|
||||
C512,220.697,504.842,186.98,491.878,156.348z M427.814,110.348c0.774,0.915,1.53,1.856,2.294,2.789
|
||||
c-1.496-0.454-2.991-0.908-4.486-1.37C426.353,111.297,427.084,110.819,427.814,110.348z M382.832,101.182
|
||||
C387.142,100.754,380.446,101.434,382.832,101.182c-1.798-0.126-3.159-0.858-4.066-2.177
|
||||
C384.579,95.217,388.747,100.585,382.832,101.182z M290.917,81.127c1.613,4.142-9.956,0.277-11.216-0.336
|
||||
c-0.739-0.739-1.294-1.58-1.663-2.52C278.021,79.203,290.388,79.749,290.917,81.127z M258.79,75.406
|
||||
c2.823,0.958,14.022-1.572,14.383,1.722c0.673,6.049-3.99,0.058-4.956,0.058c-2.622,0,1.21,2.78,1.31,2.923
|
||||
c-0.656-0.957-8.461-0.857-10.107-0.462C254.656,88.352,241.675,69.592,258.79,75.406z M271.711,87.026
|
||||
c-3.108,1.142-8.443,0.168-11.754,0.168C253.808,85.58,276.718,85.194,271.711,87.026z M79.236,313.19
|
||||
C79.06,315.812,79.346,311.544,79.236,313.19c0.042-0.663,1.126-5.755,2.084-6.049c1.513-0.453,4.613,6.999,4.487,8.041
|
||||
C85.11,321.206,78.892,318.148,79.236,313.19z M136.15,339.169c-3.252-3.983-5.192-8.461-8.284-12.528
|
||||
c-0.63-0.84-11.031-6.754-9.058-7.796c4.243-2.21,39.505,18.517,37.934,21.676c-0.152,0.303-7.889-6.797-9.847-5.52
|
||||
c-1.302,0.848,2.689,3.932,2.689,5.419c2.016,1.033,7.149,8.646,2.932,8.872C147.862,349.545,138.872,342.512,136.15,339.169z
|
||||
M154.894,340.546c0.21,1.37-3.646-1.185-3.873-1.277C151.777,337.884,154.718,339.412,154.894,340.546z M151.92,353.208
|
||||
c-3.898-2.487,12.569-0.554,13.459-0.487c3.252,0.243,11.418,0.076,13.552,3.974C179.569,357.862,155.574,355.544,151.92,353.208
|
||||
C154.356,354.77,150.122,352.066,151.92,353.208z M188.686,317.644c4.125-4.201,8.839,3.235,9.174,2.932
|
||||
c-2.596,2.369-6.486,6.94-1.252,9.671c3.932,2.058-4.672,3.537-4.672,4.268c0,2.966-4.771,10.795-7.814,12.014
|
||||
c0.177-0.067-11.913-3.419-12.972-3.722c-2.638-0.764-3.445-8.082-5.058-10.426C166.093,328.036,184.51,321.895,188.686,317.644z
|
||||
M167.53,281.869c1.756-1.328,3.378-1.479,4.848-0.454C173.546,287.801,165.514,282.785,167.53,281.869z M195.718,306.864
|
||||
c0.009-0.017,0.017-0.025,0.034-0.034c0.067-0.059,0.101-0.084,0.092-0.076c1.05-0.857,3.806-3.302,4.898-3.546
|
||||
C203.716,302.529,193.676,308.502,195.718,306.864z M201.658,294.278c-0.563-3.353-0.151-3.974,0.093-6.68
|
||||
c0.428-4.713,5.915-6.772,7.007-1.092c0.193,0.975-5.721,9.88-1.218,9.317c3.612-0.454,6.301-0.622,5.134,3.932
|
||||
c-1.076-0.714-7.814-4.075-8.637-3.898c-0.798,0.168-3.36,8.746-6.049,7.939C199.162,304.151,201.809,295.95,201.658,294.278z
|
||||
M200.734,269.073c0.084-1.328,2.042-5.125,4.176-4.688c2.461,0.513-0.731,10.141-2.487,9.032c-1.26-1.193-1.823-2.639-1.689-4.335
|
||||
C200.7,269.678,200.608,271.199,200.734,269.073z M213.479,300.403c0.016-0.008,0.016-0.016,0.034-0.016
|
||||
c4.797-2.42,3.452,4.218,3.965,4.452C216.831,304.546,213.101,300.613,213.479,300.403z M218.184,308.04
|
||||
C217.26,307.889,219.276,308.217,218.184,308.04c1.521,0.244,2.269,3.369,2.42,4.378c0.621,4.243,0.546,2.411-2.26,3.52
|
||||
c-0.765,0.294,0.084,2.747-1.672,2.865c-0.84,0.05-5.436-0.908-3.948-2.672c3.091-3.663-5.663-0.815-5.663-1.134
|
||||
c0-1.227,1.386-4.075,3.184-3.697C215.504,312.384,215.58,307.62,218.184,308.04z M223.435,335.086
|
||||
c-1.311-1.748-0.118-6.075,1.159-7.512c-1.016,1.142,2.706,3.058,2.882,2.26C227.081,331.632,223.661,335.388,223.435,335.086z
|
||||
M230.03,343.202c-0.076-0.739-6.041-0.588-6.948-0.73C221.51,342.471,229.593,338.766,230.03,343.202z M223.704,262.839
|
||||
c-3.907,0.89,0.816-2.747,1.664-2.823C226.518,259.907,223.914,262.554,223.704,262.839z M222.393,355.989
|
||||
c1.529,3.941-8.503,4.672-9.906,5.008c1.076-0.261,7.284-2.588,7.898-4.26C220.494,356.426,221.612,353.964,222.393,355.989z
|
||||
M200.557,343.369c-2.243,1.227,0.21,5.831-2.16,6.066c-3.503,0.352-1.638-8.755-0.866-11.208
|
||||
c3.798-11.998,9.352,4.495,12.469-3.47c-0.252,0.647-10.847-2.655-9.091-3.201c0.025-0.008,0.051-0.008,0.076-0.017
|
||||
c2.89-0.873,12.997-3.184,15.408-2.596c0.571,0.134-6.662,10.132-10.636,9.83c4.798,0.369,2.705,7.419,0.941,9.351
|
||||
c-0.63,0.689,4.411,2.646,3.722,3.117C207.993,352.914,197.574,345,200.557,343.369z M206.497,356.938
|
||||
c0.597,0.303-2.731,1.025-3.159,1.109c-1.377,0.47-2.688,0.412-3.932-0.176C198.465,357.115,205.691,356.535,206.497,356.938z
|
||||
M199.297,358.946c-0.647,0.404-1.336,0.404-2.067,0C195.256,357.744,198.348,357.938,199.297,358.946z M205.548,300.05
|
||||
c0,0,0,0-0.009,0c-0.025-0.025-0.051-0.042-0.076-0.058c0.017,0.008,0.034,0.025,0.058,0.041c-0.235-0.159-2.68-1.814-0.84-2.008
|
||||
C207.278,297.757,205.582,300.076,205.548,300.05z M208.783,308.284c-2.798,0-3.016-4.05-3.134-5.621
|
||||
C207.748,296.421,209.429,308.284,208.783,308.284z M209.975,358.132c0.74-0.387,8.208-2.403,7.704-1.613
|
||||
c-0.471,0.748-8.838,4.604-9.679,2.924C208.228,358.527,208.883,358.09,209.975,358.132z M218.839,343.202c0.008,0,0.017,0,0.017,0
|
||||
c-0.026,0-0.009,0-0.026,0c-1.142,0.051-1.622-0.328-1.403-1.143c2.445-1.126,3.344,1.303,1.429,1.143
|
||||
C219.318,343.244,219.091,343.226,218.839,343.202z M209.707,304.738c-0.093-0.042-0.06-0.025-0.009-0.008
|
||||
c-1.294-0.512-0.109-3.394,1.084-2.118C211.177,303.041,210.454,305.016,209.707,304.738z M212.244,305.528
|
||||
c-0.084,0.68-0.605,3.453-1.84,3.453c-0.218-0.74-0.218-1.479,0-2.218C210.539,305.629,211.151,305.218,212.244,305.528z
|
||||
M202.355,305.184L202.355,305.184c-0.58,1.168-7.78,9.838-8.839,5.184C193.542,310.486,201.884,306.142,202.355,305.184z
|
||||
M194.903,356.737c-1.512,0.395-2.789,1.294-4.47,0.344C188.719,356.115,194.836,356.737,194.903,356.737z M187.451,356.325
|
||||
c-0.992,0-0.446-0.656,0.268-0.739C188.442,355.502,188.35,356.325,187.451,356.325z M281.365,457.809
|
||||
c-5.629,13.039-11.771-7.864-10.998-7.62C277.752,452.726,286.145,446.736,281.365,457.809z M293.849,421.691
|
||||
c-1.949,5.721-3.814,11.728-7.343,16.845c-4.864,7.049-8.561,3.294-15.064,3.234c-3.723-0.034-2.849,2.866-6.916,1.278
|
||||
c-2.285-0.899-5.402-1.597-7.444-2.916c2.521,1.63-4.394-9.427-3.764-5.411c-0.344-2.167-2.823-1.084-4.848-1.479
|
||||
c1.093-1.386,1.042-3.537,2.151-4.915c-2.747-0.067-5.091,1.135-6.814,3.277c-7.738-9.561-14.93-10.452-27.372-7.327
|
||||
c-2.882,0.723-8.604,5.361-10.208,5.361c-6.537,0-8.041-0.261-12.914,3.596c-3.344,2.646-11.703,0.84-12.678-3.596
|
||||
c-0.446-2.016,3.218-4.797,3.226-6.89c0.008-1.327-3.84-2.596-4.31-3.94c-0.723-2.108,1-4.596-0.781-6.343
|
||||
c-0.748-0.731-4.125-3.15-4.243-4.15c-0.151-1.319,2.874-2.731,2.874-4.386c0-2.336,0-4.663,0-6.999
|
||||
c0-5.284,14.291-6.46,19.962-9.057c6.629-3.025,1.764-4.906,5.881-8.427c1.236-1.05,5.529,1.008,6.822,0
|
||||
c0.739-0.571-0.218-3.36,0.781-4.478c1.412-1.562,7.796-8.108,8.284-1.874c0.286,3.697-0.067,3.402,3.99,3.402
|
||||
c3.268,0-0.176-1.604,1.302-2.948c2.244-2.05,5-8.645,7.898-9.511c1.697-0.513,10.183,3.268,12.914,3.277
|
||||
c-0.95,2.73-1.874,5.478-2.865,8.2c4.267,2.16,12.258,8.788,17.223,5.906c2.318-1.352,3.268-13.014,3.949-15.745
|
||||
c2.881,4.453,7.83,7.755,9.519,12.09c2.579,6.604,6.864,6.83,10.729,12.233c3.604,5.016,6.906,9.864,9.906,15.199
|
||||
C297.757,412.423,296.564,413.717,293.849,421.691z M237.466,240.155c1.05,1.444-3.789,8.956-4.907,6.982
|
||||
c-0.772-1.37-0.982-4.73-1.554-6.428c-0.109-0.319-0.319-0.958,0-0.008C230.064,237.928,236.24,238.475,237.466,240.155z
|
||||
M267.989,202.381c3.991-0.008,4.168,5.125,8.604,3.772c-0.866,0.26,2.478,3.621-2.16,4.486c-2.916,0.529-6.965,3.73-10.166,1.63
|
||||
c0.428,0.286,0.218,0.151,0.008,0.016c1.95,1.311-3.47,2.412-3.671,0.89C260.604,213.202,270.182,202.372,267.989,202.381z
|
||||
M268.569,177.066c0,0.092,1.604-2.21,2.252-3.176c-0.706,2.824,6.435,16.526,4.335,17.173c-1.31,0.404-3.101-2.134-3.596-1.529
|
||||
c-2.209,2.697,1.74,6.83,0.479,7.973c0.319-0.286-2.958,0.723-3.772,1.193c0.017,0.152,0.042,0.236,0.067,0.236
|
||||
c-0.335,0-0.302-0.093-0.067-0.236C268.023,196.458,268.712,178.62,268.569,177.066z M291.959,363.442
|
||||
c-7.687,1.613-17.484-13.88-21.886-7.05c-3.151,4.89-16.854-2.201-17.409-0.083c0.782-2.992,5.84-0.933,2.579-5.294
|
||||
c-2.805-3.756-6.351-3.546-10.897-4.201c-3.251-0.47-2.352-2.453-3.772-4.268c-0.874-1.117-1.832,1.958-2.689,1.639
|
||||
c-0.118-0.042-4.848-6.982-4.848-7.209c5.864-7.688,7.494,2.764,10.771,4.1c6.612,2.697,6.704-4.73,13.098-1.806
|
||||
c3.251,1.487,27.7,10.418,26.877,12.493c-0.428,0.496-0.949,0.874-1.571,1.118C283.382,354.77,291.144,363.618,291.959,363.442z
|
||||
M239.138,239.726c0-0.009,0-0.009,0-0.017c0.907-2.848,8.679-3.907,7.301-0.756C245.725,240.592,238.088,243.12,239.138,239.726z
|
||||
M261.218,221.948c0.008-0.176,3.126-11.066,4.722-8.15c1.756,3.218,0.95,19.433-2.874,21.769
|
||||
c-0.991-0.084-1.352-0.597-1.076-1.538c0,2.05-13.325,5.05-13.14,5.234c-2.075-2.151,1.344-3.428-3.756-2.941
|
||||
c0.075,0-11.309,1.487-7.746-0.546c3.798-2.168,7.074-2.714,11.527-2.798c3.646-0.067,3.319-5.276,5.94-5.511
|
||||
c1.227-0.109,0.538,3.369,2.748,1.537C260.478,226.586,260.881,225.46,261.218,221.948z M292.53,351.729
|
||||
c-2,0.488-7.343-2.218-5.478-2.453c-1.646,0.201-0.588,0.067,0.017-0.009c3.285-0.412,7.612-1.697,10.217-1.966
|
||||
C299.899,347.041,293.539,351.486,292.53,351.729z M299.706,347.142c-0.513-0.538-0.908-1.134-1.16-1.806
|
||||
C296.354,341.832,303.705,348.604,299.706,347.142z M376.371,386.555c-0.924-0.143-1.252-0.672-0.991-1.58
|
||||
c0.984-2.823,3.059,1.033,3.386,1.37C377.968,386.412,377.169,386.488,376.371,386.555z M379.958,381.53
|
||||
c-0.865,0,1.924-3.042,3.588-1.395C385.26,381.824,380.295,381.446,379.958,381.53z M397.502,128.882
|
||||
c-4.588-0.865-3.108-4.159-6.713-4.462c-4.226-0.353-1.932,5.016-5.713,4c-13.897-3.73-0.63,5.469-4.882,9.612
|
||||
c0.428-0.412-7.335-1.093-8.712-0.622c-2.815,0.966-6.738,1.622-9.108,3.352c-3.193,2.336-6.385,4.672-9.578,6.999
|
||||
c-1.68,1.236-3.822-2.016-6.15-0.932c-2.731,1.269-8.78,0.513-10.595,1.697c-1.941,1.277-3.369,7.192-4.318,9.208
|
||||
c-3.756,8.007-8.108,25.087-19.727,26.222c-0.572-4.79-5.957-24.718,3.73-25.752c7.747-0.84,18.82-12.014,16.728-19.845
|
||||
c-7.771,0.311-3.847,4.739-7.183,6.235c-6.084,2.722-6.94-3.168-9.074-2.286c-6.377,2.655-6.974,2.454-8.284,8.755
|
||||
c-0.487,2.336-8.847,0.774-11.107,0.689c-6.436-0.227-22.358-2.588-27.138,0.605c-7.427,4.957-14.854,9.914-22.282,14.871
|
||||
c3.982,7.579,14.846,2.512,17.224,10.167c1.571,5.023-5.192,14.938-8.914,18.76c-3.664,3.772-7.335,7.545-11.006,11.318
|
||||
c-3.512,3.612-3.991-0.488-7.352,1.411c-5.218,2.941-5.201,11.897-12.57,12.14c3.117,4.109,3.681,7.016,4.058,11.847
|
||||
c0.378,4.89-3.243,4.487-8.007,6.192c-0.143-3.016-0.588-6.04,0-9.023c0.849-4.26-4.074-0.336-3.226-4.588
|
||||
c1.47-7.385-7.906-3.201-11.846-2.134c0.302-2.134-0.412-4.444,0-6.561c-4.067,2.294-8.132,4.595-12.199,6.889
|
||||
c3.688,3.185,6.284,7.276,11.838,4.587c0.571,4.075-3.126,4.537-6.814,6.562c5.142,3,5.436,9.83,6.948,14.779
|
||||
c1.932,6.285-2.252,8.41-6.645,13.561c-4.672,5.478-5.655,6.982-12.746,8.931c-3.932,1.092-7.864,2.176-11.796,3.26
|
||||
c-2,0.554-0.319,3.428-4.117,3.428c-0.982-6.73-8.847-1.386-11.258,1.512c-3.579,4.284,3.159,7.486,6.756,10.998
|
||||
c9.653,9.436-3.874,15.123-10.931,19.962c-2.563-6.772-10.234-8.67-12.208-14.106c-2.689,9.838-3.285,10.074,4.033,17.568
|
||||
c5.335,5.469,6.058,9.796,8.175,16.87c-4.31-1.966-9.377-2.907-11.073-7.142c-2.076-5.167-3.621-8.048-7.47-12.267
|
||||
c-3.965-4.352-4.134-17.256-6.225-23.231c-1.555,1.168-4.201,1.118-5.747,2.294c-1.16-4.436-4.217-16.913-9.687-18.694
|
||||
c-4.453-1.453-14.241,4.646-17.82,7.267c-4.721,3.47-15.476,8.226-15.636,14.09c-0.268,10.234-0.538,10.998-8.897,17.677
|
||||
c-8.007-11.67-12.108-21.239-14.359-34.775c-7.662,3.755-11.872-6.578-16.509-10.821c-2.151-1.957-6.16-2.151-10.461-1.966
|
||||
c-0.092-2.655-0.15-5.318-0.15-7.99c0-31.145,6.301-60.728,17.694-87.672c16.123-38.135,42.504-70.936,75.649-94.922
|
||||
c2.142-0.05,4.251,0.286,6.326,1.294c4.025,1.966,9.368-4.974,15.392-2.924c-2.142-8.746,18.668-9.174,13.997,0.328
|
||||
c4.537-1.336,8.486-3.184,13.208-2.487c7.746,1.126,5.578,2.403,4.529,9.368c-0.042,0.261-24.189,8.838-15.812,10.014
|
||||
c8.108,1.143,15.132-4.327,23.542-3.713c9.687,0.698,14.005,4.89,24.424,5.184c-4.159-9.99,12.728-2.789,17.946-1.638
|
||||
c-5.78,4.748,5.738,10.158,9.696,13.452c-0.143-7.301,14.871-5.973,21.6-5.629c3.706,0.194,1.983-6.638,6.57-5.839
|
||||
c6.226,1.084,12.443,2.167,18.668,3.251c5.252,0.916,8.015-0.344,13.148,2.403c3.512,1.882,1.143,6.394,5.948,5.882
|
||||
c3.42-0.362,11.595-2.806,14.048-0.362c3.655,3.638,1.597,7.814,7.596,9.805c1.352-6.663,12.871-6.612,18.66-4.924
|
||||
c2.68,0.79,14.972,10.544,10.41-1.311c10.871,2.21,21.734,4.411,32.606,6.621c4.26,0.866,4.352,4.192,7.956,5.848
|
||||
c2.344,1.076,8.856,0.748,12.2,1.966C413.851,126.9,403.425,130.008,397.502,128.882z M406.693,134.654
|
||||
c0.176-0.588,0.362-1.176,0.546-1.764c1.429-1.412,9.62-1.546,10.814,0.428C419.136,135.108,407.441,135.293,406.693,134.654z
|
||||
M437.107,168.185c4.974-4.864,9.897-9.754,15.081-14.425c-10.149,0.513-12.468,2.932-14.358-6.562
|
||||
c-2.151,1.31-4.31,2.621-6.461,3.932c-2.638-3.923-8.838-9.552-2.874-13.778c1.782-1.252,6.008,0.487,7.898-0.656
|
||||
c1.832-1.1,3.293-6.008,4.31-7.873c-5.176-1.84-5.31,2.723-9.334,2.63c-3.881-0.092-9.258-3.293-12.922-4.596
|
||||
c3.377-6.99,11.485-5.906,17.013-6.939c2.521,3.318,4.982,6.687,7.326,10.158c6.41,9.494,12.09,19.508,17.022,29.943
|
||||
C453.406,164.791,445.156,170.715,437.107,168.185z M453.18,278.096c-1.051,0.194-1.673-1.747-0.236-1.747
|
||||
C454.381,276.349,454.381,277.878,453.18,278.096z M457.011,283.785c-0.47-0.58-0.227-0.278-0.008-0.017
|
||||
c-2.243-2.739-1.663-6.209,2.403-4.352C461.498,280.356,458.43,285.499,457.011,283.785z"/>
|
||||
<path class="st0" d="M231.005,240.701v0.008C231.055,240.86,231.089,240.945,231.005,240.701z"/>
|
||||
<path class="st0" d="M287.07,349.268c-0.008,0-0.008,0.009-0.017,0.009C287.608,349.2,287.414,349.226,287.07,349.268z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.2799 6.40005L11.7399 15.94C10.7899 16.89 7.96987 17.33 7.33987 16.7C6.70987 16.07 7.13987 13.25 8.08987 12.3L17.6399 2.75002C17.8754 2.49308 18.1605 2.28654 18.4781 2.14284C18.7956 1.99914 19.139 1.92124 19.4875 1.9139C19.8359 1.90657 20.1823 1.96991 20.5056 2.10012C20.8289 2.23033 21.1225 2.42473 21.3686 2.67153C21.6147 2.91833 21.8083 3.21243 21.9376 3.53609C22.0669 3.85976 22.1294 4.20626 22.1211 4.55471C22.1128 4.90316 22.0339 5.24635 21.8894 5.5635C21.7448 5.88065 21.5375 6.16524 21.2799 6.40005V6.40005Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 4H6C4.93913 4 3.92178 4.42142 3.17163 5.17157C2.42149 5.92172 2 6.93913 2 8V18C2 19.0609 2.42149 20.0783 3.17163 20.8284C3.92178 21.5786 4.93913 22 6 22H17C19.21 22 20 20.2 20 18V13" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 453 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 431 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 392 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 187 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 342 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 444 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 480 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 149 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 161 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 149 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 771 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 492 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 329 B |
@@ -173,7 +173,6 @@ a:visited {
|
||||
&_box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
background-color: var(--light-bg-color);
|
||||
@@ -186,12 +185,6 @@ a:visited {
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
.material-symbols-outlined {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
color: white;
|
||||
background-color: var(--light-bg-color);
|
||||
@@ -214,10 +207,10 @@ a:visited {
|
||||
border-radius: 50%;
|
||||
aspect-ratio: 1 / 1;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: clamp(4px, 0.5vw, 8px);
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
}
|
||||
}
|
||||
|
||||
.button_row {
|
||||
@@ -239,9 +232,9 @@ a:visited {
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,16 +268,19 @@ 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%);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.server {
|
||||
@@ -348,10 +344,7 @@ a:visited {
|
||||
|
||||
.connection_status {
|
||||
.material-symbols-outlined {
|
||||
width: 24px;
|
||||
width: var(--control-icon-size);
|
||||
height: 24px;
|
||||
height: var(--control-icon-size);
|
||||
font-size: var(--control-icon-size);
|
||||
}
|
||||
.status_text {
|
||||
font-size: var(--control-text-size);
|
||||
@@ -363,10 +356,7 @@ a:visited {
|
||||
|
||||
.user_edit_button {
|
||||
.material-symbols-outlined {
|
||||
width: 36px;
|
||||
width: var(--user-icon-size);
|
||||
height: 36px;
|
||||
height: var(--user-icon-size);
|
||||
font-size: var(--user-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,10 +371,7 @@ a:visited {
|
||||
|
||||
.toggle_button {
|
||||
.material-symbols-outlined {
|
||||
width: 28px;
|
||||
width: var(--toggle-icon-size);
|
||||
height: 28px;
|
||||
height: var(--toggle-icon-size);
|
||||
font-size: var(--toggle-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,3 +431,349 @@ a:visited {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.server-list-page h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login_version {
|
||||
font-size: 0.55em;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.server-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Rounded card */
|
||||
.server-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.server-card__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
opacity: 0.65;
|
||||
filter: brightness(0) invert(0.8); /* light gray */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.server-card__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
flex: 1; /* pushes the connect button to the far right */
|
||||
min-width: 0; /* prevents text overflow from breaking flex layout */
|
||||
}
|
||||
|
||||
.server-card__name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.server-card__address {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
|
||||
.server-card__action {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.server-card__action img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
filter: brightness(0) invert(0.8); /* light gray */
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.server-card__action:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.server-card__action:hover img {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.server-card__action:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Add server — dashed outline style to distinguish from real cards */
|
||||
.add-server-btn {
|
||||
width: 100%;
|
||||
padding: 0.85rem;
|
||||
border-radius: 12px;
|
||||
border: 2px dashed rgba(255, 255, 255, 0.2);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-server-btn:hover {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.0);
|
||||
z-index: 999;
|
||||
animation: backdrop-fade-in 150ms ease-out forwards;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.modal {
|
||||
pointer-events: auto;
|
||||
|
||||
/* Make this solid or nearly solid instead of see-through */
|
||||
/* Old: background: rgba(255, 255, 255, 0.05); */
|
||||
background: #141414; /* or #151822, or rgb(15, 15, 20) */
|
||||
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
|
||||
|
||||
padding: 1.25rem 1.5rem 1.4rem;
|
||||
width: 500px;
|
||||
max-width: 90vw;
|
||||
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
animation: modal-pop-in 160ms ease-out forwards;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Form layout */
|
||||
|
||||
.modal-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.modal-field label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.modal-field input {
|
||||
padding: 0.55rem 0.6rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.modal-field input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.modal-field input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Actions row */
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Secondary button (Cancel) */
|
||||
|
||||
.modal-btn {
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
transform: translateY(0) scale(0.97);
|
||||
}
|
||||
|
||||
/* Primary button (Save) */
|
||||
|
||||
.modal-btn--primary {
|
||||
background: rgba(67, 156, 255, 0.85);
|
||||
border-color: rgba(67, 156, 255, 1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-btn--primary:hover {
|
||||
background: rgba(92, 174, 255, 0.95);
|
||||
border-color: rgba(135, 196, 255, 1);
|
||||
}
|
||||
|
||||
/* Delete button (danger) */
|
||||
|
||||
.modal-btn--danger {
|
||||
background: rgba(220, 60, 60, 0.85);
|
||||
border-color: rgba(220, 60, 60, 1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-btn--danger:hover {
|
||||
background: rgba(240, 80, 80, 0.95);
|
||||
border-color: rgba(255, 120, 120, 1);
|
||||
}
|
||||
|
||||
.modal-actions__spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Override mode username row */
|
||||
|
||||
.override-username-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.override-username-input {
|
||||
flex: 1;
|
||||
padding: 0.55rem 0.6rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.override-username-input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.override-username-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* Connect action button highlight */
|
||||
|
||||
.server-card__action--connect:hover {
|
||||
background: rgba(67, 156, 255, 0.3);
|
||||
border-color: rgba(67, 156, 255, 0.6);
|
||||
}
|
||||
|
||||
/* Ping info on server card */
|
||||
|
||||
.server-card__ping {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.1rem;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
flex-shrink: 0;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Keyframes */
|
||||
|
||||
@keyframes backdrop-fade-in {
|
||||
from { background: rgba(0, 0, 0, 0.0); }
|
||||
to { background: rgba(0, 0, 0, 0.4); }
|
||||
}
|
||||
|
||||
@keyframes modal-pop-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,12 @@
|
||||
#![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::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerEntry};
|
||||
use ordermap::OrderSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
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}",
|
||||
})
|
||||
}
|
||||
use crate::imp::{Platform, PlatformInterface as _};
|
||||
|
||||
pub type ChannelId = u32;
|
||||
pub type UserId = u32;
|
||||
@@ -83,7 +23,7 @@ pub enum Command {
|
||||
Connect {
|
||||
address: String,
|
||||
username: String,
|
||||
config: ProxyOverrides,
|
||||
config: ClientConfig,
|
||||
},
|
||||
SendChat {
|
||||
markdown: String,
|
||||
@@ -490,13 +430,17 @@ pub fn ChatView() -> Element {
|
||||
div {
|
||||
span {
|
||||
onclick: move |_| pick_and_send_file(&net),
|
||||
Icon { name: "attach_file", color: "#ffffff", opacity: 0.5 }
|
||||
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(),
|
||||
Icon { name: "send", color: "#ffffff", opacity: 0.5 }
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,7 +454,7 @@ pub fn ChatView() -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
let status = &STATE.status;
|
||||
let server = STATE.server.read();
|
||||
@@ -530,10 +474,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
|
||||
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
||||
|
||||
let proxy_url = overrides
|
||||
let proxy_url = config
|
||||
.read_unchecked()
|
||||
.as_ref()
|
||||
.and_then(|overrides| overrides.proxy_url.clone());
|
||||
.and_then(|gui_config| gui_config.proxy_url.clone());
|
||||
|
||||
let connecting_color = "yellow";
|
||||
let connected_color = "oklch(0.55 0.1184 141.35)";
|
||||
@@ -546,7 +490,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
class: "connection_status",
|
||||
style: "color: {connecting_color};",
|
||||
div {
|
||||
Icon { name: "signal_cellular_alt_2_bar", color: "yellow" }
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_cellular_alt_2_bar"
|
||||
}
|
||||
span {
|
||||
class: "status_text",
|
||||
" Connecting"
|
||||
@@ -559,7 +506,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
class: "connection_status",
|
||||
div {
|
||||
style: "color: {connected_color};",
|
||||
Icon { name: "signal_cellular_alt", color: "#46823e" }
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_cellular_alt"
|
||||
}
|
||||
span {
|
||||
class: "status_text",
|
||||
" Connected"
|
||||
@@ -576,7 +526,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
class: "connection_status",
|
||||
style: "color: {disconnected_color};",
|
||||
div {
|
||||
Icon { name: "signal_disconnected", color: "gray" }
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_disconnected"
|
||||
}
|
||||
span {
|
||||
class: "status_text",
|
||||
" Disconnected"
|
||||
@@ -589,7 +542,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
class: "connection_status",
|
||||
style: "color: {failed_color};",
|
||||
div {
|
||||
Icon { name: "error", color: "red" }
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"error"
|
||||
}
|
||||
span {
|
||||
class: "status_text",
|
||||
" Failed"
|
||||
@@ -611,7 +567,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
button {
|
||||
class: "toggle_button",
|
||||
onclick: move |_| net.send(Disconnect),
|
||||
Icon { name: "signal_disconnected", color: "#ffffff", opacity: 0.5 }
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
"signal_disconnected"
|
||||
}
|
||||
}
|
||||
}
|
||||
hr { style: "width: 100%;" }
|
||||
@@ -620,7 +579,11 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
class: "button_row",
|
||||
button {
|
||||
class: "user_edit_button",
|
||||
Icon { name: "person_edit", color: "#fa3f36" }
|
||||
span {
|
||||
class: "material-symbols-outlined",
|
||||
style: "color: oklch(0.65 0.2245 28.06);",
|
||||
"person_edit"
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "user_info",
|
||||
@@ -645,8 +608,8 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
net.send(UpdateMicEffects { denoise: new_denoise })
|
||||
},
|
||||
match denoise() {
|
||||
true => rsx!(Icon { name: "cadence", color: "#b23f43", opacity: 0.8938 }),
|
||||
false => rsx!(Icon { name: "graphic_eq", color: "#ffffff", opacity: 0.5 }),
|
||||
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
||||
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
||||
}
|
||||
}
|
||||
button {
|
||||
@@ -659,8 +622,8 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
disabled: mute || suppress,
|
||||
onclick: move |_| net.send(SetMute { mute: !self_mute }),
|
||||
match mute || suppress || self_mute {
|
||||
true => rsx!(Icon { name: "mic_off", color: "#b23f43", opacity: 0.8938 }),
|
||||
false => rsx!(Icon { name: "mic", color: "#ffffff", opacity: 0.5 }),
|
||||
true => rsx!(span { class: "material-symbols-outlined", "mic_off"}),
|
||||
false => rsx!(span { class: "material-symbols-outlined", "mic"}),
|
||||
}
|
||||
}
|
||||
button {
|
||||
@@ -673,8 +636,8 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
disabled: deaf,
|
||||
onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }),
|
||||
match deaf || self_deaf {
|
||||
true => rsx!(Icon { name: "volume_off", color: "#b23f43", opacity: 0.8938 }),
|
||||
false => rsx!(Icon { name: "volume_up", color: "#ffffff", opacity: 0.5 }),
|
||||
true => rsx!(span { class: "material-symbols-outlined", "volume_off"}),
|
||||
false => rsx!(span { class: "material-symbols-outlined", "volume_up"}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,7 +645,7 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ServerView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element {
|
||||
pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
let server = STATE.server.read();
|
||||
let Some(&UserState {
|
||||
@@ -713,206 +676,507 @@ pub fn ServerView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem
|
||||
}
|
||||
div {
|
||||
class: "server_control_box",
|
||||
ControlView { overrides }
|
||||
ControlView { config }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoginView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element {
|
||||
pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
||||
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 servers = use_signal(|| Platform::load_servers());
|
||||
let mut show_add_modal = use_signal(|| false);
|
||||
let mut editing_index = use_signal(|| None::<usize>);
|
||||
|
||||
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 version = option_env!("MUMBLE_WEB2_VERSION");
|
||||
|
||||
let previous_username = user_config.config_get::<String>("username");
|
||||
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
|
||||
let is_override_mode = config
|
||||
.read()
|
||||
.as_ref()
|
||||
.is_some_and(|c| !c.any_server);
|
||||
|
||||
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 status = &STATE.status;
|
||||
let bottom = match &*status.read() {
|
||||
Disconnected => rsx! {
|
||||
button {
|
||||
class: "login_bttn",
|
||||
onclick: do_connect.clone(),
|
||||
"Connect"
|
||||
}
|
||||
},
|
||||
Connecting => rsx! {
|
||||
// --- Overrides mode: single preset server, username-only input ---
|
||||
if is_override_mode {
|
||||
let proxy_url = config
|
||||
.read()
|
||||
.as_ref()
|
||||
.and_then(|c| c.proxy_url.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let previous_username = Platform::load_username();
|
||||
let mut username = use_signal(|| previous_username.unwrap_or_default());
|
||||
|
||||
let status = &STATE.status;
|
||||
let is_connecting = matches!(&*status.read(), Connecting);
|
||||
|
||||
return 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}"
|
||||
class: "server-list-page",
|
||||
h1 {
|
||||
"Mumble Web"
|
||||
match version {
|
||||
Some(v) => rsx!(div { class: "login_version", "({v})" }),
|
||||
None => rsx!(),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "server-list",
|
||||
div {
|
||||
class: "server-card",
|
||||
img {
|
||||
class: "server-card__icon",
|
||||
src: asset!("assets/earth-14-svgrepo-com.svg"),
|
||||
alt: "Server icon",
|
||||
}
|
||||
div {
|
||||
class: "server-card__info",
|
||||
span { class: "server-card__name", "Server" }
|
||||
span { class: "server-card__address", "{proxy_url}" }
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "override-username-row",
|
||||
input {
|
||||
class: "override-username-input",
|
||||
r#type: "text",
|
||||
placeholder: "Username",
|
||||
value: "{username.read()}",
|
||||
oninput: move |evt| username.set(evt.value().clone()),
|
||||
}
|
||||
button {
|
||||
class: "server-card__action server-card__action--connect",
|
||||
disabled: is_connecting || username.read().is_empty(),
|
||||
onclick: {
|
||||
let proxy_url = proxy_url.clone();
|
||||
move |_| {
|
||||
let _ = Platform::set_default_username(&username.read());
|
||||
net.send(Connect {
|
||||
address: proxy_url.clone(),
|
||||
username: username.read().clone(),
|
||||
config: config.read().clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
},
|
||||
img {
|
||||
src: asset!("assets/arrow-right-svgrepo-com.svg"),
|
||||
alt: "Connect",
|
||||
}
|
||||
}
|
||||
}
|
||||
match &*STATE.status.read() {
|
||||
Failed(msg) => rsx!(
|
||||
div {
|
||||
class: "login_error",
|
||||
"Failed to connect:"
|
||||
pre { "{msg}" }
|
||||
}
|
||||
),
|
||||
_ => rsx!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
Connected => unreachable!(),
|
||||
};
|
||||
let version = option_env!("MUMBLE_WEB2_VERSION");
|
||||
);
|
||||
}
|
||||
|
||||
// --- Normal mode: editable server list ---
|
||||
rsx!(
|
||||
div {
|
||||
class: "login",
|
||||
class: "server-list-page",
|
||||
h1 {
|
||||
"Mumble Web"
|
||||
match version {
|
||||
Some(v) => rsx!(" " span { class: "login_version", "({v})" }),
|
||||
Some(v) => rsx!(div { 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 {
|
||||
class: "server-list",
|
||||
for (idx, server) in servers.read().iter().enumerate() {
|
||||
{
|
||||
let address = format!("{}:{}", server.address, server.port);
|
||||
let connect_entry = server.clone();
|
||||
rsx!(
|
||||
div {
|
||||
key: "{idx}",
|
||||
class: "server-card",
|
||||
img {
|
||||
class: "server-card__icon",
|
||||
src: asset!("assets/earth-14-svgrepo-com.svg"),
|
||||
alt: "Server icon",
|
||||
}
|
||||
div {
|
||||
class: "server-card__info",
|
||||
span { class: "server-card__name", "{server.name}" }
|
||||
span { class: "server-card__address", "{address}" }
|
||||
}
|
||||
ServerPingInfo {
|
||||
address: server.address.clone(),
|
||||
port: server.port,
|
||||
}
|
||||
button {
|
||||
class: "server-card__action",
|
||||
onclick: move |_| editing_index.set(Some(idx)),
|
||||
img {
|
||||
src: asset!("assets/edit-3-svgrepo-com.svg"),
|
||||
alt: "Edit",
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "server-card__action server-card__action--connect",
|
||||
onclick: {
|
||||
let entry = connect_entry.clone();
|
||||
move |_| {
|
||||
let _ = Platform::set_default_username(&entry.username);
|
||||
let addr = format!("{}:{}", entry.address, entry.port);
|
||||
net.send(Connect {
|
||||
address: addr,
|
||||
username: entry.username.clone(),
|
||||
config: config.read().clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
},
|
||||
img {
|
||||
src: asset!("assets/arrow-right-svgrepo-com.svg"),
|
||||
alt: "Connect",
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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()),
|
||||
}
|
||||
match &*STATE.status.read() {
|
||||
Failed(msg) => rsx!(
|
||||
div {
|
||||
class: "server-list",
|
||||
div {
|
||||
class: "login_error",
|
||||
"Failed to connect:"
|
||||
pre { "{msg}" }
|
||||
}
|
||||
}
|
||||
),
|
||||
_ => rsx!(),
|
||||
}
|
||||
button {
|
||||
class: "add-server-btn",
|
||||
onclick: move |_| show_add_modal.set(true),
|
||||
"+ Add Server"
|
||||
}
|
||||
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}
|
||||
}
|
||||
|
||||
if *show_add_modal.read() {
|
||||
AddServerModal {
|
||||
on_save: move |entry: ServerEntry| {
|
||||
servers.write().push(entry);
|
||||
Platform::save_servers(&servers.read());
|
||||
show_add_modal.set(false);
|
||||
},
|
||||
on_cancel: move |_| show_add_modal.set(false),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = *editing_index.read() {
|
||||
if let Some(entry) = servers.read().get(idx).cloned() {
|
||||
EditServerModal {
|
||||
entry,
|
||||
on_save: move |updated: ServerEntry| {
|
||||
servers.write()[idx] = updated;
|
||||
Platform::save_servers(&servers.read());
|
||||
editing_index.set(None);
|
||||
},
|
||||
on_delete: move |_| {
|
||||
servers.write().remove(idx);
|
||||
Platform::save_servers(&servers.read());
|
||||
editing_index.set(None);
|
||||
},
|
||||
on_cancel: move |_| editing_index.set(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
// 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}
|
||||
// }
|
||||
// )
|
||||
}
|
||||
|
||||
/// Displays live ping info (user count, latency) for a server using the mumble UDP ping protocol.
|
||||
#[component]
|
||||
fn ServerPingInfo(address: String, port: u16) -> Element {
|
||||
let ping_result = use_resource(move || {
|
||||
let addr = address.clone();
|
||||
async move { Platform::ping_server(&addr, port).await }
|
||||
});
|
||||
|
||||
let read = ping_result.read();
|
||||
match &*read {
|
||||
Some(Ok(status)) => {
|
||||
let users_text = match (status.users, status.max_users) {
|
||||
(Some(u), Some(m)) => format!("{u}/{m}"),
|
||||
(Some(u), None) => format!("{u} online"),
|
||||
_ => String::new(),
|
||||
};
|
||||
rsx!(
|
||||
div {
|
||||
class: "server-card__ping",
|
||||
if !users_text.is_empty() {
|
||||
span { "{users_text}" }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Some(Err(_)) => rsx!(
|
||||
div {
|
||||
class: "server-card__ping",
|
||||
span { "offline" }
|
||||
}
|
||||
),
|
||||
None => rsx!(
|
||||
div {
|
||||
class: "server-card__ping",
|
||||
span { "..." }
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AddServerModal(on_save: EventHandler<ServerEntry>, on_cancel: EventHandler<()>) -> Element {
|
||||
let mut name = use_signal(|| String::new());
|
||||
let mut address = use_signal(|| String::new());
|
||||
let mut port = use_signal(|| "64738".to_string());
|
||||
let mut username = use_signal(|| Platform::load_username().unwrap_or_default());
|
||||
let mut password = use_signal(|| String::new());
|
||||
|
||||
let do_save = move |_| {
|
||||
let port_num: u16 = port.read().parse().unwrap_or(64738);
|
||||
on_save.call(ServerEntry {
|
||||
name: name.read().clone(),
|
||||
address: address.read().clone(),
|
||||
port: port_num,
|
||||
username: username.read().clone(),
|
||||
password: if password.read().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password.read().clone())
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "modal-backdrop",
|
||||
onclick: move |_| on_cancel.call(()),
|
||||
}
|
||||
div {
|
||||
class: "modal-container",
|
||||
onclick: move |evt| evt.stop_propagation(),
|
||||
div {
|
||||
class: "modal",
|
||||
h2 { "Add Server" }
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Name" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "My Mumble Server",
|
||||
value: "{name.read()}",
|
||||
oninput: move |evt| name.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Address" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "mumble.example.com",
|
||||
value: "{address.read()}",
|
||||
oninput: move |evt| address.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Port" }
|
||||
input {
|
||||
r#type: "number",
|
||||
placeholder: "64738",
|
||||
value: "{port.read()}",
|
||||
oninput: move |evt| port.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Username" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "Nickname",
|
||||
value: "{username.read()}",
|
||||
oninput: move |evt| username.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Password (optional)" }
|
||||
input {
|
||||
r#type: "password",
|
||||
placeholder: "Password",
|
||||
value: "{password.read()}",
|
||||
oninput: move |evt| password.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-actions",
|
||||
button {
|
||||
class: "modal-btn",
|
||||
onclick: move |_| on_cancel.call(()),
|
||||
"Cancel"
|
||||
}
|
||||
button {
|
||||
class: "modal-btn modal-btn--primary",
|
||||
disabled: address.read().is_empty() || username.read().is_empty(),
|
||||
onclick: do_save,
|
||||
"Save"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn EditServerModal(
|
||||
entry: ServerEntry,
|
||||
on_save: EventHandler<ServerEntry>,
|
||||
on_delete: EventHandler<()>,
|
||||
on_cancel: EventHandler<()>,
|
||||
) -> Element {
|
||||
let mut name = use_signal(|| entry.name.clone());
|
||||
let mut address = use_signal(|| entry.address.clone());
|
||||
let mut port = use_signal(|| entry.port.to_string());
|
||||
let mut username = use_signal(|| entry.username.clone());
|
||||
let mut password = use_signal(|| entry.password.clone().unwrap_or_default());
|
||||
|
||||
let do_save = move |_| {
|
||||
let port_num: u16 = port.read().parse().unwrap_or(64738);
|
||||
on_save.call(ServerEntry {
|
||||
name: name.read().clone(),
|
||||
address: address.read().clone(),
|
||||
port: port_num,
|
||||
username: username.read().clone(),
|
||||
password: if password.read().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password.read().clone())
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "modal-backdrop",
|
||||
onclick: move |_| on_cancel.call(()),
|
||||
}
|
||||
div {
|
||||
class: "modal-container",
|
||||
onclick: move |evt| evt.stop_propagation(),
|
||||
div {
|
||||
class: "modal",
|
||||
h2 { "Edit Server" }
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Name" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "My Mumble Server",
|
||||
value: "{name.read()}",
|
||||
oninput: move |evt| name.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Address" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "mumble.example.com",
|
||||
value: "{address.read()}",
|
||||
oninput: move |evt| address.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Port" }
|
||||
input {
|
||||
r#type: "number",
|
||||
placeholder: "64738",
|
||||
value: "{port.read()}",
|
||||
oninput: move |evt| port.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Username" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "Nickname",
|
||||
value: "{username.read()}",
|
||||
oninput: move |evt| username.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-field",
|
||||
label { "Password (optional)" }
|
||||
input {
|
||||
r#type: "password",
|
||||
placeholder: "Password",
|
||||
value: "{password.read()}",
|
||||
oninput: move |evt| password.set(evt.value().clone()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "modal-actions",
|
||||
button {
|
||||
class: "modal-btn modal-btn--danger",
|
||||
onclick: move |_| on_delete.call(()),
|
||||
"Delete"
|
||||
}
|
||||
span { class: "modal-actions__spacer" }
|
||||
button {
|
||||
class: "modal-btn",
|
||||
onclick: move |_| on_cancel.call(()),
|
||||
"Cancel"
|
||||
}
|
||||
button {
|
||||
class: "modal-btn modal-btn--primary",
|
||||
disabled: address.read().is_empty() || username.read().is_empty(),
|
||||
onclick: do_save,
|
||||
"Save"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app() -> Element {
|
||||
static STYLE: Asset = asset!("/assets/main.scss");
|
||||
|
||||
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
|
||||
let overrides = use_resource(|| async move {
|
||||
match Platform::load_proxy_overrides().await {
|
||||
Ok(overrides) => overrides,
|
||||
Err(_) => ProxyOverrides::default(),
|
||||
let config = use_resource(|| async move {
|
||||
match Platform::load_config().await {
|
||||
Ok(config) => config,
|
||||
Err(_) => ClientConfig::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 { overrides, user_config }),
|
||||
_ => rsx!(LoginView { overrides, user_config }),
|
||||
Connected => rsx!(ServerView { config }),
|
||||
_ => rsx!(LoginView { config }),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
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;
|
||||
use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
|
||||
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::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoCertificateVerification;
|
||||
@@ -73,11 +73,11 @@ pub async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
let config = ClientConfig::builder()
|
||||
let config = RlsClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::app::Command;
|
||||
use color_eyre::eyre::Error;
|
||||
use color_eyre::eyre::{bail, Error};
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -11,33 +11,74 @@ 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_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||
Ok(ProxyOverrides {
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
Ok(ClientConfig {
|
||||
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()
|
||||
}
|
||||
|
||||
fn load_servers() -> Vec<ServerEntry> {
|
||||
let config = load_config_map();
|
||||
config
|
||||
.get("servers")
|
||||
.and_then(|s| serde_json::from_str(s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn save_servers(servers: &[ServerEntry]) {
|
||||
let mut config = load_config_map();
|
||||
if let Ok(json) = serde_json::to_string(servers) {
|
||||
config.insert("servers".to_string(), json);
|
||||
let _ = save_config_map(&config);
|
||||
}
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, overrides).await
|
||||
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
|
||||
}
|
||||
|
||||
async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||
mumble_udp_ping(address, port).await
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
@@ -57,3 +98,85 @@ 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: "xyz".to_string(),
|
||||
author: "ohea".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(())
|
||||
}
|
||||
|
||||
/// Mumble UDP ping protocol.
|
||||
///
|
||||
/// Send a 12-byte packet: 4 zero bytes + 8-byte identifier.
|
||||
/// Receive a 24-byte response: 4 bytes version (1 byte each: major.minor.patch + padding)
|
||||
/// + 8 bytes identifier echo + 4 bytes current_users + 4 bytes max_users + 4 bytes bandwidth.
|
||||
async fn mumble_udp_ping(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||
use std::net::ToSocketAddrs;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
let dest = format!("{}:{}", address, port)
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| color_eyre::eyre::eyre!("could not resolve address"))?;
|
||||
|
||||
let bind_addr = if dest.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" };
|
||||
let socket = UdpSocket::bind(bind_addr).await?;
|
||||
socket.connect(dest).await?;
|
||||
|
||||
// Build ping packet: 4 zero bytes + 8-byte request ID
|
||||
let request_id: u64 = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64;
|
||||
|
||||
let mut buf = [0u8; 12];
|
||||
buf[4..12].copy_from_slice(&request_id.to_be_bytes());
|
||||
socket.send(&buf).await?;
|
||||
|
||||
let mut response = [0u8; 24];
|
||||
let timeout = tokio::time::timeout(Duration::from_secs(2), socket.recv(&mut response)).await;
|
||||
|
||||
match timeout {
|
||||
Ok(Ok(len)) if len >= 24 => {
|
||||
let version_major = response[0] as u32;
|
||||
let version_minor = response[1] as u32;
|
||||
let version_patch = response[2] as u32;
|
||||
let users = u32::from_be_bytes([response[12], response[13], response[14], response[15]]);
|
||||
let max_users = u32::from_be_bytes([response[16], response[17], response[18], response[19]]);
|
||||
let bandwidth = u32::from_be_bytes([response[20], response[21], response[22], response[23]]);
|
||||
|
||||
Ok(ServerStatus {
|
||||
success: true,
|
||||
version: Some((version_major, version_minor, version_patch)),
|
||||
users: Some(users),
|
||||
max_users: Some(max_users),
|
||||
bandwidth: Some(bandwidth),
|
||||
})
|
||||
}
|
||||
Ok(Ok(_)) => bail!("ping response too short"),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(_) => bail!("ping timed out"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::app::Command;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||
@@ -9,29 +10,54 @@ pub struct MobilePlatform;
|
||||
|
||||
impl super::PlatformInterface for MobilePlatform {
|
||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||
type ConfigSystem = super::native_config::NativeConfigSystem;
|
||||
|
||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||
Ok(ProxyOverrides {
|
||||
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
|
||||
}
|
||||
|
||||
fn load_servers() -> Vec<ServerEntry> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn save_servers(_servers: &[ServerEntry]) {}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
super::connect::network_connect(address, username, event_rx, overrides).await
|
||||
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
|
||||
}
|
||||
|
||||
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
||||
color_eyre::eyre::bail!("ping not supported on mobile yet")
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
use crate::{app::Command, effects::AudioProcessor};
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use std::collections::HashMap;
|
||||
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -51,24 +50,11 @@ 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();
|
||||
@@ -81,16 +67,40 @@ pub trait PlatformInterface {
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
proxy_overrides: &ProxyOverrides,
|
||||
gui_config: &ClientConfig,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
|
||||
/// Get server status (user count, version, etc.).
|
||||
/// Get server status (user count, version, etc.) via the web proxy status endpoint.
|
||||
fn get_status(
|
||||
client: &reqwest::Client,
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||
|
||||
/// Ping a mumble server via UDP to get version, user count, etc.
|
||||
fn ping_server(
|
||||
address: &str,
|
||||
port: u16,
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||
|
||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>>;
|
||||
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<()>;
|
||||
|
||||
/// Load the saved server list.
|
||||
fn load_servers() -> Vec<ServerEntry>;
|
||||
|
||||
/// Save the server list.
|
||||
fn save_servers(servers: &[ServerEntry]);
|
||||
|
||||
/// Async sleep for the given duration.
|
||||
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
||||
@@ -100,21 +110,15 @@ pub trait PlatformInterface {
|
||||
// Platform Modules
|
||||
// ============================================================================
|
||||
|
||||
mod stub;
|
||||
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod connect;
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod native_audio;
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod native_config;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
mod desktop;
|
||||
|
||||
#[cfg(feature = "mobile")]
|
||||
mod mobile;
|
||||
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
mod native_audio;
|
||||
mod stub;
|
||||
#[cfg(feature = "web")]
|
||||
mod web;
|
||||
|
||||
@@ -141,8 +145,6 @@ 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
|
||||
// ========================
|
||||
@@ -174,12 +176,3 @@ 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()
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
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,14 +3,13 @@
|
||||
use crate::effects::AudioProcessor;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::UnboundedReceiver;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerEntry, 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")
|
||||
@@ -24,7 +23,7 @@ impl super::PlatformInterface for StubPlatform {
|
||||
_address: String,
|
||||
_username: String,
|
||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||
_overrides: &ProxyOverrides,
|
||||
_gui_config: &ClientConfig,
|
||||
) -> impl Future<Output = Result<(), Error>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
@@ -35,10 +34,41 @@ impl super::PlatformInterface for StubPlatform {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>> {
|
||||
fn ping_server(
|
||||
_address: &str,
|
||||
_port: u16,
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
||||
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 load_servers() -> Vec<ServerEntry> {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn save_servers(_servers: &[ServerEntry]) {
|
||||
panic!("stubbed platform")
|
||||
}
|
||||
|
||||
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
@@ -77,28 +107,6 @@ 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,9 +6,8 @@ use dioxus::prelude::*;
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use js_sys::Float32Array;
|
||||
use mumble_protocol::control::ClientControlCodec;
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -63,7 +62,6 @@ pub struct WebPlatform;
|
||||
|
||||
impl super::PlatformInterface for WebPlatform {
|
||||
type AudioSystem = WebAudioSystem;
|
||||
type ConfigSystem = WebConfigSystem;
|
||||
|
||||
fn init_logging() {
|
||||
// copied from tracing_web example usage
|
||||
@@ -91,28 +89,71 @@ impl super::PlatformInterface for WebPlatform {
|
||||
// No-op on web
|
||||
}
|
||||
|
||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
||||
let overrides = match option_env!("MUMBLE_WEB2_PROXY_OVERRIDES_URL") {
|
||||
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||
let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
|
||||
Some(url) => Url::parse(url)?,
|
||||
None => absolute_url("overrides")?,
|
||||
None => absolute_url("config")?,
|
||||
};
|
||||
info!("loading config from {}", overrides);
|
||||
info!("loading config from {}", config_url);
|
||||
|
||||
let config = reqwest::get(overrides)
|
||||
let config = reqwest::get(config_url)
|
||||
.await?
|
||||
.json::<ProxyOverrides>()
|
||||
.json::<ClientConfig>()
|
||||
.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
|
||||
}
|
||||
|
||||
fn load_servers() -> Vec<ServerEntry> {
|
||||
web_sys::window()
|
||||
.and_then(|w| w.local_storage().ok()?)
|
||||
.and_then(|s| s.get_item("servers").ok()?)
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn save_servers(servers: &[ServerEntry]) {
|
||||
if let Ok(json) = serde_json::to_string(servers) {
|
||||
if let Some(storage) = web_sys::window()
|
||||
.and_then(|w| w.local_storage().ok()?)
|
||||
{
|
||||
let _ = storage.set_item("servers", &json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
network_connect(address, username, event_rx, overrides).await
|
||||
network_connect(address, username, event_rx, gui_config).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
@@ -124,6 +165,11 @@ impl super::PlatformInterface for WebPlatform {
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
||||
// UDP ping not available in browsers; use get_status via HTTP proxy instead
|
||||
color_eyre::eyre::bail!("UDP ping not supported on web platform")
|
||||
}
|
||||
|
||||
async fn sleep(duration: Duration) {
|
||||
TimeoutFuture::new(duration.as_millis() as u32).await;
|
||||
}
|
||||
@@ -433,7 +479,7 @@ pub async fn network_connect(
|
||||
address: String,
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
overrides: &ProxyOverrides,
|
||||
gui_config: &ClientConfig,
|
||||
) -> Result<(), Error> {
|
||||
info!("connecting");
|
||||
|
||||
@@ -446,7 +492,7 @@ pub async fn network_connect(
|
||||
)
|
||||
.ey()?;
|
||||
|
||||
if let Some(server_hash) = &overrides.cert_hash {
|
||||
if let Some(server_hash) = &gui_config.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()?;
|
||||
}
|
||||
@@ -500,64 +546,3 @@ 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,9 +5,6 @@ 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,8 +2,5 @@ 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::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::{ClientConfig, 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, DigitallySignedStruct};
|
||||
use tokio_rustls::rustls::{ClientConfig as RlsClientConfig, 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 overrides = ProxyOverrides {
|
||||
let mut client_config = ClientConfig {
|
||||
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());
|
||||
overrides.cert_hash = Some(hash.into());
|
||||
client_config.cert_hash = Some(hash.into());
|
||||
|
||||
(cert.pem().into(), key_pair.serialize_pem().into())
|
||||
}
|
||||
@@ -122,11 +122,14 @@ async fn main() -> Result<()> {
|
||||
};
|
||||
let rustls_config = RustlsConfig::new(Keycert::new().cert(cert.as_slice()).key(key.as_slice()));
|
||||
|
||||
info!("proxy overrides:\n{}", toml::to_string_pretty(&overrides)?);
|
||||
info!(
|
||||
"client config:\n{}",
|
||||
toml::to_string_pretty(&client_config)?
|
||||
);
|
||||
|
||||
let config_craft = ConfigCraft {
|
||||
server_config: server_config.clone(),
|
||||
overrides,
|
||||
client_config,
|
||||
};
|
||||
|
||||
let status_craft = StatusCraft {
|
||||
@@ -136,7 +139,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("/overrides").get(config_craft.get_overrides()))
|
||||
.push(Router::with_path("/config").get(config_craft.get_config()))
|
||||
.push(Router::with_path("/status").get(status_craft.get_status()))
|
||||
.hoop(Logger::new());
|
||||
if let Some(gui_path) = server_config.gui_path.clone() {
|
||||
@@ -249,14 +252,14 @@ impl StatusCraft {
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigCraft {
|
||||
server_config: Arc<Config>,
|
||||
overrides: ProxyOverrides,
|
||||
client_config: ClientConfig,
|
||||
}
|
||||
|
||||
#[craft]
|
||||
impl ConfigCraft {
|
||||
#[craft(handler)]
|
||||
async fn get_overrides(&self) -> Json<ProxyOverrides> {
|
||||
Json(self.overrides.clone())
|
||||
async fn get_config(&self) -> Json<ClientConfig> {
|
||||
Json(self.client_config.clone())
|
||||
}
|
||||
|
||||
#[craft(handler)]
|
||||
@@ -317,7 +320,7 @@ async fn connect_proxy_impl(
|
||||
) -> Result<()> {
|
||||
info!("connecting to Mumble server...");
|
||||
|
||||
let config = ClientConfig::builder()
|
||||
let config = RlsClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||