Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26a08acc36 | |||
| b20ed1ff56 | |||
| 765446392d | |||
| 2c22942fb3 | |||
| 75990ca9ce | |||
| 9f6557bb92 |
@@ -16,3 +16,13 @@ pub struct ServerStatus {
|
|||||||
pub max_users: Option<u32>,
|
pub max_users: Option<u32>,
|
||||||
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>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 |
@@ -431,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+438
-137
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry};
|
||||||
use ordermap::OrderSet;
|
use ordermap::OrderSet;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
@@ -686,173 +686,474 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
|||||||
pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
pub fn LoginView(config: Resource<ClientConfig>) -> Element {
|
||||||
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(|| Platform::load_server_url());
|
let version = option_env!("MUMBLE_WEB2_VERSION");
|
||||||
let address = use_memo(move || {
|
|
||||||
if let Some(addr) = address_input() {
|
let is_override_mode = config
|
||||||
addr.clone()
|
.read()
|
||||||
} else {
|
.as_ref()
|
||||||
config()
|
.is_some_and(|c| !c.any_server);
|
||||||
|
|
||||||
|
// --- Overrides mode: single preset server, username-only input ---
|
||||||
|
if is_override_mode {
|
||||||
|
let proxy_url = config
|
||||||
|
.read()
|
||||||
|
.as_ref()
|
||||||
.and_then(|c| c.proxy_url.clone())
|
.and_then(|c| c.proxy_url.clone())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let previous_username = Platform::load_username();
|
let previous_username = Platform::load_username();
|
||||||
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
|
let mut username = use_signal(|| previous_username.unwrap_or_default());
|
||||||
|
|
||||||
let do_connect = move |_| {
|
|
||||||
//let _ = set_default_username(&username.read());
|
|
||||||
let _ = Platform::set_default_username(&username.read());
|
|
||||||
if config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
|
||||||
Platform::set_default_server(&address.read());
|
|
||||||
}
|
|
||||||
net.send(Connect {
|
|
||||||
address: address.read().clone(),
|
|
||||||
username: username.read().clone(),
|
|
||||||
config: config.read().clone().unwrap_or_default(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let status = &STATE.status;
|
let status = &STATE.status;
|
||||||
let bottom = match &*status.read() {
|
let is_connecting = matches!(&*status.read(), Connecting);
|
||||||
Disconnected => rsx! {
|
|
||||||
button {
|
return rsx!(
|
||||||
class: "login_bttn",
|
|
||||||
onclick: do_connect.clone(),
|
|
||||||
"Connect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Connecting => rsx! {
|
|
||||||
div {
|
div {
|
||||||
class: "login_bttn",
|
class: "server-list-page",
|
||||||
"Connecting..."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Failed(msg) => rsx!(
|
|
||||||
button {
|
|
||||||
class: "login_bttn",
|
|
||||||
onclick: do_connect.clone(),
|
|
||||||
"Reconnect"
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "login_error",
|
|
||||||
"Failed to connect:"
|
|
||||||
pre {
|
|
||||||
"{msg}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Connected => unreachable!(),
|
|
||||||
};
|
|
||||||
let version = option_env!("MUMBLE_WEB2_VERSION");
|
|
||||||
rsx!(
|
|
||||||
div {
|
|
||||||
class: "login",
|
|
||||||
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 config.read().as_ref().is_some_and(|cfg| cfg.any_server) {
|
|
||||||
div {
|
div {
|
||||||
label {
|
class: "server-list",
|
||||||
for: "address-entry",
|
div {
|
||||||
"Server Address:"
|
class: "server-card",
|
||||||
}
|
img {
|
||||||
input {
|
class: "server-card__icon",
|
||||||
id: "address-entry",
|
src: asset!("assets/earth-14-svgrepo-com.svg"),
|
||||||
placeholder: "address",
|
alt: "Server icon",
|
||||||
value: "{address.read()}",
|
|
||||||
autofocus: "true",
|
|
||||||
oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
|
||||||
}
|
}
|
||||||
|
div {
|
||||||
|
class: "server-card__info",
|
||||||
|
span { class: "server-card__name", "Server" }
|
||||||
|
span { class: "server-card__address", "{proxy_url}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
label {
|
class: "override-username-row",
|
||||||
for: "username-entry",
|
|
||||||
"Username:"
|
|
||||||
//style: "color: rgba(255, 255, 255, 0.5); font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; vertical-align: middle; font-size: 35px; user-select: none;",
|
|
||||||
}
|
|
||||||
input {
|
input {
|
||||||
id: "username-entry",
|
class: "override-username-input",
|
||||||
placeholder: "username",
|
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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Normal mode: editable server list ---
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
class: "server-list-page",
|
||||||
|
h1 {
|
||||||
|
"Mumble Web"
|
||||||
|
match version {
|
||||||
|
Some(v) => rsx!(div { class: "login_version", "({v})" }),
|
||||||
|
None => rsx!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "server-list",
|
||||||
|
for (idx, server) in servers.read().iter().enumerate() {
|
||||||
|
{
|
||||||
|
let address = format!("{}:{}", server.address, server.port);
|
||||||
|
let connect_entry = server.clone();
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
key: "{idx}",
|
||||||
|
class: "server-card",
|
||||||
|
img {
|
||||||
|
class: "server-card__icon",
|
||||||
|
src: asset!("assets/earth-14-svgrepo-com.svg"),
|
||||||
|
alt: "Server icon",
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "server-card__info",
|
||||||
|
span { class: "server-card__name", "{server.name}" }
|
||||||
|
span { class: "server-card__address", "{address}" }
|
||||||
|
}
|
||||||
|
ServerPingInfo {
|
||||||
|
address: server.address.clone(),
|
||||||
|
port: server.port,
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "server-card__action",
|
||||||
|
onclick: move |_| editing_index.set(Some(idx)),
|
||||||
|
img {
|
||||||
|
src: asset!("assets/edit-3-svgrepo-com.svg"),
|
||||||
|
alt: "Edit",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "server-card__action server-card__action--connect",
|
||||||
|
onclick: {
|
||||||
|
let entry = connect_entry.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = Platform::set_default_username(&entry.username);
|
||||||
|
let addr = format!("{}:{}", entry.address, entry.port);
|
||||||
|
net.send(Connect {
|
||||||
|
address: addr,
|
||||||
|
username: entry.username.clone(),
|
||||||
|
config: config.read().clone().unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
img {
|
||||||
|
src: asset!("assets/arrow-right-svgrepo-com.svg"),
|
||||||
|
alt: "Connect",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match &*STATE.status.read() {
|
||||||
|
Failed(msg) => rsx!(
|
||||||
|
div {
|
||||||
|
class: "server-list",
|
||||||
|
div {
|
||||||
|
class: "login_error",
|
||||||
|
"Failed to connect:"
|
||||||
|
pre { "{msg}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_ => rsx!(),
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "add-server-btn",
|
||||||
|
onclick: move |_| show_add_modal.set(true),
|
||||||
|
"+ Add Server"
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays live ping info (user count, latency) for a server using the mumble UDP ping protocol.
|
||||||
|
#[component]
|
||||||
|
fn ServerPingInfo(address: String, port: u16) -> Element {
|
||||||
|
let ping_result = use_resource(move || {
|
||||||
|
let addr = address.clone();
|
||||||
|
async move { Platform::ping_server(&addr, port).await }
|
||||||
|
});
|
||||||
|
|
||||||
|
let read = ping_result.read();
|
||||||
|
match &*read {
|
||||||
|
Some(Ok(status)) => {
|
||||||
|
let users_text = match (status.users, status.max_users) {
|
||||||
|
(Some(u), Some(m)) => format!("{u}/{m}"),
|
||||||
|
(Some(u), None) => format!("{u} online"),
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
rsx!(
|
||||||
|
div {
|
||||||
|
class: "server-card__ping",
|
||||||
|
if !users_text.is_empty() {
|
||||||
|
span { "{users_text}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Some(Err(_)) => rsx!(
|
||||||
|
div {
|
||||||
|
class: "server-card__ping",
|
||||||
|
span { "offline" }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
None => rsx!(
|
||||||
|
div {
|
||||||
|
class: "server-card__ping",
|
||||||
|
span { "..." }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn AddServerModal(on_save: EventHandler<ServerEntry>, on_cancel: EventHandler<()>) -> Element {
|
||||||
|
let mut name = use_signal(|| String::new());
|
||||||
|
let mut address = use_signal(|| String::new());
|
||||||
|
let mut port = use_signal(|| "64738".to_string());
|
||||||
|
let mut username = use_signal(|| Platform::load_username().unwrap_or_default());
|
||||||
|
let mut password = use_signal(|| String::new());
|
||||||
|
|
||||||
|
let do_save = move |_| {
|
||||||
|
let port_num: u16 = port.read().parse().unwrap_or(64738);
|
||||||
|
on_save.call(ServerEntry {
|
||||||
|
name: name.read().clone(),
|
||||||
|
address: address.read().clone(),
|
||||||
|
port: port_num,
|
||||||
|
username: username.read().clone(),
|
||||||
|
password: if password.read().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(password.read().clone())
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "modal-backdrop",
|
||||||
|
onclick: move |_| on_cancel.call(()),
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "modal-container",
|
||||||
|
onclick: move |evt| evt.stop_propagation(),
|
||||||
|
div {
|
||||||
|
class: "modal",
|
||||||
|
h2 { "Add Server" }
|
||||||
|
div {
|
||||||
|
class: "modal-field",
|
||||||
|
label { "Name" }
|
||||||
|
input {
|
||||||
|
r#type: "text",
|
||||||
|
placeholder: "My Mumble Server",
|
||||||
|
value: "{name.read()}",
|
||||||
|
oninput: move |evt| name.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "modal-field",
|
||||||
|
label { "Address" }
|
||||||
|
input {
|
||||||
|
r#type: "text",
|
||||||
|
placeholder: "mumble.example.com",
|
||||||
|
value: "{address.read()}",
|
||||||
|
oninput: move |evt| address.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "modal-field",
|
||||||
|
label { "Port" }
|
||||||
|
input {
|
||||||
|
r#type: "number",
|
||||||
|
placeholder: "64738",
|
||||||
|
value: "{port.read()}",
|
||||||
|
oninput: move |evt| port.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "modal-field",
|
||||||
|
label { "Username" }
|
||||||
|
input {
|
||||||
|
r#type: "text",
|
||||||
|
placeholder: "Nickname",
|
||||||
value: "{username.read()}",
|
value: "{username.read()}",
|
||||||
autofocus: "true",
|
|
||||||
oninput: move |evt| username.set(evt.value().clone()),
|
oninput: move |evt| username.set(evt.value().clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
match &*last_status.read() {
|
class: "modal-field",
|
||||||
None => rsx!(div {
|
label { "Password (optional)" }
|
||||||
class: "login_status",
|
input {
|
||||||
span {"···"}
|
r#type: "password",
|
||||||
}),
|
placeholder: "Password",
|
||||||
Some(Ok(ServerStatus { success: false, .. })) => rsx!(div {
|
value: "{password.read()}",
|
||||||
class: "login_status is_error",
|
oninput: move |evt| password.set(evt.value().clone()),
|
||||||
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 {
|
div {
|
||||||
{bottom}
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
// rsx!(
|
|
||||||
// div {
|
|
||||||
// class: "{login_box}",
|
|
||||||
// h1 {
|
|
||||||
// "Mumble Web"
|
|
||||||
// }
|
|
||||||
// input {
|
|
||||||
// placeholder: "username",
|
|
||||||
// value: "{username.read()}",
|
|
||||||
// autofocus: "true",
|
|
||||||
// oninput: move |evt| username.set(evt.value().clone()),
|
|
||||||
// }
|
|
||||||
// input {
|
|
||||||
// placeholder: "server address",
|
|
||||||
// value: "{address.read()}",
|
|
||||||
// autofocus: "true",
|
|
||||||
// oninput: move |evt| address_input.set(Some(evt.value().clone())),
|
|
||||||
// }
|
|
||||||
// {bottom}
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app() -> Element {
|
pub fn app() -> Element {
|
||||||
|
|||||||
+78
-4
@@ -1,8 +1,8 @@
|
|||||||
use crate::app::Command;
|
use crate::app::Command;
|
||||||
use color_eyre::eyre::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::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -46,6 +46,22 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
save_config_map(&config).ok()
|
save_config_map(&config).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
let config = load_config_map();
|
||||||
|
config
|
||||||
|
.get("servers")
|
||||||
|
.and_then(|s| serde_json::from_str(s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(servers: &[ServerEntry]) {
|
||||||
|
let mut config = load_config_map();
|
||||||
|
if let Ok(json) = serde_json::to_string(servers) {
|
||||||
|
config.insert("servers".to_string(), json);
|
||||||
|
let _ = save_config_map(&config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn network_connect(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -59,6 +75,10 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
super::connect::get_status(client).await
|
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;
|
||||||
@@ -81,8 +101,8 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
|
|
||||||
fn get_config_path() -> std::path::PathBuf {
|
fn get_config_path() -> std::path::PathBuf {
|
||||||
let strategy = choose_app_strategy(AppStrategyArgs {
|
let strategy = choose_app_strategy(AppStrategyArgs {
|
||||||
top_level_domain: "com".to_string(),
|
top_level_domain: "xyz".to_string(),
|
||||||
author: "Ohea Corp".to_string(),
|
author: "ohea".to_string(),
|
||||||
app_name: "Mumble Web2".to_string(),
|
app_name: "Mumble Web2".to_string(),
|
||||||
})
|
})
|
||||||
.expect("failed to choose app strategy");
|
.expect("failed to choose app strategy");
|
||||||
@@ -106,3 +126,57 @@ fn save_config_map(config: &HashMap<String, String>) -> color_eyre::Result<()> {
|
|||||||
std::fs::write(&config_path, contents)?;
|
std::fs::write(&config_path, contents)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mumble UDP ping protocol.
|
||||||
|
///
|
||||||
|
/// Send a 12-byte packet: 4 zero bytes + 8-byte identifier.
|
||||||
|
/// Receive a 24-byte response: 4 bytes version (1 byte each: major.minor.patch + padding)
|
||||||
|
/// + 8 bytes identifier echo + 4 bytes current_users + 4 bytes max_users + 4 bytes bandwidth.
|
||||||
|
async fn mumble_udp_ping(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
let dest = format!("{}:{}", address, port)
|
||||||
|
.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| color_eyre::eyre::eyre!("could not resolve address"))?;
|
||||||
|
|
||||||
|
let bind_addr = if dest.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" };
|
||||||
|
let socket = UdpSocket::bind(bind_addr).await?;
|
||||||
|
socket.connect(dest).await?;
|
||||||
|
|
||||||
|
// Build ping packet: 4 zero bytes + 8-byte request ID
|
||||||
|
let request_id: u64 = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos() as u64;
|
||||||
|
|
||||||
|
let mut buf = [0u8; 12];
|
||||||
|
buf[4..12].copy_from_slice(&request_id.to_be_bytes());
|
||||||
|
socket.send(&buf).await?;
|
||||||
|
|
||||||
|
let mut response = [0u8; 24];
|
||||||
|
let timeout = tokio::time::timeout(Duration::from_secs(2), socket.recv(&mut response)).await;
|
||||||
|
|
||||||
|
match timeout {
|
||||||
|
Ok(Ok(len)) if len >= 24 => {
|
||||||
|
let version_major = response[0] as u32;
|
||||||
|
let version_minor = response[1] as u32;
|
||||||
|
let version_patch = response[2] as u32;
|
||||||
|
let users = u32::from_be_bytes([response[12], response[13], response[14], response[15]]);
|
||||||
|
let max_users = u32::from_be_bytes([response[16], response[17], response[18], response[19]]);
|
||||||
|
let bandwidth = u32::from_be_bytes([response[20], response[21], response[22], response[23]]);
|
||||||
|
|
||||||
|
Ok(ServerStatus {
|
||||||
|
success: true,
|
||||||
|
version: Some((version_major, version_minor, version_patch)),
|
||||||
|
users: Some(users),
|
||||||
|
max_users: Some(max_users),
|
||||||
|
bandwidth: Some(bandwidth),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(Ok(_)) => bail!("ping response too short"),
|
||||||
|
Ok(Err(e)) => Err(e.into()),
|
||||||
|
Err(_) => bail!("ping timed out"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-2
@@ -1,7 +1,7 @@
|
|||||||
use crate::app::Command;
|
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::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -31,10 +31,16 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_default_server(server: &str) -> Option<()> {
|
fn set_default_server(_server: &str) -> Option<()> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(_servers: &[ServerEntry]) {}
|
||||||
|
|
||||||
async fn network_connect(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -48,6 +54,10 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
super::connect::get_status(client).await
|
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;
|
||||||
|
|||||||
+14
-2
@@ -7,7 +7,7 @@
|
|||||||
use crate::{app::Command, effects::AudioProcessor};
|
use crate::{app::Command, effects::AudioProcessor};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use dioxus::hooks::UnboundedReceiver;
|
use dioxus::hooks::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -70,11 +70,17 @@ pub trait PlatformInterface {
|
|||||||
gui_config: &ClientConfig,
|
gui_config: &ClientConfig,
|
||||||
) -> 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>>;
|
||||||
|
|
||||||
|
/// Ping a mumble server via UDP to get version, user count, etc.
|
||||||
|
fn ping_server(
|
||||||
|
address: &str,
|
||||||
|
port: u16,
|
||||||
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||||
|
|
||||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
|
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
|
||||||
|
|
||||||
@@ -90,6 +96,12 @@ pub trait PlatformInterface {
|
|||||||
/// Save the default server URL.
|
/// Save the default server URL.
|
||||||
fn set_default_server(server: &str) -> Option<()>;
|
fn set_default_server(server: &str) -> Option<()>;
|
||||||
|
|
||||||
|
/// Load the saved server list.
|
||||||
|
fn load_servers() -> Vec<ServerEntry>;
|
||||||
|
|
||||||
|
/// Save the server list.
|
||||||
|
fn save_servers(servers: &[ServerEntry]);
|
||||||
|
|
||||||
/// Async sleep for the given duration.
|
/// Async sleep for the given duration.
|
||||||
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
fn sleep(duration: Duration) -> impl Future<Output = ()>;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -3,7 +3,7 @@
|
|||||||
use crate::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::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
pub struct StubPlatform;
|
pub struct StubPlatform;
|
||||||
@@ -34,6 +34,13 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ping_server(
|
||||||
|
_address: &str,
|
||||||
|
_port: u16,
|
||||||
|
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
||||||
|
async { panic!("stubbed platform") }
|
||||||
|
}
|
||||||
|
|
||||||
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
@@ -54,6 +61,14 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
panic!("stubbed platform")
|
panic!("stubbed platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(_servers: &[ServerEntry]) {
|
||||||
|
panic!("stubbed platform")
|
||||||
|
}
|
||||||
|
|
||||||
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
fn sleep(_duration: std::time::Duration) -> impl Future<Output = ()> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -6,7 +6,7 @@ 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::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerEntry, ServerStatus};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -129,6 +129,24 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_servers() -> Vec<ServerEntry> {
|
||||||
|
web_sys::window()
|
||||||
|
.and_then(|w| w.local_storage().ok()?)
|
||||||
|
.and_then(|s| s.get_item("servers").ok()?)
|
||||||
|
.and_then(|json| serde_json::from_str(&json).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_servers(servers: &[ServerEntry]) {
|
||||||
|
if let Ok(json) = serde_json::to_string(servers) {
|
||||||
|
if let Some(storage) = web_sys::window()
|
||||||
|
.and_then(|w| w.local_storage().ok()?)
|
||||||
|
{
|
||||||
|
let _ = storage.set_item("servers", &json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn network_connect(
|
async fn network_connect(
|
||||||
address: String,
|
address: String,
|
||||||
username: String,
|
username: String,
|
||||||
@@ -147,6 +165,11 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
.await?)
|
.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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user