From eaf3f61ebd4a3825e606163f32299fe1f0a341ae Mon Sep 17 00:00:00 2001 From: Alois Date: Tue, 11 Aug 2026 15:50:52 +0200 Subject: [PATCH] feat(android): add zoom slider fix(android): gray screen bug --- apps/tauri/package.json | 2 +- apps/tauri/src-tauri/Cargo.lock | 323 +++--------------- apps/tauri/src-tauri/Cargo.toml | 4 +- apps/tauri/src-tauri/capabilities/mobile.json | 10 +- .../gen/android/app/build.gradle.kts | 10 +- .../java/net/tensamin/client/MainActivity.kt | 32 +- .../tensamin/client/MediaProjectionService.kt | 9 +- .../tensamin/client/MtpForegroundService.kt | 14 + .../client/NativeAccessibilityBridge.kt | 51 +++ .../src-tauri/gen/android/build.gradle.kts | 6 +- .../gen/android/buildSrc/build.gradle.kts | 3 +- .../net/tensamin/client/kotlin/BuildTask.kt | 10 +- .../src-tauri/src/accessibility_backend.rs | 135 ++++++++ apps/tauri/src-tauri/src/lib.rs | 3 + apps/tauri/src-tauri/src/mtp_backend.rs | 39 ++- apps/web/index.html | 2 +- apps/web/package.json | 2 +- .../web/src/components/screens/login/form.tsx | 5 - apps/web/vite.config.ts | 4 +- packages/mtp/src/context.tsx | 73 ++-- packages/settings/package.json | 1 + packages/settings/src/manifest.ts | 7 + packages/settings/src/pages/accessibility.tsx | 80 +++++ pnpm-lock.yaml | 3 + 24 files changed, 456 insertions(+), 372 deletions(-) create mode 100644 apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeAccessibilityBridge.kt create mode 100644 apps/tauri/src-tauri/src/accessibility_backend.rs create mode 100644 packages/settings/src/pages/accessibility.tsx diff --git a/apps/tauri/package.json b/apps/tauri/package.json index 7b7e5a7..8eab808 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -14,7 +14,7 @@ } }, "scripts": { - "dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}", + "dev:mobile:raw": "adb reverse tcp:3000 tcp:3000 && tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}", "start-adb:mobile:raw": "adb devices", "build:mobile:raw": "tauri android build", "build:mobile:ci": "node render-version.ts && trap 'node render-version.ts --unrender' EXIT && tauri android build --debug", diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 8e5d93d..744d5a2 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -560,17 +560,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "cfb" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" -dependencies = [ - "fnv", - "uuid", - "web-time", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -843,20 +832,7 @@ version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" dependencies = [ - "cssparser-macros 0.6.1", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" -dependencies = [ - "cssparser-macros 0.7.0", + "cssparser-macros", "dtoa-short", "itoa", "phf", @@ -873,16 +849,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "cssparser-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a2a99df6e410a8ff4245aa2006499ea662245f967cc7c0a38c83ef8eb44dbf" -dependencies = [ - "quote", - "syn 2.0.119", -] - [[package]] name = "ctor" version = "0.8.0" @@ -1179,26 +1145,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", - "cssparser 0.36.0", + "cssparser", "foldhash", - "html5ever 0.38.0", + "html5ever", "precomputed-hash", - "selectors 0.36.1", - "tendril", -] - -[[package]] -name = "dom_query" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" -dependencies = [ - "bit-set", - "cssparser 0.37.0", - "foldhash", - "html5ever 0.39.0", - "precomputed-hash", - "selectors 0.38.0", + "selectors", "tendril", ] @@ -1383,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2073,17 +2024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "markup5ever 0.38.0", -] - -[[package]] -name = "html5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" -dependencies = [ - "log", - "markup5ever 0.39.0", + "markup5ever", ] [[package]] @@ -2182,7 +2123,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -2220,7 +2160,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -2380,16 +2320,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" dependencies = [ - "cfb 0.7.3", -] - -[[package]] -name = "infer" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" -dependencies = [ - "cfb 0.14.0", + "cfb", ] [[package]] @@ -2752,17 +2683,6 @@ dependencies = [ "web_atoms", ] -[[package]] -name = "markup5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - [[package]] name = "memchr" version = "2.8.3" @@ -3920,7 +3840,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4100,13 +4020,14 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -4120,45 +4041,12 @@ dependencies = [ "quinn", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "sync_wrapper", - "tokio", "tokio-util", "tower", "tower-http", @@ -4228,7 +4116,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4287,7 +4175,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4440,26 +4328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ "bitflags 2.13.1", - "cssparser 0.36.0", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "selectors" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" -dependencies = [ - "bitflags 2.13.1", - "cssparser 0.37.0", + "cssparser", "derive_more", "log", "new_debug_unreachable", @@ -4588,18 +4457,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_with" version = "3.21.0" @@ -5031,7 +4888,7 @@ dependencies = [ "unicode-segmentation", "url", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] @@ -5082,13 +4939,13 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.4", + "reqwest", "serde", "serde_json", "serde_repr", "serialize-to-javascript", "swift-rs", - "tauri-build 2.6.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", + "tauri-build", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", @@ -5123,26 +4980,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tauri-build" -version = "2.6.3" -source = "git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762#e2e585ad1196c9572f86ef39aae01ef4c3b1a762" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch 4.2.0", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762)", - "tauri-winres", - "walkdir", -] - [[package]] name = "tauri-codegen" version = "2.6.3" @@ -5230,7 +5067,7 @@ dependencies = [ "tracing", "url", "windows-registry", - "windows-result 0.3.4", + "windows-result", ] [[package]] @@ -5351,11 +5188,11 @@ dependencies = [ "anyhow", "cargo_metadata", "ctor 0.8.0", - "dom_query 0.27.0", + "dom_query", "dunce", "glob", "http", - "infer 0.19.0", + "infer", "json-patch 3.0.1", "log", "memchr", @@ -5387,11 +5224,11 @@ dependencies = [ "brotli", "cargo_metadata", "ctor 1.0.12", - "dom_query 0.27.0", + "dom_query", "dunce", "glob", "http", - "infer 0.19.0", + "infer", "json-patch 4.2.0", "log", "memchr", @@ -5415,42 +5252,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tauri-utils" -version = "2.9.3" -source = "git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762#e2e585ad1196c9572f86ef39aae01ef4c3b1a762" -dependencies = [ - "anyhow", - "cargo_metadata", - "ctor 1.0.12", - "dom_query 0.28.0", - "dunce", - "glob", - "http", - "infer 0.22.0", - "json-patch 4.2.0", - "log", - "memchr", - "phf", - "plist", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.19", - "toml 1.1.4+spec-1.1.0", - "url", - "urlpattern 0.6.0", - "uuid", - "walkdir", -] - [[package]] name = "tauri-winres" version = "0.3.6" @@ -5480,10 +5281,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5497,17 +5298,17 @@ dependencies = [ [[package]] name = "tensamin" -version = "0.0.0" +version = "0.0.11" dependencies = [ "base64 0.22.1", "jni 0.22.4", "mtp", "mtp-transport 0.2.0 (git+https://git.methanium.net/methanium/mtp.git?rev=b067614a684eb1856bc5db7b3fd82148c036ce6b)", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "tauri", - "tauri-build 2.6.3 (git+https://github.com/tauri-apps/tauri?rev=e2e585ad1196c9572f86ef39aae01ef4c3b1a762)", + "tauri-build", "tauri-plugin-barcode-scanner", "tauri-plugin-deep-link", "tauri-plugin-log", @@ -6265,15 +6066,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webview2-com" version = "0.38.2" @@ -6283,7 +6075,7 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows-core 0.61.2", + "windows-core", "windows-implement", "windows-interface", ] @@ -6307,7 +6099,7 @@ checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.19", "windows", - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -6332,7 +6124,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6363,7 +6155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -6375,7 +6167,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -6387,21 +6179,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[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 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-result", + "windows-strings", ] [[package]] @@ -6410,7 +6189,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", "windows-threading", ] @@ -6455,7 +6234,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", ] @@ -6466,8 +6245,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-result", + "windows-strings", ] [[package]] @@ -6479,15 +6258,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -6497,15 +6267,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -6732,7 +6493,7 @@ dependencies = [ "cookie", "crossbeam-channel", "dirs", - "dom_query 0.27.0", + "dom_query", "dpi", "dunce", "gdkx11", @@ -6760,7 +6521,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows", - "windows-core 0.61.2", + "windows-core", "windows-version", "x11-dl", ] diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 3c70f02..3b7e6b5 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -15,14 +15,14 @@ name = "mobile_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "e2e585ad1196c9572f86ef39aae01ef4c3b1a762", features = [] } +tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41", features = [] } [dependencies] tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" base64 = "0.22" -reqwest = { version = "0.13", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } mtp = { git = "https://git.methanium.net/methanium/mtp.git", rev = "7182272e3edb0079d7b74b45b2f62a298d2fee22", features = ["client", "crypto"] } mtp-transport = { git = "https://git.methanium.net/methanium/mtp.git", rev = "b067614a684eb1856bc5db7b3fd82148c036ce6b" } diff --git a/apps/tauri/src-tauri/capabilities/mobile.json b/apps/tauri/src-tauri/capabilities/mobile.json index 447a06f..7121f98 100644 --- a/apps/tauri/src-tauri/capabilities/mobile.json +++ b/apps/tauri/src-tauri/capabilities/mobile.json @@ -1,13 +1,9 @@ { "identifier": "mobile-capability", - "platforms": [ - "android", - "iOS" - ], - "windows": [ - "main" - ], + "platforms": ["android", "iOS"], + "windows": ["main"], "permissions": [ + "core:event:default", "deep-link:default", "barcode-scanner:default", "barcode-scanner:allow-scan", diff --git a/apps/tauri/src-tauri/gen/android/app/build.gradle.kts b/apps/tauri/src-tauri/gen/android/app/build.gradle.kts index e7120ba..66d6289 100644 --- a/apps/tauri/src-tauri/gen/android/app/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/app/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.util.Properties import java.io.FileInputStream @@ -61,14 +62,17 @@ android { ) } } - kotlinOptions { - jvmTarget = "1.8" - } buildFeatures { buildConfig = true } } +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 + } +} + rust { rootDirRel = "../../../" } diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt index bd10948..fc3be18 100644 --- a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MainActivity.kt @@ -3,10 +3,8 @@ package net.tensamin.client import android.Manifest import android.app.Activity import android.content.pm.PackageManager -import android.graphics.Rect import android.media.projection.MediaProjectionManager import android.os.Bundle -import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.WindowManager import android.webkit.JavascriptInterface @@ -56,7 +54,7 @@ class MainActivity : TauriActivity() { } override fun onWebViewCreate(webView: WebView) { - webView.setInitialScale(290) + NativeAccessibilityBridge.attach(webView) mediaWebView = webView MobileMediaEvents.attach(webView) webView.addJavascriptInterface(MobileMediaJavascriptInterface(), "tensaminMobileMedia") @@ -65,11 +63,12 @@ class MainActivity : TauriActivity() { override fun onCreate(savedInstanceState: Bundle?) { WindowCompat.setDecorFitsSystemWindows(window, true) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + super.onCreate(savedInstanceState) + NativeMtpBridge.nativeAttach(applicationContext) if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) { NativeMtpBridge.startService(this) } - super.onCreate(savedInstanceState) - NativeMtpBridge.nativeAttach(applicationContext) + NativeAccessibilityBridge.nativeAttach(applicationContext) installKeyboardResizeWorkaround() } @@ -90,7 +89,10 @@ class MainActivity : TauriActivity() { attachLayoutListener = null contentRoot = null contentChild = null - mediaWebView?.removeJavascriptInterface("tensaminMobileMedia") + mediaWebView?.let { + NativeAccessibilityBridge.detach(it) + it.removeJavascriptInterface("tensaminMobileMedia") + } mediaWebView = null MobileMediaEvents.detach() super.onDestroy() @@ -203,16 +205,20 @@ class MainActivity : TauriActivity() { child: android.view.View, insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child), ) { - val visibleFrame = Rect() - child.getWindowVisibleDisplayFrame(visibleFrame) - val rootHeight = child.rootView.height if (rootHeight <= 0) return - val imeHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 - val keyboardHeight = maxOf(imeHeight, rootHeight - visibleFrame.bottom) - val keyboardVisible = keyboardHeight > rootHeight * 0.15 - val usableHeight = if (keyboardVisible) rootHeight - keyboardHeight else ViewGroup.LayoutParams.MATCH_PARENT + val imeVisible = insets?.isVisible(WindowInsetsCompat.Type.ime()) == true + val imeHeight = if (imeVisible) { + insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + } else { + 0 + } + val usableHeight = if (imeHeight in 1 until rootHeight) { + rootHeight - imeHeight + } else { + WindowManager.LayoutParams.MATCH_PARENT + } if (previousUsableHeight == usableHeight) return diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MediaProjectionService.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MediaProjectionService.kt index 0afa75a..3e961df 100644 --- a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MediaProjectionService.kt +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MediaProjectionService.kt @@ -26,6 +26,7 @@ import android.os.HandlerThread import android.os.IBinder import android.util.Base64 import android.util.DisplayMetrics +import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import java.io.ByteArrayOutputStream import java.util.concurrent.atomic.AtomicBoolean @@ -107,16 +108,12 @@ class MediaProjectionService : Service() { stopIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) - val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - android.app.Notification.Builder(this, NOTIFICATION_CHANNEL_ID) - } else { - android.app.Notification.Builder(this) - } + val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) .setSmallIcon(android.R.drawable.ic_menu_share) .setContentTitle("Tensamin is sharing your screen") .setContentText("Tap Stop to end screen sharing") .setOngoing(true) - .setCategory(android.app.Notification.CATEGORY_SERVICE) + .setCategory(NotificationCompat.CATEGORY_SERVICE) .addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent) .build() diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpForegroundService.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpForegroundService.kt index 8b41c78..56cca97 100644 --- a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpForegroundService.kt +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/MtpForegroundService.kt @@ -13,6 +13,8 @@ import android.net.Network import android.net.NetworkCapabilities import android.os.Build import android.os.IBinder +import android.os.Process +import android.os.SystemClock import androidx.core.app.NotificationCompat class MtpForegroundService : Service() { @@ -74,10 +76,19 @@ class MtpForegroundService : Service() { } override fun onTaskRemoved(rootIntent: Intent?) { + val preferences = getSharedPreferences(SERVICE_PREFERENCES, Context.MODE_PRIVATE) + val now = SystemClock.elapsedRealtime() + if (now - preferences.getLong(LAST_TASK_RESTART, 0) < TASK_RESTART_COOLDOWN_MS) { + super.onTaskRemoved(rootIntent) + return + } + preferences.edit().putLong(LAST_TASK_RESTART, now).commit() if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) { startService(Intent(this, MtpForegroundService::class.java)) } super.onTaskRemoved(rootIntent) + // Tauri cannot recreate its WebView after the UI task is removed while this process survives. + Process.killProcess(Process.myPid()) } override fun onDestroy() { @@ -94,6 +105,9 @@ class MtpForegroundService : Service() { private const val CHANNEL_ID = "tensamin-connection" private const val NOTIFICATION_ID = 2201 private const val ACTION_STOP = "net.tensamin.client.STOP_MTP" + private const val SERVICE_PREFERENCES = "tensamin-service" + private const val LAST_TASK_RESTART = "last-task-restart" + private const val TASK_RESTART_COOLDOWN_MS = 15_000L @Volatile private var connectionStatus = "Connecting" fun updateNotification(context: Context, status: String) { diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeAccessibilityBridge.kt b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeAccessibilityBridge.kt new file mode 100644 index 0000000..e04365a --- /dev/null +++ b/apps/tauri/src-tauri/gen/android/app/src/main/java/net/tensamin/client/NativeAccessibilityBridge.kt @@ -0,0 +1,51 @@ +package net.tensamin.client + +import android.content.Context +import android.webkit.WebView +import androidx.annotation.Keep +import java.lang.ref.WeakReference + +@Keep +object NativeAccessibilityBridge { + const val DEFAULT_INITIAL_SCALE = 290 + private const val MIN_INITIAL_SCALE = 210 + private const val MAX_INITIAL_SCALE = 500 + private const val PREFERENCES = "tensamin-accessibility" + private const val INITIAL_SCALE = "initial-scale" + + private var webView = WeakReference(null) + + init { + System.loadLibrary("mobile_lib") + } + + @JvmStatic external fun nativeAttach(context: Context) + + fun attach(webView: WebView) { + this.webView = WeakReference(webView) + webView.setInitialScale(getInitialScale(webView.context)) + } + + fun detach(webView: WebView) { + if (this.webView.get() === webView) this.webView.clear() + } + + fun getInitialScale(context: Context): Int { + val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) + val storedScale = preferences.getInt(INITIAL_SCALE, DEFAULT_INITIAL_SCALE) + val scale = storedScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE) + if (scale != storedScale) preferences.edit().putInt(INITIAL_SCALE, scale).apply() + return scale + } + + fun setInitialScale(context: Context, initialScale: Int) { + val nextScale = initialScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE) + context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) + .edit() + .putInt(INITIAL_SCALE, nextScale) + .apply() + webView.get()?.let { currentWebView -> + currentWebView.post { currentWebView.setInitialScale(nextScale) } + } + } +} diff --git a/apps/tauri/src-tauri/gen/android/build.gradle.kts b/apps/tauri/src-tauri/gen/android/build.gradle.kts index 20ec414..1488b27 100644 --- a/apps/tauri/src-tauri/gen/android/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/build.gradle.kts @@ -1,4 +1,4 @@ -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension buildscript { repositories { @@ -6,8 +6,8 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:9.3.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10") + classpath("com.android.tools.build:gradle:8.11.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") } } diff --git a/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts b/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts index cc18ea8..09776c6 100644 --- a/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -18,6 +18,5 @@ repositories { dependencies { compileOnly(gradleApi()) - implementation("com.android.tools.build:gradle:9.3.1") + implementation("com.android.tools.build:gradle:8.11.0") } - diff --git a/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt b/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt index a7e39eb..e2be2d1 100644 --- a/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt +++ b/apps/tauri/src-tauri/gen/android/buildSrc/src/main/java/net/tensamin/client/kotlin/BuildTask.kt @@ -5,8 +5,12 @@ import org.gradle.api.GradleException import org.gradle.api.logging.LogLevel import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import javax.inject.Inject -open class BuildTask : DefaultTask() { +open class BuildTask @Inject constructor( + private val execOperations: ExecOperations, +) : DefaultTask() { @Input var rootDirRel: String? = null @Input @@ -50,7 +54,7 @@ open class BuildTask : DefaultTask() { val release = release ?: throw GradleException("release cannot be null") val args = listOf("tauri", "android", "android-studio-script"); - project.exec { + execOperations.exec { workingDir(File(project.projectDir, rootDirRel)) executable(executable) args(args) @@ -65,4 +69,4 @@ open class BuildTask : DefaultTask() { args(listOf("--target", target)) }.assertNormalExitValue() } -} \ No newline at end of file +} diff --git a/apps/tauri/src-tauri/src/accessibility_backend.rs b/apps/tauri/src-tauri/src/accessibility_backend.rs new file mode 100644 index 0000000..692b42e --- /dev/null +++ b/apps/tauri/src-tauri/src/accessibility_backend.rs @@ -0,0 +1,135 @@ +#[cfg(not(target_os = "android"))] +const DEFAULT_INITIAL_SCALE: i32 = 290; +const MIN_INITIAL_SCALE: i32 = 210; +const MAX_INITIAL_SCALE: i32 = 500; + +#[tauri::command] +pub fn accessibility_get_initial_scale() -> Result { + android_get_initial_scale() +} + +#[tauri::command] +pub fn accessibility_set_initial_scale(initial_scale: i32) -> Result<(), String> { + if !(MIN_INITIAL_SCALE..=MAX_INITIAL_SCALE).contains(&initial_scale) { + return Err(format!( + "Initial scale must be between {MIN_INITIAL_SCALE} and {MAX_INITIAL_SCALE}" + )); + } + + android_set_initial_scale(initial_scale) +} + +#[cfg(not(target_os = "android"))] +fn android_get_initial_scale() -> Result { + Ok(DEFAULT_INITIAL_SCALE) +} + +#[cfg(not(target_os = "android"))] +fn android_set_initial_scale(_: i32) -> Result<(), String> { + Ok(()) +} + +#[cfg(target_os = "android")] +mod android { + use std::sync::OnceLock; + + use jni::{ + jni_sig, jni_str, + objects::{Global, JClass, JObject, JValue}, + Env, EnvUnowned, JavaVM, + }; + + struct Host { + vm: JavaVM, + context: Global>, + bridge: Global>, + } + + static HOST: OnceLock = OnceLock::new(); + + fn attach(env: &mut Env, context: JObject) -> Result<(), String> { + if HOST.get().is_some() { + return Ok(()); + } + + let class = env + .find_class(jni_str!("net/tensamin/client/NativeAccessibilityBridge")) + .map_err(|error| error.to_string())?; + let bridge = env + .get_static_field( + class, + jni_str!("INSTANCE"), + jni_sig!("Lnet/tensamin/client/NativeAccessibilityBridge;"), + ) + .and_then(|value| value.l()) + .map_err(|error| error.to_string())?; + + HOST.set(Host { + vm: env.get_java_vm().map_err(|error| error.to_string())?, + context: env + .new_global_ref(context) + .map_err(|error| error.to_string())?, + bridge: env + .new_global_ref(bridge) + .map_err(|error| error.to_string())?, + }) + .map_err(|_| "Android accessibility host is already attached".to_string()) + } + + fn with_env(call: impl FnOnce(&mut Env, &Host) -> Result) -> Result { + let host = HOST + .get() + .ok_or("Android accessibility host is not attached")?; + host.vm + .attach_current_thread(|env| Ok::<_, jni::errors::Error>(call(env, host))) + .map_err(|error| error.to_string())? + } + + pub fn get_initial_scale() -> Result { + with_env(|env, host| { + env.call_method( + host.bridge.as_obj(), + jni_str!("getInitialScale"), + jni_sig!("(Landroid/content/Context;)I"), + &[JValue::Object(host.context.as_obj())], + ) + .and_then(|value| value.i()) + .map_err(|error| error.to_string()) + }) + } + + pub fn set_initial_scale(initial_scale: i32) -> Result<(), String> { + with_env(|env, host| { + env.call_method( + host.bridge.as_obj(), + jni_str!("setInitialScale"), + jni_sig!("(Landroid/content/Context;I)V"), + &[ + JValue::Object(host.context.as_obj()), + JValue::Int(initial_scale), + ], + ) + .map_err(|error| error.to_string())?; + Ok(()) + }) + } + + #[no_mangle] + pub extern "system" fn Java_net_tensamin_client_NativeAccessibilityBridge_nativeAttach< + 'caller, + >( + mut env: EnvUnowned<'caller>, + _class: JClass, + context: JObject<'caller>, + ) { + let _ = env.with_env(|env| { + let _ = attach(env, context); + Ok::<_, jni::errors::Error>(()) + }); + } +} + +#[cfg(target_os = "android")] +use android::{ + get_initial_scale as android_get_initial_scale, set_initial_scale as android_set_initial_scale, +}; diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 8131b57..e81a830 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +mod accessibility_backend; mod mtp_backend; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -19,6 +20,8 @@ pub fn run() { let app = builder .invoke_handler(tauri::generate_handler![ + accessibility_backend::accessibility_get_initial_scale, + accessibility_backend::accessibility_set_initial_scale, mtp_backend::mtp_request, mtp_backend::mtp_status, mtp_backend::mtp_store_credentials, diff --git a/apps/tauri/src-tauri/src/mtp_backend.rs b/apps/tauri/src-tauri/src/mtp_backend.rs index 09fa249..b343337 100644 --- a/apps/tauri/src-tauri/src/mtp_backend.rs +++ b/apps/tauri/src-tauri/src/mtp_backend.rs @@ -372,9 +372,13 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { .with_max_missed_pings(3); #[cfg(target_os = "android")] let client_config = client_config.with_pinned_pem(android_root_certificates().clone()); - let connection = MTPClient::auth_connect(client_config, &keyring, &host_key) - .await - .map_err(|error| format!("transport authentication failed: {error}"))?; + let connection = tokio::time::timeout( + Duration::from_secs(30), + MTPClient::auth_connect(client_config, &keyring, &host_key), + ) + .await + .map_err(|_| "transport authentication timed out".to_string())? + .map_err(|error| format!("transport authentication failed: {error}"))?; manager().log(2, "Native MTP authentication completed", None); let connected = CommunicationValue::new(CommunicationType::ClientConnected) @@ -385,10 +389,13 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::CacheValid, DataValue::BoolFalse) .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)); - let state = connection - .request(&connected, None) - .await - .map_err(|error| format!("initial state synchronization failed: {error}"))?; + let state = tokio::time::timeout( + Duration::from_secs(30), + connection.request(&connected, None), + ) + .await + .map_err(|_| "initial state synchronization timed out".to_string())? + .map_err(|error| format!("initial state synchronization failed: {error}"))?; if !state.is_type(CommunicationType::ClientStateSync) { return Err(format!( "expected ClientStateSync, received {}", @@ -406,9 +413,9 @@ async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { let ack = CommunicationValue::new(CommunicationType::ClientStateAck) .add_typed_default(DataType::SessionId, number_to_data(session_id)) .add_typed_default(DataType::VersionNumber, number_to_data(version)); - let response = connection - .request(&ack, None) + let response = tokio::time::timeout(Duration::from_secs(30), connection.request(&ack, None)) .await + .map_err(|_| "state acknowledgement timed out".to_string())? .map_err(|error| format!("state acknowledgement failed: {error}"))?; if response .get_type_name() @@ -433,7 +440,19 @@ async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String public_key: String, } let root = config.omega_url.trim_end_matches('/'); - let response = reqwest::get(format!("{root}/api/get/omikron/{}", config.user_id)) + let client = reqwest::Client::builder() + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(20)); + #[cfg(target_os = "android")] + let client = client.tls_certs_only( + reqwest::Certificate::from_pem_bundle(android_root_certificates()) + .map_err(|error| format!("invalid bundled root certificates: {error}"))?, + ); + let response = client + .build() + .map_err(|error| error.to_string())? + .get(format!("{root}/api/get/omikron/{}", config.user_id)) + .send() .await .map_err(|error| error.to_string())?; if !response.status().is_success() { diff --git a/apps/web/index.html b/apps/web/index.html index eef61d9..01335eb 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -2,7 +2,7 @@ - + Tensamin diff --git a/apps/web/package.json b/apps/web/package.json index 107c6f7..15a4c00 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "scripts": { "format": "pnpm exec prettier --write .", "lint": "eslint src", - "dev": "vite --port 3000 --host 0.0.0.0", + "dev": "vite", "test": "vitest run --passWithNoTests", "build": "pnpm run test && tsc -b && vite build", "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index 0bbb850..72db8f7 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -76,11 +76,6 @@ function parseTuFileContent(rawFileContent: string): { throw new Error("Invalid file"); } - console.log({ - domain, - userId, - }); - return { userId, privateKey, domain }; } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 464f57b..5e1d120 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -70,9 +70,9 @@ export default defineConfig({ ], }, server: { - port: 5173, + port: 3000, strictPort: true, - host: host || "0.0.0.0", + host: "0.0.0.0", hmr: host ? { protocol: "ws", diff --git a/packages/mtp/src/context.tsx b/packages/mtp/src/context.tsx index a86fa57..af06ee0 100644 --- a/packages/mtp/src/context.tsx +++ b/packages/mtp/src/context.tsx @@ -777,40 +777,49 @@ function TauriProvider(props: { let disposed = false; let unlisten: UnlistenFn | undefined; void (async () => { - unlisten = await listen< - | { kind: "state"; snapshot: NativeSnapshot } - | { kind: "message"; generation: number; message: unknown } - | { - kind: "log"; - level: number; - message: string; - details?: unknown; + try { + const nextUnlisten = await listen< + | { kind: "state"; snapshot: NativeSnapshot } + | { kind: "message"; generation: number; message: unknown } + | { + kind: "log"; + level: number; + message: string; + details?: unknown; + } + >("mtp://event", ({ payload }) => { + if (disposed) return; + if (payload.kind === "state") { + applySnapshot(payload.snapshot); + return; } - >("mtp://event", ({ payload }) => { - if (disposed) return; - if (payload.kind === "state") { - applySnapshot(payload.snapshot); - return; - } - if (payload.kind === "message") { - if (payload.generation === generationRef.current) { - dispatchMessage(payload.message); + if (payload.kind === "message") { + if (payload.generation === generationRef.current) { + dispatchMessage(payload.message); + } + return; } - return; - } - log( - payload.level, - "android", - "orange", - payload.message, - payload.details, - ); - }); - const current = await invoke("mtp_status"); - if (!disposed) applySnapshot(current); - })().catch((error) => { - log(0, "mtp", "red", "Failed to initialize native MTP bridge", error); - }); + log( + payload.level, + "android", + "orange", + payload.message, + payload.details, + ); + }); + if (disposed) nextUnlisten(); + else unlisten = nextUnlisten; + } catch (error) { + log(0, "mtp", "red", "Failed to subscribe to native MTP events", error); + } + + try { + const current = await invoke("mtp_status"); + if (!disposed) applySnapshot(current); + } catch (error) { + log(0, "mtp", "red", "Failed to load native MTP status", error); + } + })(); return () => { disposed = true; unlisten?.(); diff --git a/packages/settings/package.json b/packages/settings/package.json index f3833a9..d281b7f 100644 --- a/packages/settings/package.json +++ b/packages/settings/package.json @@ -14,6 +14,7 @@ "dependencies": { "@methanium/ui": "*", "@tanstack/react-router": "^1.170.21", + "@tauri-apps/api": "^2.11.1", "@tensamin/cache": "workspace:*", "@tensamin/hotkeys": "workspace:*", "@tensamin/mtp": "workspace:*", diff --git a/packages/settings/src/manifest.ts b/packages/settings/src/manifest.ts index 2daf8ff..41fe14a 100644 --- a/packages/settings/src/manifest.ts +++ b/packages/settings/src/manifest.ts @@ -1,3 +1,4 @@ +import Accessibility from "./pages/accessibility"; import Cache from "./pages/cache"; import Call from "./pages/call"; import Chat from "./pages/chat"; @@ -26,6 +27,12 @@ export const settingsPages = [ { category: "general", path: "call", label: "Call", component: Call }, { category: "application", path: "cache", label: "Cache", component: Cache }, { category: "application", path: "theme", label: "Theme", component: Theme }, + { + category: "application", + path: "accessibility", + label: "Accessibility", + component: Accessibility, + }, { category: "application", path: "hotkeys", diff --git a/packages/settings/src/pages/accessibility.tsx b/packages/settings/src/pages/accessibility.tsx new file mode 100644 index 0000000..026c3b1 --- /dev/null +++ b/packages/settings/src/pages/accessibility.tsx @@ -0,0 +1,80 @@ +import { Button, Label, Slider } from "@methanium/ui"; +import { invoke, isTauri } from "@tauri-apps/api/core"; +import { RotateCcw } from "lucide-react"; +import { useEffect, useState } from "react"; + +const DEFAULT_INITIAL_SCALE = 290; +const MIN_INITIAL_SCALE = 210; +const isAndroid = isTauri() && /Android/.test(navigator.userAgent); + +export default function Page() { + const [initialScale, setInitialScale] = useState(DEFAULT_INITIAL_SCALE); + const [loading, setLoading] = useState(isAndroid); + + useEffect(() => { + if (!isAndroid) return; + + let active = true; + void invoke("accessibility_get_initial_scale") + .then((scale) => { + if (active) setInitialScale(scale); + }) + .catch((error: unknown) => { + console.error("Failed to load the Android initial scale", error); + }) + .finally(() => { + if (active) setLoading(false); + }); + + return () => { + active = false; + }; + }, []); + + if (!isAndroid) return null; + + function updateInitialScale(nextScale: number) { + setInitialScale(nextScale); + void invoke("accessibility_set_initial_scale", { + initialScale: nextScale, + }).catch((error: unknown) => { + console.error("Failed to update the Android initial scale", error); + }); + } + + return ( +
+
+ + + {initialScale}% + +
+
+ { + const nextScale = Array.isArray(value) ? value[0] : value; + if (nextScale !== undefined) updateInitialScale(nextScale); + }} + /> + +
+
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbfb6de..38033b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -509,6 +509,9 @@ importers: '@tanstack/react-router': specifier: ^1.170.21 version: 1.170.23(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tauri-apps/api': + specifier: ^2.11.1 + version: 2.11.1 '@tensamin/cache': specifier: workspace:* version: link:../cache