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
|
path: target/release/mumble-web2-proxy
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
macos_build:
|
|
||||||
runs-on: macos
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Restore Rust cache
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo
|
|
||||||
./target
|
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
rust-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install cargo binstall
|
|
||||||
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
|
||||||
|
|
||||||
- name: Install dioxus-cli
|
|
||||||
run: cargo binstall dioxus-cli --version 0.7.3 --no-confirm
|
|
||||||
|
|
||||||
- name: Build dioxus project
|
|
||||||
run: dx bundle --platform macos --release -p mumble-web2-gui
|
|
||||||
|
|
||||||
- name: Save Rust cache
|
|
||||||
if: always()
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo
|
|
||||||
./target
|
|
||||||
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Upload mumble-web2-gui Artifact
|
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: mumble-web2-gui-macos-arm64
|
|
||||||
path: gui/dist
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
windows_build:
|
windows_build:
|
||||||
runs-on: windows
|
runs-on: windows
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
pub struct ProxyOverrides {
|
pub struct ClientConfig {
|
||||||
pub proxy_url: Option<String>,
|
pub proxy_url: Option<String>,
|
||||||
pub cert_hash: Option<Vec<u8>>,
|
pub cert_hash: Option<Vec<u8>>,
|
||||||
pub any_server: bool,
|
pub any_server: bool,
|
||||||
|
|||||||
+9
-7
@@ -1,12 +1,14 @@
|
|||||||
localhost:64444 {
|
localhost:64444 {
|
||||||
tls internal
|
tls internal
|
||||||
|
|
||||||
# Proxy /config path to mumble-web2-proxy
|
# Proxy /config path to mumble-web2-proxy
|
||||||
reverse_proxy /overrides http://127.0.0.1:4400
|
reverse_proxy /config http://127.0.0.1:4400
|
||||||
|
|
||||||
# Proxy /status path to mumble-web2-proxy
|
# Proxy /status path to mumble-web2-proxy
|
||||||
reverse_proxy /status http://127.0.0.1:4400
|
reverse_proxy /status http://127.0.0.1:4400
|
||||||
|
|
||||||
|
|
||||||
|
# Proxy root path to dx-serve
|
||||||
|
reverse_proxy http://127.0.0.1:8080
|
||||||
|
|
||||||
# Proxy root path to dx-serve
|
|
||||||
reverse_proxy http://127.0.0.1:8080
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
# volumes:
|
# volumes:
|
||||||
# - ..:/app
|
# - ..:/app
|
||||||
# environment:
|
# environment:
|
||||||
# - MUMBLE_WEB2_PROXY_OVERRIDES_URL=https://localhost:64444/overrides
|
# - MUMBLE_WEB2_GUI_CONFIG_URL=https://localhost:64444/config
|
||||||
# stdin_open: true
|
# stdin_open: true
|
||||||
# tty: true
|
# tty: true
|
||||||
# command: >
|
# command: >
|
||||||
|
|||||||
@@ -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 |
+346
-1
@@ -16,7 +16,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
visibility: visible;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -432,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+492
-238
@@ -2,17 +2,15 @@
|
|||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry};
|
||||||
use ordermap::OrderSet;
|
use ordermap::OrderSet;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::{fmt, sync::Arc};
|
|
||||||
|
|
||||||
use crate::imp::{ConfigSystem, ConfigSystemInterface as _, Platform, PlatformInterface as _};
|
use crate::imp::{Platform, PlatformInterface as _};
|
||||||
|
|
||||||
pub type ChannelId = u32;
|
pub type ChannelId = u32;
|
||||||
pub type UserId = u32;
|
pub type UserId = u32;
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ConnectionState {
|
pub enum ConnectionState {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Connecting,
|
Connecting,
|
||||||
@@ -20,17 +18,12 @@ pub enum ConnectionState {
|
|||||||
Failed(String),
|
Failed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AudioSettings {
|
|
||||||
pub denoise: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Connect {
|
Connect {
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
config: ProxyOverrides,
|
config: ClientConfig,
|
||||||
},
|
},
|
||||||
SendChat {
|
SendChat {
|
||||||
markdown: String,
|
markdown: String,
|
||||||
@@ -52,14 +45,16 @@ pub enum Command {
|
|||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
user: UserId,
|
user: UserId,
|
||||||
},
|
},
|
||||||
UpdateAudioSettings(AudioSettings),
|
UpdateMicEffects {
|
||||||
|
denoise: bool,
|
||||||
|
},
|
||||||
Disconnect,
|
Disconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
use Command::*;
|
use Command::*;
|
||||||
use ConnectionState::*;
|
use ConnectionState::*;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default)]
|
||||||
pub struct UserState {
|
pub struct UserState {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub channel: ChannelId,
|
pub channel: ChannelId,
|
||||||
@@ -84,14 +79,13 @@ impl UserState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Chat {
|
pub struct Chat {
|
||||||
pub raw: String,
|
pub raw: String,
|
||||||
pub dangerous_html: String,
|
pub dangerous_html: String,
|
||||||
pub sender: Option<UserId>,
|
pub sender: Option<UserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default)]
|
||||||
pub struct ChannelState {
|
pub struct ChannelState {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub children: OrderSet<ChannelId>,
|
pub children: OrderSet<ChannelId>,
|
||||||
@@ -117,7 +111,7 @@ impl ChannelState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default)]
|
||||||
pub struct ChannelsState {
|
pub struct ChannelsState {
|
||||||
pub channels: HashMap<ChannelId, ChannelState>,
|
pub channels: HashMap<ChannelId, ChannelState>,
|
||||||
}
|
}
|
||||||
@@ -204,7 +198,7 @@ impl ChannelsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default)]
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub channels_state: ChannelsState,
|
pub channels_state: ChannelsState,
|
||||||
pub users: HashMap<UserId, UserState>,
|
pub users: HashMap<UserId, UserState>,
|
||||||
@@ -219,21 +213,14 @@ impl ServerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub status: Signal<ConnectionState>,
|
pub status: GlobalSignal<ConnectionState>,
|
||||||
pub server: Signal<ServerState>,
|
pub server: GlobalSignal<ServerState>,
|
||||||
pub audio: Signal<AudioSettings>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for State {
|
pub static STATE: State = State {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
status: Signal::global(|| Disconnected),
|
||||||
f.debug_struct("State")
|
server: Signal::global(|| Default::default()),
|
||||||
.field("status", &self.status.read())
|
};
|
||||||
.field("server", &self.server.read())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SharedState = Arc<State>;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum UserIcon {
|
pub enum UserIcon {
|
||||||
@@ -280,8 +267,7 @@ pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn User(id: UserId) -> Element {
|
pub fn User(id: UserId) -> Element {
|
||||||
let state = use_context::<SharedState>();
|
let server = STATE.server.read();
|
||||||
let server = state.server.read();
|
|
||||||
match server.users.get(&id) {
|
match server.users.get(&id) {
|
||||||
Some(state) => rsx!(UserPill {
|
Some(state) => rsx!(UserPill {
|
||||||
name: state.name.clone(),
|
name: state.name.clone(),
|
||||||
@@ -299,8 +285,7 @@ pub fn User(id: UserId) -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn Channel(id: ChannelId) -> Element {
|
pub fn Channel(id: ChannelId) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let state = use_context::<SharedState>();
|
let server = STATE.server.read();
|
||||||
let server = state.server.read();
|
|
||||||
let user = server.session.unwrap();
|
let user = server.session.unwrap();
|
||||||
let Some(state) = server.channels_state.channels.get(&id) else {
|
let Some(state) = server.channels_state.channels.get(&id) else {
|
||||||
return rsx!("missing channel {id}");
|
return rsx!("missing channel {id}");
|
||||||
@@ -369,8 +354,7 @@ pub fn Channel(id: ChannelId) -> Element {
|
|||||||
|
|
||||||
#[cfg(any(feature = "desktop", feature = "web"))]
|
#[cfg(any(feature = "desktop", feature = "web"))]
|
||||||
pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
pub fn pick_and_send_file(net: &Coroutine<Command>) {
|
||||||
let state = use_context::<SharedState>();
|
let channels = if let Some(user) = STATE.server.read().this_user() {
|
||||||
let channels = if let Some(user) = state.server.read().this_user() {
|
|
||||||
vec![user.channel]
|
vec![user.channel]
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@@ -396,14 +380,11 @@ pub fn pick_and_send_file(net: &Coroutine<Command>) {}
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn ChatView() -> Element {
|
pub fn ChatView() -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let state = use_context::<SharedState>();
|
let server = STATE.server.read();
|
||||||
let server = state.server.read();
|
|
||||||
let mut draft = use_signal(|| "".to_string());
|
let mut draft = use_signal(|| "".to_string());
|
||||||
|
|
||||||
let mut do_send = move || {
|
let mut do_send = move || {
|
||||||
let state = use_context::<SharedState>();
|
if let Some(user) = STATE.server.read().this_user() {
|
||||||
let server = state.server.read();
|
|
||||||
if let Some(user) = server.this_user() {
|
|
||||||
net.send(SendChat {
|
net.send(SendChat {
|
||||||
markdown: draft.write().split_off(0),
|
markdown: draft.write().split_off(0),
|
||||||
channels: vec![user.channel],
|
channels: vec![user.channel],
|
||||||
@@ -473,12 +454,10 @@ pub fn ChatView() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let state = use_context::<SharedState>();
|
let status = &STATE.status;
|
||||||
let status = &state.status;
|
let server = STATE.server.read();
|
||||||
let server = state.server.read();
|
|
||||||
let audio = state.audio.read();
|
|
||||||
let Some(&UserState {
|
let Some(&UserState {
|
||||||
deaf,
|
deaf,
|
||||||
self_deaf,
|
self_deaf,
|
||||||
@@ -495,10 +474,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
|
|
||||||
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
||||||
|
|
||||||
let proxy_url = overrides
|
let proxy_url = config
|
||||||
.read_unchecked()
|
.read_unchecked()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|overrides| overrides.proxy_url.clone());
|
.and_then(|gui_config| gui_config.proxy_url.clone());
|
||||||
|
|
||||||
let connecting_color = "yellow";
|
let connecting_color = "yellow";
|
||||||
let connected_color = "oklch(0.55 0.1184 141.35)";
|
let connected_color = "oklch(0.55 0.1184 141.35)";
|
||||||
@@ -576,6 +555,7 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let denoise = use_signal(|| false);
|
||||||
rsx!(
|
rsx!(
|
||||||
// Server control
|
// Server control
|
||||||
div {
|
div {
|
||||||
@@ -616,23 +596,18 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
}
|
}
|
||||||
span { class: "spacer" }
|
span { class: "spacer" }
|
||||||
button {
|
button {
|
||||||
class: match audio.denoise {
|
class: match denoise() {
|
||||||
true => "toggle_button is_on",
|
true => "toggle_button is_on",
|
||||||
false => "toggle_button",
|
false => "toggle_button",
|
||||||
},
|
},
|
||||||
role: "switch",
|
role: "switch",
|
||||||
aria_checked: audio.denoise,
|
aria_checked: denoise(),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let state = use_context::<SharedState>();
|
let new_denoise = !denoise();
|
||||||
let mut audio = state.audio.read().clone();
|
*denoise.write_unchecked() = new_denoise;
|
||||||
audio.denoise = !audio.denoise;
|
net.send(UpdateMicEffects { denoise: new_denoise })
|
||||||
let denoise = audio.denoise;
|
|
||||||
*state.audio.write_unchecked() = audio;
|
|
||||||
net.send(UpdateAudioSettings(AudioSettings { denoise: denoise }));
|
|
||||||
let user_config = use_context::<ConfigSystem>();
|
|
||||||
user_config.config_set::<bool>("denoise", &denoise);
|
|
||||||
},
|
},
|
||||||
match audio.denoise {
|
match denoise() {
|
||||||
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
|
||||||
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
|
||||||
}
|
}
|
||||||
@@ -670,10 +645,9 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
|
pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let state = use_context::<SharedState>();
|
let server = STATE.server.read();
|
||||||
let server = state.server.read();
|
|
||||||
let Some(&UserState {
|
let Some(&UserState {
|
||||||
deaf,
|
deaf,
|
||||||
self_deaf,
|
self_deaf,
|
||||||
@@ -702,227 +676,507 @@ pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
|
|||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "server_control_box",
|
class: "server_control_box",
|
||||||
ControlView { overrides }
|
ControlView { config }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
||||||
let user_config = use_context::<ConfigSystem>();
|
|
||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
|
|
||||||
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
let mut servers = use_signal(|| Platform::load_servers());
|
||||||
use_resource(move || async move {
|
let mut show_add_modal = use_signal(|| false);
|
||||||
let client = reqwest::Client::new();
|
let mut editing_index = use_signal(|| None::<usize>);
|
||||||
loop {
|
|
||||||
*last_status.write_unchecked() = Some(Platform::get_status(&client).await);
|
|
||||||
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut address_input = use_signal(|| user_config.config_get::<String>("server_url"));
|
let version = option_env!("MUMBLE_WEB2_VERSION");
|
||||||
let address = use_memo(move || {
|
|
||||||
if let Some(addr) = address_input() {
|
|
||||||
addr.clone()
|
|
||||||
} else {
|
|
||||||
overrides()
|
|
||||||
.and_then(|c| c.proxy_url.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut username = use_signal(|| {
|
let is_override_mode = config
|
||||||
user_config
|
.read()
|
||||||
.config_get::<String>("username")
|
.as_ref()
|
||||||
.unwrap_or(String::new())
|
.is_some_and(|c| !c.any_server);
|
||||||
});
|
|
||||||
|
|
||||||
let do_connect = move |_| {
|
// --- Overrides mode: single preset server, username-only input ---
|
||||||
let _ = user_config.config_set::<String>("username", &username.read());
|
if is_override_mode {
|
||||||
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
let proxy_url = config
|
||||||
user_config.config_set::<String>("server_url", &address.read());
|
.read()
|
||||||
}
|
.as_ref()
|
||||||
net.send(Connect {
|
.and_then(|c| c.proxy_url.clone())
|
||||||
address: address.read().clone(),
|
.unwrap_or_default();
|
||||||
username: username.read().clone(),
|
|
||||||
config: overrides.read().clone().unwrap_or_default(),
|
let previous_username = Platform::load_username();
|
||||||
})
|
let mut username = use_signal(|| previous_username.unwrap_or_default());
|
||||||
};
|
|
||||||
let state = use_context::<SharedState>();
|
let status = &STATE.status;
|
||||||
let status = &state.status;
|
let is_connecting = matches!(&*status.read(), Connecting);
|
||||||
let bottom = match &*status.read() {
|
|
||||||
Disconnected => rsx! {
|
return rsx!(
|
||||||
button {
|
|
||||||
class: "login_bttn",
|
|
||||||
onclick: do_connect.clone(),
|
|
||||||
"Connect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Connecting => rsx! {
|
|
||||||
div {
|
div {
|
||||||
class: "login_bttn",
|
class: "server-list-page",
|
||||||
"Connecting..."
|
h1 {
|
||||||
}
|
"Mumble Web"
|
||||||
},
|
match version {
|
||||||
Failed(msg) => rsx!(
|
Some(v) => rsx!(div { class: "login_version", "({v})" }),
|
||||||
button {
|
None => rsx!(),
|
||||||
class: "login_bttn",
|
}
|
||||||
onclick: do_connect.clone(),
|
}
|
||||||
"Reconnect"
|
div {
|
||||||
}
|
class: "server-list",
|
||||||
div {
|
div {
|
||||||
class: "login_error",
|
class: "server-card",
|
||||||
"Failed to connect:"
|
img {
|
||||||
pre {
|
class: "server-card__icon",
|
||||||
"{msg}"
|
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!(
|
rsx!(
|
||||||
div {
|
div {
|
||||||
class: "login",
|
class: "server-list-page",
|
||||||
h1 {
|
h1 {
|
||||||
"Mumble Web"
|
"Mumble Web"
|
||||||
match version {
|
match version {
|
||||||
Some(v) => rsx!(" " span { class: "login_version", "({v})" }),
|
Some(v) => rsx!(div { class: "login_version", "({v})" }),
|
||||||
None => rsx!(),
|
None => rsx!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
div {
|
||||||
div {
|
class: "server-list",
|
||||||
label {
|
for (idx, server) in servers.read().iter().enumerate() {
|
||||||
for: "address-entry",
|
{
|
||||||
"Server Address:"
|
let address = format!("{}:{}", server.address, server.port);
|
||||||
}
|
let connect_entry = server.clone();
|
||||||
input {
|
rsx!(
|
||||||
id: "address-entry",
|
div {
|
||||||
placeholder: "address",
|
key: "{idx}",
|
||||||
value: "{address.read()}",
|
class: "server-card",
|
||||||
autofocus: "true",
|
img {
|
||||||
oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
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 {
|
match &*STATE.status.read() {
|
||||||
label {
|
Failed(msg) => rsx!(
|
||||||
for: "username-entry",
|
div {
|
||||||
"Username:"
|
class: "server-list",
|
||||||
//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;",
|
div {
|
||||||
}
|
class: "login_error",
|
||||||
input {
|
"Failed to connect:"
|
||||||
id: "username-entry",
|
pre { "{msg}" }
|
||||||
placeholder: "username",
|
}
|
||||||
value: "{username.read()}",
|
}
|
||||||
autofocus: "true",
|
),
|
||||||
oninput: move |evt| username.set(evt.value().clone()),
|
_ => 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}",
|
/// Displays live ping info (user count, latency) for a server using the mumble UDP ping protocol.
|
||||||
// h1 {
|
#[component]
|
||||||
// "Mumble Web"
|
fn ServerPingInfo(address: String, port: u16) -> Element {
|
||||||
// }
|
let ping_result = use_resource(move || {
|
||||||
// input {
|
let addr = address.clone();
|
||||||
// placeholder: "username",
|
async move { Platform::ping_server(&addr, port).await }
|
||||||
// value: "{username.read()}",
|
});
|
||||||
// autofocus: "true",
|
|
||||||
// oninput: move |evt| username.set(evt.value().clone()),
|
let read = ping_result.read();
|
||||||
// }
|
match &*read {
|
||||||
// input {
|
Some(Ok(status)) => {
|
||||||
// placeholder: "server address",
|
let users_text = match (status.users, status.max_users) {
|
||||||
// value: "{address.read()}",
|
(Some(u), Some(m)) => format!("{u}/{m}"),
|
||||||
// autofocus: "true",
|
(Some(u), None) => format!("{u} online"),
|
||||||
// oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
_ => String::new(),
|
||||||
// }
|
};
|
||||||
// {bottom}
|
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]
|
#[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 {
|
pub fn app() -> Element {
|
||||||
static STYLE: Asset = asset!("/assets/main.scss");
|
static STYLE: Asset = asset!("/assets/main.scss");
|
||||||
|
|
||||||
use_effect(|| {
|
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
|
||||||
Platform::request_permissions();
|
let config = use_resource(|| async move {
|
||||||
});
|
match Platform::load_config().await {
|
||||||
|
Ok(config) => config,
|
||||||
let user_config = use_root_context(|| ConfigSystem::new().unwrap());
|
Err(_) => ClientConfig::default(),
|
||||||
let state = use_root_context(|| {
|
|
||||||
SharedState::new(State {
|
|
||||||
status: Signal::new(Disconnected),
|
|
||||||
server: Signal::new(Default::default()),
|
|
||||||
audio: Signal::new(AudioSettings {
|
|
||||||
denoise: user_config.config_get::<bool>("denoise").unwrap_or(true),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let network_state = state.clone();
|
|
||||||
use_coroutine(move |rx: UnboundedReceiver<Command>| {
|
|
||||||
super::network_entrypoint(rx, network_state.clone())
|
|
||||||
});
|
|
||||||
let overrides = use_resource(|| async move {
|
|
||||||
match Platform::load_proxy_overrides().await {
|
|
||||||
Ok(overrides) => overrides,
|
|
||||||
Err(_) => ProxyOverrides::default(),
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Platform::request_permissions();
|
||||||
|
|
||||||
rsx!(
|
rsx!(
|
||||||
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
|
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
|
||||||
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" }
|
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" }
|
||||||
document::Link{ rel: "stylesheet", href: STYLE }
|
document::Link{ rel: "stylesheet", href: STYLE }
|
||||||
|
|
||||||
match *state.status.read() {
|
match *STATE.status.read() {
|
||||||
Connected => rsx!(ServerView { overrides }),
|
Connected => rsx!(ServerView { config }),
|
||||||
_ => rsx!(LoginView { overrides }),
|
_ => rsx!(LoginView { config }),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::Command;
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
@@ -8,13 +8,13 @@ use tokio::net::TcpStream;
|
|||||||
use tokio_rustls::rustls;
|
use tokio_rustls::rustls;
|
||||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use tokio_rustls::rustls::ClientConfig;
|
use tokio_rustls::rustls::ClientConfig as RlsClientConfig;
|
||||||
use tokio_rustls::rustls::DigitallySignedStruct;
|
use tokio_rustls::rustls::DigitallySignedStruct;
|
||||||
use tokio_rustls::TlsConnector;
|
use tokio_rustls::TlsConnector;
|
||||||
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NoCertificateVerification;
|
struct NoCertificateVerification;
|
||||||
@@ -73,12 +73,11 @@ pub async fn network_connect(
|
|||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
gui_config: &ClientConfig,
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
let config = ClientConfig::builder()
|
let config = RlsClientConfig::builder()
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||||
.with_no_client_auth();
|
.with_no_client_auth();
|
||||||
@@ -103,7 +102,7 @@ pub async fn network_connect(
|
|||||||
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
||||||
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
||||||
|
|
||||||
crate::network_loop(username, state, event_rx, reader, writer).await
|
crate::network_loop(username, event_rx, reader, writer).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
|
|||||||
+31
-17
@@ -1,8 +1,8 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::Command;
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -11,36 +11,33 @@ pub struct DesktopPlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for DesktopPlatform {
|
impl super::PlatformInterface for DesktopPlatform {
|
||||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||||
type ConfigSystem = super::native_config::NativeConfigSystem;
|
|
||||||
|
|
||||||
async fn sleep(duration: Duration) {
|
async fn sleep(duration: Duration) {
|
||||||
tokio::time::sleep(duration).await;
|
tokio::time::sleep(duration).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||||
Ok(ProxyOverrides {
|
Ok(ClientConfig {
|
||||||
proxy_url: None,
|
proxy_url: None,
|
||||||
cert_hash: None,
|
cert_hash: None,
|
||||||
any_server: true,
|
any_server: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn network_connect(
|
fn load_username() -> Option<String> {
|
||||||
address: String,
|
let config = load_config_map();
|
||||||
username: String,
|
config.get("username").cloned()
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
|
||||||
overrides: &ProxyOverrides,
|
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
fn load_server_url() -> Option<String> {
|
||||||
super::connect::get_status(client).await
|
let config = load_config_map();
|
||||||
|
config.get("server").cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
fn set_default_username(username: &str) -> Option<()> {
|
||||||
mumble_udp_ping(address, port).await
|
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<()> {
|
fn set_default_server(server: &str) -> Option<()> {
|
||||||
@@ -65,6 +62,23 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
mumble_udp_ping(address, port).await
|
||||||
|
}
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
|
|||||||
+28
-17
@@ -1,7 +1,8 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::Command;
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
|
||||||
@@ -9,32 +10,25 @@ pub struct MobilePlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for MobilePlatform {
|
impl super::PlatformInterface for MobilePlatform {
|
||||||
type AudioSystem = super::native_audio::NativeAudioSystem;
|
type AudioSystem = super::native_audio::NativeAudioSystem;
|
||||||
type ConfigSystem = super::native_config::NativeConfigSystem;
|
|
||||||
|
|
||||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||||
Ok(ProxyOverrides {
|
Ok(ClientConfig {
|
||||||
proxy_url: None,
|
proxy_url: None,
|
||||||
cert_hash: None,
|
cert_hash: None,
|
||||||
any_server: true,
|
any_server: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn network_connect(
|
fn load_username() -> Option<String> {
|
||||||
address: String,
|
None
|
||||||
username: String,
|
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
|
||||||
overrides: &ProxyOverrides,
|
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
fn load_server_url() -> Option<String> {
|
||||||
super::connect::get_status(client).await
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
fn set_default_username(_username: &str) -> Option<()> {
|
||||||
color_eyre::eyre::bail!("ping not supported on mobile yet")
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_default_server(_server: &str) -> Option<()> {
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
@@ -47,6 +41,23 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
|
|
||||||
fn save_servers(_servers: &[ServerEntry]) {}
|
fn save_servers(_servers: &[ServerEntry]) {}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
fn init_logging() {
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
|
|||||||
+17
-41
@@ -4,12 +4,10 @@
|
|||||||
//! The traits make the platform boundary explicit and provide compile-time verification.
|
//! The traits make the platform boundary explicit and provide compile-time verification.
|
||||||
#![allow(async_fn_in_trait)]
|
#![allow(async_fn_in_trait)]
|
||||||
|
|
||||||
use crate::app::{Command, SharedState};
|
use crate::{app::Command, effects::AudioProcessor};
|
||||||
use crate::effects::AudioProcessor;
|
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -52,24 +50,11 @@ pub trait AudioPlayerInterface {
|
|||||||
fn play_opus(&mut self, payload: &[u8]);
|
fn play_opus(&mut self, payload: &[u8]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ConfigSystemInterface: Sized + Clone {
|
|
||||||
fn new() -> Result<Self, Error>;
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned;
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is the main trait that each platform must implement. It combines all
|
/// This is the main trait that each platform must implement. It combines all
|
||||||
/// platform-specific functionality into a single interface, providing compile-time
|
/// platform-specific functionality into a single interface, providing compile-time
|
||||||
/// verification that all platforms implement the required functionality.
|
/// verification that all platforms implement the required functionality.
|
||||||
pub trait PlatformInterface {
|
pub trait PlatformInterface {
|
||||||
type AudioSystem: AudioSystemInterface;
|
type AudioSystem: AudioSystemInterface;
|
||||||
type ConfigSystem: ConfigSystemInterface;
|
|
||||||
|
|
||||||
/// Initialize logging for the platform.
|
/// Initialize logging for the platform.
|
||||||
fn init_logging();
|
fn init_logging();
|
||||||
@@ -82,11 +67,10 @@ pub trait PlatformInterface {
|
|||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
proxy_overrides: &ProxyOverrides,
|
gui_config: &ClientConfig,
|
||||||
state: SharedState,
|
|
||||||
) -> impl Future<Output = Result<(), Error>>;
|
) -> impl Future<Output = Result<(), Error>>;
|
||||||
|
|
||||||
/// Get server status (user count, version, etc.).
|
/// Get server status (user count, version, etc.) via the web proxy status endpoint.
|
||||||
fn get_status(
|
fn get_status(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
@@ -98,7 +82,16 @@ pub trait PlatformInterface {
|
|||||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
|
|
||||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||||
fn load_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.
|
/// Save the default server URL.
|
||||||
fn set_default_server(server: &str) -> Option<()>;
|
fn set_default_server(server: &str) -> Option<()>;
|
||||||
@@ -117,21 +110,15 @@ pub trait PlatformInterface {
|
|||||||
// Platform Modules
|
// Platform Modules
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
mod stub;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||||
mod connect;
|
mod connect;
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
|
||||||
mod native_audio;
|
|
||||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
|
||||||
mod native_config;
|
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mod desktop;
|
mod desktop;
|
||||||
|
|
||||||
#[cfg(feature = "mobile")]
|
#[cfg(feature = "mobile")]
|
||||||
mod mobile;
|
mod mobile;
|
||||||
|
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||||
|
mod native_audio;
|
||||||
|
mod stub;
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
@@ -158,8 +145,6 @@ pub type Platform = stub::StubPlatform;
|
|||||||
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
|
pub type AudioSystem = <Platform as PlatformInterface>::AudioSystem;
|
||||||
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
|
pub type AudioPlayer = <AudioSystem as AudioSystemInterface>::AudioPlayer;
|
||||||
|
|
||||||
pub type ConfigSystem = <Platform as PlatformInterface>::ConfigSystem;
|
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Platform Async Runtime
|
// Platform Async Runtime
|
||||||
// ========================
|
// ========================
|
||||||
@@ -191,12 +176,3 @@ const _: () = {
|
|||||||
let _ = assert_platform::<mobile::MobilePlatform>;
|
let _ = assert_platform::<mobile::MobilePlatform>;
|
||||||
let _ = assert_platform::<stub::StubPlatform>;
|
let _ = assert_platform::<stub::StubPlatform>;
|
||||||
};
|
};
|
||||||
|
|
||||||
fn global_default_config() -> HashMap<String, serde_json::Value> {
|
|
||||||
serde_json::json!({})
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
use color_eyre::eyre::Error;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct NativeConfigSystem {
|
|
||||||
config_path: std::path::PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::ConfigSystemInterface for NativeConfigSystem {
|
|
||||||
fn new() -> color_eyre::Result<Self, Error> {
|
|
||||||
return Ok(NativeConfigSystem {
|
|
||||||
config_path: get_config_path()?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
let config = load_config_map(&self.config_path);
|
|
||||||
|
|
||||||
let Some(value_untyped) = config.get(key).cloned().or_else(|| config_get_default(key))
|
|
||||||
else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
match serde_json::from_value::<T>(value_untyped) {
|
|
||||||
Ok(v) => Some(v),
|
|
||||||
Err(_) => {
|
|
||||||
let default_value = config_get_default(key)
|
|
||||||
.expect("Default value required after config parse failure");
|
|
||||||
Some(
|
|
||||||
serde_json::from_value::<T>(default_value)
|
|
||||||
.expect("Default value could not be parsed"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize,
|
|
||||||
{
|
|
||||||
let mut config = load_config_map(&self.config_path);
|
|
||||||
let json_value = serde_json::to_value(value).expect("failed to serialize config value");
|
|
||||||
config.insert(key.to_string(), json_value);
|
|
||||||
save_config_map(&config).expect("failed to set config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "desktop"))]
|
|
||||||
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
|
||||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
|
||||||
|
|
||||||
let strategy = choose_app_strategy(AppStrategyArgs {
|
|
||||||
top_level_domain: "xyz".to_string(),
|
|
||||||
author: "ohea".to_string(),
|
|
||||||
app_name: "Mumble Web2".to_string(),
|
|
||||||
})
|
|
||||||
.expect("failed to choose app strategy");
|
|
||||||
Ok(strategy.config_dir().join("config.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
fn get_config_path() -> color_eyre::Result<std::path::PathBuf> {
|
|
||||||
let ctx = ndk_context::android_context();
|
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?;
|
|
||||||
let mut env = vm.attach_current_thread()?;
|
|
||||||
let ctx = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
|
|
||||||
let cache_dir = env
|
|
||||||
.call_method(ctx, "getFilesDir", "()Ljava/io/File;", &[])?
|
|
||||||
.l()?;
|
|
||||||
let cache_dir: jni::objects::JString = env
|
|
||||||
.call_method(&cache_dir, "toString", "()Ljava/lang/String;", &[])?
|
|
||||||
.l()?
|
|
||||||
.try_into()?;
|
|
||||||
let cache_dir = env.get_string(&cache_dir)?;
|
|
||||||
let cache_dir = cache_dir.to_str()?;
|
|
||||||
Ok(std::path::PathBuf::from(cache_dir).join("config.json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_config_map(config_path: &std::path::PathBuf) -> HashMap<String, serde_json::Value> {
|
|
||||||
match std::fs::read_to_string(config_path) {
|
|
||||||
Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(),
|
|
||||||
Err(_) => HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_config_map(config: &HashMap<String, serde_json::Value>) -> color_eyre::Result<()> {
|
|
||||||
let config_path = get_config_path().expect("Could not get config file path.");
|
|
||||||
if let Some(parent) = config_path.parent() {
|
|
||||||
info!("Creating config directory: {}", parent.display());
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let contents = serde_json::to_string_pretty(config)?;
|
|
||||||
info!("Writing config to {}", config_path.display());
|
|
||||||
std::fs::write(&config_path, contents)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
|
||||||
let default_config = platform_default_config();
|
|
||||||
default_config
|
|
||||||
.get(key)
|
|
||||||
.cloned()
|
|
||||||
.or(super::global_default_config().get(key).cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
|
||||||
serde_json::json!({})
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
+16
-29
@@ -1,16 +1,15 @@
|
|||||||
/// Stub implementation of the platform interface, so that we can
|
/// Stub implementation of the platform interface, so that we can
|
||||||
/// `cargo check` without any --feature flags.
|
/// `cargo check` without any --feature flags.
|
||||||
use crate::{app::SharedState, effects::AudioProcessor};
|
use crate::effects::AudioProcessor;
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
pub struct StubPlatform;
|
pub struct StubPlatform;
|
||||||
|
|
||||||
impl super::PlatformInterface for StubPlatform {
|
impl super::PlatformInterface for StubPlatform {
|
||||||
type AudioSystem = StubAudioSystem;
|
type AudioSystem = StubAudioSystem;
|
||||||
type ConfigSystem = StubConfigSystem;
|
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
panic!("stubbed platform")
|
panic!("stubbed platform")
|
||||||
@@ -24,8 +23,7 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
_address: String,
|
_address: String,
|
||||||
_username: String,
|
_username: String,
|
||||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||||
_overrides: &ProxyOverrides,
|
_gui_config: &ClientConfig,
|
||||||
_state: SharedState,
|
|
||||||
) -> impl Future<Output = Result<(), Error>> {
|
) -> impl Future<Output = Result<(), Error>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
@@ -43,10 +41,22 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_proxy_overrides() -> impl Future<Output = color_eyre::Result<ProxyOverrides>> {
|
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_username() -> Option<String> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_server_url() -> Option<String> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_username(_username: &str) -> Option<()> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
fn set_default_server(_server: &str) -> Option<()> {
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
panic!("stubbed platform")
|
panic!("stubbed platform")
|
||||||
}
|
}
|
||||||
@@ -97,29 +107,6 @@ impl super::AudioPlayerInterface for StubAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct StubConfigSystem;
|
|
||||||
|
|
||||||
impl super::ConfigSystemInterface for StubConfigSystem {
|
|
||||||
fn new() -> Result<Self, Error> {
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize,
|
|
||||||
{
|
|
||||||
panic!("stubbed platform")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct SpawnHandle;
|
pub struct SpawnHandle;
|
||||||
|
|
||||||
|
|||||||
+49
-93
@@ -1,4 +1,4 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::Command;
|
||||||
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
||||||
use color_eyre::eyre::{bail, eyre, Error};
|
use color_eyre::eyre::{bail, eyre, Error};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
@@ -6,9 +6,8 @@ use dioxus::prelude::*;
|
|||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use js_sys::Float32Array;
|
use js_sys::Float32Array;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerEntry, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -63,7 +62,6 @@ pub struct WebPlatform;
|
|||||||
|
|
||||||
impl super::PlatformInterface for WebPlatform {
|
impl super::PlatformInterface for WebPlatform {
|
||||||
type AudioSystem = WebAudioSystem;
|
type AudioSystem = WebAudioSystem;
|
||||||
type ConfigSystem = WebConfigSystem;
|
|
||||||
|
|
||||||
fn init_logging() {
|
fn init_logging() {
|
||||||
// copied from tracing_web example usage
|
// copied from tracing_web example usage
|
||||||
@@ -91,43 +89,40 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
// No-op on web
|
// No-op on web
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_proxy_overrides() -> color_eyre::Result<ProxyOverrides> {
|
async fn load_config() -> color_eyre::Result<ClientConfig> {
|
||||||
let overrides = match option_env!("MUMBLE_WEB2_PROXY_OVERRIDES_URL") {
|
let config_url = match option_env!("MUMBLE_WEB2_GUI_CONFIG_URL") {
|
||||||
Some(url) => Url::parse(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?
|
.await?
|
||||||
.json::<ProxyOverrides>()
|
.json::<ClientConfig>()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn network_connect(
|
fn load_username() -> Option<String> {
|
||||||
address: String,
|
web_sys::window()
|
||||||
username: String,
|
.unwrap()
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
.local_storage()
|
||||||
overrides: &ProxyOverrides,
|
.ok()??
|
||||||
state: SharedState,
|
.get_item("username")
|
||||||
) -> Result<(), Error> {
|
.ok()?
|
||||||
network_connect(address, username, event_rx, overrides, state).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
fn load_server_url() -> Option<String> {
|
||||||
Ok(client
|
None
|
||||||
.get(absolute_url("status")?)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<ServerStatus>()
|
|
||||||
.await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ping_server(_address: &str, _port: u16) -> color_eyre::Result<ServerStatus> {
|
fn set_default_username(username: &str) -> Option<()> {
|
||||||
// UDP ping not available in browsers; use get_status via HTTP proxy instead
|
web_sys::window()?
|
||||||
color_eyre::eyre::bail!("UDP ping not supported on web platform")
|
.local_storage()
|
||||||
|
.ok()??
|
||||||
|
.set_item("username", username)
|
||||||
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_default_server(_server: &str) -> Option<()> {
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
@@ -152,6 +147,29 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn network_connect(
|
||||||
|
address: String,
|
||||||
|
username: String,
|
||||||
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
|
gui_config: &ClientConfig,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
network_connect(address, username, event_rx, gui_config).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||||
|
Ok(client
|
||||||
|
.get(absolute_url("status")?)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<ServerStatus>()
|
||||||
|
.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) {
|
async fn sleep(duration: Duration) {
|
||||||
TimeoutFuture::new(duration.as_millis() as u32).await;
|
TimeoutFuture::new(duration.as_millis() as u32).await;
|
||||||
}
|
}
|
||||||
@@ -461,8 +479,7 @@ pub async fn network_connect(
|
|||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
gui_config: &ClientConfig,
|
||||||
state: SharedState,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
@@ -475,7 +492,7 @@ pub async fn network_connect(
|
|||||||
)
|
)
|
||||||
.ey()?;
|
.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());
|
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
|
||||||
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
||||||
}
|
}
|
||||||
@@ -521,7 +538,7 @@ pub async fn network_connect(
|
|||||||
let writer =
|
let writer =
|
||||||
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
||||||
|
|
||||||
crate::network_loop(username, state, event_rx, reader, writer).await
|
crate::network_loop(username, event_rx, reader, writer).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
||||||
@@ -529,64 +546,3 @@ pub fn absolute_url(path: &str) -> Result<Url, Error> {
|
|||||||
let location = window.location();
|
let location = window.location();
|
||||||
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
Ok(Url::parse(&location.href().ey()?)?.join(path)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct WebConfigSystem {}
|
|
||||||
|
|
||||||
impl super::ConfigSystemInterface for WebConfigSystem {
|
|
||||||
fn new() -> Result<Self, Error> {
|
|
||||||
return Ok(WebConfigSystem {});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned,
|
|
||||||
{
|
|
||||||
// Get Storage
|
|
||||||
let storage = web_sys::window()?.local_storage().ok()??;
|
|
||||||
|
|
||||||
// Try localStorage first
|
|
||||||
if let Ok(Some(raw)) = storage.get_item(key) {
|
|
||||||
if let Ok(parsed) = serde_json::from_str::<T>(&raw) {
|
|
||||||
return Some(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to default if deserialization fails or key missing
|
|
||||||
let default_value = config_get_default(key)?;
|
|
||||||
serde_json::from_value::<T>(default_value).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_set<T>(&self, key: &str, value: &T)
|
|
||||||
where
|
|
||||||
T: serde::Serialize,
|
|
||||||
{
|
|
||||||
let storage = window()
|
|
||||||
.and_then(|w| w.local_storage().ok().flatten())
|
|
||||||
.expect("localStorage not available");
|
|
||||||
|
|
||||||
let json_value =
|
|
||||||
serde_json::to_string(value).expect("failed to serialize config value to JSON string");
|
|
||||||
|
|
||||||
storage
|
|
||||||
.set_item(key, &json_value)
|
|
||||||
.expect("failed to write to localStorage");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_get_default(key: &str) -> Option<serde_json::Value> {
|
|
||||||
let default_config = platform_default_config();
|
|
||||||
default_config
|
|
||||||
.get(key)
|
|
||||||
.cloned()
|
|
||||||
.or(super::global_default_config().get(key).cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn platform_default_config() -> HashMap<String, serde_json::Value> {
|
|
||||||
serde_json::json!({})
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|||||||
+22
-35
@@ -1,6 +1,7 @@
|
|||||||
use app::Chat;
|
use app::Chat;
|
||||||
use app::Command;
|
use app::Command;
|
||||||
use app::ConnectionState;
|
use app::ConnectionState;
|
||||||
|
use app::STATE;
|
||||||
use asynchronous_codec::FramedRead;
|
use asynchronous_codec::FramedRead;
|
||||||
use asynchronous_codec::FramedWrite;
|
use asynchronous_codec::FramedWrite;
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
@@ -26,9 +27,6 @@ use std::time::Duration;
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::app::AudioSettings;
|
|
||||||
use crate::app::SharedState;
|
|
||||||
use crate::app::State;
|
|
||||||
use crate::effects::AudioProcessor;
|
use crate::effects::AudioProcessor;
|
||||||
use crate::imp::{
|
use crate::imp::{
|
||||||
AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform,
|
AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform,
|
||||||
@@ -40,7 +38,7 @@ mod effects;
|
|||||||
pub mod imp;
|
pub mod imp;
|
||||||
mod msghtml;
|
mod msghtml;
|
||||||
|
|
||||||
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
|
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
|
||||||
loop {
|
loop {
|
||||||
let Some(Command::Connect {
|
let Some(Command::Connect {
|
||||||
address,
|
address,
|
||||||
@@ -51,29 +49,25 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state:
|
|||||||
panic!("did not receive connect command")
|
panic!("did not receive connect command")
|
||||||
};
|
};
|
||||||
|
|
||||||
*state.server.write_unchecked() = Default::default();
|
*STATE.server.write() = Default::default();
|
||||||
*state.status.write_unchecked() = ConnectionState::Connecting;
|
*STATE.status.write() = ConnectionState::Connecting;
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
Platform::network_connect(address, username, &mut event_rx, &config, state.clone())
|
Platform::network_connect(address, username, &mut event_rx, &config).await
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
error!("could not connect {:?}", error);
|
error!("could not connect {:?}", error);
|
||||||
*state.status.write_unchecked() = ConnectionState::Failed(error.to_string());
|
*STATE.status.write() = ConnectionState::Failed(error.to_string());
|
||||||
} else {
|
} else {
|
||||||
*state.status.write_unchecked() = ConnectionState::Disconnected;
|
*STATE.status.write() = ConnectionState::Disconnected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
|
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
|
||||||
username: String,
|
username: String,
|
||||||
state: SharedState,
|
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
||||||
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let audio_settings = state.audio.read().clone();
|
|
||||||
|
|
||||||
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
|
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
while let Some(msg) = writer_recv_chan.next().await {
|
while let Some(msg) = writer_recv_chan.next().await {
|
||||||
@@ -123,13 +117,10 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut audio = AudioSystem::new().await?;
|
let mut audio = AudioSystem::new().await?;
|
||||||
if audio_settings.denoise {
|
|
||||||
audio.set_processor(AudioProcessor::new_denoising());
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
let send_chan = send_chan.clone();
|
let send_chan = send_chan.clone();
|
||||||
let mut sequence_num = 0;
|
let mut sequence_num = 0;
|
||||||
if let Err(err) = audio.start_recording(move |opus_frame, is_terminator| {
|
audio.start_recording(move |opus_frame, is_terminator| {
|
||||||
let _ =
|
let _ =
|
||||||
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
|
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
|
||||||
_dst: std::marker::PhantomData,
|
_dst: std::marker::PhantomData,
|
||||||
@@ -140,9 +131,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
position_info: None,
|
position_info: None,
|
||||||
})));
|
})));
|
||||||
sequence_num = sequence_num.wrapping_add(2);
|
sequence_num = sequence_num.wrapping_add(2);
|
||||||
}) {
|
});
|
||||||
error!("could not begin recording: {err:?}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create map of session_id -> AudioDecoder
|
// Create map of session_id -> AudioDecoder
|
||||||
@@ -160,7 +149,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
|
||||||
info!("receiving packet {:#?}", msg);
|
info!("receiving packet {:#?}", msg);
|
||||||
}
|
}
|
||||||
let res = accept_packet(msg, &mut audio, &mut decoder_map, &state);
|
let res = accept_packet(msg, &mut audio, &mut decoder_map);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("error accepting packet {:?}", err)
|
error!("error accepting packet {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -179,7 +168,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
|
|||||||
match command {
|
match command {
|
||||||
Some(Command::Disconnect) => break,
|
Some(Command::Disconnect) => break,
|
||||||
Some(command) => {
|
Some(command) => {
|
||||||
let res = accept_command(command, &mut send_chan, &mut audio, &state);
|
let res = accept_command(command, &mut send_chan, &mut audio);
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
info!("error accepting command {:?}", err)
|
info!("error accepting command {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -198,10 +187,9 @@ fn accept_command(
|
|||||||
command: Command,
|
command: Command,
|
||||||
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
||||||
audio: &mut AudioSystem,
|
audio: &mut AudioSystem,
|
||||||
state: &State,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
use Command::*;
|
use Command::*;
|
||||||
let Some(session) = state.server.read().session else {
|
let Some(session) = STATE.server.read().session else {
|
||||||
bail!("no session id")
|
bail!("no session id")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -224,7 +212,7 @@ fn accept_command(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -265,7 +253,7 @@ fn accept_command(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -300,7 +288,7 @@ fn accept_command(
|
|||||||
let _ = send_chan.unbounded_send(u.into());
|
let _ = send_chan.unbounded_send(u.into());
|
||||||
}
|
}
|
||||||
Connect { .. } | Disconnect => (),
|
Connect { .. } | Disconnect => (),
|
||||||
UpdateAudioSettings(AudioSettings { denoise }) => {
|
UpdateMicEffects { denoise } => {
|
||||||
if denoise {
|
if denoise {
|
||||||
audio.set_processor(AudioProcessor::new_denoising());
|
audio.set_processor(AudioProcessor::new_denoising());
|
||||||
} else {
|
} else {
|
||||||
@@ -316,7 +304,6 @@ fn accept_packet(
|
|||||||
msg: ControlPacket<mumble_protocol::Clientbound>,
|
msg: ControlPacket<mumble_protocol::Clientbound>,
|
||||||
audio_context: &mut AudioSystem,
|
audio_context: &mut AudioSystem,
|
||||||
player_map: &mut HashMap<u32, AudioPlayer>,
|
player_map: &mut HashMap<u32, AudioPlayer>,
|
||||||
state: &State,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match msg {
|
match msg {
|
||||||
ControlPacket::UDPTunnel(u) => {
|
ControlPacket::UDPTunnel(u) => {
|
||||||
@@ -353,15 +340,15 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelState(u) => {
|
ControlPacket::ChannelState(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
server.channels_state.update_from_channel_state(&u);
|
server.channels_state.update_from_channel_state(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelRemove(u) => {
|
ControlPacket::ChannelRemove(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
server.channels_state.update_from_channel_remove(&u);
|
server.channels_state.update_from_channel_remove(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::UserState(u) => {
|
ControlPacket::UserState(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let server = &mut *server;
|
let server = &mut *server;
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
|
|
||||||
@@ -405,7 +392,7 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::UserRemove(u) => {
|
ControlPacket::UserRemove(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
if let Some(state) = server.users.remove(&id) {
|
if let Some(state) = server.users.remove(&id) {
|
||||||
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||||
@@ -414,7 +401,7 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::TextMessage(u) => {
|
ControlPacket::TextMessage(u) => {
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
if u.has_message() {
|
if u.has_message() {
|
||||||
let text = u.get_message().to_string();
|
let text = u.get_message().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
@@ -429,8 +416,8 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ServerSync(u) => {
|
ControlPacket::ServerSync(u) => {
|
||||||
*state.status.write_unchecked() = ConnectionState::Connected;
|
*STATE.status.write() = ConnectionState::Connected;
|
||||||
let mut server = state.server.write_unchecked();
|
let mut server = STATE.server.write();
|
||||||
if u.has_welcome_text() {
|
if u.has_welcome_text() {
|
||||||
let text = u.get_welcome_text().to_string();
|
let text = u.get_welcome_text().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
|
|||||||
+1
-17
@@ -1,22 +1,6 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
|
use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
Platform::init_logging();
|
Platform::init_logging();
|
||||||
dioxus::LaunchBuilder::new()
|
dioxus::launch(app::app);
|
||||||
.with_cfg(desktop! {
|
|
||||||
dioxus::desktop::Config::new()
|
|
||||||
// Reduce white flash on startup by setting background color and hiding main element
|
|
||||||
.with_background_color((0, 0, 0, 255))
|
|
||||||
.with_custom_head("<style>html, body { background: black; } #main { visibility: hidden; }</style>".into())
|
|
||||||
.with_disable_context_menu(cfg!(not(debug_assertions)))
|
|
||||||
.with_window(
|
|
||||||
dioxus::desktop::WindowBuilder::new()
|
|
||||||
.with_title("Mumble Web 2")
|
|
||||||
.with_min_inner_size(dioxus::desktop::LogicalSize::new(600.0, 300.0))
|
|
||||||
.with_inner_size(dioxus::desktop::LogicalSize::new(900.0, 700.0))
|
|
||||||
.with_maximized(false),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.launch(app::app);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-11
@@ -1,5 +1,5 @@
|
|||||||
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
||||||
use salvo::cors::{AllowOrigin, Cors};
|
use salvo::cors::{AllowOrigin, Cors};
|
||||||
@@ -16,7 +16,7 @@ use tokio::net::TcpStream;
|
|||||||
use tokio::pin;
|
use tokio::pin;
|
||||||
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct};
|
use tokio_rustls::rustls::{ClientConfig as RlsClientConfig, DigitallySignedStruct};
|
||||||
use tokio_rustls::{rustls, TlsConnector};
|
use tokio_rustls::{rustls, TlsConnector};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing::info_span;
|
use tracing::info_span;
|
||||||
@@ -77,7 +77,7 @@ async fn main() -> Result<()> {
|
|||||||
.install_default()
|
.install_default()
|
||||||
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
.map_err(|e| anyhow!("could not install crypto provider {e:?}"))?;
|
||||||
|
|
||||||
let mut overrides = ProxyOverrides {
|
let mut client_config = ClientConfig {
|
||||||
proxy_url: match &server_config.proxy_url {
|
proxy_url: match &server_config.proxy_url {
|
||||||
Some(url) => Some(url.to_string()),
|
Some(url) => Some(url.to_string()),
|
||||||
None => None,
|
None => None,
|
||||||
@@ -102,7 +102,7 @@ async fn main() -> Result<()> {
|
|||||||
let cert = cert_params.self_signed(&key_pair)?;
|
let cert = cert_params.self_signed(&key_pair)?;
|
||||||
|
|
||||||
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
let hash = hmac_sha256::Hash::hash(cert.der().as_ref());
|
||||||
overrides.cert_hash = Some(hash.into());
|
client_config.cert_hash = Some(hash.into());
|
||||||
|
|
||||||
(cert.pem().into(), key_pair.serialize_pem().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()));
|
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 {
|
let config_craft = ConfigCraft {
|
||||||
server_config: server_config.clone(),
|
server_config: server_config.clone(),
|
||||||
overrides,
|
client_config,
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_craft = StatusCraft {
|
let status_craft = StatusCraft {
|
||||||
@@ -136,7 +139,7 @@ async fn main() -> Result<()> {
|
|||||||
// Server routing
|
// Server routing
|
||||||
let mut router = Router::new()
|
let mut router = Router::new()
|
||||||
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
|
.push(Router::with_path("/proxy").goal(config_craft.connect_proxy()))
|
||||||
.push(Router::with_path("/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()))
|
.push(Router::with_path("/status").get(status_craft.get_status()))
|
||||||
.hoop(Logger::new());
|
.hoop(Logger::new());
|
||||||
if let Some(gui_path) = server_config.gui_path.clone() {
|
if let Some(gui_path) = server_config.gui_path.clone() {
|
||||||
@@ -249,14 +252,14 @@ impl StatusCraft {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigCraft {
|
pub struct ConfigCraft {
|
||||||
server_config: Arc<Config>,
|
server_config: Arc<Config>,
|
||||||
overrides: ProxyOverrides,
|
client_config: ClientConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[craft]
|
#[craft]
|
||||||
impl ConfigCraft {
|
impl ConfigCraft {
|
||||||
#[craft(handler)]
|
#[craft(handler)]
|
||||||
async fn get_overrides(&self) -> Json<ProxyOverrides> {
|
async fn get_config(&self) -> Json<ClientConfig> {
|
||||||
Json(self.overrides.clone())
|
Json(self.client_config.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[craft(handler)]
|
#[craft(handler)]
|
||||||
@@ -317,7 +320,7 @@ async fn connect_proxy_impl(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("connecting to Mumble server...");
|
info!("connecting to Mumble server...");
|
||||||
|
|
||||||
let config = ClientConfig::builder()
|
let config = RlsClientConfig::builder()
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
|
||||||
.with_no_client_auth();
|
.with_no_client_auth();
|
||||||
|
|||||||
Reference in New Issue
Block a user