1 Commits

Author SHA1 Message Date
sam 25730858f7 gui: new login screen
Build Mumble Web 2 / macos_build (push) Successful in 1m6s
Build Mumble Web 2 / linux_build (push) Successful in 1m26s
Build Mumble Web 2 / windows_build (push) Successful in 3m7s
Build Mumble Web 2 / android_build (push) Successful in 4m34s
2026-05-04 23:04:03 -06:00
11 changed files with 1000 additions and 173 deletions
+5 -1
View File
@@ -37,7 +37,11 @@ impl super::PlatformInterface for DesktopPlatform {
_client: &reqwest::Client, _client: &reqwest::Client,
address: &str, address: &str,
) -> color_eyre::Result<ServerStatus> { ) -> color_eyre::Result<ServerStatus> {
mumble_web2_common::ping_server(address, 64738).await let (host, port) = match address.rsplit_once(':') {
Some((h, p)) => (h, p.parse().unwrap_or(64738)),
None => (address, 64738),
};
mumble_web2_common::ping_server(host, port).await
} }
fn init_logging() { fn init_logging() {
+5 -1
View File
@@ -33,7 +33,11 @@ impl super::PlatformInterface for MobilePlatform {
_client: &reqwest::Client, _client: &reqwest::Client,
address: &str, address: &str,
) -> color_eyre::Result<ServerStatus> { ) -> color_eyre::Result<ServerStatus> {
mumble_web2_common::ping_server(address, 64738).await let (host, port) = match address.rsplit_once(':') {
Some((h, p)) => (h, p.parse().unwrap_or(64738)),
None => (address, 64738),
};
mumble_web2_common::ping_server(host, port).await
} }
fn init_logging() { fn init_logging() {
+2 -6
View File
@@ -28,12 +28,8 @@ impl super::ConfigSystemInterface for NativeConfigSystem {
match serde_json::from_value::<T>(value_untyped) { match serde_json::from_value::<T>(value_untyped) {
Ok(v) => Some(v), Ok(v) => Some(v),
Err(_) => { Err(_) => {
let default_value = config_get_default(key) let default_value = config_get_default(key)?;
.expect("Default value required after config parse failure"); serde_json::from_value::<T>(default_value).ok()
Some(
serde_json::from_value::<T>(default_value)
.expect("Default value could not be parsed"),
)
} }
} }
} }
-2
View File
@@ -9,5 +9,3 @@ pub use imp::*;
pub use mainloop::*; pub use mainloop::*;
pub use mime_guess; pub use mime_guess;
pub use reqwest; pub use reqwest;
pub const VERSION: Option<&str> = option_env!("MUMBLE_WEB2_VERSION");
+14 -3
View File
@@ -17,6 +17,16 @@ pub struct ServerStatus {
pub bandwidth: Option<u32>, pub bandwidth: Option<u32>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
pub struct ServerEntry {
pub name: String,
pub address: String,
pub port: u16,
pub username: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
}
/// Mumble UDP ping protocol. /// Mumble UDP ping protocol.
/// ///
/// Send a 12-byte packet: 4 zero bytes + 8-byte identifier. /// Send a 12-byte packet: 4 zero bytes + 8-byte identifier.
@@ -25,11 +35,12 @@ pub struct ServerStatus {
#[cfg(feature = "networking")] #[cfg(feature = "networking")]
pub async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> { pub async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
use color_eyre::eyre::{bail, eyre}; use color_eyre::eyre::{bail, eyre};
use std::net::ToSocketAddrs;
use std::time::Duration; use std::time::Duration;
use tokio::net::{lookup_host, UdpSocket}; use tokio::net::UdpSocket;
let dest = lookup_host(format!("{}:{}", address, port)) let dest = format!("{}:{}", address, port)
.await? .to_socket_addrs()?
.next() .next()
.ok_or_else(|| eyre!("could not resolve address"))?; .ok_or_else(|| eyre!("could not resolve address"))?;
+4
View File
@@ -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

+8
View File
@@ -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

+135
View File
@@ -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

+5
View File
@@ -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

+345
View File
@@ -432,3 +432,348 @@ 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 */
background: #141414;
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);
}
}
+477 -160
View File
@@ -3,12 +3,10 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use mumble_web2_client::{ use mumble_web2_client::{
network_entrypoint, reqwest, AudioSettings, ChannelId, Command, ConfigSystem, network_entrypoint, reqwest, AudioSettings, ChannelId, Command, ConfigSystem,
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, ServerState, ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, SharedState,
SharedState, State, UserId, UserState, VERSION, State, UserId, UserState,
}; };
use mumble_web2_common::{ProxyOverrides, ServerStatus}; use mumble_web2_common::{ProxyOverrides, ServerEntry};
use std::collections::{HashMap, HashSet};
use std::{fmt, sync::Arc};
use Command::*; use Command::*;
use ConnectionState::*; use ConnectionState::*;
@@ -501,180 +499,499 @@ pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element { pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
let user_config = use_context::<ConfigSystem>(); let user_config = use_context::<ConfigSystem>();
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let mut address_input = use_signal(|| user_config.config_get::<String>("server_url"));
let address = use_memo(move || {
if let Some(addr) = address_input() {
addr.clone()
} else {
overrides()
.and_then(|c| c.proxy_url.clone())
.unwrap_or_default()
}
});
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
use_resource(move || {
let addr = address();
async move {
let client = reqwest::Client::new();
loop {
*last_status.write_unchecked() =
Some(Platform::get_status(&client, &addr).await);
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
}
}
});
let mut username = use_signal(|| {
user_config
.config_get::<String>("username")
.unwrap_or(String::new())
});
let do_connect = move |_| {
let _ = user_config.config_set::<String>("username", &username.read());
if overrides.read().as_ref().is_some_and(|cfg| cfg.any_server) {
user_config.config_set::<String>("server_url", &address.read());
}
net.send(Connect {
address: address.read().clone(),
username: username.read().clone(),
config: overrides.read().clone().unwrap_or_default(),
})
};
let state = use_context::<SharedState>(); let state = use_context::<SharedState>();
let status = &state.status;
let bottom = match &*status.read() { let mut servers = use_signal(|| {
Disconnected => rsx! { user_config
button { .config_get::<Vec<ServerEntry>>("servers")
class: "login_bttn", .unwrap_or_default()
onclick: do_connect.clone(), });
"Connect" let mut show_add_modal = use_signal(|| false);
} let mut editing_index = use_signal(|| None::<usize>);
},
Connecting => rsx! { let version = option_env!("MUMBLE_WEB2_VERSION");
let is_override_mode = overrides
.read()
.as_ref()
.is_some_and(|c| !c.any_server);
// --- Overrides mode: single preset server, username-only input ---
if is_override_mode {
let proxy_url = overrides
.read()
.as_ref()
.and_then(|c| c.proxy_url.clone())
.unwrap_or_default();
let mut username = use_signal(|| {
user_config
.config_get::<String>("username")
.unwrap_or_default()
});
let status = &state.status;
let is_connecting = matches!(&*status.read(), Connecting);
return 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();
let user_config = user_config.clone();
move |_| {
user_config.config_set("username", &*username.read());
net.send(Connect {
address: proxy_url.clone(),
username: username.read().clone(),
config: overrides.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!(), }
};
// --- 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();
let user_config = user_config.clone();
move |_| {
user_config.config_set("username", &entry.username);
let addr = format!("{}:{}", entry.address, entry.port);
net.send(Connect {
address: addr,
username: entry.username.clone(),
config: overrides.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 server"
}
}),
}
div {
{bottom}
}
if *show_add_modal.read() {
{
let user_config = user_config.clone();
rsx!(AddServerModal {
on_save: move |entry: ServerEntry| {
servers.write().push(entry);
user_config.config_set("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() {
{
let user_config_save = user_config.clone();
let user_config_del = user_config.clone();
rsx!(EditServerModal {
entry,
on_save: move |updated: ServerEntry| {
servers.write()[idx] = updated;
user_config_save.config_set("servers", &*servers.read());
editing_index.set(None);
},
on_delete: move |_| {
servers.write().remove(idx);
user_config_del.config_set("servers", &*servers.read());
editing_index.set(None);
},
on_cancel: move |_| editing_index.set(None),
})
}
}
} }
} }
) )
// rsx!( }
// div {
// class: "{login_box}", #[component]
// h1 { fn ServerPingInfo(address: String, port: u16) -> Element {
// "Mumble Web" let ping_result = use_resource(move || {
// } let addr = format!("{}:{}", address.clone(), port);
// input { async move {
// placeholder: "username", let client = reqwest::Client::new();
// value: "{username.read()}", Platform::get_status(&client, &addr).await
// autofocus: "true", }
// oninput: move |evt| username.set(evt.value().clone()), });
// }
// input { let read = ping_result.read();
// placeholder: "server address", match &*read {
// value: "{address.read()}", Some(Ok(status)) => {
// autofocus: "true", let users_text = match (status.users, status.max_users) {
// oninput: move |evt| address_input.set(Some(evt.value().clone())), (Some(u), Some(m)) => format!("{u}/{m}"),
// } (Some(u), None) => format!("{u} online"),
// {bottom} _ => String::new(),
// } };
// ) rsx!(
div {
class: "server-card__ping",
if !users_text.is_empty() {
span { "{users_text}" }
}
}
)
}
Some(Err(_)) => rsx!(
div {
class: "server-card__ping",
span { "offline" }
}
),
None => rsx!(
div {
class: "server-card__ping",
span { "..." }
}
),
}
}
#[component]
fn AddServerModal(on_save: EventHandler<ServerEntry>, on_cancel: EventHandler<()>) -> Element {
let user_config = use_context::<ConfigSystem>();
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(|| {
user_config
.config_get::<String>("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"
}
}
}
}
}
} }
#[component] #[component]