Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25730858f7 |
Generated
+13
-262
@@ -607,12 +607,6 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cassowary"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "castaway"
|
name = "castaway"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -845,20 +839,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "compact_str"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
|
||||||
dependencies = [
|
|
||||||
"castaway",
|
|
||||||
"cfg-if",
|
|
||||||
"itoa 1.0.15",
|
|
||||||
"rustversion",
|
|
||||||
"ryu",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compact_str"
|
name = "compact_str"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -1213,32 +1193,6 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm"
|
|
||||||
version = "0.28.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"crossterm_winapi",
|
|
||||||
"futures-core",
|
|
||||||
"mio",
|
|
||||||
"parking_lot",
|
|
||||||
"rustix 0.38.44",
|
|
||||||
"signal-hook",
|
|
||||||
"signal-hook-mio",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm_winapi"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -1367,18 +1321,8 @@ version = "0.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core 0.21.3",
|
"darling_core",
|
||||||
"darling_macro 0.21.3",
|
"darling_macro",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling"
|
|
||||||
version = "0.23.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core 0.23.0",
|
|
||||||
"darling_macro 0.23.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1394,37 +1338,13 @@ dependencies = [
|
|||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_core"
|
|
||||||
version = "0.23.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
|
||||||
dependencies = [
|
|
||||||
"ident_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"strsim",
|
|
||||||
"syn 2.0.108",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core 0.21.3",
|
"darling_core",
|
||||||
"quote",
|
|
||||||
"syn 2.0.108",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_macro"
|
|
||||||
version = "0.23.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core 0.23.0",
|
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
]
|
]
|
||||||
@@ -2301,7 +2221,7 @@ version = "0.14.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce"
|
checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.21.3",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.108",
|
||||||
@@ -2449,12 +2369,6 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foldhash"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2758,7 +2672,7 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix 1.1.2",
|
"rustix",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3136,17 +3050,6 @@ dependencies = [
|
|||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.15.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
|
||||||
dependencies = [
|
|
||||||
"allocator-api2",
|
|
||||||
"equivalent",
|
|
||||||
"foldhash 0.1.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -3155,7 +3058,7 @@ checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"foldhash 0.2.0",
|
"foldhash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3539,15 +3442,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indoc"
|
|
||||||
version = "2.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "infer"
|
name = "infer"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
@@ -3566,19 +3460,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "instability"
|
|
||||||
version = "0.3.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
|
|
||||||
dependencies = [
|
|
||||||
"darling 0.23.0",
|
|
||||||
"indoc",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.108",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inventory"
|
name = "inventory"
|
||||||
version = "0.3.21"
|
version = "0.3.21"
|
||||||
@@ -3882,12 +3763,6 @@ dependencies = [
|
|||||||
"x11",
|
"x11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linux-raw-sys"
|
|
||||||
version = "0.4.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -4034,15 +3909,6 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown 0.15.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -4209,7 +4075,7 @@ version = "0.6.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227"
|
checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix 1.1.2",
|
"rustix",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4288,7 +4154,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -4370,6 +4235,7 @@ dependencies = [
|
|||||||
"dasp_ring_buffer",
|
"dasp_ring_buffer",
|
||||||
"deep_filter",
|
"deep_filter",
|
||||||
"dioxus-asset-resolver",
|
"dioxus-asset-resolver",
|
||||||
|
"dioxus-signals",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -4448,25 +4314,6 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mumble-web2-tui"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"color-eyre",
|
|
||||||
"crossterm",
|
|
||||||
"dioxus-core",
|
|
||||||
"dioxus-signals",
|
|
||||||
"futures",
|
|
||||||
"futures-channel",
|
|
||||||
"generational-box",
|
|
||||||
"mumble-web2-client",
|
|
||||||
"mumble-web2-common",
|
|
||||||
"ratatui",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -5780,27 +5627,6 @@ dependencies = [
|
|||||||
"rand_core 0.5.1",
|
"rand_core 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ratatui"
|
|
||||||
version = "0.29.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"cassowary",
|
|
||||||
"compact_str 0.8.1",
|
|
||||||
"crossterm",
|
|
||||||
"indoc",
|
|
||||||
"instability",
|
|
||||||
"itertools 0.13.0",
|
|
||||||
"lru",
|
|
||||||
"paste",
|
|
||||||
"strum",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-truncate",
|
|
||||||
"unicode-width 0.2.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -6181,19 +6007,6 @@ dependencies = [
|
|||||||
"transpose",
|
"transpose",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustix"
|
|
||||||
version = "0.38.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
"linux-raw-sys 0.4.15",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -6203,7 +6016,7 @@ dependencies = [
|
|||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.11.0",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6374,7 +6187,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"compact_str 0.9.0",
|
"compact_str",
|
||||||
"eyre",
|
"eyre",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
@@ -6933,17 +6746,6 @@ dependencies = [
|
|||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook-mio"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"mio",
|
|
||||||
"signal-hook",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.6"
|
version = "1.4.6"
|
||||||
@@ -7162,34 +6964,6 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum"
|
|
||||||
version = "0.26.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
|
||||||
dependencies = [
|
|
||||||
"strum_macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum_macros"
|
|
||||||
version = "0.26.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
|
||||||
dependencies = [
|
|
||||||
"heck 0.5.0",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rustversion",
|
|
||||||
"syn 2.0.108",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subsecond"
|
name = "subsecond"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -7384,7 +7158,7 @@ dependencies = [
|
|||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.2",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8079,29 +7853,6 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-truncate"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
|
||||||
dependencies = [
|
|
||||||
"itertools 0.13.0",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width 0.1.14",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -8984,7 +8735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gethostname",
|
"gethostname",
|
||||||
"rustix 1.1.2",
|
"rustix",
|
||||||
"x11rb-protocol",
|
"x11rb-protocol",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -9001,7 +8752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rustix 1.1.2",
|
"rustix",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["client", "common", "gui", "proxy", "tui"]
|
members = ["client", "common", "gui", "proxy"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
|
|||||||
+1
-1
@@ -66,6 +66,7 @@ etcetera = { version = "0.10.0", optional = true }
|
|||||||
|
|
||||||
# Base Dependencies
|
# Base Dependencies
|
||||||
# ================
|
# ================
|
||||||
|
dioxus-signals = "0.7.2"
|
||||||
manganis = "0.7.2"
|
manganis = "0.7.2"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
asynchronous-codec = { workspace = true }
|
asynchronous-codec = { workspace = true }
|
||||||
@@ -113,7 +114,6 @@ tract-onnx = "=0.12.4"
|
|||||||
tract-pulse = "=0.12.4"
|
tract-pulse = "=0.12.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
embed-denoiser = []
|
|
||||||
web = [
|
web = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
|||||||
+9
-17
@@ -1,8 +1,8 @@
|
|||||||
|
use dioxus_signals::{ReadableExt as _, Signal};
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use mumble_web2_common::ProxyOverrides;
|
use mumble_web2_common::ProxyOverrides;
|
||||||
use ordermap::OrderSet;
|
use ordermap::OrderSet;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::{fmt, sync::Arc};
|
use std::{fmt, sync::Arc};
|
||||||
|
|
||||||
pub type ChannelId = u32;
|
pub type ChannelId = u32;
|
||||||
@@ -197,27 +197,19 @@ impl ServerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Reactivity {
|
pub struct State {
|
||||||
type Signal<T>;
|
pub status: Signal<ConnectionState>,
|
||||||
|
pub server: Signal<ServerState>,
|
||||||
fn new<T: 'static>(value: T) -> Self::Signal<T>;
|
pub audio: Signal<AudioSettings>,
|
||||||
fn read<T: 'static>(signal: &Self::Signal<T>) -> impl Deref<Target = T>;
|
|
||||||
fn write<T: 'static>(signal: &Self::Signal<T>) -> impl DerefMut<Target = T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct State<R: Reactivity> {
|
impl fmt::Debug for State {
|
||||||
pub status: R::Signal<ConnectionState>,
|
|
||||||
pub server: R::Signal<ServerState>,
|
|
||||||
pub audio: R::Signal<AudioSettings>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: Reactivity> fmt::Debug for State<R> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("State")
|
f.debug_struct("State")
|
||||||
.field("status", &*R::read(&self.status))
|
.field("status", &self.status.read())
|
||||||
.field("server", &*R::read(&self.server))
|
.field("server", &self.server.read())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SharedState<R> = Arc<State<R>>;
|
pub type SharedState = Arc<State>;
|
||||||
|
|||||||
+6
-24
@@ -1,34 +1,15 @@
|
|||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
use df::tract::{mut_slice_as_arrayviewmut, slice_as_arrayview};
|
||||||
use df::tract::{DfParams, DfTract, RuntimeParams};
|
use df::tract::{DfParams, DfTract, RuntimeParams};
|
||||||
use std::borrow::Cow;
|
use dioxus_asset_resolver::read_asset_bytes;
|
||||||
|
use manganis::{asset, Asset};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::imp::SpawnHandle;
|
use crate::imp::SpawnHandle;
|
||||||
|
|
||||||
#[cfg(not(feature = "embed-denoiser"))]
|
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
|
||||||
async fn denoiser_model_bytes() -> color_eyre::Result<Cow<'static, [u8]>> {
|
|
||||||
use color_eyre::eyre::eyre;
|
|
||||||
use manganis::{asset, Asset};
|
|
||||||
|
|
||||||
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
|
|
||||||
let bytes = dioxus_asset_resolver::read_asset_bytes(&DF_MODEL.to_string())
|
|
||||||
.await
|
|
||||||
.map_err(|err| eyre!("could not read denoising model: {err}"))?;
|
|
||||||
Ok(Cow::Owned(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "embed-denoiser")]
|
|
||||||
async fn denoiser_model_bytes() -> color_eyre::Result<Cow<'static, [u8]>> {
|
|
||||||
static DF_MODEL: &[u8] = include_bytes!(concat!(
|
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
|
||||||
"/assets/DeepFilterNet3_ll_onnx.tar.gz"
|
|
||||||
));
|
|
||||||
Ok(Cow::Borrowed(DF_MODEL))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make this user configurable.
|
// TODO: make this user configurable.
|
||||||
static DEFAULT_NOISE_FLOOR: f32 = 0.001;
|
static DEFAULT_NOISE_FLOOR: f32 = 0.001;
|
||||||
// 200ms hold at 48kHz sample rate
|
// 200ms hold at 48kHz sample rate
|
||||||
@@ -63,11 +44,12 @@ fn with_denoising_model<O>(spawn: &SpawnHandle, func: impl FnOnce(&mut DfTract)
|
|||||||
let cell = Arc::new(AtomicCell::new(None));
|
let cell = Arc::new(AtomicCell::new(None));
|
||||||
let cell_task = cell.clone();
|
let cell_task = cell.clone();
|
||||||
*state = DenoisingModelState::Downloading(cell);
|
*state = DenoisingModelState::Downloading(cell);
|
||||||
|
let model = DF_MODEL.to_string();
|
||||||
spawn.spawn(async move {
|
spawn.spawn(async move {
|
||||||
let model_bytes = match denoiser_model_bytes().await {
|
let model_bytes = match read_asset_bytes(&model).await {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{e}");
|
error!("could not read denoising model from \"{model}\": {e:?}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use crate::Reactivity;
|
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_protocol::control::ClientControlCodec;
|
use mumble_protocol::control::ClientControlCodec;
|
||||||
@@ -75,7 +74,7 @@ pub async fn network_connect(
|
|||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
overrides: &ProxyOverrides,
|
||||||
state: SharedState<impl Reactivity>,
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use crate::Reactivity;
|
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
@@ -29,7 +28,7 @@ impl super::PlatformInterface for DesktopPlatform {
|
|||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
overrides: &ProxyOverrides,
|
||||||
state: SharedState<impl Reactivity>,
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||||
}
|
}
|
||||||
@@ -38,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() {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use crate::Reactivity;
|
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
@@ -25,7 +24,7 @@ impl super::PlatformInterface for MobilePlatform {
|
|||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
overrides: &ProxyOverrides,
|
||||||
state: SharedState<impl Reactivity>,
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||||
}
|
}
|
||||||
@@ -34,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() {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use crate::effects::AudioProcessor;
|
use crate::effects::AudioProcessor;
|
||||||
use crate::Reactivity;
|
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
@@ -84,7 +83,7 @@ pub trait PlatformInterface {
|
|||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
proxy_overrides: &ProxyOverrides,
|
proxy_overrides: &ProxyOverrides,
|
||||||
state: SharedState<impl Reactivity>,
|
state: SharedState,
|
||||||
) -> impl Future<Output = Result<(), Error>>;
|
) -> impl Future<Output = Result<(), Error>>;
|
||||||
|
|
||||||
/// Get server status (user count, version, etc.) for the given address.
|
/// Get server status (user count, version, etc.) for the given address.
|
||||||
|
|||||||
@@ -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"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Stub implementation of the platform interface, so that we can
|
/// Stub implementation of the platform interface, so that we can
|
||||||
/// `cargo check` without any --feature flags.
|
/// `cargo check` without any --feature flags.
|
||||||
use crate::{app::SharedState, effects::AudioProcessor, Reactivity};
|
use crate::{app::SharedState, effects::AudioProcessor};
|
||||||
use color_eyre::eyre::Error;
|
use color_eyre::eyre::Error;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||||
@@ -25,7 +25,7 @@ impl super::PlatformInterface for StubPlatform {
|
|||||||
_username: String,
|
_username: String,
|
||||||
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
_event_rx: &mut UnboundedReceiver<crate::app::Command>,
|
||||||
_overrides: &ProxyOverrides,
|
_overrides: &ProxyOverrides,
|
||||||
_state: SharedState<impl Reactivity>,
|
_state: SharedState,
|
||||||
) -> impl Future<Output = Result<(), Error>> {
|
) -> impl Future<Output = Result<(), Error>> {
|
||||||
async { panic!("stubbed platform") }
|
async { panic!("stubbed platform") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::app::{Command, SharedState};
|
use crate::app::{Command, SharedState};
|
||||||
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
|
||||||
use crate::Reactivity;
|
|
||||||
use color_eyre::eyre::{bail, eyre, Error};
|
use color_eyre::eyre::{bail, eyre, Error};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use futures_channel::mpsc::UnboundedReceiver;
|
use futures_channel::mpsc::UnboundedReceiver;
|
||||||
@@ -113,7 +112,7 @@ impl super::PlatformInterface for WebPlatform {
|
|||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
overrides: &ProxyOverrides,
|
||||||
state: SharedState<impl Reactivity>,
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
network_connect(address, username, event_rx, overrides, state).await
|
network_connect(address, username, event_rx, overrides, state).await
|
||||||
}
|
}
|
||||||
@@ -443,7 +442,7 @@ pub async fn network_connect(
|
|||||||
username: String,
|
username: String,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
overrides: &ProxyOverrides,
|
overrides: &ProxyOverrides,
|
||||||
state: SharedState<impl Reactivity>,
|
state: SharedState,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
info!("connecting");
|
info!("connecting");
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
|
||||||
|
|||||||
+24
-26
@@ -3,10 +3,11 @@ use crate::AudioSettings;
|
|||||||
use crate::Chat;
|
use crate::Chat;
|
||||||
use crate::Command;
|
use crate::Command;
|
||||||
use crate::ConnectionState;
|
use crate::ConnectionState;
|
||||||
use crate::Reactivity;
|
|
||||||
use asynchronous_codec::FramedRead;
|
use asynchronous_codec::FramedRead;
|
||||||
use asynchronous_codec::FramedWrite;
|
use asynchronous_codec::FramedWrite;
|
||||||
use color_eyre::eyre::{bail, Error};
|
use color_eyre::eyre::{bail, Error};
|
||||||
|
use dioxus_signals::ReadableExt as _;
|
||||||
|
use dioxus_signals::WritableExt as _;
|
||||||
use futures::select;
|
use futures::select;
|
||||||
use futures::AsyncRead;
|
use futures::AsyncRead;
|
||||||
use futures::AsyncWrite;
|
use futures::AsyncWrite;
|
||||||
@@ -35,10 +36,7 @@ use crate::imp::{
|
|||||||
Platform, PlatformInterface as _,
|
Platform, PlatformInterface as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn network_entrypoint<X: Reactivity>(
|
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
|
||||||
mut event_rx: UnboundedReceiver<Command>,
|
|
||||||
state: SharedState<X>,
|
|
||||||
) {
|
|
||||||
loop {
|
loop {
|
||||||
let Some(Command::Connect {
|
let Some(Command::Connect {
|
||||||
address,
|
address,
|
||||||
@@ -49,16 +47,16 @@ pub async fn network_entrypoint<X: Reactivity>(
|
|||||||
panic!("did not receive connect command")
|
panic!("did not receive connect command")
|
||||||
};
|
};
|
||||||
|
|
||||||
*X::write(&state.server) = Default::default();
|
*state.server.write_unchecked() = Default::default();
|
||||||
*X::write(&state.status) = ConnectionState::Connecting;
|
*state.status.write_unchecked() = ConnectionState::Connecting;
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
Platform::network_connect(address, username, &mut event_rx, &config, state.clone())
|
Platform::network_connect(address, username, &mut event_rx, &config, state.clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
error!("could not connect {:?}", error);
|
error!("could not connect {:?}", error);
|
||||||
*X::write(&state.status) = ConnectionState::Failed(error.to_string());
|
*state.status.write_unchecked() = ConnectionState::Failed(error.to_string());
|
||||||
} else {
|
} else {
|
||||||
*X::write(&state.status) = ConnectionState::Disconnected;
|
*state.status.write_unchecked() = ConnectionState::Disconnected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,14 +76,14 @@ pub(crate) async fn sender_loop<W: AsyncWrite + Unpin + 'static>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static, X: Reactivity>(
|
pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static>(
|
||||||
username: String,
|
username: String,
|
||||||
state: SharedState<X>,
|
state: SharedState,
|
||||||
event_rx: &mut UnboundedReceiver<Command>,
|
event_rx: &mut UnboundedReceiver<Command>,
|
||||||
mut outgoing: UnboundedSender<ControlPacket<Serverbound>>,
|
mut outgoing: UnboundedSender<ControlPacket<Serverbound>>,
|
||||||
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let audio_settings = X::read(&state.audio).clone();
|
let audio_settings = state.audio.read().clone();
|
||||||
|
|
||||||
// Get version packet
|
// Get version packet
|
||||||
let version = match reader.next().await {
|
let version = match reader.next().await {
|
||||||
@@ -192,14 +190,14 @@ pub(crate) async fn network_loop<R: AsyncRead + Unpin + 'static, X: Reactivity>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_command<X: Reactivity>(
|
fn accept_command(
|
||||||
command: Command,
|
command: Command,
|
||||||
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
|
||||||
audio: &mut AudioSystem,
|
audio: &mut AudioSystem,
|
||||||
state: &State<X>,
|
state: &State,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
use Command::*;
|
use Command::*;
|
||||||
let Some(session) = X::read(&state.server).session else {
|
let Some(session) = state.server.read().session else {
|
||||||
bail!("no session id")
|
bail!("no session id")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -222,7 +220,7 @@ fn accept_command<X: Reactivity>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -263,7 +261,7 @@ fn accept_command<X: Reactivity>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
let Some(me) = server.session else {
|
let Some(me) = server.session else {
|
||||||
bail!("not signed in with a session id")
|
bail!("not signed in with a session id")
|
||||||
};
|
};
|
||||||
@@ -306,11 +304,11 @@ fn accept_command<X: Reactivity>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_packet<X: Reactivity>(
|
fn accept_packet(
|
||||||
msg: ControlPacket<mumble_protocol::Clientbound>,
|
msg: ControlPacket<mumble_protocol::Clientbound>,
|
||||||
audio_context: &mut AudioSystem,
|
audio_context: &mut AudioSystem,
|
||||||
player_map: &mut HashMap<u32, AudioPlayer>,
|
player_map: &mut HashMap<u32, AudioPlayer>,
|
||||||
state: &State<X>,
|
state: &State,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match msg {
|
match msg {
|
||||||
ControlPacket::UDPTunnel(u) => {
|
ControlPacket::UDPTunnel(u) => {
|
||||||
@@ -347,15 +345,15 @@ fn accept_packet<X: Reactivity>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelState(u) => {
|
ControlPacket::ChannelState(u) => {
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
server.channels_state.update_from_channel_state(&u);
|
server.channels_state.update_from_channel_state(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelRemove(u) => {
|
ControlPacket::ChannelRemove(u) => {
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
server.channels_state.update_from_channel_remove(&u);
|
server.channels_state.update_from_channel_remove(&u);
|
||||||
}
|
}
|
||||||
ControlPacket::UserState(u) => {
|
ControlPacket::UserState(u) => {
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
let server = &mut *server;
|
let server = &mut *server;
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
|
|
||||||
@@ -399,7 +397,7 @@ fn accept_packet<X: Reactivity>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::UserRemove(u) => {
|
ControlPacket::UserRemove(u) => {
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
if let Some(state) = server.users.remove(&id) {
|
if let Some(state) = server.users.remove(&id) {
|
||||||
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||||
@@ -408,7 +406,7 @@ fn accept_packet<X: Reactivity>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::TextMessage(u) => {
|
ControlPacket::TextMessage(u) => {
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
if u.has_message() {
|
if u.has_message() {
|
||||||
let text = u.get_message().to_string();
|
let text = u.get_message().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
@@ -423,8 +421,8 @@ fn accept_packet<X: Reactivity>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlPacket::ServerSync(u) => {
|
ControlPacket::ServerSync(u) => {
|
||||||
*X::write(&state.status) = ConnectionState::Connected;
|
*state.status.write_unchecked() = ConnectionState::Connected;
|
||||||
let mut server = X::write(&state.server);
|
let mut server = state.server.write_unchecked();
|
||||||
if u.has_welcome_text() {
|
if u.has_welcome_text() {
|
||||||
let text = u.get_welcome_text().to_string();
|
let text = u.get_welcome_text().to_string();
|
||||||
server.chat.push(Chat {
|
server.chat.push(Chat {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 |
@@ -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
-178
@@ -3,34 +3,13 @@
|
|||||||
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 _, UserId,
|
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, SharedState,
|
||||||
UserState, VERSION,
|
State, UserId, UserState,
|
||||||
};
|
};
|
||||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
use mumble_web2_common::{ProxyOverrides, ServerEntry};
|
||||||
use Command::*;
|
use Command::*;
|
||||||
use ConnectionState::*;
|
use ConnectionState::*;
|
||||||
|
|
||||||
pub struct DioxusReactivity;
|
|
||||||
|
|
||||||
impl mumble_web2_client::Reactivity for DioxusReactivity {
|
|
||||||
type Signal<T> = Signal<T>;
|
|
||||||
|
|
||||||
fn new<T: 'static>(value: T) -> Signal<T> {
|
|
||||||
Signal::new(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read<T: 'static>(signal: &Signal<T>) -> impl std::ops::Deref<Target = T> {
|
|
||||||
signal.read_unchecked()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write<T: 'static>(signal: &Signal<T>) -> impl std::ops::DerefMut<Target = T> {
|
|
||||||
signal.write_unchecked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SharedState = mumble_web2_client::SharedState<DioxusReactivity>;
|
|
||||||
pub type State = mumble_web2_client::State<DioxusReactivity>;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum UserIcon {
|
pub enum UserIcon {
|
||||||
Normal,
|
Normal,
|
||||||
@@ -520,179 +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]
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "mumble-web2-tui"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
mumble-web2-client = { version = "0.1.0", path = "../client", features = ["desktop", "embed-denoiser"] }
|
|
||||||
mumble-web2-common = { version = "0.1.0", path = "../common" }
|
|
||||||
ratatui = "0.29"
|
|
||||||
crossterm = { version = "0.28", features = ["event-stream"] }
|
|
||||||
tokio = { version = "^1.41.1", features = ["rt", "macros"] }
|
|
||||||
futures-channel = "^0.3.30"
|
|
||||||
futures = "^0.3.30"
|
|
||||||
dioxus-signals = "0.7.2"
|
|
||||||
dioxus-core = "0.7.2"
|
|
||||||
generational-box = "0.7.2"
|
|
||||||
color-eyre = "^0.6.3"
|
|
||||||
tracing-subscriber = { version = "^0.3.18", features = ["env-filter"] }
|
|
||||||
tracing = "^0.1.40"
|
|
||||||
-775
@@ -1,775 +0,0 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
|
|
||||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
|
|
||||||
use dioxus_core::with_owner;
|
|
||||||
use futures_channel::mpsc;
|
|
||||||
use generational_box::Owner;
|
|
||||||
use mumble_web2_client::{
|
|
||||||
network_entrypoint, AudioSettings, ChannelId, Command, ConfigSystem,
|
|
||||||
ConfigSystemInterface as _, ConnectionState, Platform, PlatformInterface as _, ServerState,
|
|
||||||
UserState,
|
|
||||||
};
|
|
||||||
use mumble_web2_common::ProxyOverrides;
|
|
||||||
use ratatui::{
|
|
||||||
layout::{Constraint, Direction, Layout},
|
|
||||||
style::{Color, Modifier, Style},
|
|
||||||
text::{Line, Span},
|
|
||||||
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
|
|
||||||
Frame,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct RefCellReactivity;
|
|
||||||
|
|
||||||
impl mumble_web2_client::Reactivity for RefCellReactivity {
|
|
||||||
type Signal<T> = RefCell<T>;
|
|
||||||
|
|
||||||
fn new<T: 'static>(value: T) -> Self::Signal<T> {
|
|
||||||
RefCell::new(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read<T: 'static>(signal: &Self::Signal<T>) -> impl std::ops::Deref<Target = T> {
|
|
||||||
signal.borrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write<T: 'static>(signal: &Self::Signal<T>) -> impl std::ops::DerefMut<Target = T> {
|
|
||||||
signal.borrow_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type State = mumble_web2_client::State<RefCellReactivity>;
|
|
||||||
pub type SharedState = mumble_web2_client::SharedState<RefCellReactivity>;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// App state (TUI-local, not shared with client)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum Focus {
|
|
||||||
Address,
|
|
||||||
Username,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum Pane {
|
|
||||||
Channels,
|
|
||||||
Chat,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct App {
|
|
||||||
state: SharedState,
|
|
||||||
tx: mpsc::UnboundedSender<Command>,
|
|
||||||
config: ConfigSystem,
|
|
||||||
overrides: ProxyOverrides,
|
|
||||||
|
|
||||||
// Login fields
|
|
||||||
address: String,
|
|
||||||
username: String,
|
|
||||||
login_focus: Focus,
|
|
||||||
|
|
||||||
// Server view
|
|
||||||
active_pane: Pane,
|
|
||||||
chat_input: String,
|
|
||||||
chat_focused: bool,
|
|
||||||
channel_list: Vec<(ChannelId, u16)>, // (id, depth) - flattened tree for navigation
|
|
||||||
channel_cursor: usize,
|
|
||||||
|
|
||||||
should_quit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
fn new(
|
|
||||||
state: SharedState,
|
|
||||||
tx: mpsc::UnboundedSender<Command>,
|
|
||||||
config: ConfigSystem,
|
|
||||||
overrides: ProxyOverrides,
|
|
||||||
) -> Self {
|
|
||||||
let address = config
|
|
||||||
.config_get::<String>("server_url")
|
|
||||||
.or_else(|| overrides.proxy_url.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let username = config.config_get::<String>("username").unwrap_or_default();
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
tx,
|
|
||||||
config,
|
|
||||||
overrides,
|
|
||||||
address,
|
|
||||||
username,
|
|
||||||
login_focus: Focus::Username,
|
|
||||||
active_pane: Pane::Channels,
|
|
||||||
chat_input: String::new(),
|
|
||||||
chat_focused: false,
|
|
||||||
channel_list: Vec::new(),
|
|
||||||
channel_cursor: 0,
|
|
||||||
should_quit: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, cmd: Command) {
|
|
||||||
let _ = self.tx.unbounded_send(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_connected(&self) -> bool {
|
|
||||||
matches!(&*self.state.status.borrow(), ConnectionState::Connected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a flat list of (channel_id, depth) by walking the tree.
|
|
||||||
fn rebuild_channel_list(&mut self) {
|
|
||||||
self.channel_list.clear();
|
|
||||||
let server = self.state.server.borrow();
|
|
||||||
// Find root channels (no parent)
|
|
||||||
let mut roots: Vec<ChannelId> = server
|
|
||||||
.channels_state
|
|
||||||
.channels
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, ch)| ch.parent.is_none())
|
|
||||||
.map(|(&id, _)| id)
|
|
||||||
.collect();
|
|
||||||
roots.sort();
|
|
||||||
for root in roots {
|
|
||||||
Self::walk_channel(&mut self.channel_list, &server, root, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_channel(
|
|
||||||
list: &mut Vec<(ChannelId, u16)>,
|
|
||||||
server: &ServerState,
|
|
||||||
id: ChannelId,
|
|
||||||
depth: u16,
|
|
||||||
) {
|
|
||||||
list.push((id, depth));
|
|
||||||
let Some(ch) = server.channels_state.channels.get(&id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for &child in ch.children.iter() {
|
|
||||||
Self::walk_channel(list, server, child, depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// User icon helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn user_indicator(user: &UserState) -> &'static str {
|
|
||||||
if user.deaf || user.self_deaf {
|
|
||||||
"D"
|
|
||||||
} else if user.mute || user.self_mute {
|
|
||||||
"M"
|
|
||||||
} else if user.suppress {
|
|
||||||
"S"
|
|
||||||
} else {
|
|
||||||
" "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_style(user: &UserState) -> Style {
|
|
||||||
if user.deaf || user.self_deaf {
|
|
||||||
Style::default().fg(Color::Red)
|
|
||||||
} else if user.mute || user.self_mute || user.suppress {
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::Green)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Rendering
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn draw(frame: &mut Frame, app: &mut App) {
|
|
||||||
if app.is_connected() {
|
|
||||||
draw_server(frame, app);
|
|
||||||
} else {
|
|
||||||
draw_login(frame, app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_login(frame: &mut Frame, app: &App) {
|
|
||||||
let area = frame.area();
|
|
||||||
|
|
||||||
// Center a box
|
|
||||||
let vert = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(10),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
let horiz = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(50),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(vert[1]);
|
|
||||||
let box_area = horiz[1];
|
|
||||||
|
|
||||||
let block = Block::default()
|
|
||||||
.title(" Mumble Web 2 ")
|
|
||||||
.borders(Borders::ALL);
|
|
||||||
let inner = block.inner(box_area);
|
|
||||||
frame.render_widget(Clear, box_area);
|
|
||||||
frame.render_widget(block, box_area);
|
|
||||||
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1), // address label
|
|
||||||
Constraint::Length(1), // address input
|
|
||||||
Constraint::Length(1), // spacer
|
|
||||||
Constraint::Length(1), // username label
|
|
||||||
Constraint::Length(1), // username input
|
|
||||||
Constraint::Length(1), // spacer
|
|
||||||
Constraint::Length(1), // status / button hint
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(inner);
|
|
||||||
|
|
||||||
let status = &*app.state.status.borrow();
|
|
||||||
|
|
||||||
// Address
|
|
||||||
if app.overrides.any_server {
|
|
||||||
let label_style = if app.login_focus == Focus::Address {
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
};
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new("Server Address:").style(label_style),
|
|
||||||
chunks[0],
|
|
||||||
);
|
|
||||||
let input_style = if app.login_focus == Focus::Address {
|
|
||||||
Style::default().fg(Color::White)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::DarkGray)
|
|
||||||
};
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(format!("> {}", app.address)).style(input_style),
|
|
||||||
chunks[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Username
|
|
||||||
let label_style = if app.login_focus == Focus::Username {
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
};
|
|
||||||
frame.render_widget(Paragraph::new("Username:").style(label_style), chunks[3]);
|
|
||||||
let input_style = if app.login_focus == Focus::Username {
|
|
||||||
Style::default().fg(Color::White)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::DarkGray)
|
|
||||||
};
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(format!("> {}", app.username)).style(input_style),
|
|
||||||
chunks[4],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Status line
|
|
||||||
let status_line = match status {
|
|
||||||
ConnectionState::Disconnected => Line::from(Span::styled(
|
|
||||||
"[Enter] Connect",
|
|
||||||
Style::default().fg(Color::Green),
|
|
||||||
)),
|
|
||||||
ConnectionState::Connecting => Line::from(Span::styled(
|
|
||||||
"Connecting...",
|
|
||||||
Style::default().fg(Color::Yellow),
|
|
||||||
)),
|
|
||||||
ConnectionState::Failed(msg) => Line::from(vec![
|
|
||||||
Span::styled("Failed: ", Style::default().fg(Color::Red)),
|
|
||||||
Span::raw(msg.clone()),
|
|
||||||
Span::styled(" [Enter] Retry", Style::default().fg(Color::Green)),
|
|
||||||
]),
|
|
||||||
ConnectionState::Connected => unreachable!(),
|
|
||||||
};
|
|
||||||
frame.render_widget(Paragraph::new(status_line), chunks[6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_server(frame: &mut Frame, app: &mut App) {
|
|
||||||
app.rebuild_channel_list();
|
|
||||||
let server = app.state.server.borrow();
|
|
||||||
let audio = app.state.audio.borrow();
|
|
||||||
|
|
||||||
// Main layout: channels left, chat right, controls bottom
|
|
||||||
let vert = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(0), Constraint::Length(3)])
|
|
||||||
.split(frame.area());
|
|
||||||
|
|
||||||
let horiz = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([Constraint::Percentage(35), Constraint::Percentage(65)])
|
|
||||||
.split(vert[0]);
|
|
||||||
|
|
||||||
// --- Channel tree ---
|
|
||||||
let chan_block = Block::default()
|
|
||||||
.title(" Channels ")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(if app.active_pane == Pane::Channels && !app.chat_focused {
|
|
||||||
Style::default().fg(Color::Cyan)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut items: Vec<ListItem> = Vec::new();
|
|
||||||
for (i, &(ch_id, depth)) in app.channel_list.iter().enumerate() {
|
|
||||||
let Some(ch) = server.channels_state.channels.get(&ch_id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let indent = " ".repeat(depth as usize);
|
|
||||||
let marker = if ch.children.is_empty() { " " } else { "▾ " };
|
|
||||||
let is_selected = i == app.channel_cursor;
|
|
||||||
|
|
||||||
let style = if is_selected {
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::White)
|
|
||||||
};
|
|
||||||
let prefix = if is_selected { ">" } else { " " };
|
|
||||||
|
|
||||||
// Channel name line
|
|
||||||
let mut lines = vec![Line::from(Span::styled(
|
|
||||||
format!("{prefix}{indent}{marker}{}", ch.name),
|
|
||||||
style,
|
|
||||||
))];
|
|
||||||
|
|
||||||
// Users in this channel
|
|
||||||
for &uid in ch.users.iter() {
|
|
||||||
if let Some(user) = server.users.get(&uid) {
|
|
||||||
let is_self = server.session == Some(uid);
|
|
||||||
let ind = user_indicator(user);
|
|
||||||
let u_style = if is_self {
|
|
||||||
user_style(user).add_modifier(Modifier::UNDERLINED)
|
|
||||||
} else {
|
|
||||||
user_style(user)
|
|
||||||
};
|
|
||||||
lines.push(Line::from(Span::styled(
|
|
||||||
format!(" {indent} [{ind}] {}", user.name),
|
|
||||||
u_style,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push(ListItem::new(lines));
|
|
||||||
}
|
|
||||||
|
|
||||||
let channel_list = List::new(items).block(chan_block);
|
|
||||||
frame.render_widget(channel_list, horiz[0]);
|
|
||||||
|
|
||||||
// --- Chat panel ---
|
|
||||||
let chat_area = horiz[1];
|
|
||||||
let chat_layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(0), Constraint::Length(3)])
|
|
||||||
.split(chat_area);
|
|
||||||
|
|
||||||
let chat_block = Block::default()
|
|
||||||
.title(" Chat ")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(if app.active_pane == Pane::Chat && !app.chat_focused {
|
|
||||||
Style::default().fg(Color::Cyan)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let chat_lines: Vec<Line> = server
|
|
||||||
.chat
|
|
||||||
.iter()
|
|
||||||
.map(|msg| {
|
|
||||||
let sender = msg
|
|
||||||
.sender
|
|
||||||
.and_then(|uid| server.users.get(&uid))
|
|
||||||
.map(|u| u.name.as_str())
|
|
||||||
.unwrap_or("server");
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(
|
|
||||||
format!("{sender}: "),
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
),
|
|
||||||
Span::raw(&msg.raw),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Show last N lines that fit
|
|
||||||
let chat_inner_height = chat_block.inner(chat_layout[0]).height as usize;
|
|
||||||
let skip = chat_lines.len().saturating_sub(chat_inner_height);
|
|
||||||
let visible_lines: Vec<Line> = chat_lines.into_iter().skip(skip).collect();
|
|
||||||
|
|
||||||
let chat_widget = Paragraph::new(visible_lines)
|
|
||||||
.block(chat_block)
|
|
||||||
.wrap(Wrap { trim: false });
|
|
||||||
frame.render_widget(chat_widget, chat_layout[0]);
|
|
||||||
|
|
||||||
// Chat input
|
|
||||||
let input_block = Block::default()
|
|
||||||
.title(if app.chat_focused {
|
|
||||||
" Input (Esc to cancel) "
|
|
||||||
} else {
|
|
||||||
" [t] to type "
|
|
||||||
})
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(if app.chat_focused {
|
|
||||||
Style::default().fg(Color::Green)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
});
|
|
||||||
let input_widget = Paragraph::new(app.chat_input.as_str()).block(input_block);
|
|
||||||
frame.render_widget(input_widget, chat_layout[1]);
|
|
||||||
|
|
||||||
// --- Controls bar ---
|
|
||||||
let this_user = server.this_user();
|
|
||||||
let (self_mute, mute, suppress, self_deaf, deaf) = this_user
|
|
||||||
.map(|u| (u.self_mute, u.mute, u.suppress, u.self_deaf, u.deaf))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let muted = mute || suppress || self_mute;
|
|
||||||
let deafened = deaf || self_deaf;
|
|
||||||
|
|
||||||
let status_text = match &*app.state.status.borrow() {
|
|
||||||
ConnectionState::Connected => "Connected",
|
|
||||||
ConnectionState::Connecting => "Connecting",
|
|
||||||
ConnectionState::Disconnected => "Disconnected",
|
|
||||||
ConnectionState::Failed(_) => "Failed",
|
|
||||||
};
|
|
||||||
|
|
||||||
let current_channel = this_user
|
|
||||||
.and_then(|u| server.channels_state.channels.get(&u.channel))
|
|
||||||
.map(|ch| ch.name.as_str())
|
|
||||||
.unwrap_or("?");
|
|
||||||
|
|
||||||
let controls = Line::from(vec![
|
|
||||||
Span::styled(
|
|
||||||
format!(" {status_text} "),
|
|
||||||
Style::default().fg(Color::Green),
|
|
||||||
),
|
|
||||||
Span::styled(
|
|
||||||
format!("#{current_channel} "),
|
|
||||||
Style::default().fg(Color::White),
|
|
||||||
),
|
|
||||||
Span::raw("│ "),
|
|
||||||
Span::styled(
|
|
||||||
if muted { "[m]ute ✓ " } else { "[m]ute " },
|
|
||||||
if muted {
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Span::styled(
|
|
||||||
if deafened { "[d]eaf ✓ " } else { "[d]eaf " },
|
|
||||||
if deafened {
|
|
||||||
Style::default().fg(Color::Red)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Span::styled(
|
|
||||||
if audio.denoise {
|
|
||||||
"[n]oise ✓ "
|
|
||||||
} else {
|
|
||||||
"[n]oise "
|
|
||||||
},
|
|
||||||
if audio.denoise {
|
|
||||||
Style::default().fg(Color::Cyan)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Span::raw("│ "),
|
|
||||||
Span::styled("[q]uit", Style::default().fg(Color::DarkGray)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let controls_block = Block::default().borders(Borders::ALL);
|
|
||||||
let controls_widget = Paragraph::new(controls).block(controls_block);
|
|
||||||
frame.render_widget(controls_widget, vert[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Event handling
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn handle_login_key(app: &mut App, code: KeyCode) {
|
|
||||||
match code {
|
|
||||||
KeyCode::Tab | KeyCode::BackTab => {
|
|
||||||
app.login_focus = match app.login_focus {
|
|
||||||
Focus::Address => Focus::Username,
|
|
||||||
Focus::Username => {
|
|
||||||
if app.overrides.any_server {
|
|
||||||
Focus::Address
|
|
||||||
} else {
|
|
||||||
Focus::Username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
let status = &*app.state.status.borrow();
|
|
||||||
if matches!(
|
|
||||||
status,
|
|
||||||
ConnectionState::Disconnected | ConnectionState::Failed(_)
|
|
||||||
) {
|
|
||||||
app.config.config_set::<String>("username", &app.username);
|
|
||||||
if app.overrides.any_server {
|
|
||||||
app.config.config_set::<String>("server_url", &app.address);
|
|
||||||
}
|
|
||||||
app.send(Command::Connect {
|
|
||||||
address: app.address.clone(),
|
|
||||||
username: app.username.clone(),
|
|
||||||
config: app.overrides.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
let field = match app.login_focus {
|
|
||||||
Focus::Address => &mut app.address,
|
|
||||||
Focus::Username => &mut app.username,
|
|
||||||
};
|
|
||||||
field.push(c);
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
let field = match app.login_focus {
|
|
||||||
Focus::Address => &mut app.address,
|
|
||||||
Focus::Username => &mut app.username,
|
|
||||||
};
|
|
||||||
field.pop();
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
app.should_quit = true;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_server_key(app: &mut App, code: KeyCode) {
|
|
||||||
if app.chat_focused {
|
|
||||||
match code {
|
|
||||||
KeyCode::Esc => {
|
|
||||||
app.chat_focused = false;
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if !app.chat_input.is_empty() {
|
|
||||||
let server = app.state.server.borrow();
|
|
||||||
if let Some(user) = server.this_user() {
|
|
||||||
let channels = vec![user.channel];
|
|
||||||
let markdown = std::mem::take(&mut app.chat_input);
|
|
||||||
drop(server);
|
|
||||||
app.send(Command::SendChat { markdown, channels });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
app.chat_input.push(c);
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
app.chat_input.pop();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match code {
|
|
||||||
KeyCode::Char('q') => {
|
|
||||||
app.send(Command::Disconnect);
|
|
||||||
app.should_quit = true;
|
|
||||||
}
|
|
||||||
KeyCode::Char('m') => {
|
|
||||||
let server = app.state.server.borrow();
|
|
||||||
if let Some(user) = server.this_user() {
|
|
||||||
if !user.mute && !user.suppress {
|
|
||||||
let new_mute = !user.self_mute;
|
|
||||||
drop(server);
|
|
||||||
app.send(Command::SetMute { mute: new_mute });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('d') => {
|
|
||||||
let server = app.state.server.borrow();
|
|
||||||
if let Some(user) = server.this_user() {
|
|
||||||
if !user.deaf {
|
|
||||||
let new_deaf = !user.self_deaf;
|
|
||||||
drop(server);
|
|
||||||
app.send(Command::SetDeaf { deaf: new_deaf });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('n') => {
|
|
||||||
let audio = app.state.audio.borrow().clone();
|
|
||||||
let new_denoise = !audio.denoise;
|
|
||||||
*app.state.audio.borrow_mut() = AudioSettings {
|
|
||||||
denoise: new_denoise,
|
|
||||||
};
|
|
||||||
app.send(Command::UpdateAudioSettings(AudioSettings {
|
|
||||||
denoise: new_denoise,
|
|
||||||
}));
|
|
||||||
app.config.config_set::<bool>("denoise", &new_denoise);
|
|
||||||
}
|
|
||||||
KeyCode::Char('t') => {
|
|
||||||
app.chat_focused = true;
|
|
||||||
}
|
|
||||||
KeyCode::Tab => {
|
|
||||||
app.active_pane = match app.active_pane {
|
|
||||||
Pane::Channels => Pane::Chat,
|
|
||||||
Pane::Chat => Pane::Channels,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
KeyCode::Char('j') | KeyCode::Down => {
|
|
||||||
if !app.channel_list.is_empty() {
|
|
||||||
app.channel_cursor = (app.channel_cursor + 1).min(app.channel_list.len() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('k') | KeyCode::Up => {
|
|
||||||
app.channel_cursor = app.channel_cursor.saturating_sub(1);
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if let Some(&(ch_id, _)) = app.channel_list.get(app.channel_cursor) {
|
|
||||||
let server = app.state.server.borrow();
|
|
||||||
if let Some(uid) = server.session {
|
|
||||||
drop(server);
|
|
||||||
app.send(Command::EnterChannel {
|
|
||||||
channel: ch_id,
|
|
||||||
user: uid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(app: &mut App, ev: Event) {
|
|
||||||
let Event::Key(key) = ev else { return };
|
|
||||||
if key.kind != KeyEventKind::Press {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl-C always quits
|
|
||||||
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
||||||
app.send(Command::Disconnect);
|
|
||||||
app.should_quit = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.is_connected() {
|
|
||||||
handle_server_key(app, key.code);
|
|
||||||
} else {
|
|
||||||
handle_login_key(app, key.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Main
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn init_file_logging() -> color_eyre::Result<()> {
|
|
||||||
use tracing::level_filters::LevelFilter;
|
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
|
||||||
|
|
||||||
let log_path = std::env::var("MUMBLE_TUI_LOG")
|
|
||||||
.unwrap_or_else(|_| std::env::temp_dir().join("mumble-tui.log").to_string_lossy().into_owned());
|
|
||||||
let file = std::fs::OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(&log_path)?;
|
|
||||||
|
|
||||||
let env_filter = EnvFilter::builder()
|
|
||||||
.with_default_directive(LevelFilter::INFO.into())
|
|
||||||
.from_env_lossy();
|
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_target(true)
|
|
||||||
.with_level(true)
|
|
||||||
.with_ansi(false)
|
|
||||||
.with_env_filter(env_filter)
|
|
||||||
.with_writer(file)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
eprintln!("logging to {log_path}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> color_eyre::Result<()> {
|
|
||||||
color_eyre::install()?;
|
|
||||||
init_file_logging()?;
|
|
||||||
|
|
||||||
// Use a single-threaded runtime since dioxus Signals are !Send.
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?;
|
|
||||||
let local = tokio::task::LocalSet::new();
|
|
||||||
|
|
||||||
local.block_on(&rt, async {
|
|
||||||
let config = ConfigSystem::new()?;
|
|
||||||
let overrides = Platform::load_proxy_overrides().await.unwrap_or_default();
|
|
||||||
|
|
||||||
let state = SharedState::new(State {
|
|
||||||
status: RefCell::new(ConnectionState::Disconnected),
|
|
||||||
server: RefCell::new(Default::default()),
|
|
||||||
audio: RefCell::new(AudioSettings {
|
|
||||||
denoise: config.config_get::<bool>("denoise").unwrap_or(true),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::unbounded::<Command>();
|
|
||||||
|
|
||||||
// Spawn the network loop on the local task set (not Send-bound).
|
|
||||||
let net_state = state.clone();
|
|
||||||
tokio::task::spawn_local(async move {
|
|
||||||
network_entrypoint(rx, net_state).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup terminal
|
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
|
||||||
let mut stdout = std::io::stdout();
|
|
||||||
crossterm::execute!(
|
|
||||||
stdout,
|
|
||||||
crossterm::terminal::EnterAlternateScreen,
|
|
||||||
crossterm::event::EnableMouseCapture
|
|
||||||
)?;
|
|
||||||
let backend = ratatui::backend::CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = ratatui::Terminal::new(backend)?;
|
|
||||||
|
|
||||||
let mut app = App::new(state, tx, config, overrides);
|
|
||||||
|
|
||||||
// Event loop
|
|
||||||
loop {
|
|
||||||
terminal.draw(|f| draw(f, &mut app))?;
|
|
||||||
|
|
||||||
if app.should_quit {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poll with a short timeout so we re-render when state changes.
|
|
||||||
// Yield to the tokio runtime between polls so network tasks can progress.
|
|
||||||
if crossterm::event::poll(std::time::Duration::from_millis(16))? {
|
|
||||||
let ev = crossterm::event::read()?;
|
|
||||||
handle_event(&mut app, ev);
|
|
||||||
}
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore terminal
|
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
|
||||||
crossterm::execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
crossterm::terminal::LeaveAlternateScreen,
|
|
||||||
crossterm::event::DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user