feat: multi-platform bug fixes, Android audio path, and build tooling
Flutter UI fixes: - Fix stale channel badge/speaker when moved by others (derive current channel from ownClientId instead of optimistic local state) - Fix Linux PTT via focused fallback key handler - Distinguish ServerQuery clients with terminal icon in client list - Reduce duplicate current-channel badge display - Prevent PTT key-bind save from permanently closing voice settings - Fix Linux GTK reopen-after-close (quit app on window destroy) - Fix focused PTT: consume key events, release held keys on disconnect/leave-channel/mode/backend changes, suppress stale errors Flutter Rust bridge: - Thread is_server_query flag through protocol→bridge→Dart - Add own_client_id to BridgeSnapshot DTO - Add log_file_path_str() for platform log path queries Rust protocol: - Add ServerQuery test coverage (query_client_type_maps_to_server_query_flag) - Split reqwest TLS: native-tls for desktop/iOS, rustls for Android Rust audio: - Upgrade cpal 0.16→0.17.3 with API adjustments (SampleRate, description()) - Suppress Android-only dead-code warnings (open_log_file, keyring_account) Android build tooling: - tools/build-opus-android.sh: NDK auto-discovery, correct CMake Android variables (ANDROID_ABI, ANDROID_PLATFORM), portable baseline - tools/build-android-rust.sh: build+copy Rust cdylib for arm64-v8a, armeabi-v7a, x86_64 into android/app/src/main/jniLibs/ - Add jniLibs/ to .gitignore Rust bridge: - Guard open_log_file() on non-Android (Android uses logcat)
This commit is contained in:
Generated
+369
-30
@@ -72,9 +72,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alsa"
|
name = "alsa"
|
||||||
version = "0.9.1"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa-sys",
|
"alsa-sys",
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
@@ -84,9 +84,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alsa-sys"
|
name = "alsa-sys"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
@@ -193,6 +193,28 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-rs"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-sys",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-sys"
|
||||||
|
version = "0.41.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cmake",
|
||||||
|
"dunce",
|
||||||
|
"fs_extra",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.76"
|
version = "0.3.76"
|
||||||
@@ -256,6 +278,15 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "build-target"
|
name = "build-target"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -293,6 +324,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -308,6 +341,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -349,7 +388,7 @@ version = "0.2.0-beta.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"audiopus",
|
"audiopus",
|
||||||
"chanora_protocol",
|
"chanora_protocol",
|
||||||
"coreaudio-rs 0.14.2",
|
"coreaudio-rs",
|
||||||
"cpal",
|
"cpal",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
@@ -362,7 +401,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tsclientlib",
|
"tsclientlib",
|
||||||
"windows",
|
"windows 0.54.0",
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -422,6 +461,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tsclientlib",
|
"tsclientlib",
|
||||||
"tsproto-packets",
|
"tsproto-packets",
|
||||||
|
"tsproto-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -540,20 +580,6 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "coreaudio-rs"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"libc",
|
|
||||||
"objc2-audio-toolbox",
|
|
||||||
"objc2-core-audio",
|
|
||||||
"objc2-core-audio-types",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coreaudio-rs"
|
name = "coreaudio-rs"
|
||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
@@ -570,12 +596,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpal"
|
name = "cpal"
|
||||||
version = "0.16.0"
|
version = "0.17.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
|
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa",
|
"alsa",
|
||||||
"coreaudio-rs 0.13.0",
|
"coreaudio-rs",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -585,13 +611,17 @@ dependencies = [
|
|||||||
"ndk-context",
|
"ndk-context",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"objc2",
|
||||||
"objc2-audio-toolbox",
|
"objc2-audio-toolbox",
|
||||||
|
"objc2-avf-audio",
|
||||||
"objc2-core-audio",
|
"objc2-core-audio",
|
||||||
"objc2-core-audio-types",
|
"objc2-core-audio-types",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -846,6 +876,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dunce"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eax"
|
name = "eax"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1095,6 +1131,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -1214,8 +1256,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"r-efi 5.3.0",
|
||||||
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1226,7 +1284,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi 6.0.0",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasip3",
|
"wasip3",
|
||||||
@@ -1784,6 +1842,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.98"
|
version = "0.3.98"
|
||||||
@@ -1888,10 +1956,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mach2"
|
name = "lru-slab"
|
||||||
version = "0.4.3"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mach2"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -2145,6 +2219,16 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-avf-audio"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-core-audio"
|
name = "objc2-core-audio"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2155,6 +2239,7 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-audio-types",
|
"objc2-core-audio-types",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2174,7 +2259,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
|
"block2",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2190,7 +2277,11 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2475,6 +2566,62 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"lru-slab",
|
||||||
|
"rand 0.9.4",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -2484,6 +2631,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -2497,10 +2650,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -2522,6 +2685,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -2531,6 +2704,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -2619,10 +2801,14 @@ dependencies = [
|
|||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
|
"rustls-platform-verifier",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-rustls",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -2682,6 +2868,12 @@ version = "0.1.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2710,6 +2902,7 @@ version = "0.23.40"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
@@ -2717,21 +2910,62 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-native-certs"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||||
|
dependencies = [
|
||||||
|
"openssl-probe",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"schannel",
|
||||||
|
"security-framework 3.7.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.14.1"
|
version = "1.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-platform-verifier"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation 0.10.1",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"jni 0.22.4",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-platform-verifier-android",
|
||||||
|
"rustls-webpki",
|
||||||
|
"security-framework 3.7.0",
|
||||||
|
"security-framework-sys",
|
||||||
|
"webpki-root-certs",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-platform-verifier-android"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.13"
|
version = "0.103.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
@@ -3872,6 +4106,25 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-root-certs"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "widestring"
|
name = "widestring"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -3893,10 +4146,31 @@ version = "0.54.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.54.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||||
|
dependencies = [
|
||||||
|
"windows-collections",
|
||||||
|
"windows-core 0.62.2",
|
||||||
|
"windows-future",
|
||||||
|
"windows-numerics",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-collections"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.62.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.54.0"
|
version = "0.54.0"
|
||||||
@@ -3907,12 +4181,68 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result 0.4.1",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-future"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.62.2",
|
||||||
|
"windows-link",
|
||||||
|
"windows-threading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-numerics"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.62.2",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -4035,6 +4365,15 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc 0.53.1",
|
"windows_x86_64_msvc 0.53.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-threading"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ GeneratedPluginRegistrant.java
|
|||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
/app/src/main/jniLibs/
|
||||||
|
|||||||
+126
-140
@@ -32,6 +32,48 @@ bool get _isMacOS => !kIsWeb && Platform.isMacOS;
|
|||||||
/// Top padding for macOS to clear traffic-light buttons.
|
/// Top padding for macOS to clear traffic-light buttons.
|
||||||
const double _macOSTrafficLightPad = 56.0;
|
const double _macOSTrafficLightPad = 56.0;
|
||||||
|
|
||||||
|
/// Translate a [LogicalKeyboardKey] into the platform-neutral label
|
||||||
|
/// stored by the PTT binding flow.
|
||||||
|
String? _pttDisplayLabelForKey(LogicalKeyboardKey k) {
|
||||||
|
if (k == LogicalKeyboardKey.space) return 'Space';
|
||||||
|
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
||||||
|
return 'Enter';
|
||||||
|
}
|
||||||
|
if (k == LogicalKeyboardKey.tab) return 'Tab';
|
||||||
|
if (k == LogicalKeyboardKey.escape) return 'Escape';
|
||||||
|
if (k == LogicalKeyboardKey.backspace) return 'Backspace';
|
||||||
|
if (k == LogicalKeyboardKey.delete) return 'Delete';
|
||||||
|
if (k == LogicalKeyboardKey.insert) return 'Insert';
|
||||||
|
if (k == LogicalKeyboardKey.home) return 'Home';
|
||||||
|
if (k == LogicalKeyboardKey.end) return 'End';
|
||||||
|
if (k == LogicalKeyboardKey.pageUp) return 'Page Up';
|
||||||
|
if (k == LogicalKeyboardKey.pageDown) return 'Page Down';
|
||||||
|
if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up';
|
||||||
|
if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down';
|
||||||
|
if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left';
|
||||||
|
if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right';
|
||||||
|
if (k == LogicalKeyboardKey.shift ||
|
||||||
|
k == LogicalKeyboardKey.shiftLeft ||
|
||||||
|
k == LogicalKeyboardKey.shiftRight ||
|
||||||
|
k == LogicalKeyboardKey.control ||
|
||||||
|
k == LogicalKeyboardKey.controlLeft ||
|
||||||
|
k == LogicalKeyboardKey.controlRight ||
|
||||||
|
k == LogicalKeyboardKey.alt ||
|
||||||
|
k == LogicalKeyboardKey.altLeft ||
|
||||||
|
k == LogicalKeyboardKey.altRight ||
|
||||||
|
k == LogicalKeyboardKey.meta ||
|
||||||
|
k == LogicalKeyboardKey.metaLeft ||
|
||||||
|
k == LogicalKeyboardKey.metaRight ||
|
||||||
|
k == LogicalKeyboardKey.capsLock ||
|
||||||
|
k == LogicalKeyboardKey.numLock ||
|
||||||
|
k == LogicalKeyboardKey.scrollLock) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final fallback = k.keyLabel.trim();
|
||||||
|
if (fallback.isEmpty) return null;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
/// True when the host is a touch-only mobile platform without a
|
/// True when the host is a touch-only mobile platform without a
|
||||||
/// hardware keyboard. Mirrors the helpers in widgets/voice_bar.dart
|
/// hardware keyboard. Mirrors the helpers in widgets/voice_bar.dart
|
||||||
/// and widgets/voice_settings.dart so the AppBar + narrow-mode
|
/// and widgets/voice_settings.dart so the AppBar + narrow-mode
|
||||||
@@ -241,17 +283,50 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
// only the platform-neutral label that already crossed into
|
// only the platform-neutral label that already crossed into
|
||||||
// the Rust side.
|
// the Rust side.
|
||||||
String _pttBoundKeyLabel = '';
|
String _pttBoundKeyLabel = '';
|
||||||
|
final Set<LogicalKeyboardKey> _focusedPttHeldKeys = <LogicalKeyboardKey>{};
|
||||||
|
|
||||||
List<rust.BridgeBookmark> _bookmarks = const [];
|
List<rust.BridgeBookmark> _bookmarks = const [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
HardwareKeyboard.instance.addHandler(_handleFocusedPttKey);
|
||||||
_eventsSub = rust.eventsStream().listen(_onEvent);
|
_eventsSub = rust.eventsStream().listen(_onEvent);
|
||||||
unawaited(_reloadBookmarks());
|
unawaited(_reloadBookmarks());
|
||||||
unawaited(_hydratePttBinding());
|
unawaited(_hydratePttBinding());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _handleFocusedPttKey(KeyEvent event) {
|
||||||
|
final label = _pttDisplayLabelForKey(event.logicalKey);
|
||||||
|
final isBoundKey = _pttBoundKeyLabel.isNotEmpty && label == _pttBoundKeyLabel;
|
||||||
|
if (event is KeyUpEvent && _focusedPttHeldKeys.contains(event.logicalKey)) {
|
||||||
|
if (_focusedPttHeldKeys.remove(event.logicalKey)) {
|
||||||
|
unawaited(_setPtt(false));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_pttBackendId != 'focused' ||
|
||||||
|
_phase != _Phase.connected ||
|
||||||
|
!_inChannel ||
|
||||||
|
_transmitMode != rust.BridgeTransmitMode.ptt ||
|
||||||
|
!isBoundKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
if (_focusedPttHeldKeys.add(event.logicalKey)) {
|
||||||
|
unawaited(_setPtt(true));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _releaseFocusedPttIfHeld() {
|
||||||
|
if (_focusedPttHeldKeys.isEmpty) return;
|
||||||
|
_focusedPttHeldKeys.clear();
|
||||||
|
unawaited(_setPtt(false, reportError: false));
|
||||||
|
}
|
||||||
|
|
||||||
/// Hydrate the bound-key display state from the bridge so the
|
/// Hydrate the bound-key display state from the bridge so the
|
||||||
/// Voice Bar shows the user's persisted hotkey label immediately
|
/// Voice Bar shows the user's persisted hotkey label immediately
|
||||||
/// at launch — without having to wait for them to open the
|
/// at launch — without having to wait for them to open the
|
||||||
@@ -326,6 +401,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
_pttBackendId = backendId;
|
_pttBackendId = backendId;
|
||||||
_pttBoundInputClass = boundInputClass;
|
_pttBoundInputClass = boundInputClass;
|
||||||
});
|
});
|
||||||
|
if (backendId != 'focused') {
|
||||||
|
_releaseFocusedPttIfHeld();
|
||||||
|
}
|
||||||
case rust.BridgeEvent_VoiceState(
|
case rust.BridgeEvent_VoiceState(
|
||||||
:final inChannel,
|
:final inChannel,
|
||||||
:final transmitMode,
|
:final transmitMode,
|
||||||
@@ -341,9 +419,15 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
});
|
});
|
||||||
if (inChannel) {
|
if (inChannel) {
|
||||||
_ensureStatsTimer();
|
_ensureStatsTimer();
|
||||||
|
unawaited(_onRefresh());
|
||||||
} else {
|
} else {
|
||||||
_statsTimer?.cancel();
|
_statsTimer?.cancel();
|
||||||
_statsTimer = null;
|
_statsTimer = null;
|
||||||
|
_releaseFocusedPttIfHeld();
|
||||||
|
setState(() => _currentVoiceChannelId = null);
|
||||||
|
}
|
||||||
|
if (transmitMode != rust.BridgeTransmitMode.ptt) {
|
||||||
|
_releaseFocusedPttIfHeld();
|
||||||
}
|
}
|
||||||
case rust.BridgeEvent_InterruptionState(
|
case rust.BridgeEvent_InterruptionState(
|
||||||
:final began,
|
:final began,
|
||||||
@@ -403,6 +487,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
|
||||||
_eventsSub?.cancel();
|
_eventsSub?.cancel();
|
||||||
_statsTimer?.cancel();
|
_statsTimer?.cancel();
|
||||||
_hostCtl.dispose();
|
_hostCtl.dispose();
|
||||||
@@ -421,6 +506,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
_error = null;
|
_error = null;
|
||||||
_snapshot = null;
|
_snapshot = null;
|
||||||
});
|
});
|
||||||
|
_releaseFocusedPttIfHeld();
|
||||||
try {
|
try {
|
||||||
final snap = await rust.connect(
|
final snap = await rust.connect(
|
||||||
host: (host ?? _hostCtl.text).trim(),
|
host: (host ?? _hostCtl.text).trim(),
|
||||||
@@ -430,7 +516,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_phase = _Phase.connected;
|
_phase = _Phase.connected;
|
||||||
_snapshot = snap;
|
_applySnapshot(snap);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -483,11 +569,11 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
Future<void> _setPtt(bool active) async {
|
Future<void> _setPtt(bool active, {bool reportError = true}) async {
|
||||||
try {
|
try {
|
||||||
await rust.setPtt(active: active);
|
await rust.setPtt(active: active);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted || !reportError) return;
|
||||||
setState(() => _error = e.toString());
|
setState(() => _error = e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,6 +805,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
}
|
}
|
||||||
if (result.bindKeyRequested && mounted) {
|
if (result.bindKeyRequested && mounted) {
|
||||||
await _onConfigurePtt(context);
|
await _onConfigurePtt(context);
|
||||||
|
if (mounted) {
|
||||||
|
unawaited(_onOpenVoiceSettings());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,7 +830,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
try {
|
try {
|
||||||
final snap = await rust.snapshot();
|
final snap = await rust.snapshot();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _snapshot = snap);
|
setState(() => _applySnapshot(snap));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _error = e.toString());
|
setState(() => _error = e.toString());
|
||||||
@@ -766,6 +855,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
_inChannel = false;
|
_inChannel = false;
|
||||||
_currentVoiceChannelId = null;
|
_currentVoiceChannelId = null;
|
||||||
});
|
});
|
||||||
|
_releaseFocusedPttIfHeld();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _currentVoiceChannelName() {
|
String _currentVoiceChannelName() {
|
||||||
@@ -778,6 +868,21 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applySnapshot(rust.BridgeSnapshot snap) {
|
||||||
|
_snapshot = snap;
|
||||||
|
if (!_inChannel) {
|
||||||
|
_currentVoiceChannelId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final client in snap.clients) {
|
||||||
|
if (client.id == snap.ownClientId) {
|
||||||
|
_currentVoiceChannelId = client.channel;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_currentVoiceChannelId = null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onShowDiagnostics(BuildContext context) async {
|
Future<void> _onShowDiagnostics(BuildContext context) async {
|
||||||
final l10n = AppL10n.of(context);
|
final l10n = AppL10n.of(context);
|
||||||
final text = rust.exportDiagnostics();
|
final text = rust.exportDiagnostics();
|
||||||
@@ -1045,11 +1150,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
final headerTitle = _AppBarTitle(
|
const headerTitle = _AppBarTitle();
|
||||||
phase: _phase,
|
|
||||||
inChannel: _inChannel,
|
|
||||||
channelName: _currentVoiceChannelName(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final bodyContent = LayoutBuilder(
|
final bodyContent = LayoutBuilder(
|
||||||
builder: (ctx, bodyConstraints) {
|
builder: (ctx, bodyConstraints) {
|
||||||
@@ -1260,81 +1361,16 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AppBar title that shows just the app name when idle / connecting,
|
/// App header title. The current channel is highlighted only in the
|
||||||
/// and 'app name · #channel' (channel as an outlined chip-style
|
/// channel tree and Voice Bar to avoid repeating the same badge in
|
||||||
/// pill) when the user is in a voice channel. The chip is read-
|
/// multiple places.
|
||||||
/// only — tapping does nothing because the source of truth for
|
|
||||||
/// "current channel" is the channel tree below. Long names ellipsize.
|
|
||||||
class _AppBarTitle extends StatelessWidget {
|
class _AppBarTitle extends StatelessWidget {
|
||||||
const _AppBarTitle({
|
const _AppBarTitle();
|
||||||
required this.phase,
|
|
||||||
required this.inChannel,
|
|
||||||
required this.channelName,
|
|
||||||
});
|
|
||||||
|
|
||||||
final _Phase phase;
|
|
||||||
final bool inChannel;
|
|
||||||
final String channelName;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppL10n.of(context);
|
final l10n = AppL10n.of(context);
|
||||||
final theme = Theme.of(context);
|
return Text(l10n.appTitle);
|
||||||
if (phase != _Phase.connected || !inChannel || channelName.isEmpty) {
|
|
||||||
return Text(l10n.appTitle);
|
|
||||||
}
|
|
||||||
// When in a voice channel on a narrow phone width, the AppBar
|
|
||||||
// is already crowded with mic / headset / settings / about /
|
|
||||||
// diagnostics / disconnect icons (5-6 action buttons). Keeping
|
|
||||||
// the "Chanora" app-name label in the title here causes the
|
|
||||||
// Row to overflow at typical iPhone widths and Flutter renders
|
|
||||||
// its yellow-and-black "OVERFLOWED BY X PIXELS" debug strip
|
|
||||||
// next to the title — the user reported seeing "RFLOWED BY"
|
|
||||||
// there. Drop the app-name label on narrow widths and let the
|
|
||||||
// channel pill be the only title content; the user knows
|
|
||||||
// they're in Chanora because they just opened it. On wide
|
|
||||||
// widths (>= 840 dp, tablet/desktop) restore the app name
|
|
||||||
// because there's plenty of room.
|
|
||||||
final isNarrow = MediaQuery.of(context).size.width < 840.0;
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (!isNarrow) ...[Text(l10n.appTitle), const SizedBox(width: 12)],
|
|
||||||
Flexible(
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.tag,
|
|
||||||
size: 14,
|
|
||||||
color: theme.colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
channelName,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
softWrap: false,
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.onPrimaryContainer,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1995,8 +2031,16 @@ class _SnapshotView extends StatelessWidget {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
leading: const Icon(Icons.person, size: 18),
|
leading: Icon(
|
||||||
title: Text(cl.name),
|
cl.isServerQuery ? Icons.terminal : Icons.person,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
cl.name,
|
||||||
|
style: cl.isServerQuery
|
||||||
|
? TextStyle(color: theme.colorScheme.onSurfaceVariant)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2049,7 +2093,7 @@ class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> {
|
|||||||
|
|
||||||
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
||||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||||
final label = _displayLabelForKey(event.logicalKey);
|
final label = _pttDisplayLabelForKey(event.logicalKey);
|
||||||
if (label == null) return KeyEventResult.ignored;
|
if (label == null) return KeyEventResult.ignored;
|
||||||
setState(() {
|
setState(() {
|
||||||
_captured = label;
|
_captured = label;
|
||||||
@@ -2058,64 +2102,6 @@ class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> {
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Translate a [LogicalKeyboardKey] into the platform-neutral
|
|
||||||
/// label string the bridge expects (matching the entries in
|
|
||||||
/// `crates/chanora_audio/src/ptt_backends/windows_keymap.rs`).
|
|
||||||
///
|
|
||||||
/// `LogicalKeyboardKey.keyLabel` returns `" "` for Space, empty
|
|
||||||
/// for pure modifiers (shift / ctrl / alt / meta), and localised
|
|
||||||
/// strings for some special keys; we normalise to the canonical
|
|
||||||
/// English label so the badge displays something readable AND so
|
|
||||||
/// the Windows backend's keymap can resolve to a `VK_*`. Pure
|
|
||||||
/// modifier keys are intentionally rejected — chording into the
|
|
||||||
/// real binding (e.g. Ctrl+Shift+M) is supported by ignoring the
|
|
||||||
/// individual modifier-down events.
|
|
||||||
String? _displayLabelForKey(LogicalKeyboardKey k) {
|
|
||||||
// Whitespace / common control keys whose keyLabel is unhelpful.
|
|
||||||
if (k == LogicalKeyboardKey.space) return 'Space';
|
|
||||||
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
|
||||||
return 'Enter';
|
|
||||||
}
|
|
||||||
if (k == LogicalKeyboardKey.tab) return 'Tab';
|
|
||||||
if (k == LogicalKeyboardKey.escape) return 'Escape';
|
|
||||||
if (k == LogicalKeyboardKey.backspace) return 'Backspace';
|
|
||||||
if (k == LogicalKeyboardKey.delete) return 'Delete';
|
|
||||||
if (k == LogicalKeyboardKey.insert) return 'Insert';
|
|
||||||
if (k == LogicalKeyboardKey.home) return 'Home';
|
|
||||||
if (k == LogicalKeyboardKey.end) return 'End';
|
|
||||||
if (k == LogicalKeyboardKey.pageUp) return 'Page Up';
|
|
||||||
if (k == LogicalKeyboardKey.pageDown) return 'Page Down';
|
|
||||||
if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up';
|
|
||||||
if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down';
|
|
||||||
if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left';
|
|
||||||
if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right';
|
|
||||||
// Pure modifier keys are not bindable on their own (user can
|
|
||||||
// still chord by pressing a non-modifier while holding them).
|
|
||||||
if (k == LogicalKeyboardKey.shift ||
|
|
||||||
k == LogicalKeyboardKey.shiftLeft ||
|
|
||||||
k == LogicalKeyboardKey.shiftRight ||
|
|
||||||
k == LogicalKeyboardKey.control ||
|
|
||||||
k == LogicalKeyboardKey.controlLeft ||
|
|
||||||
k == LogicalKeyboardKey.controlRight ||
|
|
||||||
k == LogicalKeyboardKey.alt ||
|
|
||||||
k == LogicalKeyboardKey.altLeft ||
|
|
||||||
k == LogicalKeyboardKey.altRight ||
|
|
||||||
k == LogicalKeyboardKey.meta ||
|
|
||||||
k == LogicalKeyboardKey.metaLeft ||
|
|
||||||
k == LogicalKeyboardKey.metaRight ||
|
|
||||||
k == LogicalKeyboardKey.capsLock ||
|
|
||||||
k == LogicalKeyboardKey.numLock ||
|
|
||||||
k == LogicalKeyboardKey.scrollLock) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Fall back to keyLabel for letters, digits, function keys,
|
|
||||||
// numpad digits, and punctuation. Trim whitespace as a final
|
|
||||||
// belt-and-braces guard.
|
|
||||||
final fallback = k.keyLabel.trim();
|
|
||||||
if (fallback.isEmpty) return null;
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _captureMouseSideButton(int button) {
|
void _captureMouseSideButton(int button) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_captured = 'mouse-side-button:$button';
|
_captured = 'mouse-side-button:$button';
|
||||||
|
|||||||
@@ -338,14 +338,19 @@ class BridgeClient {
|
|||||||
/// Nickname.
|
/// Nickname.
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
|
/// True for TeamSpeak ServerQuery clients.
|
||||||
|
final bool isServerQuery;
|
||||||
|
|
||||||
const BridgeClient({
|
const BridgeClient({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.channel,
|
required this.channel,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.isServerQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode ^ channel.hashCode ^ name.hashCode;
|
int get hashCode =>
|
||||||
|
id.hashCode ^ channel.hashCode ^ name.hashCode ^ isServerQuery.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -354,7 +359,8 @@ class BridgeClient {
|
|||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
id == other.id &&
|
id == other.id &&
|
||||||
channel == other.channel &&
|
channel == other.channel &&
|
||||||
name == other.name;
|
name == other.name &&
|
||||||
|
isServerQuery == other.isServerQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
|||||||
@@ -1157,12 +1157,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
BridgeClient dco_decode_bridge_client(dynamic raw) {
|
BridgeClient dco_decode_bridge_client(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
final arr = raw as List<dynamic>;
|
final arr = raw as List<dynamic>;
|
||||||
if (arr.length != 3)
|
if (arr.length != 4)
|
||||||
throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||||
return BridgeClient(
|
return BridgeClient(
|
||||||
id: dco_decode_u_64(arr[0]),
|
id: dco_decode_u_64(arr[0]),
|
||||||
channel: dco_decode_u_64(arr[1]),
|
channel: dco_decode_u_64(arr[1]),
|
||||||
name: dco_decode_String(arr[2]),
|
name: dco_decode_String(arr[2]),
|
||||||
|
isServerQuery: dco_decode_bool(arr[3]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1454,7 +1455,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
var var_id = sse_decode_u_64(deserializer);
|
var var_id = sse_decode_u_64(deserializer);
|
||||||
var var_channel = sse_decode_u_64(deserializer);
|
var var_channel = sse_decode_u_64(deserializer);
|
||||||
var var_name = sse_decode_String(deserializer);
|
var var_name = sse_decode_String(deserializer);
|
||||||
return BridgeClient(id: var_id, channel: var_channel, name: var_name);
|
var var_isServerQuery = sse_decode_bool(deserializer);
|
||||||
|
return BridgeClient(
|
||||||
|
id: var_id,
|
||||||
|
channel: var_channel,
|
||||||
|
name: var_name,
|
||||||
|
isServerQuery: var_isServerQuery,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
@@ -1799,6 +1806,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_u_64(self.id, serializer);
|
sse_encode_u_64(self.id, serializer);
|
||||||
sse_encode_u_64(self.channel, serializer);
|
sse_encode_u_64(self.channel, serializer);
|
||||||
sse_encode_String(self.name, serializer);
|
sse_encode_String(self.name, serializer);
|
||||||
|
sse_encode_bool(self.isServerQuery, serializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ static void first_frame_cb(MyApplication* self, FlView* view) {
|
|||||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void window_destroy_cb(GtkWidget* widget, gpointer user_data) {
|
||||||
|
g_application_quit(G_APPLICATION(user_data));
|
||||||
|
}
|
||||||
|
|
||||||
// Implements GApplication::activate.
|
// Implements GApplication::activate.
|
||||||
static void my_application_activate(GApplication* application) {
|
static void my_application_activate(GApplication* application) {
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
@@ -45,13 +49,15 @@ static void my_application_activate(GApplication* application) {
|
|||||||
if (use_header_bar) {
|
if (use_header_bar) {
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
gtk_header_bar_set_title(header_bar, "chanora_flutter");
|
gtk_header_bar_set_title(header_bar, "Chanora");
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
} else {
|
} else {
|
||||||
gtk_window_set_title(window, "chanora_flutter");
|
gtk_window_set_title(window, "Chanora");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g_signal_connect(window, "destroy", G_CALLBACK(window_destroy_cb), application);
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|
||||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
|||||||
@@ -1735,6 +1735,7 @@ mod tests {
|
|||||||
id: chanora_protocol::ClientId(10),
|
id: chanora_protocol::ClientId(10),
|
||||||
channel: chanora_protocol::ChannelId(1),
|
channel: chanora_protocol::ChannelId(1),
|
||||||
name: "u".into(),
|
name: "u".into(),
|
||||||
|
is_server_query: false,
|
||||||
}],
|
}],
|
||||||
own_client_id: 10,
|
own_client_id: 10,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ thiserror.workspace = true
|
|||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|
||||||
# Cross-platform audio I/O (DEC-011.1).
|
# Cross-platform audio I/O (DEC-011.1).
|
||||||
cpal = "0.16"
|
cpal = "0.17.3"
|
||||||
# Opus encoder. tsclientlib already pulls this; we depend explicitly so
|
# Opus encoder. tsclientlib already pulls this; we depend explicitly so
|
||||||
# this crate can compile against it without going through tsclientlib.
|
# this crate can compile against it without going through tsclientlib.
|
||||||
audiopus = "0.3.0-rc.0"
|
audiopus = "0.3.0-rc.0"
|
||||||
@@ -24,19 +24,13 @@ audiopus = "0.3.0-rc.0"
|
|||||||
# feature. We import the crate just for the AudioHandler type; the
|
# feature. We import the crate just for the AudioHandler type; the
|
||||||
# Connection type stays inside chanora_protocol.
|
# Connection type stays inside chanora_protocol.
|
||||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
||||||
# Force the reqwest TLS backend to native-tls (Security.framework on
|
|
||||||
# Apple, SChannel on Windows, system OpenSSL on Linux/BSD) instead of
|
|
||||||
# the rustls + aws-lc-rs combination tsclientlib's `default-tls`
|
|
||||||
# feature would otherwise pick. aws-lc-sys does not cross-compile
|
|
||||||
# cleanly to aarch64-apple-ios, and native-tls is the standard
|
|
||||||
# answer for "TLS that just works on every desktop+mobile platform
|
|
||||||
# without a vendored C dependency". Cargo's feature unification means
|
|
||||||
# this single direct dep applies to the transitive
|
|
||||||
# tsclientlib -> reqwest chain too.
|
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
|
# Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework
|
||||||
|
# on Apple, SChannel on Windows, system OpenSSL on Linux/BSD).
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
# Direct CoreAudio AudioUnit access on iOS (DEC-011.x follow-up).
|
# Direct CoreAudio AudioUnit access on iOS (DEC-011.x follow-up).
|
||||||
# cpal's iOS backend is unsuitable for VoIP: it opens
|
# cpal's iOS backend is unsuitable for VoIP: it opens
|
||||||
@@ -59,6 +53,9 @@ tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
|||||||
coreaudio-rs = "0.14"
|
coreaudio-rs = "0.14"
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
|
||||||
|
# native-tls for Apple targets where aws-lc/rustls is problematic for iOS.
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] }
|
||||||
# JNI bindings to flip Android's AudioManager into MODE_IN_COMMUNICATION
|
# JNI bindings to flip Android's AudioManager into MODE_IN_COMMUNICATION
|
||||||
# when the voice-comm preset is requested. ndk_context is initialised
|
# when the voice-comm preset is requested. ndk_context is initialised
|
||||||
# by the bridge crate's android_init shim.
|
# by the bridge crate's android_init shim.
|
||||||
|
|||||||
@@ -251,8 +251,8 @@ impl AudioEngine {
|
|||||||
|
|
||||||
info!(
|
info!(
|
||||||
target: "chanora_audio",
|
target: "chanora_audio",
|
||||||
in_device = %in_dev.name().unwrap_or_default(),
|
in_device = %in_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||||
out_device = %out_dev.name().unwrap_or_default(),
|
out_device = %out_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||||
"starting audio engine"
|
"starting audio engine"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ impl AudioEngine {
|
|||||||
Ok(c) => info!(
|
Ok(c) => info!(
|
||||||
target: "chanora_audio",
|
target: "chanora_audio",
|
||||||
channels = c.channels(),
|
channels = c.channels(),
|
||||||
sample_rate = c.sample_rate().0,
|
sample_rate = c.sample_rate(),
|
||||||
sample_format = ?c.sample_format(),
|
sample_format = ?c.sample_format(),
|
||||||
"default_input_config reported"
|
"default_input_config reported"
|
||||||
),
|
),
|
||||||
@@ -284,7 +284,7 @@ impl AudioEngine {
|
|||||||
Ok(c) => info!(
|
Ok(c) => info!(
|
||||||
target: "chanora_audio",
|
target: "chanora_audio",
|
||||||
channels = c.channels(),
|
channels = c.channels(),
|
||||||
sample_rate = c.sample_rate().0,
|
sample_rate = c.sample_rate(),
|
||||||
sample_format = ?c.sample_format(),
|
sample_format = ?c.sample_format(),
|
||||||
"default_output_config reported"
|
"default_output_config reported"
|
||||||
),
|
),
|
||||||
@@ -409,7 +409,7 @@ impl AudioEngine {
|
|||||||
.default_output_config()
|
.default_output_config()
|
||||||
.map_err(|e| AudioError::StreamConfig(format!("output default: {e}")))?;
|
.map_err(|e| AudioError::StreamConfig(format!("output default: {e}")))?;
|
||||||
let out_format = out_cfg.sample_format();
|
let out_format = out_cfg.sample_format();
|
||||||
let dev_sample_rate = out_cfg.sample_rate().0;
|
let dev_sample_rate = out_cfg.sample_rate();
|
||||||
let dev_channels = out_cfg.channels() as usize;
|
let dev_channels = out_cfg.channels() as usize;
|
||||||
// Buffer-size rationale:
|
// Buffer-size rationale:
|
||||||
// * Windows (WASAPI via cpal): the default period is
|
// * Windows (WASAPI via cpal): the default period is
|
||||||
@@ -840,7 +840,7 @@ fn try_open_capture(
|
|||||||
let in_cfg = in_dev
|
let in_cfg = in_dev
|
||||||
.default_input_config()
|
.default_input_config()
|
||||||
.map_err(|e| AudioError::StreamConfig(format!("input default: {e}")))?;
|
.map_err(|e| AudioError::StreamConfig(format!("input default: {e}")))?;
|
||||||
let in_sample_rate = in_cfg.sample_rate().0;
|
let in_sample_rate = in_cfg.sample_rate();
|
||||||
let in_channels = in_cfg.channels() as usize;
|
let in_channels = in_cfg.channels() as usize;
|
||||||
let in_format = in_cfg.sample_format();
|
let in_format = in_cfg.sample_format();
|
||||||
// Buffer-size rationale (same shape as the output path):
|
// Buffer-size rationale (same shape as the output path):
|
||||||
@@ -1339,7 +1339,7 @@ where
|
|||||||
target: "chanora_audio",
|
target: "chanora_audio",
|
||||||
error = %e,
|
error = %e,
|
||||||
requested_channels = config.channels,
|
requested_channels = config.channels,
|
||||||
requested_sample_rate = config.sample_rate.0,
|
requested_sample_rate = config.sample_rate,
|
||||||
"build_output_stream FAILED"
|
"build_output_stream FAILED"
|
||||||
);
|
);
|
||||||
AudioError::Backend(format!("build_output_stream: {e}"))
|
AudioError::Backend(format!("build_output_stream: {e}"))
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ fn log_file_path() -> Option<std::path::PathBuf> {
|
|||||||
/// Open the platform log file in append mode, rotating once on
|
/// Open the platform log file in append mode, rotating once on
|
||||||
/// startup so each launch begins with a fresh file. Best-effort:
|
/// startup so each launch begins with a fresh file. Best-effort:
|
||||||
/// returns `None` on any I/O error.
|
/// returns `None` on any I/O error.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
fn open_log_file() -> Option<std::fs::File> {
|
fn open_log_file() -> Option<std::fs::File> {
|
||||||
let path = log_file_path()?;
|
let path = log_file_path()?;
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
@@ -278,6 +279,8 @@ pub struct BridgeClient {
|
|||||||
pub channel: u64,
|
pub channel: u64,
|
||||||
/// Nickname.
|
/// Nickname.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// True for TeamSpeak ServerQuery clients.
|
||||||
|
pub is_server_query: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Server snapshot as seen by Dart.
|
/// Server snapshot as seen by Dart.
|
||||||
@@ -325,6 +328,7 @@ impl From<chanora_protocol::ServerSnapshot> for BridgeSnapshot {
|
|||||||
id: c.id.0,
|
id: c.id.0,
|
||||||
channel: c.channel.0,
|
channel: c.channel.0,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
|
is_server_query: c.is_server_query,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
own_client_id: s.own_client_id,
|
own_client_id: s.own_client_id,
|
||||||
|
|||||||
@@ -1290,10 +1290,12 @@ impl SseDecode for crate::api::BridgeClient {
|
|||||||
let mut var_id = <u64>::sse_decode(deserializer);
|
let mut var_id = <u64>::sse_decode(deserializer);
|
||||||
let mut var_channel = <u64>::sse_decode(deserializer);
|
let mut var_channel = <u64>::sse_decode(deserializer);
|
||||||
let mut var_name = <String>::sse_decode(deserializer);
|
let mut var_name = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_isServerQuery = <bool>::sse_decode(deserializer);
|
||||||
return crate::api::BridgeClient {
|
return crate::api::BridgeClient {
|
||||||
id: var_id,
|
id: var_id,
|
||||||
channel: var_channel,
|
channel: var_channel,
|
||||||
name: var_name,
|
name: var_name,
|
||||||
|
is_server_query: var_isServerQuery,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1721,6 +1723,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeClient {
|
|||||||
self.id.into_into_dart().into_dart(),
|
self.id.into_into_dart().into_dart(),
|
||||||
self.channel.into_into_dart().into_dart(),
|
self.channel.into_into_dart().into_dart(),
|
||||||
self.name.into_into_dart().into_dart(),
|
self.name.into_into_dart().into_dart(),
|
||||||
|
self.is_server_query.into_into_dart().into_dart(),
|
||||||
]
|
]
|
||||||
.into_dart()
|
.into_dart()
|
||||||
}
|
}
|
||||||
@@ -1999,6 +2002,7 @@ impl SseEncode for crate::api::BridgeClient {
|
|||||||
<u64>::sse_encode(self.id, serializer);
|
<u64>::sse_encode(self.id, serializer);
|
||||||
<u64>::sse_encode(self.channel, serializer);
|
<u64>::sse_encode(self.channel, serializer);
|
||||||
<String>::sse_encode(self.name, serializer);
|
<String>::sse_encode(self.name, serializer);
|
||||||
|
<bool>::sse_encode(self.is_server_query, serializer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,23 @@ once_cell = "1"
|
|||||||
# pulls in `audiopus` only — `sdl2` is a dev-dep used by upstream
|
# pulls in `audiopus` only — `sdl2` is a dev-dep used by upstream
|
||||||
# examples; the library itself does not link SDL.
|
# examples; the library itself does not link SDL.
|
||||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
||||||
# Force the reqwest TLS backend to native-tls (Security.framework on
|
|
||||||
# Apple, SChannel on Windows, system OpenSSL on Linux/BSD) instead of
|
|
||||||
# the rustls + aws-lc-rs combination tsclientlib's `default-tls`
|
|
||||||
# feature would otherwise pick. aws-lc-sys does not cross-compile
|
|
||||||
# cleanly to aarch64-apple-ios, and native-tls is the standard
|
|
||||||
# answer for "TLS that just works on every desktop+mobile platform
|
|
||||||
# without a vendored C dependency". Cargo's feature unification means
|
|
||||||
# this single direct dep applies to the transitive
|
|
||||||
# tsclientlib -> reqwest chain too.
|
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
|
||||||
|
|
||||||
# tsproto_packets exposes OutAudio / InAudioBuf / AudioData /
|
# tsproto_packets exposes OutAudio / InAudioBuf / AudioData /
|
||||||
# CodecType / Direction. Pinning to the same git rev as tsclientlib
|
# CodecType / Direction. Pinning to the same git rev as tsclientlib
|
||||||
# avoids any version-skew confusion.
|
# avoids any version-skew confusion.
|
||||||
tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491" }
|
tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491" }
|
||||||
|
tsproto-types = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491" }
|
||||||
|
|
||||||
# Async runtime utilities used by the connection task.
|
# Async runtime utilities used by the connection task.
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
|
# Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework
|
||||||
|
# on Apple, SChannel on Windows, system OpenSSL on Linux/BSD).
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
|
||||||
|
# native-tls for Apple targets where aws-lc/rustls is problematic for iOS.
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] }
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use tsclientlib::{
|
|||||||
OutCommandExt, StreamItem, Version,
|
OutCommandExt, StreamItem, Version,
|
||||||
};
|
};
|
||||||
use tsproto_packets::packets::{InAudioBuf, OutPacket};
|
use tsproto_packets::packets::{InAudioBuf, OutPacket};
|
||||||
|
use tsproto_types::ClientType;
|
||||||
|
|
||||||
use crate::dto::{ChannelId, ChannelInfo, ClientId, ClientInfo, ServerSnapshot};
|
use crate::dto::{ChannelId, ChannelInfo, ClientId, ClientInfo, ServerSnapshot};
|
||||||
use crate::ProtocolError;
|
use crate::ProtocolError;
|
||||||
@@ -872,6 +873,7 @@ fn build_snapshot(con: &Connection) -> Result<ServerSnapshot, ProtocolError> {
|
|||||||
id: ClientId(c.id.0 as u64),
|
id: ClientId(c.id.0 as u64),
|
||||||
channel: ChannelId(c.channel.0),
|
channel: ChannelId(c.channel.0),
|
||||||
name: sanitize(&c.name),
|
name: sanitize(&c.name),
|
||||||
|
is_server_query: is_server_query_client_type(&c.client_type),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -886,6 +888,10 @@ fn build_snapshot(con: &Connection) -> Result<ServerSnapshot, ProtocolError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_server_query_client_type(client_type: &ClientType) -> bool {
|
||||||
|
matches!(client_type, ClientType::Query { .. })
|
||||||
|
}
|
||||||
|
|
||||||
/// Light sanitisation of strings before they cross the protocol
|
/// Light sanitisation of strings before they cross the protocol
|
||||||
/// boundary. The redaction policy proper lives in
|
/// boundary. The redaction policy proper lives in
|
||||||
/// `chanora_diagnostics`; this filter only strips control characters
|
/// `chanora_diagnostics`; this filter only strips control characters
|
||||||
@@ -905,7 +911,8 @@ const _ROOT_MATCHES_UPSTREAM: () = {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::sort_channels_tree_by;
|
use super::{is_server_query_client_type, sort_channels_tree_by};
|
||||||
|
use tsproto_types::ClientType;
|
||||||
|
|
||||||
/// Lightweight fixture mirroring just the (id, parent, order)
|
/// Lightweight fixture mirroring just the (id, parent, order)
|
||||||
/// triple that the linked-list sort needs. Avoids constructing
|
/// triple that the linked-list sort needs. Avoids constructing
|
||||||
@@ -922,6 +929,13 @@ mod tests {
|
|||||||
(c.id, c.parent, c.order)
|
(c.id, c.parent, c.order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_client_type_maps_to_server_query_flag() {
|
||||||
|
assert!(!is_server_query_client_type(&ClientType::Normal));
|
||||||
|
assert!(is_server_query_client_type(&ClientType::Query { admin: false }));
|
||||||
|
assert!(is_server_query_client_type(&ClientType::Query { admin: true }));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn channel_sort_linked_list_under_one_parent() {
|
fn channel_sort_linked_list_under_one_parent() {
|
||||||
// Server emits four root-level channels in arbitrary HashMap
|
// Server emits four root-level channels in arbitrary HashMap
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ pub struct ClientInfo {
|
|||||||
pub channel: ChannelId,
|
pub channel: ChannelId,
|
||||||
/// Nickname, preserved verbatim per ADR-008.
|
/// Nickname, preserved verbatim per ADR-008.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// True for TeamSpeak ServerQuery clients.
|
||||||
|
pub is_server_query: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot of the server's published state at a moment in time.
|
/// Snapshot of the server's published state at a moment in time.
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ pub struct IdentityFileStore {
|
|||||||
/// Stable per-install identifier used as the keyring account
|
/// Stable per-install identifier used as the keyring account
|
||||||
/// name. Derived from the install directory so the same store
|
/// name. Derived from the install directory so the same store
|
||||||
/// finds the same keyring entry across restarts.
|
/// finds the same keyring entry across restarts.
|
||||||
|
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||||
keyring_account: String,
|
keyring_account: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
REPO_ROOT="$(pwd)"
|
||||||
|
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-}"
|
||||||
|
if [[ -z "$NDK_ROOT" && -n "${ANDROID_HOME:-}" ]]; then
|
||||||
|
NDK_ROOT="$(find "$ANDROID_HOME/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$NDK_ROOT" && -n "${ANDROID_SDK_ROOT:-}" ]]; then
|
||||||
|
NDK_ROOT="$(find "$ANDROID_SDK_ROOT/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ANDROID_PLATFORM=28
|
||||||
|
JNI_LIBS_DIR="$REPO_ROOT/apps/chanora_flutter/android/app/src/main/jniLibs"
|
||||||
|
|
||||||
|
declare -a BUILDS=(
|
||||||
|
"arm64-v8a;aarch64-linux-android;/tmp/opus-android-arm64-v8a"
|
||||||
|
"armeabi-v7a;armv7-linux-androideabi;/tmp/opus-android-armeabi-v7a"
|
||||||
|
"x86_64;x86_64-linux-android;/tmp/opus-android-x86_64"
|
||||||
|
)
|
||||||
|
|
||||||
|
bar() { printf '%s\n' '========================================================================'; }
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
local cmd=$1
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: '$cmd' is required but not on PATH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
bar
|
||||||
|
echo "Chanora Android Rust build"
|
||||||
|
echo "Repo root : $REPO_ROOT"
|
||||||
|
echo "NDK : $NDK_ROOT"
|
||||||
|
echo "Android platform: $ANDROID_PLATFORM"
|
||||||
|
bar
|
||||||
|
|
||||||
|
require_cmd cargo
|
||||||
|
require_cmd cargo-ndk
|
||||||
|
|
||||||
|
if [[ ! -d "$NDK_ROOT" ]]; then
|
||||||
|
echo "ERROR: Android NDK not found. Set ANDROID_NDK_HOME or ANDROID_HOME." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROID_NDK_HOME="$NDK_ROOT"
|
||||||
|
export LIBOPUS_STATIC=1
|
||||||
|
export LIBOPUS_NO_PKG=1
|
||||||
|
|
||||||
|
for build in "${BUILDS[@]}"; do
|
||||||
|
IFS=';' read -r abi triple opus_dir <<< "$build"
|
||||||
|
if [[ ! -f "$opus_dir/lib/libopus.a" ]]; then
|
||||||
|
echo "ERROR: missing $opus_dir/lib/libopus.a" >&2
|
||||||
|
echo "Run tools/build-opus-android.sh first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building Rust bridge for $abi ($triple)"
|
||||||
|
export LIBOPUS_LIB_DIR="$opus_dir"
|
||||||
|
cargo ndk --platform "$ANDROID_PLATFORM" -t "$abi" build --release -p chanora_bridge
|
||||||
|
|
||||||
|
mkdir -p "$JNI_LIBS_DIR/$abi"
|
||||||
|
cp "$REPO_ROOT/target/$triple/release/libchanora_bridge.so" "$JNI_LIBS_DIR/$abi/"
|
||||||
|
done
|
||||||
|
|
||||||
|
bar
|
||||||
|
echo "Android Rust libraries copied to $JNI_LIBS_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-}"
|
||||||
|
if [[ -z "$NDK_ROOT" && -n "${ANDROID_HOME:-}" ]]; then
|
||||||
|
NDK_ROOT="$(find "$ANDROID_HOME/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$NDK_ROOT" && -n "${ANDROID_SDK_ROOT:-}" ]]; then
|
||||||
|
NDK_ROOT="$(find "$ANDROID_SDK_ROOT/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||||
|
fi
|
||||||
|
OPUS_VERSION="v1.3.1"
|
||||||
|
OPUS_SRC_DIR="/tmp/opus-1.3.1"
|
||||||
|
API_LEVEL=21
|
||||||
|
|
||||||
|
declare -a ABIS=(
|
||||||
|
"arm64-v8a"
|
||||||
|
"armeabi-v7a"
|
||||||
|
"x86_64"
|
||||||
|
)
|
||||||
|
|
||||||
|
bar() { printf '%s\n' '========================================================================'; }
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
local cmd=$1
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: '$cmd' is required but not on PATH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
clone_or_update_opus() {
|
||||||
|
if [[ ! -d "$OPUS_SRC_DIR/.git" ]]; then
|
||||||
|
echo "Cloning opus ${OPUS_VERSION} into $OPUS_SRC_DIR"
|
||||||
|
rm -rf "$OPUS_SRC_DIR"
|
||||||
|
git clone --branch "$OPUS_VERSION" --depth 1 https://github.com/xiph/opus.git "$OPUS_SRC_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Reusing existing opus checkout at $OPUS_SRC_DIR"
|
||||||
|
git -C "$OPUS_SRC_DIR" fetch --tags origin
|
||||||
|
git -C "$OPUS_SRC_DIR" checkout "$OPUS_VERSION"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_one() {
|
||||||
|
local abi=$1
|
||||||
|
local build_dir="/tmp/opus-android-${abi}-build"
|
||||||
|
local install_dir="/tmp/opus-android-${abi}"
|
||||||
|
|
||||||
|
echo "Building opus for ${abi} (API ${API_LEVEL})"
|
||||||
|
rm -rf "$build_dir" "$install_dir"
|
||||||
|
mkdir -p "$build_dir"
|
||||||
|
|
||||||
|
# Opus 1.3.1's ARM NEON CMake path fails with recent NDK clang
|
||||||
|
# (`celt_inner_prod_neon` implicit declaration). Keep this portable
|
||||||
|
# baseline until we either patch Opus or upgrade the vendored version.
|
||||||
|
cmake -S "$OPUS_SRC_DIR" -B "$build_dir" \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$NDK_ROOT/build/cmake/android.toolchain.cmake" \
|
||||||
|
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||||
|
-DANDROID_ABI="$abi" \
|
||||||
|
-DANDROID_PLATFORM="android-${API_LEVEL}" \
|
||||||
|
-DCMAKE_INSTALL_PREFIX="$install_dir" \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DOPUS_BUILD_PROGRAMS=OFF \
|
||||||
|
-DOPUS_BUILD_TESTING=OFF \
|
||||||
|
-DOPUS_DISABLE_INTRINSICS=ON \
|
||||||
|
-DOPUS_USE_NEON=OFF \
|
||||||
|
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||||
|
|
||||||
|
cmake --build "$build_dir" --parallel
|
||||||
|
cmake --install "$build_dir"
|
||||||
|
|
||||||
|
if [[ ! -f "$install_dir/lib/libopus.a" ]]; then
|
||||||
|
echo "ERROR: expected $install_dir/lib/libopus.a" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
bar
|
||||||
|
echo "Chanora Android opus build"
|
||||||
|
echo "NDK : $NDK_ROOT"
|
||||||
|
echo "Opus tag : $OPUS_VERSION"
|
||||||
|
echo "API level : $API_LEVEL"
|
||||||
|
bar
|
||||||
|
|
||||||
|
require_cmd git
|
||||||
|
require_cmd cmake
|
||||||
|
|
||||||
|
if [[ ! -d "$NDK_ROOT" ]]; then
|
||||||
|
echo "ERROR: Android NDK not found at $NDK_ROOT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
clone_or_update_opus
|
||||||
|
|
||||||
|
for abi_entry in "${ABIS[@]}"; do
|
||||||
|
build_one "$abi_entry"
|
||||||
|
done
|
||||||
|
|
||||||
|
bar
|
||||||
|
echo "Android opus builds complete."
|
||||||
|
for abi_entry in "${ABIS[@]}"; do
|
||||||
|
echo " /tmp/opus-android-${abi_entry}/lib/libopus.a"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user